syncenginetestutils.cpp 44 KB

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