syncenginetestutils.cpp 36 KB

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