syncenginetestutils.h 45 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268
  1. /*
  2. * This software is in the public domain, furnished "as is", without technical
  3. * support, and with no warranty, express or implied, as to its usefulness for
  4. * any purpose.
  5. *
  6. */
  7. #pragma once
  8. #include "account.h"
  9. #include "creds/abstractcredentials.h"
  10. #include "logger.h"
  11. #include "filesystem.h"
  12. #include "syncengine.h"
  13. #include "common/syncjournaldb.h"
  14. #include "common/syncjournalfilerecord.h"
  15. #include "common/vfs.h"
  16. #include "csync_exclude.h"
  17. #include <QDir>
  18. #include <QNetworkReply>
  19. #include <QMap>
  20. #include <QtTest>
  21. #include <memory>
  22. #include <cookiejar.h>
  23. /*
  24. * TODO: In theory we should use QVERIFY instead of Q_ASSERT for testing, but this
  25. * only works when directly called from a QTest :-(
  26. */
  27. static const QUrl sRootUrl("owncloud://somehost/owncloud/remote.php/webdav/");
  28. static const QUrl sRootUrl2("owncloud://somehost/owncloud/remote.php/dav/files/admin/");
  29. static const QUrl sUploadUrl("owncloud://somehost/owncloud/remote.php/dav/uploads/admin/");
  30. inline QString getFilePathFromUrl(const QUrl &url) {
  31. QString path = url.path();
  32. if (path.startsWith(sRootUrl.path()))
  33. return path.mid(sRootUrl.path().length());
  34. if (path.startsWith(sRootUrl2.path()))
  35. return path.mid(sRootUrl2.path().length());
  36. if (path.startsWith(sUploadUrl.path()))
  37. return path.mid(sUploadUrl.path().length());
  38. return {};
  39. }
  40. inline QString generateEtag() {
  41. return QString::number(QDateTime::currentDateTimeUtc().toMSecsSinceEpoch(), 16) + QByteArray::number(qrand(), 16);
  42. }
  43. inline QByteArray generateFileId() {
  44. return QByteArray::number(qrand(), 16);
  45. }
  46. class PathComponents : public QStringList {
  47. public:
  48. PathComponents(const char *path) : PathComponents{QString::fromUtf8(path)} {}
  49. PathComponents(const QString &path) : QStringList{path.split('/', QString::SkipEmptyParts)} { }
  50. PathComponents(const QStringList &pathComponents) : QStringList{pathComponents} { }
  51. PathComponents parentDirComponents() const {
  52. return PathComponents{mid(0, size() - 1)};
  53. }
  54. PathComponents subComponents() const& { return PathComponents{mid(1)}; }
  55. PathComponents subComponents() && { removeFirst(); return std::move(*this); }
  56. QString pathRoot() const { return first(); }
  57. QString fileName() const { return last(); }
  58. };
  59. class FileModifier
  60. {
  61. public:
  62. virtual ~FileModifier() = default;
  63. virtual void remove(const QString &relativePath) = 0;
  64. virtual void insert(const QString &relativePath, qint64 size = 64, char contentChar = 'W') = 0;
  65. virtual void setContents(const QString &relativePath, char contentChar) = 0;
  66. virtual void appendByte(const QString &relativePath) = 0;
  67. virtual void mkdir(const QString &relativePath) = 0;
  68. virtual void rename(const QString &relativePath, const QString &relativeDestinationDirectory) = 0;
  69. virtual void setModTime(const QString &relativePath, const QDateTime &modTime) = 0;
  70. };
  71. class DiskFileModifier : public FileModifier
  72. {
  73. QDir _rootDir;
  74. public:
  75. DiskFileModifier(const QString &rootDirPath) : _rootDir(rootDirPath) { }
  76. void remove(const QString &relativePath) override {
  77. QFileInfo fi{_rootDir.filePath(relativePath)};
  78. if (fi.isFile())
  79. QVERIFY(_rootDir.remove(relativePath));
  80. else
  81. QVERIFY(QDir{fi.filePath()}.removeRecursively());
  82. }
  83. void insert(const QString &relativePath, qint64 size = 64, char contentChar = 'W') override {
  84. QFile file{_rootDir.filePath(relativePath)};
  85. QVERIFY(!file.exists());
  86. file.open(QFile::WriteOnly);
  87. QByteArray buf(1024, contentChar);
  88. for (int x = 0; x < size/buf.size(); ++x) {
  89. file.write(buf);
  90. }
  91. file.write(buf.data(), size % buf.size());
  92. file.close();
  93. // Set the mtime 30 seconds in the past, for some tests that need to make sure that the mtime differs.
  94. OCC::FileSystem::setModTime(file.fileName(), OCC::Utility::qDateTimeToTime_t(QDateTime::currentDateTimeUtc().addSecs(-30)));
  95. QCOMPARE(file.size(), size);
  96. }
  97. void setContents(const QString &relativePath, char contentChar) override {
  98. QFile file{_rootDir.filePath(relativePath)};
  99. QVERIFY(file.exists());
  100. qint64 size = file.size();
  101. file.open(QFile::WriteOnly);
  102. file.write(QByteArray{}.fill(contentChar, size));
  103. }
  104. void appendByte(const QString &relativePath) override {
  105. QFile file{_rootDir.filePath(relativePath)};
  106. QVERIFY(file.exists());
  107. file.open(QFile::ReadWrite);
  108. QByteArray contents = file.read(1);
  109. file.seek(file.size());
  110. file.write(contents);
  111. }
  112. void mkdir(const QString &relativePath) override {
  113. _rootDir.mkpath(relativePath);
  114. }
  115. void rename(const QString &from, const QString &to) override {
  116. QVERIFY(_rootDir.exists(from));
  117. QVERIFY(_rootDir.rename(from, to));
  118. }
  119. void setModTime(const QString &relativePath, const QDateTime &modTime) override {
  120. OCC::FileSystem::setModTime(_rootDir.filePath(relativePath), OCC::Utility::qDateTimeToTime_t(modTime));
  121. }
  122. };
  123. class FileInfo : public FileModifier
  124. {
  125. public:
  126. static FileInfo A12_B12_C12_S12() {
  127. FileInfo fi{QString{}, {
  128. {QStringLiteral("A"), {
  129. {QStringLiteral("a1"), 4},
  130. {QStringLiteral("a2"), 4}
  131. }},
  132. {QStringLiteral("B"), {
  133. {QStringLiteral("b1"), 16},
  134. {QStringLiteral("b2"), 16}
  135. }},
  136. {QStringLiteral("C"), {
  137. {QStringLiteral("c1"), 24},
  138. {QStringLiteral("c2"), 24}
  139. }},
  140. }};
  141. FileInfo sharedFolder{QStringLiteral("S"), {
  142. {QStringLiteral("s1"), 32},
  143. {QStringLiteral("s2"), 32}
  144. }};
  145. sharedFolder.isShared = true;
  146. sharedFolder.children[QStringLiteral("s1")].isShared = true;
  147. sharedFolder.children[QStringLiteral("s2")].isShared = true;
  148. fi.children.insert(sharedFolder.name, std::move(sharedFolder));
  149. return fi;
  150. }
  151. FileInfo() = default;
  152. FileInfo(const QString &name) : name{name} { }
  153. FileInfo(const QString &name, qint64 size) : name{name}, isDir{false}, size{size} { }
  154. FileInfo(const QString &name, qint64 size, char contentChar) : name{name}, isDir{false}, size{size}, contentChar{contentChar} { }
  155. FileInfo(const QString &name, const std::initializer_list<FileInfo> &children) : name{name} {
  156. for (const auto &source : children)
  157. addChild(source);
  158. }
  159. void addChild(const FileInfo &info)
  160. {
  161. auto &dest = this->children[info.name] = info;
  162. dest.parentPath = path();
  163. dest.fixupParentPathRecursively();
  164. }
  165. void remove(const QString &relativePath) override {
  166. const PathComponents pathComponents{relativePath};
  167. FileInfo *parent = findInvalidatingEtags(pathComponents.parentDirComponents());
  168. Q_ASSERT(parent);
  169. parent->children.erase(std::find_if(parent->children.begin(), parent->children.end(),
  170. [&pathComponents](const FileInfo &fi){ return fi.name == pathComponents.fileName(); }));
  171. }
  172. void insert(const QString &relativePath, qint64 size = 64, char contentChar = 'W') override {
  173. create(relativePath, size, contentChar);
  174. }
  175. void setContents(const QString &relativePath, char contentChar) override {
  176. FileInfo *file = findInvalidatingEtags(relativePath);
  177. Q_ASSERT(file);
  178. file->contentChar = contentChar;
  179. }
  180. void appendByte(const QString &relativePath) override {
  181. FileInfo *file = findInvalidatingEtags(relativePath);
  182. Q_ASSERT(file);
  183. file->size += 1;
  184. }
  185. void mkdir(const QString &relativePath) override {
  186. createDir(relativePath);
  187. }
  188. void rename(const QString &oldPath, const QString &newPath) override {
  189. const PathComponents newPathComponents{newPath};
  190. FileInfo *dir = findInvalidatingEtags(newPathComponents.parentDirComponents());
  191. Q_ASSERT(dir);
  192. Q_ASSERT(dir->isDir);
  193. const PathComponents pathComponents{oldPath};
  194. FileInfo *parent = findInvalidatingEtags(pathComponents.parentDirComponents());
  195. Q_ASSERT(parent);
  196. FileInfo fi = parent->children.take(pathComponents.fileName());
  197. fi.parentPath = dir->path();
  198. fi.name = newPathComponents.fileName();
  199. fi.fixupParentPathRecursively();
  200. dir->children.insert(newPathComponents.fileName(), std::move(fi));
  201. }
  202. void setModTime(const QString &relativePath, const QDateTime &modTime) override {
  203. FileInfo *file = findInvalidatingEtags(relativePath);
  204. Q_ASSERT(file);
  205. file->lastModified = modTime;
  206. }
  207. FileInfo *find(PathComponents pathComponents, const bool invalidateEtags = false) {
  208. if (pathComponents.isEmpty()) {
  209. if (invalidateEtags) {
  210. etag = generateEtag();
  211. }
  212. return this;
  213. }
  214. QString childName = pathComponents.pathRoot();
  215. auto it = children.find(childName);
  216. if (it != children.end()) {
  217. auto file = it->find(std::move(pathComponents).subComponents(), invalidateEtags);
  218. if (file && invalidateEtags) {
  219. // Update parents on the way back
  220. etag = generateEtag();
  221. }
  222. return file;
  223. }
  224. return nullptr;
  225. }
  226. FileInfo *createDir(const QString &relativePath) {
  227. const PathComponents pathComponents{relativePath};
  228. FileInfo *parent = findInvalidatingEtags(pathComponents.parentDirComponents());
  229. Q_ASSERT(parent);
  230. FileInfo &child = parent->children[pathComponents.fileName()] = FileInfo{pathComponents.fileName()};
  231. child.parentPath = parent->path();
  232. child.etag = generateEtag();
  233. return &child;
  234. }
  235. FileInfo *create(const QString &relativePath, qint64 size, char contentChar) {
  236. const PathComponents pathComponents{relativePath};
  237. FileInfo *parent = findInvalidatingEtags(pathComponents.parentDirComponents());
  238. Q_ASSERT(parent);
  239. FileInfo &child = parent->children[pathComponents.fileName()] = FileInfo{pathComponents.fileName(), size};
  240. child.parentPath = parent->path();
  241. child.contentChar = contentChar;
  242. child.etag = generateEtag();
  243. return &child;
  244. }
  245. bool operator<(const FileInfo &other) const {
  246. return name < other.name;
  247. }
  248. bool operator==(const FileInfo &other) const {
  249. // Consider files to be equal between local<->remote as a user would.
  250. return name == other.name
  251. && isDir == other.isDir
  252. && size == other.size
  253. && contentChar == other.contentChar
  254. && children == other.children;
  255. }
  256. bool operator!=(const FileInfo &other) const {
  257. return !operator==(other);
  258. }
  259. QString path() const {
  260. return (parentPath.isEmpty() ? QString() : (parentPath + '/')) + name;
  261. }
  262. void fixupParentPathRecursively() {
  263. auto p = path();
  264. for (auto it = children.begin(); it != children.end(); ++it) {
  265. Q_ASSERT(it.key() == it->name);
  266. it->parentPath = p;
  267. it->fixupParentPathRecursively();
  268. }
  269. }
  270. QString name;
  271. bool isDir = true;
  272. bool isShared = false;
  273. OCC::RemotePermissions permissions; // When uset, defaults to everything
  274. QDateTime lastModified = QDateTime::currentDateTimeUtc().addDays(-7);
  275. QString etag = generateEtag();
  276. QByteArray fileId = generateFileId();
  277. QByteArray checksums;
  278. QByteArray extraDavProperties;
  279. qint64 size = 0;
  280. char contentChar = 'W';
  281. // Sorted by name to be able to compare trees
  282. QMap<QString, FileInfo> children;
  283. QString parentPath;
  284. FileInfo *findInvalidatingEtags(PathComponents pathComponents) {
  285. return find(std::move(pathComponents), true);
  286. }
  287. friend inline QDebug operator<<(QDebug dbg, const FileInfo& fi) {
  288. return dbg << "{ " << fi.path() << ": " << fi.children;
  289. }
  290. };
  291. class FakePropfindReply : public QNetworkReply
  292. {
  293. Q_OBJECT
  294. public:
  295. QByteArray payload;
  296. FakePropfindReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent)
  297. : QNetworkReply{parent} {
  298. setRequest(request);
  299. setUrl(request.url());
  300. setOperation(op);
  301. open(QIODevice::ReadOnly);
  302. QString fileName = getFilePathFromUrl(request.url());
  303. Q_ASSERT(!fileName.isNull()); // for root, it should be empty
  304. const FileInfo *fileInfo = remoteRootFileInfo.find(fileName);
  305. if (!fileInfo) {
  306. QMetaObject::invokeMethod(this, "respond404", Qt::QueuedConnection);
  307. return;
  308. }
  309. QString prefix = request.url().path().left(request.url().path().size() - fileName.size());
  310. // Don't care about the request and just return a full propfind
  311. const QString davUri{QStringLiteral("DAV:")};
  312. const QString ocUri{QStringLiteral("http://owncloud.org/ns")};
  313. QBuffer buffer{&payload};
  314. buffer.open(QIODevice::WriteOnly);
  315. QXmlStreamWriter xml( &buffer );
  316. xml.writeNamespace(davUri, "d");
  317. xml.writeNamespace(ocUri, "oc");
  318. xml.writeStartDocument();
  319. xml.writeStartElement(davUri, QStringLiteral("multistatus"));
  320. auto writeFileResponse = [&](const FileInfo &fileInfo) {
  321. xml.writeStartElement(davUri, QStringLiteral("response"));
  322. xml.writeTextElement(davUri, QStringLiteral("href"), prefix + QUrl::toPercentEncoding(fileInfo.path(), "/"));
  323. xml.writeStartElement(davUri, QStringLiteral("propstat"));
  324. xml.writeStartElement(davUri, QStringLiteral("prop"));
  325. if (fileInfo.isDir) {
  326. xml.writeStartElement(davUri, QStringLiteral("resourcetype"));
  327. xml.writeEmptyElement(davUri, QStringLiteral("collection"));
  328. xml.writeEndElement(); // resourcetype
  329. } else
  330. xml.writeEmptyElement(davUri, QStringLiteral("resourcetype"));
  331. auto gmtDate = fileInfo.lastModified.toUTC();
  332. auto stringDate = QLocale::c().toString(gmtDate, "ddd, dd MMM yyyy HH:mm:ss 'GMT'");
  333. xml.writeTextElement(davUri, QStringLiteral("getlastmodified"), stringDate);
  334. xml.writeTextElement(davUri, QStringLiteral("getcontentlength"), QString::number(fileInfo.size));
  335. xml.writeTextElement(davUri, QStringLiteral("getetag"), QStringLiteral("\"%1\"").arg(fileInfo.etag));
  336. xml.writeTextElement(ocUri, QStringLiteral("permissions"), !fileInfo.permissions.isNull()
  337. ? QString(fileInfo.permissions.toString())
  338. : fileInfo.isShared ? QStringLiteral("SRDNVCKW") : QStringLiteral("RDNVCKW"));
  339. xml.writeTextElement(ocUri, QStringLiteral("id"), fileInfo.fileId);
  340. xml.writeTextElement(ocUri, QStringLiteral("checksums"), fileInfo.checksums);
  341. buffer.write(fileInfo.extraDavProperties);
  342. xml.writeEndElement(); // prop
  343. xml.writeTextElement(davUri, QStringLiteral("status"), "HTTP/1.1 200 OK");
  344. xml.writeEndElement(); // propstat
  345. xml.writeEndElement(); // response
  346. };
  347. writeFileResponse(*fileInfo);
  348. foreach(const FileInfo &childFileInfo, fileInfo->children)
  349. writeFileResponse(childFileInfo);
  350. xml.writeEndElement(); // multistatus
  351. xml.writeEndDocument();
  352. QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
  353. }
  354. Q_INVOKABLE void respond() {
  355. setHeader(QNetworkRequest::ContentLengthHeader, payload.size());
  356. setHeader(QNetworkRequest::ContentTypeHeader, "application/xml; charset=utf-8");
  357. setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 207);
  358. setFinished(true);
  359. emit metaDataChanged();
  360. if (bytesAvailable())
  361. emit readyRead();
  362. emit finished();
  363. }
  364. Q_INVOKABLE void respond404() {
  365. setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 404);
  366. setError(InternalServerError, "Not Found");
  367. emit metaDataChanged();
  368. emit finished();
  369. }
  370. void abort() override { }
  371. qint64 bytesAvailable() const override { return payload.size() + QIODevice::bytesAvailable(); }
  372. qint64 readData(char *data, qint64 maxlen) override {
  373. qint64 len = std::min(qint64{payload.size()}, maxlen);
  374. strncpy(data, payload.constData(), len);
  375. payload.remove(0, len);
  376. return len;
  377. }
  378. };
  379. class FakePutReply : public QNetworkReply
  380. {
  381. Q_OBJECT
  382. FileInfo *fileInfo;
  383. public:
  384. FakePutReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, const QByteArray &putPayload, QObject *parent)
  385. : QNetworkReply{parent} {
  386. setRequest(request);
  387. setUrl(request.url());
  388. setOperation(op);
  389. open(QIODevice::ReadOnly);
  390. fileInfo = perform(remoteRootFileInfo, request, putPayload);
  391. QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
  392. }
  393. static FileInfo *perform(FileInfo &remoteRootFileInfo, const QNetworkRequest &request, const QByteArray &putPayload)
  394. {
  395. QString fileName = getFilePathFromUrl(request.url());
  396. Q_ASSERT(!fileName.isEmpty());
  397. FileInfo *fileInfo = remoteRootFileInfo.find(fileName);
  398. if (fileInfo) {
  399. fileInfo->size = putPayload.size();
  400. fileInfo->contentChar = putPayload.at(0);
  401. } else {
  402. // Assume that the file is filled with the same character
  403. fileInfo = remoteRootFileInfo.create(fileName, putPayload.size(), putPayload.at(0));
  404. }
  405. fileInfo->lastModified = OCC::Utility::qDateTimeFromTime_t(request.rawHeader("X-OC-Mtime").toLongLong());
  406. remoteRootFileInfo.find(fileName, /*invalidateEtags=*/true);
  407. return fileInfo;
  408. }
  409. Q_INVOKABLE virtual void respond()
  410. {
  411. emit uploadProgress(fileInfo->size, fileInfo->size);
  412. setRawHeader("OC-ETag", fileInfo->etag.toLatin1());
  413. setRawHeader("ETag", fileInfo->etag.toLatin1());
  414. setRawHeader("OC-FileID", fileInfo->fileId);
  415. setRawHeader("X-OC-MTime", "accepted"); // Prevents Q_ASSERT(!_runningNow) since we'll call PropagateItemJob::done twice in that case.
  416. setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 200);
  417. emit metaDataChanged();
  418. emit finished();
  419. }
  420. void abort() override
  421. {
  422. setError(OperationCanceledError, "abort");
  423. emit finished();
  424. }
  425. qint64 readData(char *, qint64) override { return 0; }
  426. };
  427. class FakeMkcolReply : public QNetworkReply
  428. {
  429. Q_OBJECT
  430. FileInfo *fileInfo;
  431. public:
  432. FakeMkcolReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent)
  433. : QNetworkReply{parent} {
  434. setRequest(request);
  435. setUrl(request.url());
  436. setOperation(op);
  437. open(QIODevice::ReadOnly);
  438. QString fileName = getFilePathFromUrl(request.url());
  439. Q_ASSERT(!fileName.isEmpty());
  440. fileInfo = remoteRootFileInfo.createDir(fileName);
  441. if (!fileInfo) {
  442. abort();
  443. return;
  444. }
  445. QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
  446. }
  447. Q_INVOKABLE void respond() {
  448. setRawHeader("OC-FileId", fileInfo->fileId);
  449. setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 201);
  450. emit metaDataChanged();
  451. emit finished();
  452. }
  453. void abort() override { }
  454. qint64 readData(char *, qint64) override { return 0; }
  455. };
  456. class FakeDeleteReply : public QNetworkReply
  457. {
  458. Q_OBJECT
  459. public:
  460. FakeDeleteReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent)
  461. : QNetworkReply{parent} {
  462. setRequest(request);
  463. setUrl(request.url());
  464. setOperation(op);
  465. open(QIODevice::ReadOnly);
  466. QString fileName = getFilePathFromUrl(request.url());
  467. Q_ASSERT(!fileName.isEmpty());
  468. remoteRootFileInfo.remove(fileName);
  469. QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
  470. }
  471. Q_INVOKABLE void respond() {
  472. setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 204);
  473. emit metaDataChanged();
  474. emit finished();
  475. }
  476. void abort() override { }
  477. qint64 readData(char *, qint64) override { return 0; }
  478. };
  479. class FakeMoveReply : public QNetworkReply
  480. {
  481. Q_OBJECT
  482. public:
  483. FakeMoveReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent)
  484. : QNetworkReply{parent} {
  485. setRequest(request);
  486. setUrl(request.url());
  487. setOperation(op);
  488. open(QIODevice::ReadOnly);
  489. QString fileName = getFilePathFromUrl(request.url());
  490. Q_ASSERT(!fileName.isEmpty());
  491. QString dest = getFilePathFromUrl(QUrl::fromEncoded(request.rawHeader("Destination")));
  492. Q_ASSERT(!dest.isEmpty());
  493. remoteRootFileInfo.rename(fileName, dest);
  494. QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
  495. }
  496. Q_INVOKABLE void respond() {
  497. setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 201);
  498. emit metaDataChanged();
  499. emit finished();
  500. }
  501. void abort() override { }
  502. qint64 readData(char *, qint64) override { return 0; }
  503. };
  504. class FakeGetReply : public QNetworkReply
  505. {
  506. Q_OBJECT
  507. public:
  508. const FileInfo *fileInfo;
  509. char payload;
  510. int size;
  511. bool aborted = false;
  512. FakeGetReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent)
  513. : QNetworkReply{parent} {
  514. setRequest(request);
  515. setUrl(request.url());
  516. setOperation(op);
  517. open(QIODevice::ReadOnly);
  518. QString fileName = getFilePathFromUrl(request.url());
  519. Q_ASSERT(!fileName.isEmpty());
  520. fileInfo = remoteRootFileInfo.find(fileName);
  521. if (!fileInfo)
  522. qWarning() << "Could not find file" << fileName << "on the remote";
  523. QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
  524. }
  525. Q_INVOKABLE void respond() {
  526. if (aborted) {
  527. setError(OperationCanceledError, "Operation Canceled");
  528. emit metaDataChanged();
  529. emit finished();
  530. return;
  531. }
  532. payload = fileInfo->contentChar;
  533. size = fileInfo->size;
  534. setHeader(QNetworkRequest::ContentLengthHeader, size);
  535. setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 200);
  536. setRawHeader("OC-ETag", fileInfo->etag.toLatin1());
  537. setRawHeader("ETag", fileInfo->etag.toLatin1());
  538. setRawHeader("OC-FileId", fileInfo->fileId);
  539. emit metaDataChanged();
  540. if (bytesAvailable())
  541. emit readyRead();
  542. emit finished();
  543. }
  544. void abort() override {
  545. aborted = true;
  546. }
  547. qint64 bytesAvailable() const override {
  548. if (aborted)
  549. return 0;
  550. return size + QIODevice::bytesAvailable();
  551. }
  552. qint64 readData(char *data, qint64 maxlen) override {
  553. qint64 len = std::min(qint64{size}, maxlen);
  554. std::fill_n(data, len, payload);
  555. size -= len;
  556. return len;
  557. }
  558. // useful to be public for testing
  559. using QNetworkReply::setRawHeader;
  560. };
  561. class FakeChunkMoveReply : public QNetworkReply
  562. {
  563. Q_OBJECT
  564. FileInfo *fileInfo;
  565. public:
  566. FakeChunkMoveReply(FileInfo &uploadsFileInfo, FileInfo &remoteRootFileInfo,
  567. QNetworkAccessManager::Operation op, const QNetworkRequest &request,
  568. QObject *parent)
  569. : QNetworkReply{ parent }
  570. {
  571. setRequest(request);
  572. setUrl(request.url());
  573. setOperation(op);
  574. open(QIODevice::ReadOnly);
  575. fileInfo = perform(uploadsFileInfo, remoteRootFileInfo, request);
  576. if (!fileInfo) {
  577. QTimer::singleShot(0, this, &FakeChunkMoveReply::respondPreconditionFailed);
  578. } else {
  579. QTimer::singleShot(0, this, &FakeChunkMoveReply::respond);
  580. }
  581. }
  582. static FileInfo *perform(FileInfo &uploadsFileInfo, FileInfo &remoteRootFileInfo, const QNetworkRequest &request)
  583. {
  584. QString source = getFilePathFromUrl(request.url());
  585. Q_ASSERT(!source.isEmpty());
  586. Q_ASSERT(source.endsWith("/.file"));
  587. source = source.left(source.length() - static_cast<int>(qstrlen("/.file")));
  588. auto sourceFolder = uploadsFileInfo.find(source);
  589. Q_ASSERT(sourceFolder);
  590. Q_ASSERT(sourceFolder->isDir);
  591. qint64 count = 0;
  592. qint64 size = 0;
  593. char payload = '\0';
  594. do {
  595. QString chunkName = QString::number(count).rightJustified(16, '0');
  596. if (!sourceFolder->children.contains(chunkName))
  597. break;
  598. auto &x = sourceFolder->children[chunkName];
  599. Q_ASSERT(!x.isDir);
  600. Q_ASSERT(x.size > 0); // There should not be empty chunks
  601. size += x.size;
  602. Q_ASSERT(!payload || payload == x.contentChar);
  603. payload = x.contentChar;
  604. ++count;
  605. } while(true);
  606. Q_ASSERT(count > 1); // There should be at least two chunks, otherwise why would we use chunking?
  607. Q_ASSERT(sourceFolder->children.count() == count); // There should not be holes or extra files
  608. QString fileName = getFilePathFromUrl(QUrl::fromEncoded(request.rawHeader("Destination")));
  609. Q_ASSERT(!fileName.isEmpty());
  610. FileInfo *fileInfo = remoteRootFileInfo.find(fileName);
  611. if (fileInfo) {
  612. // The client should put this header
  613. Q_ASSERT(request.hasRawHeader("If"));
  614. // And it should condition on the destination file
  615. auto start = QByteArray("<" + request.rawHeader("Destination") + ">");
  616. Q_ASSERT(request.rawHeader("If").startsWith(start));
  617. if (request.rawHeader("If") != start + " ([\"" + fileInfo->etag.toLatin1() + "\"])") {
  618. return nullptr;
  619. }
  620. fileInfo->size = size;
  621. fileInfo->contentChar = payload;
  622. } else {
  623. Q_ASSERT(!request.hasRawHeader("If"));
  624. // Assume that the file is filled with the same character
  625. fileInfo = remoteRootFileInfo.create(fileName, size, payload);
  626. }
  627. fileInfo->lastModified = OCC::Utility::qDateTimeFromTime_t(request.rawHeader("X-OC-Mtime").toLongLong());
  628. remoteRootFileInfo.find(fileName, /*invalidateEtags=*/true);
  629. return fileInfo;
  630. }
  631. Q_INVOKABLE virtual void respond()
  632. {
  633. setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 201);
  634. setRawHeader("OC-ETag", fileInfo->etag.toLatin1());
  635. setRawHeader("ETag", fileInfo->etag.toLatin1());
  636. setRawHeader("OC-FileId", fileInfo->fileId);
  637. emit metaDataChanged();
  638. emit finished();
  639. }
  640. Q_INVOKABLE void respondPreconditionFailed() {
  641. setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 412);
  642. setError(InternalServerError, "Precondition Failed");
  643. emit metaDataChanged();
  644. emit finished();
  645. }
  646. void abort() override
  647. {
  648. setError(OperationCanceledError, "abort");
  649. emit finished();
  650. }
  651. qint64 readData(char *, qint64) override { return 0; }
  652. };
  653. class FakePayloadReply : public QNetworkReply
  654. {
  655. Q_OBJECT
  656. public:
  657. FakePayloadReply(QNetworkAccessManager::Operation op, const QNetworkRequest &request,
  658. const QByteArray &body, QObject *parent)
  659. : QNetworkReply{ parent }
  660. , _body(body)
  661. {
  662. setRequest(request);
  663. setUrl(request.url());
  664. setOperation(op);
  665. open(QIODevice::ReadOnly);
  666. QTimer::singleShot(10, this, &FakePayloadReply::respond);
  667. }
  668. void respond()
  669. {
  670. setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 200);
  671. setHeader(QNetworkRequest::ContentLengthHeader, _body.size());
  672. emit metaDataChanged();
  673. emit readyRead();
  674. setFinished(true);
  675. emit finished();
  676. }
  677. void abort() override {}
  678. qint64 readData(char *buf, qint64 max) override
  679. {
  680. max = qMin<qint64>(max, _body.size());
  681. memcpy(buf, _body.constData(), max);
  682. _body = _body.mid(max);
  683. return max;
  684. }
  685. qint64 bytesAvailable() const override
  686. {
  687. return _body.size();
  688. }
  689. QByteArray _body;
  690. };
  691. class FakeErrorReply : public QNetworkReply
  692. {
  693. Q_OBJECT
  694. public:
  695. FakeErrorReply(QNetworkAccessManager::Operation op, const QNetworkRequest &request,
  696. QObject *parent, int httpErrorCode, const QByteArray &body = QByteArray())
  697. : QNetworkReply{parent}, _body(body) {
  698. setRequest(request);
  699. setUrl(request.url());
  700. setOperation(op);
  701. open(QIODevice::ReadOnly);
  702. setAttribute(QNetworkRequest::HttpStatusCodeAttribute, httpErrorCode);
  703. setError(InternalServerError, "Internal Server Fake Error");
  704. QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
  705. }
  706. Q_INVOKABLE virtual void respond() {
  707. emit metaDataChanged();
  708. emit readyRead();
  709. // finishing can come strictly after readyRead was called
  710. QTimer::singleShot(5, this, &FakeErrorReply::slotSetFinished);
  711. }
  712. // make public to give tests easy interface
  713. using QNetworkReply::setError;
  714. using QNetworkReply::setAttribute;
  715. public slots:
  716. void slotSetFinished() {
  717. setFinished(true);
  718. emit finished();
  719. }
  720. public:
  721. void abort() override { }
  722. qint64 readData(char *buf, qint64 max) override {
  723. max = qMin<qint64>(max, _body.size());
  724. memcpy(buf, _body.constData(), max);
  725. _body = _body.mid(max);
  726. return max;
  727. }
  728. qint64 bytesAvailable() const override {
  729. return _body.size();
  730. }
  731. QByteArray _body;
  732. };
  733. // A reply that never responds
  734. class FakeHangingReply : public QNetworkReply
  735. {
  736. Q_OBJECT
  737. public:
  738. FakeHangingReply(QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent)
  739. : QNetworkReply(parent)
  740. {
  741. setRequest(request);
  742. setUrl(request.url());
  743. setOperation(op);
  744. open(QIODevice::ReadOnly);
  745. }
  746. void abort() override {
  747. // Follow more or less the implementation of QNetworkReplyImpl::abort
  748. close();
  749. setError(OperationCanceledError, tr("Operation canceled"));
  750. emit error(OperationCanceledError);
  751. setFinished(true);
  752. emit finished();
  753. }
  754. qint64 readData(char *, qint64) override { return 0; }
  755. };
  756. // A delayed reply
  757. template <class OriginalReply>
  758. class DelayedReply : public OriginalReply
  759. {
  760. public:
  761. template <typename... Args>
  762. explicit DelayedReply(quint64 delayMS, Args &&... args)
  763. : OriginalReply(std::forward<Args>(args)...)
  764. , _delayMs(delayMS)
  765. {
  766. }
  767. quint64 _delayMs;
  768. void respond() override
  769. {
  770. QTimer::singleShot(_delayMs, static_cast<OriginalReply *>(this), [this] {
  771. // Explicit call to bases's respond();
  772. this->OriginalReply::respond();
  773. });
  774. }
  775. };
  776. class FakeQNAM : public QNetworkAccessManager
  777. {
  778. public:
  779. using Override = std::function<QNetworkReply *(Operation, const QNetworkRequest &, QIODevice *)>;
  780. private:
  781. FileInfo _remoteRootFileInfo;
  782. FileInfo _uploadFileInfo;
  783. // maps a path to an HTTP error
  784. QHash<QString, int> _errorPaths;
  785. // monitor requests and optionally provide custom replies
  786. Override _override;
  787. public:
  788. FakeQNAM(FileInfo initialRoot)
  789. : _remoteRootFileInfo{std::move(initialRoot)}
  790. {
  791. setCookieJar(new OCC::CookieJar);
  792. }
  793. FileInfo &currentRemoteState() { return _remoteRootFileInfo; }
  794. FileInfo &uploadState() { return _uploadFileInfo; }
  795. QHash<QString, int> &errorPaths() { return _errorPaths; }
  796. void setOverride(const Override &override) { _override = override; }
  797. protected:
  798. QNetworkReply *createRequest(Operation op, const QNetworkRequest &request,
  799. QIODevice *outgoingData = nullptr) {
  800. if (_override) {
  801. if (auto reply = _override(op, request, outgoingData))
  802. return reply;
  803. }
  804. const QString fileName = getFilePathFromUrl(request.url());
  805. Q_ASSERT(!fileName.isNull());
  806. if (_errorPaths.contains(fileName))
  807. return new FakeErrorReply{op, request, this, _errorPaths[fileName]};
  808. bool isUpload = request.url().path().startsWith(sUploadUrl.path());
  809. FileInfo &info = isUpload ? _uploadFileInfo : _remoteRootFileInfo;
  810. auto verb = request.attribute(QNetworkRequest::CustomVerbAttribute);
  811. if (verb == "PROPFIND")
  812. // Ignore outgoingData always returning somethign good enough, works for now.
  813. return new FakePropfindReply{info, op, request, this};
  814. else if (verb == QLatin1String("GET") || op == QNetworkAccessManager::GetOperation)
  815. return new FakeGetReply{info, op, request, this};
  816. else if (verb == QLatin1String("PUT") || op == QNetworkAccessManager::PutOperation)
  817. return new FakePutReply{info, op, request, outgoingData->readAll(), this};
  818. else if (verb == QLatin1String("MKCOL"))
  819. return new FakeMkcolReply{info, op, request, this};
  820. else if (verb == QLatin1String("DELETE") || op == QNetworkAccessManager::DeleteOperation)
  821. return new FakeDeleteReply{info, op, request, this};
  822. else if (verb == QLatin1String("MOVE") && !isUpload)
  823. return new FakeMoveReply{info, op, request, this};
  824. else if (verb == QLatin1String("MOVE") && isUpload)
  825. return new FakeChunkMoveReply{ info, _remoteRootFileInfo, op, request, this };
  826. else {
  827. qDebug() << verb << outgoingData;
  828. Q_UNREACHABLE();
  829. }
  830. }
  831. };
  832. class FakeCredentials : public OCC::AbstractCredentials
  833. {
  834. QNetworkAccessManager *_qnam;
  835. public:
  836. FakeCredentials(QNetworkAccessManager *qnam) : _qnam{qnam} { }
  837. virtual QString authType() const { return "test"; }
  838. virtual QString user() const { return "admin"; }
  839. virtual QNetworkAccessManager *createQNAM() const { return _qnam; }
  840. virtual bool ready() const { return true; }
  841. virtual void fetchFromKeychain() { }
  842. virtual void askFromUser() { }
  843. virtual bool stillValid(QNetworkReply *) { return true; }
  844. virtual void persist() { }
  845. virtual void invalidateToken() { }
  846. virtual void forgetSensitiveData() { }
  847. };
  848. class FakeFolder
  849. {
  850. QTemporaryDir _tempDir;
  851. DiskFileModifier _localModifier;
  852. // FIXME: Clarify ownership, double delete
  853. FakeQNAM *_fakeQnam;
  854. OCC::AccountPtr _account;
  855. std::unique_ptr<OCC::SyncJournalDb> _journalDb;
  856. std::unique_ptr<OCC::SyncEngine> _syncEngine;
  857. public:
  858. FakeFolder(const FileInfo &fileTemplate)
  859. : _localModifier(_tempDir.path())
  860. {
  861. // Needs to be done once
  862. OCC::SyncEngine::minimumFileAgeForUpload = std::chrono::milliseconds(0);
  863. OCC::Logger::instance()->setLogFile("-");
  864. QDir rootDir{_tempDir.path()};
  865. qDebug() << "FakeFolder operating on" << rootDir;
  866. toDisk(rootDir, fileTemplate);
  867. _fakeQnam = new FakeQNAM(fileTemplate);
  868. _account = OCC::Account::create();
  869. _account->setUrl(QUrl(QStringLiteral("http://admin:admin@localhost/owncloud")));
  870. _account->setCredentials(new FakeCredentials{_fakeQnam});
  871. _account->setDavDisplayName("fakename");
  872. _account->setServerVersion("10.0.0");
  873. _journalDb = std::make_unique<OCC::SyncJournalDb>(localPath() + ".sync_test.db");
  874. _syncEngine = std::make_unique<OCC::SyncEngine>(_account, localPath(), "", _journalDb.get());
  875. // Ignore temporary files from the download. (This is in the default exclude list, but we don't load it)
  876. _syncEngine->excludedFiles().addManualExclude("]*.~*");
  877. // Ensure we have a valid VfsOff instance "running"
  878. switchToVfs(_syncEngine->syncOptions()._vfs);
  879. // A new folder will update the local file state database on first sync.
  880. // To have a state matching what users will encounter, we have to a sync
  881. // using an identical local/remote file tree first.
  882. ENFORCE(syncOnce());
  883. }
  884. void switchToVfs(QSharedPointer<OCC::Vfs> vfs)
  885. {
  886. auto opts = _syncEngine->syncOptions();
  887. opts._vfs->stop();
  888. QObject::disconnect(_syncEngine.get(), 0, opts._vfs.data(), 0);
  889. opts._vfs = vfs;
  890. _syncEngine->setSyncOptions(opts);
  891. OCC::VfsSetupParams vfsParams;
  892. vfsParams.filesystemPath = localPath();
  893. vfsParams.remotePath = "/";
  894. vfsParams.account = _account;
  895. vfsParams.journal = _journalDb.get();
  896. vfsParams.providerName = "OC-TEST";
  897. vfsParams.providerVersion = "0.1";
  898. vfsParams.enableShellIntegration = false;
  899. QObject::connect(_syncEngine.get(), &QObject::destroyed, vfs.data(), [vfs]() {
  900. vfs->stop();
  901. vfs->unregisterFolder();
  902. });
  903. vfs->start(vfsParams);
  904. }
  905. OCC::AccountPtr account() const { return _account; }
  906. OCC::SyncEngine &syncEngine() const { return *_syncEngine; }
  907. OCC::SyncJournalDb &syncJournal() const { return *_journalDb; }
  908. FileModifier &localModifier() { return _localModifier; }
  909. FileInfo &remoteModifier() { return _fakeQnam->currentRemoteState(); }
  910. FileInfo currentLocalState() {
  911. QDir rootDir{_tempDir.path()};
  912. FileInfo rootTemplate;
  913. fromDisk(rootDir, rootTemplate);
  914. rootTemplate.fixupParentPathRecursively();
  915. return rootTemplate;
  916. }
  917. FileInfo currentRemoteState() { return _fakeQnam->currentRemoteState(); }
  918. FileInfo &uploadState() { return _fakeQnam->uploadState(); }
  919. FileInfo dbState() const;
  920. struct ErrorList {
  921. FakeQNAM *_qnam;
  922. void append(const QString &path, int error = 500)
  923. { _qnam->errorPaths().insert(path, error); }
  924. void clear() { _qnam->errorPaths().clear(); }
  925. };
  926. ErrorList serverErrorPaths() { return {_fakeQnam}; }
  927. void setServerOverride(const FakeQNAM::Override &override) { _fakeQnam->setOverride(override); }
  928. QString localPath() const {
  929. // SyncEngine wants a trailing slash
  930. if (_tempDir.path().endsWith('/'))
  931. return _tempDir.path();
  932. return _tempDir.path() + '/';
  933. }
  934. void scheduleSync() {
  935. // Have to be done async, else, an error before exec() does not terminate the event loop.
  936. QMetaObject::invokeMethod(_syncEngine.get(), "startSync", Qt::QueuedConnection);
  937. }
  938. void execUntilBeforePropagation() {
  939. QSignalSpy spy(_syncEngine.get(), SIGNAL(aboutToPropagate(SyncFileItemVector&)));
  940. QVERIFY(spy.wait());
  941. }
  942. void execUntilItemCompleted(const QString &relativePath) {
  943. QSignalSpy spy(_syncEngine.get(), SIGNAL(itemCompleted(const SyncFileItemPtr &)));
  944. QElapsedTimer t;
  945. t.start();
  946. while (t.elapsed() < 5000) {
  947. spy.clear();
  948. QVERIFY(spy.wait());
  949. for(const QList<QVariant> &args : spy) {
  950. auto item = args[0].value<OCC::SyncFileItemPtr>();
  951. if (item->destination() == relativePath)
  952. return;
  953. }
  954. }
  955. QVERIFY(false);
  956. }
  957. bool execUntilFinished() {
  958. QSignalSpy spy(_syncEngine.get(), SIGNAL(finished(bool)));
  959. bool ok = spy.wait(3600000);
  960. Q_ASSERT(ok && "Sync timed out");
  961. return spy[0][0].toBool();
  962. }
  963. bool syncOnce() {
  964. scheduleSync();
  965. return execUntilFinished();
  966. }
  967. private:
  968. static void toDisk(QDir &dir, const FileInfo &templateFi) {
  969. foreach (const FileInfo &child, templateFi.children) {
  970. if (child.isDir) {
  971. QDir subDir(dir);
  972. dir.mkdir(child.name);
  973. subDir.cd(child.name);
  974. toDisk(subDir, child);
  975. } else {
  976. QFile file{dir.filePath(child.name)};
  977. file.open(QFile::WriteOnly);
  978. file.write(QByteArray{}.fill(child.contentChar, child.size));
  979. file.close();
  980. OCC::FileSystem::setModTime(file.fileName(), OCC::Utility::qDateTimeToTime_t(child.lastModified));
  981. }
  982. }
  983. }
  984. static void fromDisk(QDir &dir, FileInfo &templateFi) {
  985. foreach (const QFileInfo &diskChild, dir.entryInfoList(QDir::AllEntries | QDir::NoDotAndDotDot)) {
  986. if (diskChild.isDir()) {
  987. QDir subDir = dir;
  988. subDir.cd(diskChild.fileName());
  989. FileInfo &subFi = templateFi.children[diskChild.fileName()] = FileInfo{diskChild.fileName()};
  990. fromDisk(subDir, subFi);
  991. } else {
  992. QFile f{diskChild.filePath()};
  993. f.open(QFile::ReadOnly);
  994. auto content = f.read(1);
  995. if (content.size() == 0) {
  996. qWarning() << "Empty file at:" << diskChild.filePath();
  997. continue;
  998. }
  999. char contentChar = content.at(0);
  1000. templateFi.children.insert(diskChild.fileName(), FileInfo{diskChild.fileName(), diskChild.size(), contentChar});
  1001. }
  1002. }
  1003. }
  1004. };
  1005. static FileInfo &findOrCreateDirs(FileInfo &base, PathComponents components)
  1006. {
  1007. if (components.isEmpty())
  1008. return base;
  1009. auto childName = components.pathRoot();
  1010. auto it = base.children.find(childName);
  1011. if (it != base.children.end()) {
  1012. return findOrCreateDirs(*it, components.subComponents());
  1013. }
  1014. auto &newDir = base.children[childName] = FileInfo{childName};
  1015. newDir.parentPath = base.path();
  1016. return findOrCreateDirs(newDir, components.subComponents());
  1017. }
  1018. inline FileInfo FakeFolder::dbState() const
  1019. {
  1020. FileInfo result;
  1021. _journalDb->getFilesBelowPath("", [&](const OCC::SyncJournalFileRecord &record) {
  1022. auto components = PathComponents(QString::fromUtf8(record._path));
  1023. auto &parentDir = findOrCreateDirs(result, components.parentDirComponents());
  1024. auto name = components.fileName();
  1025. auto &item = parentDir.children[name];
  1026. item.name = name;
  1027. item.parentPath = parentDir.path();
  1028. item.size = record._fileSize;
  1029. item.isDir = record._type == ItemTypeDirectory;
  1030. item.permissions = record._remotePerm;
  1031. item.etag = record._etag;
  1032. item.lastModified = OCC::Utility::qDateTimeFromTime_t(record._modtime);
  1033. item.fileId = record._fileId;
  1034. item.checksums = record._checksumHeader;
  1035. // item.contentChar can't be set from the db
  1036. });
  1037. return result;
  1038. }
  1039. /* Return the FileInfo for a conflict file for the specified relative filename */
  1040. inline const FileInfo *findConflict(FileInfo &dir, const QString &filename)
  1041. {
  1042. QFileInfo info(filename);
  1043. const FileInfo *parentDir = dir.find(info.path());
  1044. if (!parentDir)
  1045. return nullptr;
  1046. QString start = info.baseName() + " (conflicted copy";
  1047. for (const auto &item : parentDir->children) {
  1048. if (item.name.startsWith(start)) {
  1049. return &item;
  1050. }
  1051. }
  1052. return nullptr;
  1053. }
  1054. struct ItemCompletedSpy : QSignalSpy {
  1055. explicit ItemCompletedSpy(FakeFolder &folder)
  1056. : QSignalSpy(&folder.syncEngine(), &OCC::SyncEngine::itemCompleted)
  1057. {}
  1058. OCC::SyncFileItemPtr findItem(const QString &path) const
  1059. {
  1060. for (const QList<QVariant> &args : *this) {
  1061. auto item = args[0].value<OCC::SyncFileItemPtr>();
  1062. if (item->destination() == path)
  1063. return item;
  1064. }
  1065. return OCC::SyncFileItemPtr::create();
  1066. }
  1067. };
  1068. // QTest::toString overloads
  1069. namespace OCC {
  1070. inline char *toString(const SyncFileStatus &s) {
  1071. return QTest::toString(QString("SyncFileStatus(" + s.toSocketAPIString() + ")"));
  1072. }
  1073. }
  1074. inline void addFiles(QStringList &dest, const FileInfo &fi)
  1075. {
  1076. if (fi.isDir) {
  1077. dest += QString("%1 - dir").arg(fi.path());
  1078. foreach (const FileInfo &fi, fi.children)
  1079. addFiles(dest, fi);
  1080. } else {
  1081. dest += QString("%1 - %2 %3-bytes").arg(fi.path()).arg(fi.size).arg(fi.contentChar);
  1082. }
  1083. }
  1084. inline QString toStringNoElide(const FileInfo &fi)
  1085. {
  1086. QStringList files;
  1087. foreach (const FileInfo &fi, fi.children)
  1088. addFiles(files, fi);
  1089. files.sort();
  1090. return QString("FileInfo with %1 files(\n\t%2\n)").arg(files.size()).arg(files.join("\n\t"));
  1091. }
  1092. inline char *toString(const FileInfo &fi)
  1093. {
  1094. return QTest::toString(toStringNoElide(fi));
  1095. }
  1096. inline void addFilesDbData(QStringList &dest, const FileInfo &fi)
  1097. {
  1098. // could include etag, permissions etc, but would need extra work
  1099. if (fi.isDir) {
  1100. dest += QString("%1 - %2 %3 %4").arg(
  1101. fi.name,
  1102. fi.isDir ? "dir" : "file",
  1103. QString::number(fi.lastModified.toSecsSinceEpoch()),
  1104. fi.fileId);
  1105. foreach (const FileInfo &fi, fi.children)
  1106. addFilesDbData(dest, fi);
  1107. } else {
  1108. dest += QString("%1 - %2 %3 %4 %5").arg(
  1109. fi.name,
  1110. fi.isDir ? "dir" : "file",
  1111. QString::number(fi.size),
  1112. QString::number(fi.lastModified.toSecsSinceEpoch()),
  1113. fi.fileId);
  1114. }
  1115. }
  1116. inline char *printDbData(const FileInfo &fi)
  1117. {
  1118. QStringList files;
  1119. foreach (const FileInfo &fi, fi.children)
  1120. addFilesDbData(files, fi);
  1121. return QTest::toString(QString("FileInfo with %1 files(%2)").arg(files.size()).arg(files.join(", ")));
  1122. }