testchunkingng.cpp 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629
  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 <QtTest>
  8. #include "syncenginetestutils.h"
  9. #include <syncengine.h>
  10. using namespace OCC;
  11. /* Upload a 1/3 of a file of given size.
  12. * fakeFolder needs to be synchronized */
  13. static void partialUpload(FakeFolder &fakeFolder, const QString &name, qint64 size)
  14. {
  15. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  16. QCOMPARE(fakeFolder.uploadState().children.count(), 0); // The state should be clean
  17. fakeFolder.localModifier().insert(name, size);
  18. // Abort when the upload is at 1/3
  19. qint64 sizeWhenAbort = -1;
  20. auto con = QObject::connect(&fakeFolder.syncEngine(), &SyncEngine::transmissionProgress,
  21. [&](const ProgressInfo &progress) {
  22. if (progress.completedSize() > (progress.totalSize() /3 )) {
  23. sizeWhenAbort = progress.completedSize();
  24. fakeFolder.syncEngine().abort();
  25. }
  26. });
  27. QVERIFY(!fakeFolder.syncOnce()); // there should have been an error
  28. QObject::disconnect(con);
  29. QVERIFY(sizeWhenAbort > 0);
  30. QVERIFY(sizeWhenAbort < size);
  31. QCOMPARE(fakeFolder.uploadState().children.count(), 1); // the transfer was done with chunking
  32. auto upStateChildren = fakeFolder.uploadState().children.first().children;
  33. QCOMPARE(sizeWhenAbort, std::accumulate(upStateChildren.cbegin(), upStateChildren.cend(), 0,
  34. [](int s, const FileInfo &i) { return s + i.size; }));
  35. }
  36. // Reduce max chunk size a bit so we get more chunks
  37. static void setChunkSize(SyncEngine &engine, qint64 size)
  38. {
  39. SyncOptions options;
  40. options._maxChunkSize = size;
  41. options._initialChunkSize = size;
  42. options._minChunkSize = size;
  43. engine.setSyncOptions(options);
  44. }
  45. class TestChunkingNG : public QObject
  46. {
  47. Q_OBJECT
  48. private slots:
  49. void testFileUpload() {
  50. FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()};
  51. fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ {"chunking", "1.0"} } } });
  52. setChunkSize(fakeFolder.syncEngine(), 1 * 1000 * 1000);
  53. const int size = 10 * 1000 * 1000; // 10 MB
  54. fakeFolder.localModifier().insert("A/a0", size);
  55. QVERIFY(fakeFolder.syncOnce());
  56. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  57. QCOMPARE(fakeFolder.uploadState().children.count(), 1); // the transfer was done with chunking
  58. QCOMPARE(fakeFolder.currentRemoteState().find("A/a0")->size, size);
  59. // Check that another upload of the same file also work.
  60. fakeFolder.localModifier().appendByte("A/a0");
  61. QVERIFY(fakeFolder.syncOnce());
  62. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  63. QCOMPARE(fakeFolder.uploadState().children.count(), 2); // the transfer was done with chunking
  64. }
  65. // Test resuming when there's a confusing chunk added
  66. void testResume1() {
  67. FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()};
  68. fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ {"chunking", "1.0"} } } });
  69. const int size = 10 * 1000 * 1000; // 10 MB
  70. setChunkSize(fakeFolder.syncEngine(), 1 * 1000 * 1000);
  71. partialUpload(fakeFolder, "A/a0", size);
  72. QCOMPARE(fakeFolder.uploadState().children.count(), 1);
  73. auto chunkingId = fakeFolder.uploadState().children.first().name;
  74. const auto &chunkMap = fakeFolder.uploadState().children.first().children;
  75. qint64 uploadedSize = std::accumulate(chunkMap.begin(), chunkMap.end(), 0LL, [](qint64 s, const FileInfo &f) { return s + f.size; });
  76. QVERIFY(uploadedSize > 2 * 1000 * 1000); // at least 2 MB
  77. // Add a fake chunk to make sure it gets deleted
  78. fakeFolder.uploadState().children.first().insert("10000", size);
  79. fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *) -> QNetworkReply * {
  80. if (op == QNetworkAccessManager::PutOperation) {
  81. // Test that we properly resuming and are not sending past data again.
  82. Q_ASSERT(request.rawHeader("OC-Chunk-Offset").toLongLong() >= uploadedSize);
  83. } else if (op == QNetworkAccessManager::DeleteOperation) {
  84. Q_ASSERT(request.url().path().endsWith("/10000"));
  85. }
  86. return nullptr;
  87. });
  88. QVERIFY(fakeFolder.syncOnce());
  89. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  90. QCOMPARE(fakeFolder.currentRemoteState().find("A/a0")->size, size);
  91. // The same chunk id was re-used
  92. QCOMPARE(fakeFolder.uploadState().children.count(), 1);
  93. QCOMPARE(fakeFolder.uploadState().children.first().name, chunkingId);
  94. }
  95. // Test resuming when one of the uploaded chunks got removed
  96. void testResume2() {
  97. FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()};
  98. fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ {"chunking", "1.0"} } } });
  99. setChunkSize(fakeFolder.syncEngine(), 1 * 1000 * 1000);
  100. const int size = 30 * 1000 * 1000; // 30 MB
  101. partialUpload(fakeFolder, "A/a0", size);
  102. QCOMPARE(fakeFolder.uploadState().children.count(), 1);
  103. auto chunkingId = fakeFolder.uploadState().children.first().name;
  104. const auto &chunkMap = fakeFolder.uploadState().children.first().children;
  105. qint64 uploadedSize = std::accumulate(chunkMap.begin(), chunkMap.end(), 0LL, [](qint64 s, const FileInfo &f) { return s + f.size; });
  106. QVERIFY(uploadedSize > 2 * 1000 * 1000); // at least 50 MB
  107. QVERIFY(chunkMap.size() >= 3); // at least three chunks
  108. QStringList chunksToDelete;
  109. // Remove the second chunk, so all further chunks will be deleted and resent
  110. auto firstChunk = chunkMap.first();
  111. auto secondChunk = *(chunkMap.begin() + 1);
  112. const auto chunksList = chunkMap.keys().mid(2);
  113. for (const auto& name : chunksList) {
  114. chunksToDelete.append(name);
  115. }
  116. fakeFolder.uploadState().children.first().remove(secondChunk.name);
  117. QStringList deletedPaths;
  118. fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *) -> QNetworkReply * {
  119. if (op == QNetworkAccessManager::PutOperation) {
  120. // Test that we properly resuming, not resending the first chunk
  121. Q_ASSERT(request.rawHeader("OC-Chunk-Offset").toLongLong() >= firstChunk.size);
  122. } else if (op == QNetworkAccessManager::DeleteOperation) {
  123. deletedPaths.append(request.url().path());
  124. }
  125. return nullptr;
  126. });
  127. QVERIFY(fakeFolder.syncOnce());
  128. for (const auto& toDelete : chunksToDelete) {
  129. bool wasDeleted = false;
  130. for (const auto& deleted : deletedPaths) {
  131. if (deleted.mid(deleted.lastIndexOf('/') + 1) == toDelete) {
  132. wasDeleted = true;
  133. break;
  134. }
  135. }
  136. QVERIFY(wasDeleted);
  137. }
  138. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  139. QCOMPARE(fakeFolder.currentRemoteState().find("A/a0")->size, size);
  140. // The same chunk id was re-used
  141. QCOMPARE(fakeFolder.uploadState().children.count(), 1);
  142. QCOMPARE(fakeFolder.uploadState().children.first().name, chunkingId);
  143. }
  144. // Test resuming when all chunks are already present
  145. void testResume3() {
  146. FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()};
  147. fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ {"chunking", "1.0"} } } });
  148. const int size = 30 * 1000 * 1000; // 30 MB
  149. setChunkSize(fakeFolder.syncEngine(), 1 * 1000 * 1000);
  150. partialUpload(fakeFolder, "A/a0", size);
  151. QCOMPARE(fakeFolder.uploadState().children.count(), 1);
  152. auto chunkingId = fakeFolder.uploadState().children.first().name;
  153. const auto &chunkMap = fakeFolder.uploadState().children.first().children;
  154. qint64 uploadedSize = std::accumulate(chunkMap.begin(), chunkMap.end(), 0LL, [](qint64 s, const FileInfo &f) { return s + f.size; });
  155. QVERIFY(uploadedSize > 5 * 1000 * 1000); // at least 5 MB
  156. // Add a chunk that makes the file completely uploaded
  157. fakeFolder.uploadState().children.first().insert(
  158. QString::number(chunkMap.size()).rightJustified(16, '0'), size - uploadedSize);
  159. bool sawPut = false;
  160. bool sawDelete = false;
  161. bool sawMove = false;
  162. fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *) -> QNetworkReply * {
  163. if (op == QNetworkAccessManager::PutOperation) {
  164. sawPut = true;
  165. } else if (op == QNetworkAccessManager::DeleteOperation) {
  166. sawDelete = true;
  167. } else if (request.attribute(QNetworkRequest::CustomVerbAttribute) == "MOVE") {
  168. sawMove = true;
  169. }
  170. return nullptr;
  171. });
  172. QVERIFY(fakeFolder.syncOnce());
  173. QVERIFY(sawMove);
  174. QVERIFY(!sawPut);
  175. QVERIFY(!sawDelete);
  176. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  177. QCOMPARE(fakeFolder.currentRemoteState().find("A/a0")->size, size);
  178. // The same chunk id was re-used
  179. QCOMPARE(fakeFolder.uploadState().children.count(), 1);
  180. QCOMPARE(fakeFolder.uploadState().children.first().name, chunkingId);
  181. }
  182. // Test resuming (or rather not resuming!) for the error case of the sum of
  183. // chunk sizes being larger than the file size
  184. void testResume4() {
  185. FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()};
  186. fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ {"chunking", "1.0"} } } });
  187. const int size = 30 * 1000 * 1000; // 30 MB
  188. setChunkSize(fakeFolder.syncEngine(), 1 * 1000 * 1000);
  189. partialUpload(fakeFolder, "A/a0", size);
  190. QCOMPARE(fakeFolder.uploadState().children.count(), 1);
  191. auto chunkingId = fakeFolder.uploadState().children.first().name;
  192. const auto &chunkMap = fakeFolder.uploadState().children.first().children;
  193. qint64 uploadedSize = std::accumulate(chunkMap.begin(), chunkMap.end(), 0LL, [](qint64 s, const FileInfo &f) { return s + f.size; });
  194. QVERIFY(uploadedSize > 5 * 1000 * 1000); // at least 5 MB
  195. // Add a chunk that makes the file more than completely uploaded
  196. fakeFolder.uploadState().children.first().insert(
  197. QString::number(chunkMap.size()).rightJustified(16, '0'), size - uploadedSize + 100);
  198. QVERIFY(fakeFolder.syncOnce());
  199. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  200. QCOMPARE(fakeFolder.currentRemoteState().find("A/a0")->size, size);
  201. // Used a new transfer id but wiped the old one
  202. QCOMPARE(fakeFolder.uploadState().children.count(), 1);
  203. QVERIFY(fakeFolder.uploadState().children.first().name != chunkingId);
  204. }
  205. // Check what happens when we abort during the final MOVE and the
  206. // the final MOVE takes longer than the abort-delay
  207. void testLateAbortHard()
  208. {
  209. FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
  210. fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ { "chunking", "1.0" } } }, { "checksums", QVariantMap{ { "supportedTypes", QStringList() << "SHA1" } } } });
  211. const int size = 15 * 1000 * 1000; // 15 MB
  212. setChunkSize(fakeFolder.syncEngine(), 1 * 1000 * 1000);
  213. // Make the MOVE never reply, but trigger a client-abort and apply the change remotely
  214. QObject parent;
  215. QByteArray moveChecksumHeader;
  216. int nGET = 0;
  217. int responseDelay = 100000; // bigger than abort-wait timeout
  218. fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *) -> QNetworkReply * {
  219. if (request.attribute(QNetworkRequest::CustomVerbAttribute) == "MOVE") {
  220. QTimer::singleShot(50, &parent, [&]() { fakeFolder.syncEngine().abort(); });
  221. moveChecksumHeader = request.rawHeader("OC-Checksum");
  222. return new DelayedReply<FakeChunkMoveReply>(responseDelay, fakeFolder.uploadState(), fakeFolder.remoteModifier(), op, request, &parent);
  223. } else if (op == QNetworkAccessManager::GetOperation) {
  224. nGET++;
  225. }
  226. return nullptr;
  227. });
  228. // Test 1: NEW file aborted
  229. fakeFolder.localModifier().insert("A/a0", size);
  230. QVERIFY(!fakeFolder.syncOnce()); // error: abort!
  231. // Now the next sync gets a NEW/NEW conflict and since there's no checksum
  232. // it just becomes a UPDATE_METADATA
  233. auto checkEtagUpdated = [&](SyncFileItemVector &items) {
  234. QCOMPARE(items.size(), 1);
  235. QCOMPARE(items[0]->_file, QLatin1String("A"));
  236. SyncJournalFileRecord record;
  237. QVERIFY(fakeFolder.syncJournal().getFileRecord(QByteArray("A/a0"), &record));
  238. QCOMPARE(record._etag, fakeFolder.remoteModifier().find("A/a0")->etag);
  239. };
  240. auto connection = connect(&fakeFolder.syncEngine(), &SyncEngine::aboutToPropagate, checkEtagUpdated);
  241. QVERIFY(fakeFolder.syncOnce());
  242. disconnect(connection);
  243. QCOMPARE(nGET, 0);
  244. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  245. // Test 2: modified file upload aborted
  246. fakeFolder.localModifier().appendByte("A/a0");
  247. QVERIFY(!fakeFolder.syncOnce()); // error: abort!
  248. // An EVAL/EVAL conflict is also UPDATE_METADATA when there's no checksums
  249. connection = connect(&fakeFolder.syncEngine(), &SyncEngine::aboutToPropagate, checkEtagUpdated);
  250. QVERIFY(fakeFolder.syncOnce());
  251. disconnect(connection);
  252. QCOMPARE(nGET, 0);
  253. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  254. // Test 3: modified file upload aborted, with good checksums
  255. fakeFolder.localModifier().appendByte("A/a0");
  256. QVERIFY(!fakeFolder.syncOnce()); // error: abort!
  257. // Set the remote checksum -- the test setup doesn't do it automatically
  258. QVERIFY(!moveChecksumHeader.isEmpty());
  259. fakeFolder.remoteModifier().find("A/a0")->checksums = moveChecksumHeader;
  260. QVERIFY(fakeFolder.syncOnce());
  261. disconnect(connection);
  262. QCOMPARE(nGET, 0); // no new download, just a metadata update!
  263. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  264. // Test 4: New file, that gets deleted locally before the next sync
  265. fakeFolder.localModifier().insert("A/a3", size);
  266. QVERIFY(!fakeFolder.syncOnce()); // error: abort!
  267. fakeFolder.localModifier().remove("A/a3");
  268. // bug: in this case we must expect a re-download of A/A3
  269. QVERIFY(fakeFolder.syncOnce());
  270. QCOMPARE(nGET, 1);
  271. QVERIFY(fakeFolder.currentLocalState().find("A/a3"));
  272. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  273. }
  274. // Check what happens when we abort during the final MOVE and the
  275. // the final MOVE is short enough for the abort-delay to help
  276. void testLateAbortRecoverable()
  277. {
  278. FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
  279. fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ { "chunking", "1.0" } } }, { "checksums", QVariantMap{ { "supportedTypes", QStringList() << "SHA1" } } } });
  280. const int size = 15 * 1000 * 1000; // 15 MB
  281. setChunkSize(fakeFolder.syncEngine(), 1 * 1000 * 1000);
  282. // Make the MOVE never reply, but trigger a client-abort and apply the change remotely
  283. QObject parent;
  284. int responseDelay = 200; // smaller than abort-wait timeout
  285. fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *) -> QNetworkReply * {
  286. if (request.attribute(QNetworkRequest::CustomVerbAttribute) == "MOVE") {
  287. QTimer::singleShot(50, &parent, [&]() { fakeFolder.syncEngine().abort(); });
  288. return new DelayedReply<FakeChunkMoveReply>(responseDelay, fakeFolder.uploadState(), fakeFolder.remoteModifier(), op, request, &parent);
  289. }
  290. return nullptr;
  291. });
  292. // Test 1: NEW file aborted
  293. fakeFolder.localModifier().insert("A/a0", size);
  294. QVERIFY(fakeFolder.syncOnce());
  295. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  296. // Test 2: modified file upload aborted
  297. fakeFolder.localModifier().appendByte("A/a0");
  298. QVERIFY(fakeFolder.syncOnce());
  299. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  300. }
  301. // We modify the file locally after it has been partially uploaded
  302. void testRemoveStale1() {
  303. FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()};
  304. fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ {"chunking", "1.0"} } } });
  305. const int size = 10 * 1000 * 1000; // 10 MB
  306. setChunkSize(fakeFolder.syncEngine(), 1 * 1000 * 1000);
  307. partialUpload(fakeFolder, "A/a0", size);
  308. QCOMPARE(fakeFolder.uploadState().children.count(), 1);
  309. auto chunkingId = fakeFolder.uploadState().children.first().name;
  310. fakeFolder.localModifier().setContents("A/a0", 'B');
  311. fakeFolder.localModifier().appendByte("A/a0");
  312. QVERIFY(fakeFolder.syncOnce());
  313. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  314. QCOMPARE(fakeFolder.currentRemoteState().find("A/a0")->size, size + 1);
  315. // A different chunk id was used, and the previous one is removed
  316. QCOMPARE(fakeFolder.uploadState().children.count(), 1);
  317. QVERIFY(fakeFolder.uploadState().children.first().name != chunkingId);
  318. }
  319. // We remove the file locally after it has been partially uploaded
  320. void testRemoveStale2() {
  321. FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()};
  322. fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ {"chunking", "1.0"} } } });
  323. const int size = 10 * 1000 * 1000; // 10 MB
  324. setChunkSize(fakeFolder.syncEngine(), 1 * 1000 * 1000);
  325. partialUpload(fakeFolder, "A/a0", size);
  326. QCOMPARE(fakeFolder.uploadState().children.count(), 1);
  327. fakeFolder.localModifier().remove("A/a0");
  328. QVERIFY(fakeFolder.syncOnce());
  329. QCOMPARE(fakeFolder.uploadState().children.count(), 0);
  330. }
  331. void testCreateConflictWhileSyncing() {
  332. FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()};
  333. fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ {"chunking", "1.0"} } } });
  334. const int size = 10 * 1000 * 1000; // 10 MB
  335. setChunkSize(fakeFolder.syncEngine(), 1 * 1000 * 1000);
  336. // Put a file on the server and download it.
  337. fakeFolder.remoteModifier().insert("A/a0", size);
  338. QVERIFY(fakeFolder.syncOnce());
  339. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  340. // Modify the file locally and start the upload
  341. fakeFolder.localModifier().setContents("A/a0", 'B');
  342. fakeFolder.localModifier().appendByte("A/a0");
  343. // But in the middle of the sync, modify the file on the server
  344. QMetaObject::Connection con = QObject::connect(&fakeFolder.syncEngine(), &SyncEngine::transmissionProgress,
  345. [&](const ProgressInfo &progress) {
  346. if (progress.completedSize() > (progress.totalSize() / 2 )) {
  347. fakeFolder.remoteModifier().setContents("A/a0", 'C');
  348. QObject::disconnect(con);
  349. }
  350. });
  351. QVERIFY(!fakeFolder.syncOnce());
  352. // There was a precondition failed error, this means wen need to sync again
  353. QCOMPARE(fakeFolder.syncEngine().isAnotherSyncNeeded(), ImmediateFollowUp);
  354. QCOMPARE(fakeFolder.uploadState().children.count(), 1); // We did not clean the chunks at this point
  355. // Now we will download the server file and create a conflict
  356. QVERIFY(fakeFolder.syncOnce());
  357. auto localState = fakeFolder.currentLocalState();
  358. // A0 is the one from the server
  359. QCOMPARE(localState.find("A/a0")->size, size);
  360. QCOMPARE(localState.find("A/a0")->contentChar, 'C');
  361. // There is a conflict file with our version
  362. auto &stateAChildren = localState.find("A")->children;
  363. auto it = std::find_if(stateAChildren.cbegin(), stateAChildren.cend(), [&](const FileInfo &fi) {
  364. return fi.name.startsWith("a0 (conflicted copy");
  365. });
  366. QVERIFY(it != stateAChildren.cend());
  367. QCOMPARE(it->contentChar, 'B');
  368. QCOMPARE(it->size, size+1);
  369. // Remove the conflict file so the comparison works!
  370. fakeFolder.localModifier().remove("A/" + it->name);
  371. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  372. QCOMPARE(fakeFolder.uploadState().children.count(), 0); // The last sync cleaned the chunks
  373. }
  374. void testModifyLocalFileWhileUploading() {
  375. FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()};
  376. fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ {"chunking", "1.0"} } } });
  377. const int size = 10 * 1000 * 1000; // 10 MB
  378. setChunkSize(fakeFolder.syncEngine(), 1 * 1000 * 1000);
  379. fakeFolder.localModifier().insert("A/a0", size);
  380. // middle of the sync, modify the file
  381. QMetaObject::Connection con = QObject::connect(&fakeFolder.syncEngine(), &SyncEngine::transmissionProgress,
  382. [&](const ProgressInfo &progress) {
  383. if (progress.completedSize() > (progress.totalSize() / 2 )) {
  384. fakeFolder.localModifier().setContents("A/a0", 'B');
  385. fakeFolder.localModifier().appendByte("A/a0");
  386. QObject::disconnect(con);
  387. }
  388. });
  389. QVERIFY(!fakeFolder.syncOnce());
  390. // There should be a followup sync
  391. QCOMPARE(fakeFolder.syncEngine().isAnotherSyncNeeded(), ImmediateFollowUp);
  392. QCOMPARE(fakeFolder.uploadState().children.count(), 1); // We did not clean the chunks at this point
  393. auto chunkingId = fakeFolder.uploadState().children.first().name;
  394. // Now we make a new sync which should upload the file for good.
  395. QVERIFY(fakeFolder.syncOnce());
  396. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  397. QCOMPARE(fakeFolder.currentRemoteState().find("A/a0")->size, size+1);
  398. // A different chunk id was used, and the previous one is removed
  399. QCOMPARE(fakeFolder.uploadState().children.count(), 1);
  400. QVERIFY(fakeFolder.uploadState().children.first().name != chunkingId);
  401. }
  402. void testResumeServerDeletedChunks() {
  403. FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()};
  404. fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ {"chunking", "1.0"} } } });
  405. const int size = 30 * 1000 * 1000; // 30 MB
  406. setChunkSize(fakeFolder.syncEngine(), 1 * 1000 * 1000);
  407. partialUpload(fakeFolder, "A/a0", size);
  408. QCOMPARE(fakeFolder.uploadState().children.count(), 1);
  409. auto chunkingId = fakeFolder.uploadState().children.first().name;
  410. // Delete the chunks on the server
  411. fakeFolder.uploadState().children.clear();
  412. QVERIFY(fakeFolder.syncOnce());
  413. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  414. QCOMPARE(fakeFolder.currentRemoteState().find("A/a0")->size, size);
  415. // A different chunk id was used
  416. QCOMPARE(fakeFolder.uploadState().children.count(), 1);
  417. QVERIFY(fakeFolder.uploadState().children.first().name != chunkingId);
  418. }
  419. // Check what happens when the connection is dropped on the PUT (non-chunking) or MOVE (chunking)
  420. // for on the issue #5106
  421. void connectionDroppedBeforeEtagRecieved_data()
  422. {
  423. QTest::addColumn<bool>("chunking");
  424. QTest::newRow("big file") << true;
  425. QTest::newRow("small file") << false;
  426. }
  427. void connectionDroppedBeforeEtagRecieved()
  428. {
  429. QFETCH(bool, chunking);
  430. FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
  431. fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ { "chunking", "1.0" } } }, { "checksums", QVariantMap{ { "supportedTypes", QStringList() << "SHA1" } } } });
  432. const int size = chunking ? 1 * 1000 * 1000 : 300;
  433. setChunkSize(fakeFolder.syncEngine(), 300 * 1000);
  434. // Make the MOVE never reply, but trigger a client-abort and apply the change remotely
  435. QByteArray checksumHeader;
  436. int nGET = 0;
  437. QScopedValueRollback<int> setHttpTimeout(AbstractNetworkJob::httpTimeout, 1);
  438. int responseDelay = AbstractNetworkJob::httpTimeout * 1000 * 1000; // much bigger than http timeout (so a timeout will occur)
  439. // This will perform the operation on the server, but the reply will not come to the client
  440. fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *outgoingData) -> QNetworkReply * {
  441. if (!chunking) {
  442. Q_ASSERT(!request.url().path().contains("/uploads/")
  443. && "Should not touch uploads endpoint when not chunking");
  444. }
  445. if (!chunking && op == QNetworkAccessManager::PutOperation) {
  446. checksumHeader = request.rawHeader("OC-Checksum");
  447. return new DelayedReply<FakePutReply>(responseDelay, fakeFolder.remoteModifier(), op, request, outgoingData->readAll(), &fakeFolder.syncEngine());
  448. } else if (chunking && request.attribute(QNetworkRequest::CustomVerbAttribute) == "MOVE") {
  449. checksumHeader = request.rawHeader("OC-Checksum");
  450. return new DelayedReply<FakeChunkMoveReply>(responseDelay, fakeFolder.uploadState(), fakeFolder.remoteModifier(), op, request, &fakeFolder.syncEngine());
  451. } else if (op == QNetworkAccessManager::GetOperation) {
  452. nGET++;
  453. }
  454. return nullptr;
  455. });
  456. // Test 1: a NEW file
  457. fakeFolder.localModifier().insert("A/a0", size);
  458. QVERIFY(!fakeFolder.syncOnce()); // timeout!
  459. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); // but the upload succeeded
  460. QVERIFY(!checksumHeader.isEmpty());
  461. fakeFolder.remoteModifier().find("A/a0")->checksums = checksumHeader; // The test system don't do that automatically
  462. // Should be resolved properly
  463. QVERIFY(fakeFolder.syncOnce());
  464. QCOMPARE(nGET, 0);
  465. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  466. // Test 2: Modify the file further
  467. fakeFolder.localModifier().appendByte("A/a0");
  468. QVERIFY(!fakeFolder.syncOnce()); // timeout!
  469. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); // but the upload succeeded
  470. fakeFolder.remoteModifier().find("A/a0")->checksums = checksumHeader;
  471. // modify again, should not cause conflict
  472. fakeFolder.localModifier().appendByte("A/a0");
  473. QVERIFY(!fakeFolder.syncOnce()); // now it's trying to upload the modified file
  474. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  475. fakeFolder.remoteModifier().find("A/a0")->checksums = checksumHeader;
  476. QVERIFY(fakeFolder.syncOnce());
  477. QCOMPARE(nGET, 0);
  478. }
  479. void testPercentEncoding() {
  480. QTextCodec::codecForLocale()->setCodecForLocale(QTextCodec::codecForName("UTF-8"));
  481. FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()};
  482. fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ {"chunking", "1.0"} } } });
  483. const int size = 5 * 1000 * 1000;
  484. setChunkSize(fakeFolder.syncEngine(), 1 * 1000 * 1000);
  485. fakeFolder.localModifier().insert("A/file % \u20ac", size);
  486. QVERIFY(fakeFolder.syncOnce());
  487. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  488. // Only the second upload contains an "If" header
  489. fakeFolder.localModifier().appendByte("A/file % \u20ac");
  490. QVERIFY(fakeFolder.syncOnce());
  491. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  492. }
  493. // Test uploading large files (2.5GiB)
  494. void testVeryBigFiles() {
  495. FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()};
  496. fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ {"chunking", "1.0"} } } });
  497. const qint64 size = 2.5 * 1024 * 1024 * 1024; // 2.5 GiB
  498. // Partial upload of big files
  499. partialUpload(fakeFolder, "A/a0", size);
  500. QCOMPARE(fakeFolder.uploadState().children.count(), 1);
  501. auto chunkingId = fakeFolder.uploadState().children.first().name;
  502. // Now resume
  503. QVERIFY(fakeFolder.syncOnce());
  504. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  505. QCOMPARE(fakeFolder.currentRemoteState().find("A/a0")->size, size);
  506. // The same chunk id was re-used
  507. QCOMPARE(fakeFolder.uploadState().children.count(), 1);
  508. QCOMPARE(fakeFolder.uploadState().children.first().name, chunkingId);
  509. // Upload another file again, this time without interruption
  510. fakeFolder.localModifier().appendByte("A/a0");
  511. QVERIFY(fakeFolder.syncOnce());
  512. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  513. QCOMPARE(fakeFolder.currentRemoteState().find("A/a0")->size, size + 1);
  514. }
  515. };
  516. QTEST_GUILESS_MAIN(TestChunkingNG)
  517. #include "testchunkingng.moc"