syncenginetestutils.cpp 49 KB

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