discovery.cpp 95 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987
  1. /*
  2. * Copyright (C) by Olivier Goffart <ogoffart@woboq.com>
  3. *
  4. * This program is free software; you can redistribute it and/or modify
  5. * it under the terms of the GNU General Public License as published by
  6. * the Free Software Foundation; either version 2 of the License, or
  7. * (at your option) any later version.
  8. *
  9. * This program is distributed in the hope that it will be useful, but
  10. * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
  11. * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
  12. * for more details.
  13. */
  14. #include "account.h"
  15. #include "discovery.h"
  16. #include "common/filesystembase.h"
  17. #include "common/syncjournaldb.h"
  18. #include "filesystem.h"
  19. #include "syncfileitem.h"
  20. #include "progressdispatcher.h"
  21. #include <QDebug>
  22. #include <algorithm>
  23. #include <QEventLoop>
  24. #include <QDir>
  25. #include <set>
  26. #include <QTextCodec>
  27. #include "vio/csync_vio_local.h"
  28. #include <QFileInfo>
  29. #include <QFile>
  30. #include <QThreadPool>
  31. #include <common/checksums.h>
  32. #include <common/constants.h>
  33. #include "csync_exclude.h"
  34. #include "csync.h"
  35. namespace OCC {
  36. Q_LOGGING_CATEGORY(lcDisco, "nextcloud.sync.discovery", QtInfoMsg)
  37. ProcessDirectoryJob::ProcessDirectoryJob(DiscoveryPhase *data, PinState basePinState, qint64 lastSyncTimestamp, QObject *parent)
  38. : QObject(parent)
  39. , _lastSyncTimestamp(lastSyncTimestamp)
  40. , _discoveryData(data)
  41. {
  42. qCDebug(lcDisco) << data;
  43. computePinState(basePinState);
  44. }
  45. ProcessDirectoryJob::ProcessDirectoryJob(const PathTuple &path, const SyncFileItemPtr &dirItem, QueryMode queryLocal, QueryMode queryServer, qint64 lastSyncTimestamp, ProcessDirectoryJob *parent)
  46. : QObject(parent)
  47. , _dirItem(dirItem)
  48. , _lastSyncTimestamp(lastSyncTimestamp)
  49. , _queryServer(queryServer)
  50. , _queryLocal(queryLocal)
  51. , _discoveryData(parent->_discoveryData)
  52. , _currentFolder(path)
  53. {
  54. qCDebug(lcDisco) << path._server << queryServer << path._local << queryLocal << lastSyncTimestamp;
  55. computePinState(parent->_pinState);
  56. }
  57. ProcessDirectoryJob::ProcessDirectoryJob(DiscoveryPhase *data, PinState basePinState, const PathTuple &path, const SyncFileItemPtr &dirItem, QueryMode queryLocal, qint64 lastSyncTimestamp, QObject *parent)
  58. : QObject(parent)
  59. , _dirItem(dirItem)
  60. , _lastSyncTimestamp(lastSyncTimestamp)
  61. , _queryLocal(queryLocal)
  62. , _discoveryData(data)
  63. , _currentFolder(path)
  64. {
  65. computePinState(basePinState);
  66. }
  67. void ProcessDirectoryJob::start()
  68. {
  69. qCInfo(lcDisco) << "STARTING" << _currentFolder._server << _queryServer << _currentFolder._local << _queryLocal;
  70. if (_queryServer == NormalQuery) {
  71. _serverJob = startAsyncServerQuery();
  72. } else {
  73. _serverQueryDone = true;
  74. }
  75. // Check whether a normal local query is even necessary
  76. if (_queryLocal == NormalQuery) {
  77. if (!_discoveryData->_shouldDiscoverLocaly(_currentFolder._local)
  78. && (_currentFolder._local == _currentFolder._original || !_discoveryData->_shouldDiscoverLocaly(_currentFolder._original))
  79. && !_discoveryData->isInSelectiveSyncBlackList(_currentFolder._original)) {
  80. _queryLocal = ParentNotChanged;
  81. qCDebug(lcDisco) << "adjusted discovery policy" << _currentFolder._server << _queryServer << _currentFolder._local << _queryLocal;
  82. }
  83. }
  84. if (_queryLocal == NormalQuery) {
  85. startAsyncLocalQuery();
  86. } else {
  87. _localQueryDone = true;
  88. }
  89. if (_localQueryDone && _serverQueryDone) {
  90. process();
  91. }
  92. }
  93. void ProcessDirectoryJob::process()
  94. {
  95. ASSERT(_localQueryDone && _serverQueryDone);
  96. // Build lookup tables for local, remote and db entries.
  97. // For suffix-virtual files, the key will normally be the base file name
  98. // without the suffix.
  99. // However, if foo and foo.owncloud exists locally, there'll be "foo"
  100. // with local, db, server entries and "foo.owncloud" with only a local
  101. // entry.
  102. std::map<QString, Entries> entries;
  103. for (auto &e : _serverNormalQueryEntries) {
  104. entries[e.name].serverEntry = std::move(e);
  105. }
  106. _serverNormalQueryEntries.clear();
  107. // fetch all the name from the DB
  108. auto pathU8 = _currentFolder._original.toUtf8();
  109. if (!_discoveryData->_statedb->listFilesInPath(pathU8, [&](const SyncJournalFileRecord &rec) {
  110. auto name = pathU8.isEmpty() ? rec._path : QString::fromUtf8(rec._path.constData() + (pathU8.size() + 1));
  111. if (rec.isVirtualFile() && isVfsWithSuffix())
  112. chopVirtualFileSuffix(name);
  113. auto &dbEntry = entries[name].dbEntry;
  114. dbEntry = rec;
  115. setupDbPinStateActions(dbEntry);
  116. })) {
  117. dbError();
  118. return;
  119. }
  120. for (auto &e : _localNormalQueryEntries) {
  121. entries[e.name].localEntry = e;
  122. }
  123. if (isVfsWithSuffix()) {
  124. // For vfs-suffix the local data for suffixed files should usually be associated
  125. // with the non-suffixed name. Unless both names exist locally or there's
  126. // other data about the suffixed file.
  127. // This is done in a second path in order to not depend on the order of
  128. // _localNormalQueryEntries.
  129. for (auto &e : _localNormalQueryEntries) {
  130. if (!e.isVirtualFile)
  131. continue;
  132. auto &suffixedEntry = entries[e.name];
  133. bool hasOtherData = suffixedEntry.serverEntry.isValid() || suffixedEntry.dbEntry.isValid();
  134. auto nonvirtualName = e.name;
  135. chopVirtualFileSuffix(nonvirtualName);
  136. auto &nonvirtualEntry = entries[nonvirtualName];
  137. // If the non-suffixed entry has no data, move it
  138. if (!nonvirtualEntry.localEntry.isValid()) {
  139. std::swap(nonvirtualEntry.localEntry, suffixedEntry.localEntry);
  140. if (!hasOtherData)
  141. entries.erase(e.name);
  142. } else if (!hasOtherData) {
  143. // Normally a lone local suffixed file would be processed under the
  144. // unsuffixed name. In this special case it's under the suffixed name.
  145. // To avoid lots of special casing, make sure PathTuple::addName()
  146. // will be called with the unsuffixed name anyway.
  147. suffixedEntry.nameOverride = nonvirtualName;
  148. }
  149. }
  150. }
  151. _localNormalQueryEntries.clear();
  152. //
  153. // Iterate over entries and process them
  154. //
  155. for (auto &f : entries) {
  156. auto &e = f.second;
  157. PathTuple path;
  158. path = _currentFolder.addName(e.nameOverride.isEmpty() ? f.first : e.nameOverride);
  159. if (!_discoveryData->_listExclusiveFiles.isEmpty() && !_discoveryData->_listExclusiveFiles.contains(path._server)) {
  160. qCInfo(lcDisco) << "Skipping a file:" << path._server << "as it is not listed in the _listExclusiveFiles";
  161. continue;
  162. }
  163. if (isVfsWithSuffix()) {
  164. // Without suffix vfs the paths would be good. But since the dbEntry and localEntry
  165. // can have different names from f.first when suffix vfs is on, make sure the
  166. // corresponding _original and _local paths are right.
  167. if (e.dbEntry.isValid()) {
  168. path._original = e.dbEntry._path;
  169. } else if (e.localEntry.isVirtualFile) {
  170. // We don't have a db entry - but it should be at this path
  171. path._original = PathTuple::pathAppend(_currentFolder._original, e.localEntry.name);
  172. }
  173. if (e.localEntry.isValid()) {
  174. path._local = PathTuple::pathAppend(_currentFolder._local, e.localEntry.name);
  175. } else if (e.dbEntry.isVirtualFile()) {
  176. // We don't have a local entry - but it should be at this path
  177. addVirtualFileSuffix(path._local);
  178. }
  179. }
  180. // On the server the path is mangled in case of E2EE
  181. if (!e.serverEntry.e2eMangledName.isEmpty()) {
  182. Q_ASSERT(_discoveryData->_remoteFolder.startsWith('/'));
  183. Q_ASSERT(_discoveryData->_remoteFolder.endsWith('/'));
  184. const auto rootPath = _discoveryData->_remoteFolder.mid(1);
  185. Q_ASSERT(e.serverEntry.e2eMangledName.startsWith(rootPath));
  186. path._server = e.serverEntry.e2eMangledName.mid(rootPath.length());
  187. }
  188. // If the filename starts with a . we consider it a hidden file
  189. // For windows, the hidden state is also discovered within the vio
  190. // local stat function.
  191. // Recall file shall not be ignored (#4420)
  192. bool isHidden = e.localEntry.isHidden || (!f.first.isEmpty() && f.first[0] == '.' && f.first != QLatin1String(".sys.admin#recall#"));
  193. if (handleExcluded(path._target, e, isHidden))
  194. continue;
  195. const auto isEncryptedFolderButE2eIsNotSetup = e.serverEntry.isValid() && e.serverEntry.isE2eEncrypted() &&
  196. _discoveryData->_account->e2e() && !_discoveryData->_account->e2e()->_publicKey.isNull() && _discoveryData->_account->e2e()->_privateKey.isNull();
  197. if (isEncryptedFolderButE2eIsNotSetup) {
  198. checkAndUpdateSelectiveSyncListsForE2eeFolders(path._server + "/");
  199. }
  200. if (_queryServer == InBlackList || _discoveryData->isInSelectiveSyncBlackList(path._original) || isEncryptedFolderButE2eIsNotSetup) {
  201. processBlacklisted(path, e.localEntry, e.dbEntry);
  202. continue;
  203. }
  204. // HACK: Sometimes the serverEntry.etag does not correctly have its quotation marks amputated in the string.
  205. // We are once again making sure they are chopped off here, but we should really find the root cause for why
  206. // exactly they are not being lobbed off at any of the prior points of processing.
  207. e.serverEntry.etag = Utility::normalizeEtag(e.serverEntry.etag);
  208. processFile(std::move(path), e.localEntry, e.serverEntry, e.dbEntry);
  209. }
  210. _discoveryData->_listExclusiveFiles.clear();
  211. QTimer::singleShot(0, _discoveryData, &DiscoveryPhase::scheduleMoreJobs);
  212. }
  213. bool ProcessDirectoryJob::handleExcluded(const QString &path, const Entries &entries, bool isHidden)
  214. {
  215. const auto isDirectory = entries.localEntry.isDirectory || entries.serverEntry.isDirectory;
  216. auto excluded = _discoveryData->_excludes->traversalPatternMatch(path, isDirectory ? ItemTypeDirectory : ItemTypeFile);
  217. const auto fileName = path.mid(path.lastIndexOf('/') + 1);
  218. if (excluded == CSYNC_NOT_EXCLUDED) {
  219. const auto endsWithSpace = fileName.endsWith(QLatin1Char(' '));
  220. const auto startsWithSpace = fileName.startsWith(QLatin1Char(' '));
  221. if (startsWithSpace && endsWithSpace) {
  222. excluded = CSYNC_FILE_EXCLUDE_LEADING_AND_TRAILING_SPACE;
  223. } else if (endsWithSpace) {
  224. excluded = CSYNC_FILE_EXCLUDE_TRAILING_SPACE;
  225. } else if (startsWithSpace) {
  226. excluded = CSYNC_FILE_EXCLUDE_LEADING_SPACE;
  227. }
  228. }
  229. // we don't need to trigger a warning if trailing/leading space file is already on the server or has already been synced down
  230. // only if the OS supports trailing/leading spaces
  231. const auto wasSyncedAlreadyAndOsSupportsSpaces = !Utility::isWindows() && (entries.serverEntry.isValid() || entries.dbEntry.isValid());
  232. if ((excluded == CSYNC_FILE_EXCLUDE_LEADING_SPACE || excluded == CSYNC_FILE_EXCLUDE_TRAILING_SPACE || excluded == CSYNC_FILE_EXCLUDE_LEADING_AND_TRAILING_SPACE)
  233. && (wasSyncedAlreadyAndOsSupportsSpaces || _discoveryData->_leadingAndTrailingSpacesFilesAllowed.contains(_discoveryData->_localDir + path))) {
  234. excluded = CSYNC_NOT_EXCLUDED;
  235. }
  236. // FIXME: move to ExcludedFiles 's regexp ?
  237. bool isInvalidPattern = false;
  238. if (excluded == CSYNC_NOT_EXCLUDED && !_discoveryData->_invalidFilenameRx.pattern().isEmpty()) {
  239. if (path.contains(_discoveryData->_invalidFilenameRx)) {
  240. excluded = CSYNC_FILE_EXCLUDE_INVALID_CHAR;
  241. isInvalidPattern = true;
  242. }
  243. }
  244. if (excluded == CSYNC_NOT_EXCLUDED && _discoveryData->_ignoreHiddenFiles && isHidden) {
  245. excluded = CSYNC_FILE_EXCLUDE_HIDDEN;
  246. }
  247. const auto &localName = entries.localEntry.name;
  248. if (excluded == CSYNC_NOT_EXCLUDED && !localName.isEmpty()
  249. && _discoveryData->_serverBlacklistedFiles.contains(localName)) {
  250. excluded = CSYNC_FILE_EXCLUDE_SERVER_BLACKLISTED;
  251. isInvalidPattern = true;
  252. }
  253. auto localCodec = QTextCodec::codecForLocale();
  254. if (localCodec->mibEnum() != 106) {
  255. // If the locale codec is not UTF-8, we must check that the filename from the server can
  256. // be encoded in the local file system.
  257. //
  258. // We cannot use QTextCodec::canEncode() since that can incorrectly return true, see
  259. // https://bugreports.qt.io/browse/QTBUG-6925.
  260. QTextEncoder encoder(localCodec, QTextCodec::ConvertInvalidToNull);
  261. if (encoder.fromUnicode(path).contains('\0')) {
  262. qCWarning(lcDisco) << "Cannot encode " << path << " to local encoding " << localCodec->name();
  263. excluded = CSYNC_FILE_EXCLUDE_CANNOT_ENCODE;
  264. }
  265. }
  266. if (excluded == CSYNC_NOT_EXCLUDED && !entries.localEntry.isSymLink) {
  267. return false;
  268. } else if (excluded == CSYNC_FILE_SILENTLY_EXCLUDED || excluded == CSYNC_FILE_EXCLUDE_AND_REMOVE) {
  269. emit _discoveryData->silentlyExcluded(path);
  270. return true;
  271. }
  272. auto item = SyncFileItemPtr::create();
  273. item->_file = path;
  274. item->_originalFile = path;
  275. item->_instruction = CSYNC_INSTRUCTION_IGNORE;
  276. if (entries.localEntry.isSymLink) {
  277. /* Symbolic links are ignored. */
  278. item->_errorString = tr("Symbolic links are not supported in syncing.");
  279. } else {
  280. switch (excluded) {
  281. case CSYNC_NOT_EXCLUDED:
  282. case CSYNC_FILE_SILENTLY_EXCLUDED:
  283. case CSYNC_FILE_EXCLUDE_AND_REMOVE:
  284. qFatal("These were handled earlier");
  285. case CSYNC_FILE_EXCLUDE_LIST:
  286. item->_errorString = tr("File is listed on the ignore list.");
  287. break;
  288. case CSYNC_FILE_EXCLUDE_INVALID_CHAR:
  289. if (item->_file.endsWith('.')) {
  290. item->_errorString = tr("File names ending with a period are not supported on this file system.");
  291. } else {
  292. char invalid = '\0';
  293. foreach (char x, QByteArray("\\:?*\"<>|")) {
  294. if (item->_file.contains(x)) {
  295. invalid = x;
  296. break;
  297. }
  298. }
  299. if (invalid) {
  300. item->_errorString = tr("File names containing the character \"%1\" are not supported on this file system.").arg(QLatin1Char(invalid));
  301. } else if (isInvalidPattern) {
  302. item->_errorString = tr("File name contains at least one invalid character");
  303. } else {
  304. item->_errorString = tr("The file name is a reserved name on this file system.");
  305. }
  306. }
  307. item->_status = SyncFileItem::FileNameInvalid;
  308. break;
  309. case CSYNC_FILE_EXCLUDE_TRAILING_SPACE:
  310. item->_errorString = tr("Filename contains trailing spaces.");
  311. item->_status = SyncFileItem::FileNameInvalid;
  312. break;
  313. case CSYNC_FILE_EXCLUDE_LEADING_SPACE:
  314. item->_errorString = tr("Filename contains leading spaces.");
  315. item->_status = SyncFileItem::FileNameInvalid;
  316. break;
  317. case CSYNC_FILE_EXCLUDE_LEADING_AND_TRAILING_SPACE:
  318. item->_errorString = tr("Filename contains leading and trailing spaces.");
  319. item->_status = SyncFileItem::FileNameInvalid;
  320. break;
  321. case CSYNC_FILE_EXCLUDE_LONG_FILENAME:
  322. item->_errorString = tr("Filename is too long.");
  323. item->_status = SyncFileItem::FileNameInvalid;
  324. break;
  325. case CSYNC_FILE_EXCLUDE_HIDDEN:
  326. item->_errorString = tr("File/Folder is ignored because it's hidden.");
  327. break;
  328. case CSYNC_FILE_EXCLUDE_STAT_FAILED:
  329. item->_errorString = tr("Stat failed.");
  330. break;
  331. case CSYNC_FILE_EXCLUDE_CONFLICT:
  332. item->_errorString = tr("Conflict: Server version downloaded, local copy renamed and not uploaded.");
  333. item->_status = SyncFileItem::Conflict;
  334. break;
  335. case CSYNC_FILE_EXCLUDE_CASE_CLASH_CONFLICT:
  336. item->_errorString = tr("Case Clash Conflict: Server file downloaded and renamed to avoid clash.");
  337. item->_status = SyncFileItem::FileNameClash;
  338. break;
  339. case CSYNC_FILE_EXCLUDE_CANNOT_ENCODE:
  340. item->_errorString = tr("The filename cannot be encoded on your file system.");
  341. break;
  342. case CSYNC_FILE_EXCLUDE_SERVER_BLACKLISTED:
  343. item->_errorString = tr("The filename is blacklisted on the server.");
  344. break;
  345. }
  346. }
  347. _childIgnored = true;
  348. emit _discoveryData->itemDiscovered(item);
  349. return true;
  350. }
  351. void ProcessDirectoryJob::checkAndUpdateSelectiveSyncListsForE2eeFolders(const QString &path)
  352. {
  353. bool ok = false;
  354. const auto pathWithTrailingSpace = path.endsWith(QLatin1Char('/')) ? path : path + QLatin1Char('/');
  355. auto blackListSet = _discoveryData->_statedb->getSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, &ok).toSet();
  356. blackListSet.insert(pathWithTrailingSpace);
  357. auto blackList = blackListSet.values();
  358. blackList.sort();
  359. _discoveryData->_statedb->setSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, blackList);
  360. auto toRemoveFromBlacklistSet = _discoveryData->_statedb->getSelectiveSyncList(SyncJournalDb::SelectiveSyncE2eFoldersToRemoveFromBlacklist, &ok).toSet();
  361. toRemoveFromBlacklistSet.insert(pathWithTrailingSpace);
  362. // record it into a separate list to automatically remove from blacklist once the e2EE gets set up
  363. auto toRemoveFromBlacklist = toRemoveFromBlacklistSet.values();
  364. toRemoveFromBlacklist.sort();
  365. _discoveryData->_statedb->setSelectiveSyncList(SyncJournalDb::SelectiveSyncE2eFoldersToRemoveFromBlacklist, toRemoveFromBlacklist);
  366. }
  367. void ProcessDirectoryJob::processFile(PathTuple path,
  368. const LocalInfo &localEntry, const RemoteInfo &serverEntry,
  369. const SyncJournalFileRecord &dbEntry)
  370. {
  371. const char *hasServer = serverEntry.isValid() ? "true" : _queryServer == ParentNotChanged ? "db" : "false";
  372. const char *hasLocal = localEntry.isValid() ? "true" : _queryLocal == ParentNotChanged ? "db" : "false";
  373. const auto serverFileIsLocked = (serverEntry.isValid() ? (serverEntry.locked == SyncFileItem::LockStatus::LockedItem ? "locked" : "not locked") : "");
  374. const auto localFileIsLocked = dbEntry._lockstate._locked ? "locked" : "not locked";
  375. qCDebug(lcDisco).nospace() << "Processing " << path._original
  376. << " | (db/local/remote)"
  377. << " | valid: " << dbEntry.isValid() << "/" << hasLocal << "/" << hasServer
  378. << " | mtime: " << dbEntry._modtime << "/" << localEntry.modtime << "/" << serverEntry.modtime
  379. << " | size: " << dbEntry._fileSize << "/" << localEntry.size << "/" << serverEntry.size
  380. << " | etag: " << dbEntry._etag << "//" << serverEntry.etag
  381. << " | checksum: " << dbEntry._checksumHeader << "//" << serverEntry.checksumHeader
  382. << " | perm: " << dbEntry._remotePerm << "//" << serverEntry.remotePerm
  383. << " | fileid: " << dbEntry._fileId << "//" << serverEntry.fileId
  384. << " | inode: " << dbEntry._inode << "/" << localEntry.inode << "/"
  385. << " | type: " << dbEntry._type << "/" << localEntry.type << "/" << (serverEntry.isDirectory ? ItemTypeDirectory : ItemTypeFile)
  386. << " | e2ee: " << dbEntry.isE2eEncrypted() << "/" << serverEntry.isE2eEncrypted()
  387. << " | e2eeMangledName: " << dbEntry.e2eMangledName() << "/" << serverEntry.e2eMangledName
  388. << " | file lock: " << localFileIsLocked << "//" << serverFileIsLocked;
  389. if (localEntry.isValid()
  390. && !serverEntry.isValid()
  391. && !dbEntry.isValid()
  392. && localEntry.modtime < _lastSyncTimestamp) {
  393. qCWarning(lcDisco) << "File" << path._original << "was modified before the last sync run and is not in the sync journal and server";
  394. }
  395. if (_discoveryData->isRenamed(path._original)) {
  396. qCDebug(lcDisco) << "Ignoring renamed";
  397. return; // Ignore this.
  398. }
  399. auto item = SyncFileItem::fromSyncJournalFileRecord(dbEntry);
  400. item->_file = path._target;
  401. item->_originalFile = path._original;
  402. item->_previousSize = dbEntry._fileSize;
  403. item->_previousModtime = dbEntry._modtime;
  404. if (dbEntry._modtime == localEntry.modtime && dbEntry._type == ItemTypeVirtualFile && localEntry.type == ItemTypeFile) {
  405. item->_type = ItemTypeFile;
  406. }
  407. // The item shall only have this type if the db request for the virtual download
  408. // was successful (like: no conflicting remote remove etc). This decision is done
  409. // either in processFileAnalyzeRemoteInfo() or further down here.
  410. if (item->_type == ItemTypeVirtualFileDownload)
  411. item->_type = ItemTypeVirtualFile;
  412. // Similarly db entries with a dehydration request denote a regular file
  413. // until the request is processed.
  414. if (item->_type == ItemTypeVirtualFileDehydration)
  415. item->_type = ItemTypeFile;
  416. // VFS suffixed files on the server are ignored
  417. if (isVfsWithSuffix()) {
  418. if (hasVirtualFileSuffix(serverEntry.name)
  419. || (localEntry.isVirtualFile && !dbEntry.isVirtualFile() && hasVirtualFileSuffix(dbEntry._path))) {
  420. item->_instruction = CSYNC_INSTRUCTION_IGNORE;
  421. item->_errorString = tr("File has extension reserved for virtual files.");
  422. _childIgnored = true;
  423. emit _discoveryData->itemDiscovered(item);
  424. return;
  425. }
  426. }
  427. if (serverEntry.isValid()) {
  428. processFileAnalyzeRemoteInfo(item, path, localEntry, serverEntry, dbEntry);
  429. return;
  430. }
  431. // Downloading a virtual file is like a server action and can happen even if
  432. // server-side nothing has changed
  433. // NOTE: Normally setting the VirtualFileDownload flag means that local and
  434. // remote will be rediscovered. This is just a fallback for a similar check
  435. // in processFileAnalyzeRemoteInfo().
  436. if (_queryServer == ParentNotChanged
  437. && dbEntry.isValid()
  438. && (dbEntry._type == ItemTypeVirtualFileDownload
  439. || localEntry.type == ItemTypeVirtualFileDownload)
  440. && (localEntry.isValid() || _queryLocal == ParentNotChanged)) {
  441. item->_direction = SyncFileItem::Down;
  442. item->_instruction = CSYNC_INSTRUCTION_SYNC;
  443. item->_type = ItemTypeVirtualFileDownload;
  444. }
  445. processFileAnalyzeLocalInfo(item, path, localEntry, serverEntry, dbEntry, _queryServer);
  446. }
  447. // Compute the checksum of the given file and assign the result in item->_checksumHeader
  448. // Returns true if the checksum was successfully computed
  449. static bool computeLocalChecksum(const QByteArray &header, const QString &path, const SyncFileItemPtr &item)
  450. {
  451. auto type = parseChecksumHeaderType(header);
  452. if (!type.isEmpty()) {
  453. // TODO: compute async?
  454. QByteArray checksum = ComputeChecksum::computeNowOnFile(path, type);
  455. if (!checksum.isEmpty()) {
  456. item->_checksumHeader = makeChecksumHeader(type, checksum);
  457. return true;
  458. }
  459. }
  460. return false;
  461. }
  462. void ProcessDirectoryJob::processFileAnalyzeRemoteInfo(
  463. const SyncFileItemPtr &item, PathTuple path, const LocalInfo &localEntry,
  464. const RemoteInfo &serverEntry, const SyncJournalFileRecord &dbEntry)
  465. {
  466. item->_checksumHeader = serverEntry.checksumHeader;
  467. item->_fileId = serverEntry.fileId;
  468. item->_remotePerm = serverEntry.remotePerm;
  469. item->_isShared = serverEntry.remotePerm.hasPermission(RemotePermissions::IsShared) || serverEntry.sharedByMe;
  470. item->_sharedByMe = serverEntry.sharedByMe;
  471. item->_lastShareStateFetchedTimestamp = QDateTime::currentMSecsSinceEpoch();
  472. item->_type = serverEntry.isDirectory ? ItemTypeDirectory : ItemTypeFile;
  473. item->_etag = serverEntry.etag;
  474. item->_directDownloadUrl = serverEntry.directDownloadUrl;
  475. item->_directDownloadCookies = serverEntry.directDownloadCookies;
  476. item->_e2eEncryptionStatus = serverEntry.isE2eEncrypted() ? SyncFileItem::EncryptionStatus::Encrypted : SyncFileItem::EncryptionStatus::NotEncrypted;
  477. item->_encryptedFileName = [=] {
  478. if (serverEntry.e2eMangledName.isEmpty()) {
  479. return QString();
  480. }
  481. Q_ASSERT(_discoveryData->_remoteFolder.startsWith('/'));
  482. Q_ASSERT(_discoveryData->_remoteFolder.endsWith('/'));
  483. const auto rootPath = _discoveryData->_remoteFolder.mid(1);
  484. Q_ASSERT(serverEntry.e2eMangledName.startsWith(rootPath));
  485. return serverEntry.e2eMangledName.mid(rootPath.length());
  486. }();
  487. item->_locked = serverEntry.locked;
  488. item->_lockOwnerDisplayName = serverEntry.lockOwnerDisplayName;
  489. item->_lockOwnerId = serverEntry.lockOwnerId;
  490. item->_lockOwnerType = serverEntry.lockOwnerType;
  491. item->_lockEditorApp = serverEntry.lockEditorApp;
  492. item->_lockTime = serverEntry.lockTime;
  493. item->_lockTimeout = serverEntry.lockTimeout;
  494. qCInfo(lcDisco()) << item->_locked << item->_lockOwnerDisplayName << item->_lockOwnerId << item->_lockOwnerType << item->_lockEditorApp << item->_lockTime << item->_lockTimeout;
  495. // Check for missing server data
  496. {
  497. QStringList missingData;
  498. if (serverEntry.size == -1)
  499. missingData.append(tr("size"));
  500. if (serverEntry.remotePerm.isNull())
  501. missingData.append(tr("permission"));
  502. if (serverEntry.etag.isEmpty())
  503. missingData.append("ETag");
  504. if (serverEntry.fileId.isEmpty())
  505. missingData.append(tr("file id"));
  506. if (!missingData.isEmpty()) {
  507. item->_instruction = CSYNC_INSTRUCTION_ERROR;
  508. _childIgnored = true;
  509. item->_errorString = tr("Server reported no %1").arg(missingData.join(QLatin1String(", ")));
  510. emit _discoveryData->itemDiscovered(item);
  511. return;
  512. }
  513. }
  514. // We want to check the lock state of this file after the lock time has expired
  515. if(serverEntry.locked == SyncFileItem::LockStatus::LockedItem) {
  516. const auto lockExpirationTime = serverEntry.lockTime + serverEntry.lockTimeout;
  517. const auto timeRemaining = QDateTime::currentDateTime().secsTo(QDateTime::fromSecsSinceEpoch(lockExpirationTime));
  518. // Add on a second as a precaution, sometimes we catch the server before it has had a chance to update
  519. const auto lockExpirationTimeout = qMax(5LL, timeRemaining + 1);
  520. qCInfo(lcDisco) << "File:" << path._original << "is locked."
  521. << "Lock expires in:" << lockExpirationTimeout << "seconds."
  522. << "A sync run will be scheduled for around that time.";
  523. _discoveryData->_anotherSyncNeeded = true;
  524. _discoveryData->_filesNeedingScheduledSync.insert(path._original, lockExpirationTimeout);
  525. } else if (serverEntry.locked == SyncFileItem::LockStatus::UnlockedItem && dbEntry._lockstate._locked) {
  526. // We have received data that this file has been unlocked remotely, so let's notify the sync engine
  527. // that we no longer need a scheduled sync run for this file
  528. qCInfo(lcDisco) << "File:" << path._original << "is unlocked and a scheduled sync is no longer needed."
  529. << "Will remove scheduled sync if there is one.";
  530. _discoveryData->_filesUnscheduleSync.append(path._original);
  531. }
  532. // The file is known in the db already
  533. if (dbEntry.isValid()) {
  534. const bool isDbEntryAnE2EePlaceholder = dbEntry.isVirtualFile() && !dbEntry.e2eMangledName().isEmpty();
  535. Q_ASSERT(!isDbEntryAnE2EePlaceholder || serverEntry.size >= Constants::e2EeTagSize);
  536. const bool isVirtualE2EePlaceholder = isDbEntryAnE2EePlaceholder && serverEntry.size >= Constants::e2EeTagSize;
  537. const qint64 sizeOnServer = isVirtualE2EePlaceholder ? serverEntry.size - Constants::e2EeTagSize : serverEntry.size;
  538. const bool metaDataSizeNeedsUpdateForE2EeFilePlaceholder = isVirtualE2EePlaceholder && dbEntry._fileSize == serverEntry.size;
  539. if (serverEntry.isDirectory != dbEntry.isDirectory()) {
  540. // If the type of the entity changed, it's like NEW, but
  541. // needs to delete the other entity first.
  542. item->_instruction = CSYNC_INSTRUCTION_TYPE_CHANGE;
  543. item->_direction = SyncFileItem::Down;
  544. item->_modtime = serverEntry.modtime;
  545. item->_size = sizeOnServer;
  546. } else if ((dbEntry._type == ItemTypeVirtualFileDownload || localEntry.type == ItemTypeVirtualFileDownload)
  547. && (localEntry.isValid() || _queryLocal == ParentNotChanged)) {
  548. // The above check for the localEntry existing is important. Otherwise it breaks
  549. // the case where a file is moved and simultaneously tagged for download in the db.
  550. item->_direction = SyncFileItem::Down;
  551. item->_instruction = CSYNC_INSTRUCTION_SYNC;
  552. item->_type = ItemTypeVirtualFileDownload;
  553. } else if (dbEntry._etag != serverEntry.etag) {
  554. item->_direction = SyncFileItem::Down;
  555. item->_modtime = serverEntry.modtime;
  556. item->_size = sizeOnServer;
  557. if (serverEntry.isDirectory) {
  558. ENFORCE(dbEntry.isDirectory());
  559. item->_instruction = CSYNC_INSTRUCTION_UPDATE_METADATA;
  560. } else if (!localEntry.isValid() && _queryLocal != ParentNotChanged) {
  561. // Deleted locally, changed on server
  562. item->_instruction = CSYNC_INSTRUCTION_NEW;
  563. } else {
  564. item->_instruction = CSYNC_INSTRUCTION_SYNC;
  565. qCDebug(lcDisco) << "CSYNC_INSTRUCTION_SYNC: File" << item->_file << "if (dbEntry._etag != serverEntry.etag)"
  566. << "dbEntry._etag:" << dbEntry._etag
  567. << "serverEntry.etag:" << serverEntry.etag
  568. << "serverEntry.isDirectory:" << serverEntry.isDirectory
  569. << "dbEntry.isDirectory:" << dbEntry.isDirectory();
  570. }
  571. } else if (dbEntry._modtime != serverEntry.modtime && localEntry.size == serverEntry.size && dbEntry._fileSize == serverEntry.size && dbEntry._etag == serverEntry.etag) {
  572. item->_direction = SyncFileItem::Down;
  573. item->_modtime = serverEntry.modtime;
  574. item->_size = sizeOnServer;
  575. item->_instruction = CSYNC_INSTRUCTION_UPDATE_METADATA;
  576. } else if (dbEntry._remotePerm != serverEntry.remotePerm || dbEntry._fileId != serverEntry.fileId || metaDataSizeNeedsUpdateForE2EeFilePlaceholder) {
  577. if (metaDataSizeNeedsUpdateForE2EeFilePlaceholder) {
  578. // we are updating placeholder sizes after migrating from older versions with VFS + E2EE implicit hydration not supported
  579. qCDebug(lcDisco) << "Migrating the E2EE VFS placeholder " << dbEntry.path() << " from older version. The old size is " << item->_size << ". The new size is " << sizeOnServer;
  580. item->_size = sizeOnServer;
  581. }
  582. item->_instruction = CSYNC_INSTRUCTION_UPDATE_METADATA;
  583. item->_direction = SyncFileItem::Down;
  584. } else {
  585. // if (is virtual mode enabled and folder is encrypted - check if the size is the same as on the server and then - trigger server query
  586. // to update a placeholder with corrected size (-16 Bytes)
  587. // or, maybe, add a flag to the database - vfsE2eeSizeCorrected? if it is not set - subtract it from the placeholder's size and re-create/update a placeholder?
  588. const QueryMode serverQueryMode = [this, &dbEntry, &serverEntry]() {
  589. const bool isVfsModeOn = _discoveryData && _discoveryData->_syncOptions._vfs && _discoveryData->_syncOptions._vfs->mode() != Vfs::Off;
  590. if (isVfsModeOn && dbEntry.isDirectory() && dbEntry.isE2eEncrypted()) {
  591. qint64 localFolderSize = 0;
  592. const auto listFilesCallback = [&localFolderSize](const OCC::SyncJournalFileRecord &record) {
  593. if (record.isFile()) {
  594. // add Constants::e2EeTagSize so we will know the size of E2EE file on the server
  595. localFolderSize += record._fileSize + Constants::e2EeTagSize;
  596. } else if (record.isVirtualFile()) {
  597. // just a virtual file, so, the size must contain Constants::e2EeTagSize if it was not corrected already
  598. localFolderSize += record._fileSize;
  599. }
  600. };
  601. const bool listFilesSucceeded = _discoveryData->_statedb->listFilesInPath(dbEntry.path().toUtf8(), listFilesCallback);
  602. if (listFilesSucceeded && localFolderSize != 0 && localFolderSize == serverEntry.sizeOfFolder) {
  603. qCInfo(lcDisco) << "Migration of E2EE folder " << dbEntry.path() << " from older version to the one, supporting the implicit VFS hydration.";
  604. return NormalQuery;
  605. }
  606. }
  607. return ParentNotChanged;
  608. }();
  609. processFileAnalyzeLocalInfo(item, path, localEntry, serverEntry, dbEntry, serverQueryMode);
  610. return;
  611. }
  612. processFileAnalyzeLocalInfo(item, path, localEntry, serverEntry, dbEntry, _queryServer);
  613. return;
  614. }
  615. // Unknown in db: new file on the server
  616. Q_ASSERT(!dbEntry.isValid());
  617. item->_instruction = CSYNC_INSTRUCTION_NEW;
  618. item->_direction = SyncFileItem::Down;
  619. item->_modtime = serverEntry.modtime;
  620. item->_size = serverEntry.size;
  621. auto conflictRecord = _discoveryData->_statedb->caseConflictRecordByBasePath(item->_file);
  622. if (conflictRecord.isValid() && QString::fromUtf8(conflictRecord.path).contains(QStringLiteral("(case clash from"))) {
  623. qCInfo(lcDisco) << "should ignore" << item->_file << "has already a case clash conflict record" << conflictRecord.path;
  624. item->_instruction = CSYNC_INSTRUCTION_IGNORE;
  625. return;
  626. }
  627. auto postProcessServerNew = [=]() mutable {
  628. if (item->isDirectory()) {
  629. _pendingAsyncJobs++;
  630. _discoveryData->checkSelectiveSyncNewFolder(path._server, serverEntry.remotePerm,
  631. [=](bool result) {
  632. --_pendingAsyncJobs;
  633. if (!result) {
  634. processFileAnalyzeLocalInfo(item, path, localEntry, serverEntry, dbEntry, _queryServer);
  635. }
  636. QTimer::singleShot(0, _discoveryData, &DiscoveryPhase::scheduleMoreJobs);
  637. });
  638. return;
  639. }
  640. // Turn new remote files into virtual files if the option is enabled.
  641. auto &opts = _discoveryData->_syncOptions;
  642. if (!localEntry.isValid()
  643. && item->_type == ItemTypeFile
  644. && opts._vfs->mode() != Vfs::Off
  645. && !FileSystem::isLnkFile(item->_file)
  646. && _pinState != PinState::AlwaysLocal
  647. && !FileSystem::isExcludeFile(item->_file)) {
  648. item->_type = ItemTypeVirtualFile;
  649. if (isVfsWithSuffix())
  650. addVirtualFileSuffix(path._original);
  651. }
  652. if (opts._vfs->mode() != Vfs::Off && !item->_encryptedFileName.isEmpty()) {
  653. // We are syncing a file for the first time (local entry is invalid) and it is encrypted file that will be virtual once synced
  654. // to avoid having error of "file has changed during sync" when trying to hydrate it excplicitly - we must remove Constants::e2EeTagSize bytes from the end
  655. // as explicit hydration does not care if these bytes are present in the placeholder or not, but, the size must not change in the middle of the sync
  656. // this way it works for both implicit and explicit hydration by making a placeholder size that does not includes encryption tag Constants::e2EeTagSize bytes
  657. // another scenario - we are syncing a file which is on disk but not in the database (database was removed or file was not written there yet)
  658. item->_size = serverEntry.size - Constants::e2EeTagSize;
  659. }
  660. processFileAnalyzeLocalInfo(item, path, localEntry, serverEntry, dbEntry, _queryServer);
  661. };
  662. // Potential NEW/NEW conflict is handled in AnalyzeLocal
  663. if (localEntry.isValid()) {
  664. postProcessServerNew();
  665. return;
  666. }
  667. // Not in db or locally: either new or a rename
  668. Q_ASSERT(!dbEntry.isValid() && !localEntry.isValid());
  669. // Check for renames (if there is a file with the same file id)
  670. bool done = false;
  671. bool async = false;
  672. // This function will be executed for every candidate
  673. auto renameCandidateProcessing = [&](const OCC::SyncJournalFileRecord &base) {
  674. if (done)
  675. return;
  676. if (!base.isValid())
  677. return;
  678. // Remote rename of a virtual file we have locally scheduled for download.
  679. if (base._type == ItemTypeVirtualFileDownload) {
  680. // We just consider this NEW but mark it for download.
  681. item->_type = ItemTypeVirtualFileDownload;
  682. done = true;
  683. return;
  684. }
  685. // Remote rename targets a file that shall be locally dehydrated.
  686. if (base._type == ItemTypeVirtualFileDehydration) {
  687. // Don't worry about the rename, just consider it DELETE + NEW(virtual)
  688. done = true;
  689. return;
  690. }
  691. // Some things prohibit rename detection entirely.
  692. // Since we don't do the same checks again in reconcile, we can't
  693. // just skip the candidate, but have to give up completely.
  694. if (base.isDirectory() != item->isDirectory()) {
  695. qCInfo(lcDisco, "file types different, not a rename");
  696. done = true;
  697. return;
  698. }
  699. if (!serverEntry.isDirectory && base._etag != serverEntry.etag) {
  700. /* File with different etag, don't do a rename, but download the file again */
  701. qCInfo(lcDisco, "file etag different, not a rename");
  702. done = true;
  703. return;
  704. }
  705. // Now we know there is a sane rename candidate.
  706. QString originalPath = base.path();
  707. if (_discoveryData->isRenamed(originalPath)) {
  708. qCInfo(lcDisco, "folder already has a rename entry, skipping");
  709. return;
  710. }
  711. /* A remote rename can also mean Encryption Mangled Name.
  712. * if we find one of those in the database, we ignore it.
  713. */
  714. if (!base._e2eMangledName.isEmpty()) {
  715. qCWarning(lcDisco, "Encrypted file can not rename");
  716. done = true;
  717. return;
  718. }
  719. QString originalPathAdjusted = _discoveryData->adjustRenamedPath(originalPath, SyncFileItem::Up);
  720. if (!base.isDirectory()) {
  721. csync_file_stat_t buf;
  722. if (csync_vio_local_stat(_discoveryData->_localDir + originalPathAdjusted, &buf)) {
  723. qCInfo(lcDisco) << "Local file does not exist anymore." << originalPathAdjusted;
  724. return;
  725. }
  726. // NOTE: This prohibits some VFS renames from being detected since
  727. // suffix-file size is different from the db size. That's ok, they'll DELETE+NEW.
  728. if (buf.modtime != base._modtime || buf.size != base._fileSize || buf.type == ItemTypeDirectory) {
  729. qCInfo(lcDisco) << "File has changed locally, not a rename." << originalPath;
  730. return;
  731. }
  732. } else {
  733. if (!QFileInfo(_discoveryData->_localDir + originalPathAdjusted).isDir()) {
  734. qCInfo(lcDisco) << "Local directory does not exist anymore." << originalPathAdjusted;
  735. return;
  736. }
  737. }
  738. // Renames of virtuals are possible
  739. if (base.isVirtualFile()) {
  740. item->_type = ItemTypeVirtualFile;
  741. }
  742. bool wasDeletedOnServer = _discoveryData->findAndCancelDeletedJob(originalPath).first;
  743. auto postProcessRename = [this, item, base, originalPath](PathTuple &path) {
  744. auto adjustedOriginalPath = _discoveryData->adjustRenamedPath(originalPath, SyncFileItem::Up);
  745. _discoveryData->_renamedItemsRemote.insert(originalPath, path._target);
  746. item->_modtime = base._modtime;
  747. item->_inode = base._inode;
  748. item->_instruction = CSYNC_INSTRUCTION_RENAME;
  749. item->_direction = SyncFileItem::Down;
  750. item->_renameTarget = path._target;
  751. item->_file = adjustedOriginalPath;
  752. item->_originalFile = originalPath;
  753. path._original = originalPath;
  754. path._local = adjustedOriginalPath;
  755. qCInfo(lcDisco) << "Rename detected (down) " << item->_file << " -> " << item->_renameTarget;
  756. };
  757. if (wasDeletedOnServer) {
  758. postProcessRename(path);
  759. done = true;
  760. } else {
  761. // we need to make a request to the server to know that the original file is deleted on the server
  762. _pendingAsyncJobs++;
  763. auto job = new RequestEtagJob(_discoveryData->_account, _discoveryData->_remoteFolder + originalPath, this);
  764. connect(job, &RequestEtagJob::finishedWithResult, this, [=](const HttpResult<QByteArray> &etag) mutable {
  765. _pendingAsyncJobs--;
  766. QTimer::singleShot(0, _discoveryData, &DiscoveryPhase::scheduleMoreJobs);
  767. if (etag || etag.error().code != 404 ||
  768. // Somehow another item claimed this original path, consider as if it existed
  769. _discoveryData->isRenamed(originalPath)) {
  770. // If the file exist or if there is another error, consider it is a new file.
  771. postProcessServerNew();
  772. return;
  773. }
  774. // The file do not exist, it is a rename
  775. // In case the deleted item was discovered in parallel
  776. _discoveryData->findAndCancelDeletedJob(originalPath);
  777. postProcessRename(path);
  778. processFileFinalize(item, path, item->isDirectory(), item->_instruction == CSYNC_INSTRUCTION_RENAME ? NormalQuery : ParentDontExist, _queryServer);
  779. });
  780. job->start();
  781. done = true; // Ideally, if the origin still exist on the server, we should continue searching... but that'd be difficult
  782. async = true;
  783. }
  784. };
  785. if (!_discoveryData->_statedb->getFileRecordsByFileId(serverEntry.fileId, renameCandidateProcessing)) {
  786. dbError();
  787. return;
  788. }
  789. if (async) {
  790. return; // We went async
  791. }
  792. if (item->_instruction == CSYNC_INSTRUCTION_NEW) {
  793. postProcessServerNew();
  794. return;
  795. }
  796. processFileAnalyzeLocalInfo(item, path, localEntry, serverEntry, dbEntry, _queryServer);
  797. }
  798. void ProcessDirectoryJob::processFileAnalyzeLocalInfo(
  799. const SyncFileItemPtr &item, PathTuple path, const LocalInfo &localEntry,
  800. const RemoteInfo &serverEntry, const SyncJournalFileRecord &dbEntry, QueryMode recurseQueryServer)
  801. {
  802. bool noServerEntry = (_queryServer != ParentNotChanged && !serverEntry.isValid())
  803. || (_queryServer == ParentNotChanged && !dbEntry.isValid());
  804. if (noServerEntry)
  805. recurseQueryServer = ParentDontExist;
  806. bool serverModified = item->_instruction == CSYNC_INSTRUCTION_NEW || item->_instruction == CSYNC_INSTRUCTION_SYNC
  807. || item->_instruction == CSYNC_INSTRUCTION_RENAME || item->_instruction == CSYNC_INSTRUCTION_TYPE_CHANGE;
  808. qCDebug(lcDisco) << "File" << item->_file << "- servermodified:" << serverModified
  809. << "noServerEntry:" << noServerEntry;
  810. // Decay server modifications to UPDATE_METADATA if the local virtual exists
  811. bool hasLocalVirtual = localEntry.isVirtualFile || (_queryLocal == ParentNotChanged && dbEntry.isVirtualFile());
  812. bool virtualFileDownload = item->_type == ItemTypeVirtualFileDownload;
  813. if (serverModified && !virtualFileDownload && hasLocalVirtual) {
  814. item->_instruction = CSYNC_INSTRUCTION_UPDATE_METADATA;
  815. serverModified = false;
  816. item->_type = ItemTypeVirtualFile;
  817. }
  818. if (dbEntry.isVirtualFile() && (!localEntry.isValid() || localEntry.isVirtualFile) && !virtualFileDownload) {
  819. item->_type = ItemTypeVirtualFile;
  820. }
  821. _childModified |= serverModified;
  822. auto finalize = [&] {
  823. bool recurse = item->isDirectory() || localEntry.isDirectory || serverEntry.isDirectory;
  824. // Even if we have a local directory: If the remote is a file that's propagated as a
  825. // conflict we don't need to recurse into it. (local c1.owncloud, c1/ ; remote: c1)
  826. if (item->_instruction == CSYNC_INSTRUCTION_CONFLICT && !item->isDirectory())
  827. recurse = false;
  828. if (_queryLocal != NormalQuery && _queryServer != NormalQuery)
  829. recurse = false;
  830. if ((item->_direction == SyncFileItem::Down || item->_instruction == CSYNC_INSTRUCTION_CONFLICT || item->_instruction == CSYNC_INSTRUCTION_NEW || item->_instruction == CSYNC_INSTRUCTION_SYNC) &&
  831. (item->_modtime <= 0 || item->_modtime >= 0xFFFFFFFF)) {
  832. item->_instruction = CSYNC_INSTRUCTION_ERROR;
  833. item->_errorString = tr("Cannot sync due to invalid modification time");
  834. item->_status = SyncFileItem::Status::NormalError;
  835. }
  836. auto recurseQueryLocal = _queryLocal == ParentNotChanged ? ParentNotChanged : localEntry.isDirectory || item->_instruction == CSYNC_INSTRUCTION_RENAME ? NormalQuery : ParentDontExist;
  837. processFileFinalize(item, path, recurse, recurseQueryLocal, recurseQueryServer);
  838. };
  839. if (!localEntry.isValid()) {
  840. if (_queryLocal == ParentNotChanged && dbEntry.isValid()) {
  841. // Not modified locally (ParentNotChanged)
  842. if (noServerEntry) {
  843. // not on the server: Removed on the server, delete locally
  844. #if !defined QT_NO_DEBUG
  845. qCInfo(lcDisco) << "File" << item->_file << "is not anymore on server. Going to delete it locally.";
  846. #endif
  847. item->_instruction = CSYNC_INSTRUCTION_REMOVE;
  848. item->_direction = SyncFileItem::Down;
  849. } else if (dbEntry._type == ItemTypeVirtualFileDehydration) {
  850. // dehydration requested
  851. item->_direction = SyncFileItem::Down;
  852. item->_instruction = CSYNC_INSTRUCTION_SYNC;
  853. item->_type = ItemTypeVirtualFileDehydration;
  854. }
  855. } else if (noServerEntry) {
  856. // Not locally, not on the server. The entry is stale!
  857. qCInfo(lcDisco) << "Stale DB entry";
  858. if (!_discoveryData->_statedb->deleteFileRecord(path._original, true)) {
  859. emit _discoveryData->fatalError(tr("Error while deleting file record %1 from the database").arg(path._original), ErrorCategory::GenericError);
  860. qCWarning(lcDisco) << "Failed to delete a file record from the local DB" << path._original;
  861. }
  862. return;
  863. } else if (dbEntry._type == ItemTypeVirtualFile && isVfsWithSuffix()) {
  864. // If the virtual file is removed, recreate it.
  865. // This is a precaution since the suffix files don't look like the real ones
  866. // and we don't want users to accidentally delete server data because they
  867. // might not expect that deleting the placeholder will have a remote effect.
  868. item->_instruction = CSYNC_INSTRUCTION_NEW;
  869. item->_direction = SyncFileItem::Down;
  870. item->_type = ItemTypeVirtualFile;
  871. } else if (!serverModified) {
  872. // Removed locally: also remove on the server.
  873. if (!dbEntry._serverHasIgnoredFiles) {
  874. #if !defined QT_NO_DEBUG
  875. qCInfo(lcDisco) << "File" << item->_file << "was deleted locally. Going to delete it on the server.";
  876. #endif
  877. item->_instruction = CSYNC_INSTRUCTION_REMOVE;
  878. item->_direction = SyncFileItem::Up;
  879. }
  880. }
  881. finalize();
  882. return;
  883. }
  884. Q_ASSERT(localEntry.isValid());
  885. item->_inode = localEntry.inode;
  886. if (dbEntry.isValid()) {
  887. bool typeChange = localEntry.isDirectory != dbEntry.isDirectory();
  888. if (!typeChange && localEntry.isVirtualFile) {
  889. if (noServerEntry) {
  890. item->_instruction = CSYNC_INSTRUCTION_REMOVE;
  891. item->_direction = SyncFileItem::Down;
  892. } else if (!dbEntry.isVirtualFile() && isVfsWithSuffix()) {
  893. // If we find what looks to be a spurious "abc.owncloud" the base file "abc"
  894. // might have been renamed to that. Make sure that the base file is not
  895. // deleted from the server.
  896. if (dbEntry._modtime == localEntry.modtime && dbEntry._fileSize == localEntry.size) {
  897. qCInfo(lcDisco) << "Base file was renamed to virtual file:" << item->_file;
  898. item->_direction = SyncFileItem::Down;
  899. item->_instruction = CSYNC_INSTRUCTION_SYNC;
  900. item->_type = ItemTypeVirtualFileDehydration;
  901. addVirtualFileSuffix(item->_file);
  902. item->_renameTarget = item->_file;
  903. } else {
  904. qCInfo(lcDisco) << "Virtual file with non-virtual db entry, ignoring:" << item->_file;
  905. item->_instruction = CSYNC_INSTRUCTION_IGNORE;
  906. }
  907. }
  908. } else if (!typeChange && ((dbEntry._modtime == localEntry.modtime && dbEntry._fileSize == localEntry.size) || localEntry.isDirectory)) {
  909. // Local file unchanged.
  910. if (noServerEntry) {
  911. #if !defined QT_NO_DEBUG
  912. qCInfo(lcDisco) << "File" << item->_file << "is not anymore on server. Going to delete it locally.";
  913. #endif
  914. item->_instruction = CSYNC_INSTRUCTION_REMOVE;
  915. item->_direction = SyncFileItem::Down;
  916. } else if (dbEntry._type == ItemTypeVirtualFileDehydration || localEntry.type == ItemTypeVirtualFileDehydration) {
  917. item->_direction = SyncFileItem::Down;
  918. item->_instruction = CSYNC_INSTRUCTION_SYNC;
  919. item->_type = ItemTypeVirtualFileDehydration;
  920. } else if (!serverModified
  921. && (dbEntry._inode != localEntry.inode
  922. || _discoveryData->_syncOptions._vfs->needsMetadataUpdate(*item))) {
  923. item->_instruction = CSYNC_INSTRUCTION_UPDATE_METADATA;
  924. item->_direction = SyncFileItem::Down;
  925. }
  926. } else if (!typeChange && isVfsWithSuffix()
  927. && dbEntry.isVirtualFile() && !localEntry.isVirtualFile
  928. && dbEntry._inode == localEntry.inode
  929. && dbEntry._modtime == localEntry.modtime
  930. && localEntry.size == 1) {
  931. // A suffix vfs file can be downloaded by renaming it to remove the suffix.
  932. // This check leaks some details of VfsSuffix, particularly the size of placeholders.
  933. item->_direction = SyncFileItem::Down;
  934. if (noServerEntry) {
  935. item->_instruction = CSYNC_INSTRUCTION_REMOVE;
  936. item->_type = ItemTypeFile;
  937. } else {
  938. item->_instruction = CSYNC_INSTRUCTION_SYNC;
  939. item->_type = ItemTypeVirtualFileDownload;
  940. item->_previousSize = 1;
  941. }
  942. } else if (serverModified
  943. || (isVfsWithSuffix() && dbEntry.isVirtualFile())) {
  944. // There's a local change and a server change: Conflict!
  945. // Alternatively, this might be a suffix-file that's virtual in the db but
  946. // not locally. These also become conflicts. For in-place placeholders that's
  947. // not necessary: they could be replaced by real files and should then trigger
  948. // a regular SYNC upwards when there's no server change.
  949. processFileConflict(item, path, localEntry, serverEntry, dbEntry);
  950. } else if (typeChange) {
  951. item->_instruction = CSYNC_INSTRUCTION_TYPE_CHANGE;
  952. item->_direction = SyncFileItem::Up;
  953. item->_checksumHeader.clear();
  954. item->_size = localEntry.size;
  955. item->_modtime = localEntry.modtime;
  956. item->_type = localEntry.isDirectory ? ItemTypeDirectory : ItemTypeFile;
  957. _childModified = true;
  958. } else if (dbEntry._modtime > 0 && (localEntry.modtime <= 0 || localEntry.modtime >= 0xFFFFFFFF) && dbEntry._fileSize == localEntry.size) {
  959. item->_instruction = CSYNC_INSTRUCTION_UPDATE_METADATA;
  960. item->_direction = SyncFileItem::Down;
  961. item->_size = localEntry.size > 0 ? localEntry.size : dbEntry._fileSize;
  962. item->_modtime = dbEntry._modtime;
  963. item->_previousModtime = dbEntry._modtime;
  964. item->_type = localEntry.isDirectory ? ItemTypeDirectory : ItemTypeFile;
  965. qCDebug(lcDisco) << "CSYNC_INSTRUCTION_SYNC: File" << item->_file << "if (dbEntry._modtime > 0 && localEntry.modtime <= 0)"
  966. << "dbEntry._modtime:" << dbEntry._modtime
  967. << "localEntry.modtime:" << localEntry.modtime;
  968. _childModified = true;
  969. } else {
  970. // Local file was changed
  971. item->_instruction = CSYNC_INSTRUCTION_SYNC;
  972. if (noServerEntry) {
  973. // Special case! deleted on server, modified on client, the instruction is then NEW
  974. item->_instruction = CSYNC_INSTRUCTION_NEW;
  975. }
  976. item->_direction = SyncFileItem::Up;
  977. item->_checksumHeader.clear();
  978. item->_size = localEntry.size;
  979. item->_modtime = localEntry.modtime;
  980. _childModified = true;
  981. qCDebug(lcDisco) << "Local file was changed: File" << item->_file
  982. << "item->_instruction:" << item->_instruction
  983. << "noServerEntry:" << noServerEntry
  984. << "item->_direction:" << item->_direction
  985. << "item->_size:" << item->_size
  986. << "item->_modtime:" << item->_modtime;
  987. // Checksum comparison at this stage is only enabled for .eml files,
  988. // check #4754 #4755
  989. bool isEmlFile = path._original.endsWith(QLatin1String(".eml"), Qt::CaseInsensitive);
  990. if (isEmlFile && dbEntry._fileSize == localEntry.size && !dbEntry._checksumHeader.isEmpty()) {
  991. if (computeLocalChecksum(dbEntry._checksumHeader, _discoveryData->_localDir + path._local, item)
  992. && item->_checksumHeader == dbEntry._checksumHeader) {
  993. qCInfo(lcDisco) << "NOTE: Checksums are identical, file did not actually change: " << path._local;
  994. item->_instruction = CSYNC_INSTRUCTION_UPDATE_METADATA;
  995. }
  996. }
  997. }
  998. finalize();
  999. return;
  1000. }
  1001. Q_ASSERT(!dbEntry.isValid());
  1002. if (localEntry.isVirtualFile && !noServerEntry) {
  1003. // Somehow there is a missing DB entry while the virtual file already exists.
  1004. // The instruction should already be set correctly.
  1005. ASSERT(item->_instruction == CSYNC_INSTRUCTION_UPDATE_METADATA);
  1006. ASSERT(item->_type == ItemTypeVirtualFile);
  1007. finalize();
  1008. return;
  1009. } else if (serverModified) {
  1010. processFileConflict(item, path, localEntry, serverEntry, dbEntry);
  1011. finalize();
  1012. return;
  1013. }
  1014. // New local file or rename
  1015. item->_instruction = CSYNC_INSTRUCTION_NEW;
  1016. item->_direction = SyncFileItem::Up;
  1017. item->_checksumHeader.clear();
  1018. item->_size = localEntry.size;
  1019. item->_modtime = localEntry.modtime;
  1020. item->_type = localEntry.isDirectory ? ItemTypeDirectory : localEntry.isVirtualFile ? ItemTypeVirtualFile : ItemTypeFile;
  1021. _childModified = true;
  1022. if (!localEntry.caseClashConflictingName.isEmpty()) {
  1023. qCInfo(lcDisco) << item->_file << "case clash conflict" << localEntry.caseClashConflictingName;
  1024. item->_instruction = CSYNC_INSTRUCTION_CONFLICT;
  1025. }
  1026. auto conflictRecord = _discoveryData->_statedb->caseConflictRecordByBasePath(item->_file);
  1027. if (conflictRecord.isValid() && QString::fromUtf8(conflictRecord.path).contains(QStringLiteral("(case clash from"))) {
  1028. qCInfo(lcDisco) << "should ignore" << item->_file << "has already a case clash conflict record" << conflictRecord.path;
  1029. item->_instruction = CSYNC_INSTRUCTION_IGNORE;
  1030. return;
  1031. }
  1032. auto postProcessLocalNew = [item, localEntry, path, this]() {
  1033. // TODO: We may want to execute the same logic for non-VFS mode, as, moving/renaming the same folder by 2 or more clients at the same time is not possible in Web UI.
  1034. // Keeping it like this (for VFS files and folders only) just to fix a user issue.
  1035. if (!(_discoveryData && _discoveryData->_syncOptions._vfs && _discoveryData->_syncOptions._vfs->mode() != Vfs::Off)) {
  1036. // for VFS files and folders only
  1037. return;
  1038. }
  1039. if (!localEntry.isVirtualFile && !localEntry.isDirectory) {
  1040. return;
  1041. }
  1042. if (localEntry.isDirectory && _discoveryData->_syncOptions._vfs->mode() != Vfs::WindowsCfApi) {
  1043. // for VFS folders on Windows only
  1044. return;
  1045. }
  1046. Q_ASSERT(item->_instruction == CSYNC_INSTRUCTION_NEW);
  1047. if (item->_instruction != CSYNC_INSTRUCTION_NEW) {
  1048. qCWarning(lcDisco) << "Trying to wipe a virtual item" << path._local << " with item->_instruction" << item->_instruction;
  1049. return;
  1050. }
  1051. // must be a dehydrated placeholder
  1052. const bool isFilePlaceHolder = !localEntry.isDirectory && _discoveryData->_syncOptions._vfs->isDehydratedPlaceholder(_discoveryData->_localDir + path._local);
  1053. // either correct availability, or a result with error if the folder is new or otherwise has no availability set yet
  1054. const auto folderPlaceHolderAvailability = localEntry.isDirectory ? _discoveryData->_syncOptions._vfs->availability(path._local) : Vfs::AvailabilityResult(Vfs::AvailabilityError::NoSuchItem);
  1055. const auto folderPinState = localEntry.isDirectory ? _discoveryData->_syncOptions._vfs->pinState(path._local) : Optional<PinState>(PinState::Unspecified);
  1056. if (!isFilePlaceHolder && !folderPlaceHolderAvailability.isValid() && !folderPinState.isValid()) {
  1057. // not a file placeholder and not a synced folder placeholder (new local folder)
  1058. return;
  1059. }
  1060. const auto isFolderPinStateOnlineOnly = (folderPinState.isValid() && *folderPinState == PinState::OnlineOnly);
  1061. const auto isfolderPlaceHolderAvailabilityOnlineOnly = (folderPlaceHolderAvailability.isValid() && *folderPlaceHolderAvailability == VfsItemAvailability::OnlineOnly);
  1062. // a folder is considered online-only if: no files are hydrated, or, if it's an empty folder
  1063. const auto isOnlineOnlyFolder = isfolderPlaceHolderAvailabilityOnlineOnly || (!folderPlaceHolderAvailability && isFolderPinStateOnlineOnly);
  1064. if (!isFilePlaceHolder && !isOnlineOnlyFolder) {
  1065. if (localEntry.isDirectory && folderPlaceHolderAvailability.isValid() && !isOnlineOnlyFolder) {
  1066. // a VFS folder but is not online-only (has some files hydrated)
  1067. qCInfo(lcDisco) << "Virtual directory without db entry for" << path._local << "but it contains hydrated file(s), so let's keep it and reupload.";
  1068. return;
  1069. }
  1070. qCWarning(lcDisco) << "Virtual file without db entry for" << path._local
  1071. << "but looks odd, keeping";
  1072. item->_instruction = CSYNC_INSTRUCTION_IGNORE;
  1073. return;
  1074. }
  1075. if (isOnlineOnlyFolder) {
  1076. // if we're wiping a folder, we will only get this function called once and will wipe a folder along with it's files and also display one error in GUI
  1077. qCInfo(lcDisco) << "Wiping virtual folder without db entry for" << path._local;
  1078. if (isfolderPlaceHolderAvailabilityOnlineOnly && folderPlaceHolderAvailability.isValid()) {
  1079. qCInfo(lcDisco) << "*folderPlaceHolderAvailability:" << *folderPlaceHolderAvailability;
  1080. }
  1081. if (isFolderPinStateOnlineOnly && folderPinState.isValid()) {
  1082. qCInfo(lcDisco) << "*folderPinState:" << *folderPinState;
  1083. }
  1084. emit _discoveryData->addErrorToGui(SyncFileItem::SoftError, tr("Conflict when uploading a folder. It's going to get cleared!"), path._local, ErrorCategory::GenericError);
  1085. } else {
  1086. qCInfo(lcDisco) << "Wiping virtual file without db entry for" << path._local;
  1087. emit _discoveryData->addErrorToGui(SyncFileItem::SoftError, tr("Conflict when uploading a file. It's going to get removed!"), path._local, ErrorCategory::GenericError);
  1088. }
  1089. item->_instruction = CSYNC_INSTRUCTION_REMOVE;
  1090. item->_direction = SyncFileItem::Down;
  1091. // this flag needs to be unset, otherwise a folder would get marked as new in the processSubJobs
  1092. _childModified = false;
  1093. };
  1094. // Check if it is a move
  1095. OCC::SyncJournalFileRecord base;
  1096. if (!_discoveryData->_statedb->getFileRecordByInode(localEntry.inode, &base)) {
  1097. dbError();
  1098. return;
  1099. }
  1100. const auto originalPath = base.path();
  1101. // Function to gradually check conditions for accepting a move-candidate
  1102. auto moveCheck = [&]() {
  1103. if (!base.isValid()) {
  1104. qCInfo(lcDisco) << "Not a move, no item in db with inode" << localEntry.inode;
  1105. return false;
  1106. }
  1107. if (base.isE2eEncrypted() || isInsideEncryptedTree()) {
  1108. return false;
  1109. }
  1110. if (base.isDirectory() != item->isDirectory()) {
  1111. qCInfo(lcDisco) << "Not a move, types don't match" << base._type << item->_type << localEntry.type;
  1112. return false;
  1113. }
  1114. // Directories and virtual files don't need size/mtime equality
  1115. if (!localEntry.isDirectory && !base.isVirtualFile()
  1116. && (base._modtime != localEntry.modtime || base._fileSize != localEntry.size)) {
  1117. qCInfo(lcDisco) << "Not a move, mtime or size differs, "
  1118. << "modtime:" << base._modtime << localEntry.modtime << ", "
  1119. << "size:" << base._fileSize << localEntry.size;
  1120. return false;
  1121. }
  1122. // The old file must have been deleted.
  1123. if (QFile::exists(_discoveryData->_localDir + originalPath)
  1124. // Exception: If the rename changes case only (like "foo" -> "Foo") the
  1125. // old filename might still point to the same file.
  1126. && !(Utility::fsCasePreserving()
  1127. && originalPath.compare(path._local, Qt::CaseInsensitive) == 0
  1128. && originalPath != path._local)) {
  1129. qCInfo(lcDisco) << "Not a move, base file still exists at" << originalPath;
  1130. return false;
  1131. }
  1132. // Verify the checksum where possible
  1133. if (!base._checksumHeader.isEmpty() && item->_type == ItemTypeFile && base._type == ItemTypeFile) {
  1134. if (computeLocalChecksum(base._checksumHeader, _discoveryData->_localDir + path._original, item)) {
  1135. qCInfo(lcDisco) << "checking checksum of potential rename " << path._original << item->_checksumHeader << base._checksumHeader;
  1136. if (item->_checksumHeader != base._checksumHeader) {
  1137. qCInfo(lcDisco) << "Not a move, checksums differ";
  1138. return false;
  1139. }
  1140. }
  1141. }
  1142. if (_discoveryData->isRenamed(originalPath)) {
  1143. qCInfo(lcDisco) << "Not a move, base path already renamed";
  1144. return false;
  1145. }
  1146. return true;
  1147. };
  1148. // If it's not a move it's just a local-NEW
  1149. if (!moveCheck()) {
  1150. if (base.isE2eEncrypted()) {
  1151. // renaming the encrypted folder is done via remove + re-upload hence we need to mark the newly created folder as encrypted
  1152. // base is a record in the SyncJournal database that contains the data about the being-renamed folder with it's old name and encryption information
  1153. item->_e2eEncryptionStatus = EncryptionStatusEnums::fromDbEncryptionStatus(base._e2eEncryptionStatus);
  1154. }
  1155. postProcessLocalNew();
  1156. finalize();
  1157. return;
  1158. }
  1159. // Check local permission if we are allowed to put move the file here
  1160. // Technically we should use the permissions from the server, but we'll assume it is the same
  1161. auto movePerms = checkMovePermissions(base._remotePerm, originalPath, item->isDirectory());
  1162. if (!movePerms.sourceOk || !movePerms.destinationOk) {
  1163. qCInfo(lcDisco) << "Move without permission to rename base file, "
  1164. << "source:" << movePerms.sourceOk
  1165. << ", target:" << movePerms.destinationOk
  1166. << ", targetNew:" << movePerms.destinationNewOk;
  1167. // If we can create the destination, do that.
  1168. // Permission errors on the destination will be handled by checkPermissions later.
  1169. postProcessLocalNew();
  1170. finalize();
  1171. // If the destination upload will work, we're fine with the source deletion.
  1172. // If the source deletion can't work, checkPermissions will error.
  1173. if (movePerms.destinationNewOk)
  1174. return;
  1175. // Here we know the new location can't be uploaded: must prevent the source delete.
  1176. // Two cases: either the source item was already processed or not.
  1177. auto wasDeletedOnClient = _discoveryData->findAndCancelDeletedJob(originalPath);
  1178. if (wasDeletedOnClient.first) {
  1179. // More complicated. The REMOVE is canceled. Restore will happen next sync.
  1180. qCInfo(lcDisco) << "Undid remove instruction on source" << originalPath;
  1181. if (!_discoveryData->_statedb->deleteFileRecord(originalPath, true)) {
  1182. qCWarning(lcDisco) << "Failed to delete a file record from the local DB" << originalPath;
  1183. }
  1184. _discoveryData->_statedb->schedulePathForRemoteDiscovery(originalPath);
  1185. _discoveryData->_anotherSyncNeeded = true;
  1186. } else {
  1187. // Signal to future checkPermissions() to forbid the REMOVE and set to restore instead
  1188. qCInfo(lcDisco) << "Preventing future remove on source" << originalPath;
  1189. _discoveryData->_forbiddenDeletes[originalPath + '/'] = true;
  1190. }
  1191. return;
  1192. }
  1193. auto wasDeletedOnClient = _discoveryData->findAndCancelDeletedJob(originalPath);
  1194. auto processRename = [item, originalPath, base, this](PathTuple &path) {
  1195. auto adjustedOriginalPath = _discoveryData->adjustRenamedPath(originalPath, SyncFileItem::Down);
  1196. _discoveryData->_renamedItemsLocal.insert(originalPath, path._target);
  1197. item->_renameTarget = path._target;
  1198. path._server = adjustedOriginalPath;
  1199. item->_file = path._server;
  1200. path._original = originalPath;
  1201. item->_originalFile = path._original;
  1202. item->_modtime = base._modtime;
  1203. item->_inode = base._inode;
  1204. item->_instruction = CSYNC_INSTRUCTION_RENAME;
  1205. item->_direction = SyncFileItem::Up;
  1206. item->_fileId = base._fileId;
  1207. item->_remotePerm = base._remotePerm;
  1208. item->_isShared = base._isShared;
  1209. item->_sharedByMe = base._sharedByMe;
  1210. item->_lastShareStateFetchedTimestamp = base._lastShareStateFetchedTimestamp;
  1211. item->_etag = base._etag;
  1212. item->_type = base._type;
  1213. // Discard any download/dehydrate tags on the base file.
  1214. // They could be preserved and honored in a follow-up sync,
  1215. // but it complicates handling a lot and will happen rarely.
  1216. if (item->_type == ItemTypeVirtualFileDownload)
  1217. item->_type = ItemTypeVirtualFile;
  1218. if (item->_type == ItemTypeVirtualFileDehydration)
  1219. item->_type = ItemTypeFile;
  1220. qCInfo(lcDisco) << "Rename detected (up) " << item->_file << " -> " << item->_renameTarget;
  1221. };
  1222. if (wasDeletedOnClient.first) {
  1223. recurseQueryServer = wasDeletedOnClient.second == base._etag ? ParentNotChanged : NormalQuery;
  1224. processRename(path);
  1225. } else {
  1226. // We must query the server to know if the etag has not changed
  1227. _pendingAsyncJobs++;
  1228. QString serverOriginalPath = _discoveryData->_remoteFolder + _discoveryData->adjustRenamedPath(originalPath, SyncFileItem::Down);
  1229. if (base.isVirtualFile() && isVfsWithSuffix())
  1230. chopVirtualFileSuffix(serverOriginalPath);
  1231. auto job = new RequestEtagJob(_discoveryData->_account, serverOriginalPath, this);
  1232. connect(job, &RequestEtagJob::finishedWithResult, this, [=](const HttpResult<QByteArray> &etag) mutable {
  1233. if (!etag || (etag.get() != base._etag && !item->isDirectory()) || _discoveryData->isRenamed(originalPath)
  1234. || (isAnyParentBeingRestored(originalPath) && !isRename(originalPath))) {
  1235. qCInfo(lcDisco) << "Can't rename because the etag has changed or the directory is gone or we are restoring one of the file's parents." << originalPath;
  1236. // Can't be a rename, leave it as a new.
  1237. postProcessLocalNew();
  1238. } else {
  1239. // In case the deleted item was discovered in parallel
  1240. _discoveryData->findAndCancelDeletedJob(originalPath);
  1241. processRename(path);
  1242. recurseQueryServer = etag.get() == base._etag ? ParentNotChanged : NormalQuery;
  1243. }
  1244. processFileFinalize(item, path, item->isDirectory(), NormalQuery, recurseQueryServer);
  1245. _pendingAsyncJobs--;
  1246. QTimer::singleShot(0, _discoveryData, &DiscoveryPhase::scheduleMoreJobs);
  1247. });
  1248. job->start();
  1249. return;
  1250. }
  1251. finalize();
  1252. }
  1253. void ProcessDirectoryJob::processFileConflict(const SyncFileItemPtr &item, ProcessDirectoryJob::PathTuple path, const LocalInfo &localEntry, const RemoteInfo &serverEntry, const SyncJournalFileRecord &dbEntry)
  1254. {
  1255. item->_previousSize = localEntry.size;
  1256. item->_previousModtime = localEntry.modtime;
  1257. if (serverEntry.isDirectory && localEntry.isDirectory) {
  1258. // Folders of the same path are always considered equals
  1259. item->_instruction = CSYNC_INSTRUCTION_UPDATE_METADATA;
  1260. return;
  1261. }
  1262. // A conflict with a virtual should lead to virtual file download
  1263. if (dbEntry.isVirtualFile() || localEntry.isVirtualFile)
  1264. item->_type = ItemTypeVirtualFileDownload;
  1265. // If there's no content hash, use heuristics
  1266. if (serverEntry.checksumHeader.isEmpty()) {
  1267. // If the size or mtime is different, it's definitely a conflict.
  1268. bool isConflict = (serverEntry.size != localEntry.size) || (serverEntry.modtime != localEntry.modtime);
  1269. // It could be a conflict even if size and mtime match!
  1270. //
  1271. // In older client versions we always treated these cases as a
  1272. // non-conflict. This behavior is preserved in case the server
  1273. // doesn't provide a content checksum.
  1274. // SO: If there is no checksum, we can have !isConflict here
  1275. // even though the files might have different content! This is an
  1276. // intentional tradeoff. Downloading and comparing files would
  1277. // be technically correct in this situation but leads to too
  1278. // much waste.
  1279. // In particular this kind of NEW/NEW situation with identical
  1280. // sizes and mtimes pops up when the local database is lost for
  1281. // whatever reason.
  1282. item->_instruction = isConflict ? CSYNC_INSTRUCTION_CONFLICT : CSYNC_INSTRUCTION_UPDATE_METADATA;
  1283. item->_direction = isConflict ? SyncFileItem::None : SyncFileItem::Down;
  1284. qCDebug(lcDisco) << "CSYNC_INSTRUCTION_CONFLICT: File" << item->_file << "if (serverEntry.checksumHeader.isEmpty())";
  1285. qCDebug(lcDisco) << "CSYNC_INSTRUCTION_CONFLICT: serverEntry.size:" << serverEntry.size
  1286. << "localEntry.size:" << localEntry.size
  1287. << "serverEntry.modtime:" << serverEntry.modtime
  1288. << "localEntry.modtime:" << localEntry.modtime;
  1289. return;
  1290. }
  1291. if (!serverEntry.checksumHeader.isEmpty()) {
  1292. qCDebug(lcDisco) << "CSYNC_INSTRUCTION_CONFLICT: File" << item->_file << "if (!serverEntry.checksumHeader.isEmpty())";
  1293. qCDebug(lcDisco) << "CSYNC_INSTRUCTION_CONFLICT: serverEntry.size:" << serverEntry.size
  1294. << "localEntry.size:" << localEntry.size
  1295. << "serverEntry.modtime:" << serverEntry.modtime
  1296. << "localEntry.modtime:" << localEntry.modtime;
  1297. }
  1298. // Do we have an UploadInfo for this?
  1299. // Maybe the Upload was completed, but the connection was broken just before
  1300. // we recieved the etag (Issue #5106)
  1301. auto up = _discoveryData->_statedb->getUploadInfo(path._original);
  1302. if (up._valid && up._contentChecksum == serverEntry.checksumHeader) {
  1303. // Solve the conflict into an upload, or nothing
  1304. item->_instruction = up._modtime == localEntry.modtime && up._size == localEntry.size
  1305. ? CSYNC_INSTRUCTION_NONE : CSYNC_INSTRUCTION_SYNC;
  1306. item->_direction = SyncFileItem::Up;
  1307. qCDebug(lcDisco) << "CSYNC_INSTRUCTION_SYNC: File" << item->_file << "if (up._valid && up._contentChecksum == serverEntry.checksumHeader)";
  1308. qCDebug(lcDisco) << "CSYNC_INSTRUCTION_SYNC: up._valid:" << up._valid
  1309. << "up._contentChecksum:" << up._contentChecksum
  1310. << "serverEntry.checksumHeader:" << serverEntry.checksumHeader;
  1311. // Update the etag and other server metadata in the journal already
  1312. // (We can't use a typical CSYNC_INSTRUCTION_UPDATE_METADATA because
  1313. // we must not store the size/modtime from the file system)
  1314. OCC::SyncJournalFileRecord rec;
  1315. if (_discoveryData->_statedb->getFileRecord(path._original, &rec)) {
  1316. rec._path = path._original.toUtf8();
  1317. rec._etag = serverEntry.etag;
  1318. rec._fileId = serverEntry.fileId;
  1319. rec._modtime = serverEntry.modtime;
  1320. rec._type = item->_type;
  1321. rec._fileSize = serverEntry.size;
  1322. rec._remotePerm = serverEntry.remotePerm;
  1323. rec._isShared = serverEntry.remotePerm.hasPermission(RemotePermissions::IsShared) || serverEntry.sharedByMe;
  1324. rec._sharedByMe = serverEntry.sharedByMe;
  1325. rec._lastShareStateFetchedTimestamp = QDateTime::currentMSecsSinceEpoch();
  1326. rec._checksumHeader = serverEntry.checksumHeader;
  1327. const auto result = _discoveryData->_statedb->setFileRecord(rec);
  1328. if (!result) {
  1329. qCWarning(lcDisco) << "Error when setting the file record to the database" << rec._path << result.error();
  1330. }
  1331. }
  1332. return;
  1333. }
  1334. if (!up._valid || up._contentChecksum != serverEntry.checksumHeader) {
  1335. qCDebug(lcDisco) << "CSYNC_INSTRUCTION_SYNC: File" << item->_file << "if (!up._valid && up._contentChecksum != serverEntry.checksumHeader)";
  1336. qCDebug(lcDisco) << "CSYNC_INSTRUCTION_SYNC: up._valid:" << up._valid
  1337. << "up._contentChecksum:" << up._contentChecksum
  1338. << "serverEntry.checksumHeader:" << serverEntry.checksumHeader;
  1339. }
  1340. // Rely on content hash comparisons to optimize away non-conflicts inside the job
  1341. item->_instruction = CSYNC_INSTRUCTION_CONFLICT;
  1342. item->_direction = SyncFileItem::None;
  1343. }
  1344. void ProcessDirectoryJob::processFileFinalize(
  1345. const SyncFileItemPtr &item, PathTuple path, bool recurse,
  1346. QueryMode recurseQueryLocal, QueryMode recurseQueryServer)
  1347. {
  1348. // Adjust target path for virtual-suffix files
  1349. if (isVfsWithSuffix()) {
  1350. if (item->_type == ItemTypeVirtualFile) {
  1351. addVirtualFileSuffix(path._target);
  1352. if (item->_instruction == CSYNC_INSTRUCTION_RENAME) {
  1353. addVirtualFileSuffix(item->_renameTarget);
  1354. } else {
  1355. addVirtualFileSuffix(item->_file);
  1356. }
  1357. }
  1358. if (item->_type == ItemTypeVirtualFileDehydration
  1359. && item->_instruction == CSYNC_INSTRUCTION_SYNC) {
  1360. if (item->_renameTarget.isEmpty()) {
  1361. item->_renameTarget = item->_file;
  1362. addVirtualFileSuffix(item->_renameTarget);
  1363. }
  1364. }
  1365. }
  1366. if (path._original != path._target && (item->_instruction == CSYNC_INSTRUCTION_UPDATE_METADATA || item->_instruction == CSYNC_INSTRUCTION_NONE)) {
  1367. ASSERT(_dirItem && _dirItem->_instruction == CSYNC_INSTRUCTION_RENAME);
  1368. // This is because otherwise subitems are not updated! (ideally renaming a directory could
  1369. // update the database for all items! See PropagateDirectory::slotSubJobsFinished)
  1370. item->_instruction = CSYNC_INSTRUCTION_RENAME;
  1371. item->_renameTarget = path._target;
  1372. item->_direction = _dirItem->_direction;
  1373. }
  1374. qCDebug(lcDisco) << "Discovered" << item->_file << item->_instruction << item->_direction << item->_type;
  1375. if (item->isDirectory() && item->_instruction == CSYNC_INSTRUCTION_SYNC)
  1376. item->_instruction = CSYNC_INSTRUCTION_UPDATE_METADATA;
  1377. bool removed = item->_instruction == CSYNC_INSTRUCTION_REMOVE;
  1378. if (checkPermissions(item)) {
  1379. if (item->_isRestoration && item->isDirectory())
  1380. recurse = true;
  1381. } else {
  1382. recurse = false;
  1383. }
  1384. if (recurse) {
  1385. auto job = new ProcessDirectoryJob(path, item, recurseQueryLocal, recurseQueryServer,
  1386. _lastSyncTimestamp, this);
  1387. job->setInsideEncryptedTree(isInsideEncryptedTree() || item->isEncrypted());
  1388. if (removed) {
  1389. job->setParent(_discoveryData);
  1390. _discoveryData->enqueueDirectoryToDelete(path._original, job);
  1391. } else {
  1392. connect(job, &ProcessDirectoryJob::finished, this, &ProcessDirectoryJob::subJobFinished);
  1393. _queuedJobs.push_back(job);
  1394. }
  1395. } else {
  1396. if (removed
  1397. // For the purpose of rename deletion, restored deleted placeholder is as if it was deleted
  1398. || (item->_type == ItemTypeVirtualFile && item->_instruction == CSYNC_INSTRUCTION_NEW)) {
  1399. _discoveryData->_deletedItem[path._original] = item;
  1400. }
  1401. emit _discoveryData->itemDiscovered(item);
  1402. }
  1403. }
  1404. void ProcessDirectoryJob::processBlacklisted(const PathTuple &path, const OCC::LocalInfo &localEntry,
  1405. const SyncJournalFileRecord &dbEntry)
  1406. {
  1407. if (!localEntry.isValid())
  1408. return;
  1409. auto item = SyncFileItem::fromSyncJournalFileRecord(dbEntry);
  1410. item->_file = path._target;
  1411. item->_originalFile = path._original;
  1412. item->_inode = localEntry.inode;
  1413. item->_isSelectiveSync = true;
  1414. if (dbEntry.isValid() && ((dbEntry._modtime == localEntry.modtime && dbEntry._fileSize == localEntry.size) || (localEntry.isDirectory && dbEntry.isDirectory()))) {
  1415. item->_instruction = CSYNC_INSTRUCTION_REMOVE;
  1416. item->_direction = SyncFileItem::Down;
  1417. } else {
  1418. item->_instruction = CSYNC_INSTRUCTION_IGNORE;
  1419. item->_status = SyncFileItem::FileIgnored;
  1420. item->_errorString = tr("Ignored because of the \"choose what to sync\" blacklist");
  1421. qCInfo(lcDisco) << "Ignored because of the \"choose what to sync\" blacklist" << item->_file << "direction" << item->_direction;
  1422. _childIgnored = true;
  1423. }
  1424. qCInfo(lcDisco) << "Discovered (blacklisted) " << item->_file << item->_instruction << item->_direction << item->isDirectory();
  1425. if (item->isDirectory() && item->_instruction != CSYNC_INSTRUCTION_IGNORE) {
  1426. auto job = new ProcessDirectoryJob(path, item, NormalQuery, InBlackList, _lastSyncTimestamp, this);
  1427. connect(job, &ProcessDirectoryJob::finished, this, &ProcessDirectoryJob::subJobFinished);
  1428. _queuedJobs.push_back(job);
  1429. } else {
  1430. emit _discoveryData->itemDiscovered(item);
  1431. }
  1432. }
  1433. bool ProcessDirectoryJob::checkPermissions(const OCC::SyncFileItemPtr &item)
  1434. {
  1435. if (item->_direction != SyncFileItem::Up) {
  1436. // Currently we only check server-side permissions
  1437. return true;
  1438. }
  1439. switch (item->_instruction) {
  1440. case CSYNC_INSTRUCTION_TYPE_CHANGE:
  1441. case CSYNC_INSTRUCTION_NEW: {
  1442. const auto perms = !_rootPermissions.isNull() ? _rootPermissions
  1443. : _dirItem ? _dirItem->_remotePerm : _rootPermissions;
  1444. if (perms.isNull()) {
  1445. // No permissions set
  1446. return true;
  1447. } else if (item->isDirectory() && !perms.hasPermission(RemotePermissions::CanAddSubDirectories)) {
  1448. qCWarning(lcDisco) << "checkForPermission: ERROR" << item->_file;
  1449. item->_instruction = CSYNC_INSTRUCTION_ERROR;
  1450. item->_errorString = tr("Not allowed because you don't have permission to add subfolders to that folder");
  1451. return false;
  1452. } else if (!item->isDirectory() && !perms.hasPermission(RemotePermissions::CanAddFile)) {
  1453. qCWarning(lcDisco) << "checkForPermission: ERROR" << item->_file;
  1454. item->_instruction = CSYNC_INSTRUCTION_ERROR;
  1455. item->_errorString = tr("Not allowed because you don't have permission to add files in that folder");
  1456. return false;
  1457. }
  1458. break;
  1459. }
  1460. case CSYNC_INSTRUCTION_SYNC: {
  1461. const auto perms = item->_remotePerm;
  1462. if (perms.isNull()) {
  1463. // No permissions set
  1464. return true;
  1465. }
  1466. if (!perms.hasPermission(RemotePermissions::CanWrite)) {
  1467. item->_instruction = CSYNC_INSTRUCTION_CONFLICT;
  1468. item->_errorString = tr("Not allowed to upload this file because it is read-only on the server, restoring");
  1469. item->_direction = SyncFileItem::Down;
  1470. item->_isRestoration = true;
  1471. qCWarning(lcDisco) << "checkForPermission: RESTORING" << item->_file << item->_errorString;
  1472. // Take the things to write to the db from the "other" node (i.e: info from server).
  1473. // Do a lookup into the csync remote tree to get the metadata we need to restore.
  1474. qSwap(item->_size, item->_previousSize);
  1475. qSwap(item->_modtime, item->_previousModtime);
  1476. return false;
  1477. }
  1478. break;
  1479. }
  1480. case CSYNC_INSTRUCTION_REMOVE: {
  1481. QString fileSlash = item->_file + '/';
  1482. auto forbiddenIt = _discoveryData->_forbiddenDeletes.upperBound(fileSlash);
  1483. if (forbiddenIt != _discoveryData->_forbiddenDeletes.begin())
  1484. forbiddenIt -= 1;
  1485. if (forbiddenIt != _discoveryData->_forbiddenDeletes.end()
  1486. && fileSlash.startsWith(forbiddenIt.key())) {
  1487. item->_instruction = CSYNC_INSTRUCTION_NEW;
  1488. item->_direction = SyncFileItem::Down;
  1489. item->_isRestoration = true;
  1490. item->_errorString = tr("Moved to invalid target, restoring");
  1491. qCWarning(lcDisco) << "checkForPermission: RESTORING" << item->_file << item->_errorString;
  1492. return true; // restore sub items
  1493. }
  1494. const auto perms = item->_remotePerm;
  1495. if (perms.isNull()) {
  1496. // No permissions set
  1497. return true;
  1498. }
  1499. if (!perms.hasPermission(RemotePermissions::CanDelete) || isAnyParentBeingRestored(item->_file))
  1500. {
  1501. item->_instruction = CSYNC_INSTRUCTION_NEW;
  1502. item->_direction = SyncFileItem::Down;
  1503. item->_isRestoration = true;
  1504. item->_errorString = tr("Not allowed to remove, restoring");
  1505. qCWarning(lcDisco) << "checkForPermission: RESTORING" << item->_file << item->_errorString;
  1506. return true; // (we need to recurse to restore sub items)
  1507. }
  1508. break;
  1509. }
  1510. default:
  1511. break;
  1512. }
  1513. return true;
  1514. }
  1515. bool ProcessDirectoryJob::isAnyParentBeingRestored(const QString &file) const
  1516. {
  1517. for (const auto &directoryNameToRestore : qAsConst(_discoveryData->_directoryNamesToRestoreOnPropagation)) {
  1518. if (file.startsWith(QString(directoryNameToRestore + QLatin1Char('/')))) {
  1519. qCWarning(lcDisco) << "File" << file << " is within the tree that's being restored" << directoryNameToRestore;
  1520. return true;
  1521. }
  1522. }
  1523. return false;
  1524. }
  1525. bool ProcessDirectoryJob::isRename(const QString &originalPath) const
  1526. {
  1527. return (originalPath.startsWith(_currentFolder._original)
  1528. && originalPath.lastIndexOf('/') == _currentFolder._original.size());
  1529. /* TODO: This was needed at some point to cover an edge case which I am no longer to reproduce and it might no longer be the case.
  1530. * Still, leaving this here just in case the edge case is caught at some point in future.
  1531. *
  1532. OCC::SyncJournalFileRecord base;
  1533. // are we allowed to rename?
  1534. if (!_discoveryData || !_discoveryData->_statedb || !_discoveryData->_statedb->getFileRecord(originalPath, &base)) {
  1535. return false;
  1536. }
  1537. qCWarning(lcDisco) << "isRename from" << originalPath << " to" << targetPath << " :"
  1538. << base._remotePerm.hasPermission(RemotePermissions::CanRename);
  1539. return base._remotePerm.hasPermission(RemotePermissions::CanRename);
  1540. */
  1541. }
  1542. auto ProcessDirectoryJob::checkMovePermissions(RemotePermissions srcPerm, const QString &srcPath,
  1543. bool isDirectory)
  1544. -> MovePermissionResult
  1545. {
  1546. auto destPerms = !_rootPermissions.isNull() ? _rootPermissions
  1547. : _dirItem ? _dirItem->_remotePerm : _rootPermissions;
  1548. auto filePerms = srcPerm;
  1549. //true when it is just a rename in the same directory. (not a move)
  1550. bool isRename = srcPath.startsWith(_currentFolder._original)
  1551. && srcPath.lastIndexOf('/') == _currentFolder._original.size();
  1552. // Check if we are allowed to move to the destination.
  1553. bool destinationOK = true;
  1554. bool destinationNewOK = true;
  1555. if (destPerms.isNull()) {
  1556. } else if ((isDirectory && !destPerms.hasPermission(RemotePermissions::CanAddSubDirectories)) ||
  1557. (!isDirectory && !destPerms.hasPermission(RemotePermissions::CanAddFile))) {
  1558. destinationNewOK = false;
  1559. }
  1560. if (!isRename && !destinationNewOK) {
  1561. // no need to check for the destination dir permission for renames
  1562. destinationOK = false;
  1563. }
  1564. // check if we are allowed to move from the source
  1565. bool sourceOK = true;
  1566. if (!filePerms.isNull()
  1567. && ((isRename && !filePerms.hasPermission(RemotePermissions::CanRename))
  1568. || (!isRename && !filePerms.hasPermission(RemotePermissions::CanMove)))) {
  1569. // We are not allowed to move or rename this file
  1570. sourceOK = false;
  1571. }
  1572. return MovePermissionResult{sourceOK, destinationOK, destinationNewOK};
  1573. }
  1574. void ProcessDirectoryJob::subJobFinished()
  1575. {
  1576. auto job = qobject_cast<ProcessDirectoryJob *>(sender());
  1577. ASSERT(job);
  1578. _childIgnored |= job->_childIgnored;
  1579. _childModified |= job->_childModified;
  1580. if (job->_dirItem)
  1581. emit _discoveryData->itemDiscovered(job->_dirItem);
  1582. int count = _runningJobs.removeAll(job);
  1583. ASSERT(count == 1);
  1584. job->deleteLater();
  1585. QTimer::singleShot(0, _discoveryData, &DiscoveryPhase::scheduleMoreJobs);
  1586. }
  1587. int ProcessDirectoryJob::processSubJobs(int nbJobs)
  1588. {
  1589. if (_queuedJobs.empty() && _runningJobs.empty() && _pendingAsyncJobs == 0) {
  1590. _pendingAsyncJobs = -1; // We're finished, we don't want to emit finished again
  1591. if (_dirItem) {
  1592. if (_childModified && _dirItem->_instruction == CSYNC_INSTRUCTION_REMOVE) {
  1593. // re-create directory that has modified contents
  1594. _dirItem->_instruction = CSYNC_INSTRUCTION_NEW;
  1595. _dirItem->_direction = _dirItem->_direction == SyncFileItem::Up ? SyncFileItem::Down : SyncFileItem::Up;
  1596. }
  1597. if (_childModified && _dirItem->_instruction == CSYNC_INSTRUCTION_TYPE_CHANGE && !_dirItem->isDirectory()) {
  1598. // Replacing a directory by a file is a conflict, if the directory had modified children
  1599. _dirItem->_instruction = CSYNC_INSTRUCTION_CONFLICT;
  1600. if (_dirItem->_direction == SyncFileItem::Up) {
  1601. _dirItem->_type = ItemTypeDirectory;
  1602. _dirItem->_direction = SyncFileItem::Down;
  1603. }
  1604. }
  1605. if (_childIgnored && _dirItem->_instruction == CSYNC_INSTRUCTION_REMOVE) {
  1606. // Do not remove a directory that has ignored files
  1607. qCInfo(lcDisco) << "Child ignored for a folder to remove" << _dirItem->_file << "direction" << _dirItem->_direction;
  1608. _dirItem->_instruction = CSYNC_INSTRUCTION_NONE;
  1609. }
  1610. }
  1611. emit finished();
  1612. }
  1613. int started = 0;
  1614. foreach (auto *rj, _runningJobs) {
  1615. started += rj->processSubJobs(nbJobs - started);
  1616. if (started >= nbJobs)
  1617. return started;
  1618. }
  1619. while (started < nbJobs && !_queuedJobs.empty()) {
  1620. auto f = _queuedJobs.front();
  1621. _queuedJobs.pop_front();
  1622. _runningJobs.push_back(f);
  1623. f->start();
  1624. started++;
  1625. }
  1626. return started;
  1627. }
  1628. void ProcessDirectoryJob::dbError()
  1629. {
  1630. emit _discoveryData->fatalError(tr("Error while reading the database"), ErrorCategory::GenericError);
  1631. }
  1632. void ProcessDirectoryJob::addVirtualFileSuffix(QString &str) const
  1633. {
  1634. str.append(_discoveryData->_syncOptions._vfs->fileSuffix());
  1635. }
  1636. bool ProcessDirectoryJob::hasVirtualFileSuffix(const QString &str) const
  1637. {
  1638. if (!isVfsWithSuffix())
  1639. return false;
  1640. return str.endsWith(_discoveryData->_syncOptions._vfs->fileSuffix());
  1641. }
  1642. void ProcessDirectoryJob::chopVirtualFileSuffix(QString &str) const
  1643. {
  1644. if (!isVfsWithSuffix())
  1645. return;
  1646. bool hasSuffix = hasVirtualFileSuffix(str);
  1647. ASSERT(hasSuffix);
  1648. if (hasSuffix)
  1649. str.chop(_discoveryData->_syncOptions._vfs->fileSuffix().size());
  1650. }
  1651. DiscoverySingleDirectoryJob *ProcessDirectoryJob::startAsyncServerQuery()
  1652. {
  1653. auto serverJob = new DiscoverySingleDirectoryJob(_discoveryData->_account,
  1654. _discoveryData->_remoteFolder + _currentFolder._server, this);
  1655. if (!_dirItem)
  1656. serverJob->setIsRootPath(); // query the fingerprint on the root
  1657. connect(serverJob, &DiscoverySingleDirectoryJob::etag, this, &ProcessDirectoryJob::etag);
  1658. _discoveryData->_currentlyActiveJobs++;
  1659. _pendingAsyncJobs++;
  1660. connect(serverJob, &DiscoverySingleDirectoryJob::finished, this, [this, serverJob](const auto &results) {
  1661. if (_dirItem) {
  1662. _dirItem->_isFileDropDetected = serverJob->isFileDropDetected();
  1663. _dirItem->_isEncryptedMetadataNeedUpdate = serverJob->encryptedMetadataNeedUpdate();
  1664. qCInfo(lcDisco) << "serverJob has finished for folder:" << _dirItem->_file << " and it has _isFileDropDetected:" << true;
  1665. }
  1666. _discoveryData->_currentlyActiveJobs--;
  1667. _pendingAsyncJobs--;
  1668. if (results) {
  1669. _serverNormalQueryEntries = *results;
  1670. _serverQueryDone = true;
  1671. if (!serverJob->_dataFingerprint.isEmpty() && _discoveryData->_dataFingerprint.isEmpty())
  1672. _discoveryData->_dataFingerprint = serverJob->_dataFingerprint;
  1673. if (_localQueryDone)
  1674. this->process();
  1675. } else {
  1676. auto code = results.error().code;
  1677. qCWarning(lcDisco) << "Server error in directory" << _currentFolder._server << code;
  1678. if (_dirItem && code >= 403) {
  1679. // In case of an HTTP error, we ignore that directory
  1680. // 403 Forbidden can be sent by the server if the file firewall is active.
  1681. // A file or directory should be ignored and sync must continue. See #3490
  1682. // The server usually replies with the custom "503 Storage not available"
  1683. // if some path is temporarily unavailable. But in some cases a standard 503
  1684. // is returned too. Thus we can't distinguish the two and will treat any
  1685. // 503 as request to ignore the folder. See #3113 #2884.
  1686. // Similarly, the server might also return 404 or 50x in case of bugs. #7199 #7586
  1687. _dirItem->_instruction = CSYNC_INSTRUCTION_IGNORE;
  1688. _dirItem->_errorString = results.error().message;
  1689. emit this->finished();
  1690. } else {
  1691. // Fatal for the root job since it has no SyncFileItem, or for the network errors
  1692. emit _discoveryData->fatalError(tr("Server replied with an error while reading directory \"%1\" : %2")
  1693. .arg(_currentFolder._server, results.error().message), ErrorCategory::NetworkError);
  1694. }
  1695. }
  1696. });
  1697. connect(serverJob, &DiscoverySingleDirectoryJob::firstDirectoryPermissions, this,
  1698. [this](const RemotePermissions &perms) { _rootPermissions = perms; });
  1699. serverJob->start();
  1700. return serverJob;
  1701. }
  1702. void ProcessDirectoryJob::startAsyncLocalQuery()
  1703. {
  1704. QString localPath = _discoveryData->_localDir + _currentFolder._local;
  1705. auto localJob = new DiscoverySingleLocalDirectoryJob(_discoveryData->_account, localPath, _discoveryData->_syncOptions._vfs.data());
  1706. _discoveryData->_currentlyActiveJobs++;
  1707. _pendingAsyncJobs++;
  1708. connect(localJob, &DiscoverySingleLocalDirectoryJob::itemDiscovered, _discoveryData, &DiscoveryPhase::itemDiscovered);
  1709. connect(localJob, &DiscoverySingleLocalDirectoryJob::childIgnored, this, [this](bool b) {
  1710. _childIgnored = b;
  1711. });
  1712. connect(localJob, &DiscoverySingleLocalDirectoryJob::finishedFatalError, this, [this](const QString &msg) {
  1713. _discoveryData->_currentlyActiveJobs--;
  1714. _pendingAsyncJobs--;
  1715. if (_serverJob)
  1716. _serverJob->abort();
  1717. emit _discoveryData->fatalError(msg, ErrorCategory::NetworkError);
  1718. });
  1719. connect(localJob, &DiscoverySingleLocalDirectoryJob::finishedNonFatalError, this, [this](const QString &msg) {
  1720. _discoveryData->_currentlyActiveJobs--;
  1721. _pendingAsyncJobs--;
  1722. if (_dirItem) {
  1723. _dirItem->_instruction = CSYNC_INSTRUCTION_IGNORE;
  1724. _dirItem->_errorString = msg;
  1725. emit this->finished();
  1726. } else {
  1727. // Fatal for the root job since it has no SyncFileItem
  1728. emit _discoveryData->fatalError(msg, ErrorCategory::GenericError);
  1729. }
  1730. });
  1731. connect(localJob, &DiscoverySingleLocalDirectoryJob::finished, this, [this](const auto &results) {
  1732. _discoveryData->_currentlyActiveJobs--;
  1733. _pendingAsyncJobs--;
  1734. _localNormalQueryEntries = results;
  1735. _localQueryDone = true;
  1736. if (_serverQueryDone)
  1737. this->process();
  1738. });
  1739. QThreadPool *pool = QThreadPool::globalInstance();
  1740. pool->start(localJob); // QThreadPool takes ownership
  1741. }
  1742. bool ProcessDirectoryJob::isVfsWithSuffix() const
  1743. {
  1744. return _discoveryData->_syncOptions._vfs->mode() == Vfs::WithSuffix;
  1745. }
  1746. void ProcessDirectoryJob::computePinState(PinState parentState)
  1747. {
  1748. _pinState = parentState;
  1749. if (_queryLocal != ParentDontExist && QFileInfo::exists(_discoveryData->_localDir + _currentFolder._local)) {
  1750. if (auto state = _discoveryData->_syncOptions._vfs->pinState(_currentFolder._local)) // ouch! pin local or original?
  1751. _pinState = *state;
  1752. }
  1753. }
  1754. void ProcessDirectoryJob::setupDbPinStateActions(SyncJournalFileRecord &record)
  1755. {
  1756. // Only suffix-vfs uses the db for pin states.
  1757. // Other plugins will set localEntry._type according to the file's pin state.
  1758. if (!isVfsWithSuffix())
  1759. return;
  1760. auto pin = _discoveryData->_statedb->internalPinStates().rawForPath(record._path);
  1761. if (!pin || *pin == PinState::Inherited)
  1762. pin = _pinState;
  1763. // OnlineOnly hydrated files want to be dehydrated
  1764. if (record._type == ItemTypeFile && *pin == PinState::OnlineOnly)
  1765. record._type = ItemTypeVirtualFileDehydration;
  1766. // AlwaysLocal dehydrated files want to be hydrated
  1767. if (record._type == ItemTypeVirtualFile && *pin == PinState::AlwaysLocal)
  1768. record._type = ItemTypeVirtualFileDownload;
  1769. }
  1770. }