discovery.cpp 68 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540
  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 "discovery.h"
  15. #include "common/syncjournaldb.h"
  16. #include "syncfileitem.h"
  17. #include <QDebug>
  18. #include <algorithm>
  19. #include <set>
  20. #include <QTextCodec>
  21. #include "vio/csync_vio_local.h"
  22. #include <QFileInfo>
  23. #include <QFile>
  24. #include <QThreadPool>
  25. #include "common/checksums.h"
  26. #include "csync_exclude.h"
  27. #include "csync.h"
  28. namespace OCC {
  29. Q_LOGGING_CATEGORY(lcDisco, "sync.discovery", QtInfoMsg)
  30. void ProcessDirectoryJob::start()
  31. {
  32. qCInfo(lcDisco) << "STARTING" << _currentFolder._server << _queryServer << _currentFolder._local << _queryLocal;
  33. if (_queryServer == NormalQuery) {
  34. _serverJob = startAsyncServerQuery();
  35. } else {
  36. _serverQueryDone = true;
  37. }
  38. // Check whether a normal local query is even necessary
  39. if (_queryLocal == NormalQuery) {
  40. if (!_discoveryData->_shouldDiscoverLocaly(_currentFolder._local)
  41. && (_currentFolder._local == _currentFolder._original || !_discoveryData->_shouldDiscoverLocaly(_currentFolder._original))) {
  42. _queryLocal = ParentNotChanged;
  43. }
  44. }
  45. if (_queryLocal == NormalQuery) {
  46. startAsyncLocalQuery();
  47. } else {
  48. _localQueryDone = true;
  49. }
  50. if (_localQueryDone && _serverQueryDone) {
  51. process();
  52. }
  53. }
  54. void ProcessDirectoryJob::process()
  55. {
  56. ASSERT(_localQueryDone && _serverQueryDone);
  57. QString localDir;
  58. // Build lookup tables for local, remote and db entries.
  59. // For suffix-virtual files, the key will normally be the base file name
  60. // without the suffix.
  61. // However, if foo and foo.owncloud exists locally, there'll be "foo"
  62. // with local, db, server entries and "foo.owncloud" with only a local
  63. // entry.
  64. struct Entries {
  65. QString nameOverride;
  66. SyncJournalFileRecord dbEntry;
  67. RemoteInfo serverEntry;
  68. LocalInfo localEntry;
  69. };
  70. std::map<QString, Entries> entries;
  71. for (auto &e : _serverNormalQueryEntries) {
  72. entries[e.name].serverEntry = std::move(e);
  73. }
  74. _serverNormalQueryEntries.clear();
  75. // fetch all the name from the DB
  76. auto pathU8 = _currentFolder._original.toUtf8();
  77. if (!_discoveryData->_statedb->listFilesInPath(pathU8, [&](const SyncJournalFileRecord &rec) {
  78. auto name = pathU8.isEmpty() ? rec._path : QString::fromUtf8(rec._path.constData() + (pathU8.size() + 1));
  79. if (rec.isVirtualFile() && isVfsWithSuffix())
  80. chopVirtualFileSuffix(name);
  81. auto &dbEntry = entries[name].dbEntry;
  82. dbEntry = rec;
  83. setupDbPinStateActions(dbEntry);
  84. })) {
  85. dbError();
  86. return;
  87. }
  88. for (auto &e : _localNormalQueryEntries) {
  89. entries[e.name].localEntry = e;
  90. }
  91. if (isVfsWithSuffix()) {
  92. // For vfs-suffix the local data for suffixed files should usually be associated
  93. // with the non-suffixed name. Unless both names exist locally or there's
  94. // other data about the suffixed file.
  95. // This is done in a second path in order to not depend on the order of
  96. // _localNormalQueryEntries.
  97. for (auto &e : _localNormalQueryEntries) {
  98. if (!e.isVirtualFile)
  99. continue;
  100. auto &suffixedEntry = entries[e.name];
  101. bool hasOtherData = suffixedEntry.serverEntry.isValid() || suffixedEntry.dbEntry.isValid();
  102. auto nonvirtualName = e.name;
  103. chopVirtualFileSuffix(nonvirtualName);
  104. auto &nonvirtualEntry = entries[nonvirtualName];
  105. // If the non-suffixed entry has no data, move it
  106. if (!nonvirtualEntry.localEntry.isValid()) {
  107. std::swap(nonvirtualEntry.localEntry, suffixedEntry.localEntry);
  108. if (!hasOtherData)
  109. entries.erase(e.name);
  110. } else if (!hasOtherData) {
  111. // Normally a lone local suffixed file would be processed under the
  112. // unsuffixed name. In this special case it's under the suffixed name.
  113. // To avoid lots of special casing, make sure PathTuple::addName()
  114. // will be called with the unsuffixed name anyway.
  115. suffixedEntry.nameOverride = nonvirtualName;
  116. }
  117. }
  118. }
  119. _localNormalQueryEntries.clear();
  120. //
  121. // Iterate over entries and process them
  122. //
  123. for (const auto &f : entries) {
  124. const auto &e = f.second;
  125. PathTuple path;
  126. path = _currentFolder.addName(e.nameOverride.isEmpty() ? f.first : e.nameOverride);
  127. if (isVfsWithSuffix()) {
  128. // Without suffix vfs the paths would be good. But since the dbEntry and localEntry
  129. // can have different names from f.first when suffix vfs is on, make sure the
  130. // corresponding _original and _local paths are right.
  131. if (e.dbEntry.isValid()) {
  132. path._original = e.dbEntry._path;
  133. } else if (e.localEntry.isVirtualFile) {
  134. // We don't have a db entry - but it should be at this path
  135. path._original = PathTuple::pathAppend(_currentFolder._original, e.localEntry.name);
  136. }
  137. if (e.localEntry.isValid()) {
  138. path._local = PathTuple::pathAppend(_currentFolder._local, e.localEntry.name);
  139. } else if (e.dbEntry.isVirtualFile()) {
  140. // We don't have a local entry - but it should be at this path
  141. addVirtualFileSuffix(path._local);
  142. }
  143. }
  144. // On the server the path is mangled in case of E2EE
  145. if (!e.serverEntry.e2eMangledName.isEmpty()) {
  146. path._server = e.serverEntry.e2eMangledName;
  147. }
  148. // If the filename starts with a . we consider it a hidden file
  149. // For windows, the hidden state is also discovered within the vio
  150. // local stat function.
  151. // Recall file shall not be ignored (#4420)
  152. bool isHidden = e.localEntry.isHidden || (f.first[0] == '.' && f.first != QLatin1String(".sys.admin#recall#"));
  153. if (handleExcluded(path._target, e.localEntry.name,
  154. e.localEntry.isDirectory || e.serverEntry.isDirectory, isHidden,
  155. e.localEntry.isSymLink))
  156. continue;
  157. if (_queryServer == InBlackList || _discoveryData->isInSelectiveSyncBlackList(path._original)) {
  158. processBlacklisted(path, e.localEntry, e.dbEntry);
  159. continue;
  160. }
  161. processFile(std::move(path), e.localEntry, e.serverEntry, e.dbEntry);
  162. }
  163. QTimer::singleShot(0, _discoveryData, &DiscoveryPhase::scheduleMoreJobs);
  164. }
  165. bool ProcessDirectoryJob::handleExcluded(const QString &path, const QString &localName, bool isDirectory, bool isHidden, bool isSymlink)
  166. {
  167. auto excluded = _discoveryData->_excludes->traversalPatternMatch(path, isDirectory ? ItemTypeDirectory : ItemTypeFile);
  168. // FIXME: move to ExcludedFiles 's regexp ?
  169. bool isInvalidPattern = false;
  170. if (excluded == CSYNC_NOT_EXCLUDED && !_discoveryData->_invalidFilenameRx.isEmpty()) {
  171. if (path.contains(_discoveryData->_invalidFilenameRx)) {
  172. excluded = CSYNC_FILE_EXCLUDE_INVALID_CHAR;
  173. isInvalidPattern = true;
  174. }
  175. }
  176. if (excluded == CSYNC_NOT_EXCLUDED && _discoveryData->_ignoreHiddenFiles && isHidden) {
  177. excluded = CSYNC_FILE_EXCLUDE_HIDDEN;
  178. }
  179. if (excluded == CSYNC_NOT_EXCLUDED && !localName.isEmpty()
  180. && _discoveryData->_serverBlacklistedFiles.contains(localName)) {
  181. excluded = CSYNC_FILE_EXCLUDE_SERVER_BLACKLISTED;
  182. isInvalidPattern = true;
  183. }
  184. auto localCodec = QTextCodec::codecForLocale();
  185. if (localCodec->mibEnum() != 106) {
  186. // If the locale codec is not UTF-8, we must check that the filename from the server can
  187. // be encoded in the local file system.
  188. //
  189. // We cannot use QTextCodec::canEncode() since that can incorrectly return true, see
  190. // https://bugreports.qt.io/browse/QTBUG-6925.
  191. QTextEncoder encoder(localCodec, QTextCodec::ConvertInvalidToNull);
  192. if (encoder.fromUnicode(path).contains('\0')) {
  193. qCWarning(lcDisco) << "Cannot encode " << path << " to local encoding " << localCodec->name();
  194. excluded = CSYNC_FILE_EXCLUDE_CANNOT_ENCODE;
  195. }
  196. }
  197. if (excluded == CSYNC_NOT_EXCLUDED && !isSymlink) {
  198. return false;
  199. } else if (excluded == CSYNC_FILE_SILENTLY_EXCLUDED || excluded == CSYNC_FILE_EXCLUDE_AND_REMOVE) {
  200. emit _discoveryData->silentlyExcluded(path);
  201. return true;
  202. }
  203. auto item = SyncFileItemPtr::create();
  204. item->_file = path;
  205. item->_originalFile = path;
  206. item->_instruction = CSYNC_INSTRUCTION_IGNORE;
  207. if (isSymlink) {
  208. /* Symbolic links are ignored. */
  209. item->_errorString = tr("Symbolic links are not supported in syncing.");
  210. } else {
  211. switch (excluded) {
  212. case CSYNC_NOT_EXCLUDED:
  213. case CSYNC_FILE_SILENTLY_EXCLUDED:
  214. case CSYNC_FILE_EXCLUDE_AND_REMOVE:
  215. qFatal("These were handled earlier");
  216. case CSYNC_FILE_EXCLUDE_LIST:
  217. item->_errorString = tr("File is listed on the ignore list.");
  218. break;
  219. case CSYNC_FILE_EXCLUDE_INVALID_CHAR:
  220. if (item->_file.endsWith('.')) {
  221. item->_errorString = tr("File names ending with a period are not supported on this file system.");
  222. } else {
  223. char invalid = '\0';
  224. foreach (char x, QByteArray("\\:?*\"<>|")) {
  225. if (item->_file.contains(x)) {
  226. invalid = x;
  227. break;
  228. }
  229. }
  230. if (invalid) {
  231. item->_errorString = tr("File names containing the character '%1' are not supported on this file system.")
  232. .arg(QLatin1Char(invalid));
  233. }
  234. if (isInvalidPattern) {
  235. item->_errorString = tr("File name contains at least one invalid character");
  236. } else {
  237. item->_errorString = tr("The file name is a reserved name on this file system.");
  238. }
  239. }
  240. break;
  241. case CSYNC_FILE_EXCLUDE_TRAILING_SPACE:
  242. item->_errorString = tr("Filename contains trailing spaces.");
  243. break;
  244. case CSYNC_FILE_EXCLUDE_LONG_FILENAME:
  245. item->_errorString = tr("Filename is too long.");
  246. break;
  247. case CSYNC_FILE_EXCLUDE_HIDDEN:
  248. item->_errorString = tr("File/Folder is ignored because it's hidden.");
  249. break;
  250. case CSYNC_FILE_EXCLUDE_STAT_FAILED:
  251. item->_errorString = tr("Stat failed.");
  252. break;
  253. case CSYNC_FILE_EXCLUDE_CONFLICT:
  254. item->_errorString = tr("Conflict: Server version downloaded, local copy renamed and not uploaded.");
  255. item->_status = SyncFileItem::Conflict;
  256. break;
  257. case CSYNC_FILE_EXCLUDE_CANNOT_ENCODE:
  258. item->_errorString = tr("The filename cannot be encoded on your file system.");
  259. break;
  260. case CSYNC_FILE_EXCLUDE_SERVER_BLACKLISTED:
  261. item->_errorString = tr("The filename is blacklisted on the server.");
  262. break;
  263. }
  264. }
  265. _childIgnored = true;
  266. emit _discoveryData->itemDiscovered(item);
  267. return true;
  268. }
  269. void ProcessDirectoryJob::processFile(PathTuple path,
  270. const LocalInfo &localEntry, const RemoteInfo &serverEntry,
  271. const SyncJournalFileRecord &dbEntry)
  272. {
  273. const char *hasServer = serverEntry.isValid() ? "true" : _queryServer == ParentNotChanged ? "db" : "false";
  274. const char *hasLocal = localEntry.isValid() ? "true" : _queryLocal == ParentNotChanged ? "db" : "false";
  275. qCInfo(lcDisco).nospace() << "Processing " << path._original
  276. << " | valid: " << dbEntry.isValid() << "/" << hasLocal << "/" << hasServer
  277. << " | mtime: " << dbEntry._modtime << "/" << localEntry.modtime << "/" << serverEntry.modtime
  278. << " | size: " << dbEntry._fileSize << "/" << localEntry.size << "/" << serverEntry.size
  279. << " | etag: " << dbEntry._etag << "//" << serverEntry.etag
  280. << " | checksum: " << dbEntry._checksumHeader << "//" << serverEntry.checksumHeader
  281. << " | perm: " << dbEntry._remotePerm << "//" << serverEntry.remotePerm
  282. << " | fileid: " << dbEntry._fileId << "//" << serverEntry.fileId
  283. << " | inode: " << dbEntry._inode << "/" << localEntry.inode << "/"
  284. << " | type: " << dbEntry._type << "/" << localEntry.type << "/" << (serverEntry.isDirectory ? ItemTypeDirectory : ItemTypeFile)
  285. << " | e2ee: " << dbEntry._isE2eEncrypted << "/" << serverEntry.isE2eEncrypted
  286. << " | e2eeMangledName: " << dbEntry._e2eMangledName << "/" << serverEntry.e2eMangledName;
  287. if (_discoveryData->isRenamed(path._original)) {
  288. qCDebug(lcDisco) << "Ignoring renamed";
  289. return; // Ignore this.
  290. }
  291. auto item = SyncFileItem::fromSyncJournalFileRecord(dbEntry);
  292. item->_file = path._target;
  293. item->_originalFile = path._original;
  294. item->_previousSize = dbEntry._fileSize;
  295. item->_previousModtime = dbEntry._modtime;
  296. // The item shall only have this type if the db request for the virtual download
  297. // was successful (like: no conflicting remote remove etc). This decision is done
  298. // either in processFileAnalyzeRemoteInfo() or further down here.
  299. if (item->_type == ItemTypeVirtualFileDownload)
  300. item->_type = ItemTypeVirtualFile;
  301. // Similarly db entries with a dehydration request denote a regular file
  302. // until the request is processed.
  303. if (item->_type == ItemTypeVirtualFileDehydration)
  304. item->_type = ItemTypeFile;
  305. // VFS suffixed files on the server are ignored
  306. if (isVfsWithSuffix()) {
  307. if (hasVirtualFileSuffix(serverEntry.name)
  308. || (localEntry.isVirtualFile && !dbEntry.isVirtualFile() && hasVirtualFileSuffix(dbEntry._path))) {
  309. item->_instruction = CSYNC_INSTRUCTION_IGNORE;
  310. item->_errorString = tr("File has extension reserved for virtual files.");
  311. _childIgnored = true;
  312. emit _discoveryData->itemDiscovered(item);
  313. return;
  314. }
  315. }
  316. if (serverEntry.isValid()) {
  317. processFileAnalyzeRemoteInfo(item, path, localEntry, serverEntry, dbEntry);
  318. return;
  319. }
  320. // Downloading a virtual file is like a server action and can happen even if
  321. // server-side nothing has changed
  322. // NOTE: Normally setting the VirtualFileDownload flag means that local and
  323. // remote will be rediscovered. This is just a fallback for a similar check
  324. // in processFileAnalyzeRemoteInfo().
  325. if (_queryServer == ParentNotChanged
  326. && dbEntry.isValid()
  327. && (dbEntry._type == ItemTypeVirtualFileDownload
  328. || localEntry.type == ItemTypeVirtualFileDownload)
  329. && (localEntry.isValid() || _queryLocal == ParentNotChanged)) {
  330. item->_direction = SyncFileItem::Down;
  331. item->_instruction = CSYNC_INSTRUCTION_SYNC;
  332. item->_type = ItemTypeVirtualFileDownload;
  333. }
  334. processFileAnalyzeLocalInfo(item, path, localEntry, serverEntry, dbEntry, _queryServer);
  335. }
  336. // Compute the checksum of the given file and assign the result in item->_checksumHeader
  337. // Returns true if the checksum was successfully computed
  338. static bool computeLocalChecksum(const QByteArray &header, const QString &path, const SyncFileItemPtr &item)
  339. {
  340. auto type = parseChecksumHeaderType(header);
  341. if (!type.isEmpty()) {
  342. // TODO: compute async?
  343. QByteArray checksum = ComputeChecksum::computeNowOnFile(path, type);
  344. if (!checksum.isEmpty()) {
  345. item->_checksumHeader = makeChecksumHeader(type, checksum);
  346. return true;
  347. }
  348. }
  349. return false;
  350. }
  351. void ProcessDirectoryJob::processFileAnalyzeRemoteInfo(
  352. const SyncFileItemPtr &item, PathTuple path, const LocalInfo &localEntry,
  353. const RemoteInfo &serverEntry, const SyncJournalFileRecord &dbEntry)
  354. {
  355. item->_checksumHeader = serverEntry.checksumHeader;
  356. item->_fileId = serverEntry.fileId;
  357. item->_remotePerm = serverEntry.remotePerm;
  358. item->_type = serverEntry.isDirectory ? ItemTypeDirectory : ItemTypeFile;
  359. item->_etag = serverEntry.etag;
  360. item->_directDownloadUrl = serverEntry.directDownloadUrl;
  361. item->_directDownloadCookies = serverEntry.directDownloadCookies;
  362. item->_encryptedFileName = serverEntry.e2eMangledName;
  363. item->_isEncrypted = serverEntry.isE2eEncrypted;
  364. // Check for missing server data
  365. {
  366. QStringList missingData;
  367. if (serverEntry.size == -1)
  368. missingData.append(tr("size"));
  369. if (serverEntry.remotePerm.isNull())
  370. missingData.append(tr("permissions"));
  371. if (serverEntry.etag.isEmpty())
  372. missingData.append(tr("etag"));
  373. if (serverEntry.fileId.isEmpty())
  374. missingData.append(tr("file id"));
  375. if (!missingData.isEmpty()) {
  376. item->_instruction = CSYNC_INSTRUCTION_ERROR;
  377. _childIgnored = true;
  378. item->_errorString = tr("server reported no %1").arg(missingData.join(QLatin1String(", ")));
  379. emit _discoveryData->itemDiscovered(item);
  380. return;
  381. }
  382. }
  383. // The file is known in the db already
  384. if (dbEntry.isValid()) {
  385. if (serverEntry.isDirectory != dbEntry.isDirectory()) {
  386. // If the type of the entity changed, it's like NEW, but
  387. // needs to delete the other entity first.
  388. item->_instruction = CSYNC_INSTRUCTION_TYPE_CHANGE;
  389. item->_direction = SyncFileItem::Down;
  390. item->_modtime = serverEntry.modtime;
  391. item->_size = serverEntry.size;
  392. } else if ((dbEntry._type == ItemTypeVirtualFileDownload || localEntry.type == ItemTypeVirtualFileDownload)
  393. && (localEntry.isValid() || _queryLocal == ParentNotChanged)) {
  394. // The above check for the localEntry existing is important. Otherwise it breaks
  395. // the case where a file is moved and simultaneously tagged for download in the db.
  396. item->_direction = SyncFileItem::Down;
  397. item->_instruction = CSYNC_INSTRUCTION_SYNC;
  398. item->_type = ItemTypeVirtualFileDownload;
  399. } else if (dbEntry._etag != serverEntry.etag) {
  400. item->_direction = SyncFileItem::Down;
  401. item->_modtime = serverEntry.modtime;
  402. item->_size = serverEntry.size;
  403. if (serverEntry.isDirectory) {
  404. ENFORCE(dbEntry.isDirectory());
  405. item->_instruction = CSYNC_INSTRUCTION_UPDATE_METADATA;
  406. } else if (!localEntry.isValid() && _queryLocal != ParentNotChanged) {
  407. // Deleted locally, changed on server
  408. item->_instruction = CSYNC_INSTRUCTION_NEW;
  409. } else {
  410. item->_instruction = CSYNC_INSTRUCTION_SYNC;
  411. }
  412. } else if (dbEntry._remotePerm != serverEntry.remotePerm || dbEntry._fileId != serverEntry.fileId) {
  413. item->_instruction = CSYNC_INSTRUCTION_UPDATE_METADATA;
  414. item->_direction = SyncFileItem::Down;
  415. } else {
  416. processFileAnalyzeLocalInfo(item, path, localEntry, serverEntry, dbEntry, ParentNotChanged);
  417. return;
  418. }
  419. processFileAnalyzeLocalInfo(item, path, localEntry, serverEntry, dbEntry, _queryServer);
  420. return;
  421. }
  422. // Unknown in db: new file on the server
  423. Q_ASSERT(!dbEntry.isValid());
  424. item->_instruction = CSYNC_INSTRUCTION_NEW;
  425. item->_direction = SyncFileItem::Down;
  426. item->_modtime = serverEntry.modtime;
  427. item->_size = serverEntry.size;
  428. auto postProcessServerNew = [=] () {
  429. auto tmp_path = path;
  430. if (item->isDirectory()) {
  431. _pendingAsyncJobs++;
  432. _discoveryData->checkSelectiveSyncNewFolder(tmp_path._server, serverEntry.remotePerm,
  433. [=](bool result) {
  434. --_pendingAsyncJobs;
  435. if (!result) {
  436. processFileAnalyzeLocalInfo(item, tmp_path, localEntry, serverEntry, dbEntry, _queryServer);
  437. }
  438. QTimer::singleShot(0, _discoveryData, &DiscoveryPhase::scheduleMoreJobs);
  439. });
  440. return;
  441. }
  442. // Turn new remote files into virtual files if the option is enabled.
  443. auto &opts = _discoveryData->_syncOptions;
  444. if (!localEntry.isValid()
  445. && item->_type == ItemTypeFile
  446. && opts._vfs->mode() != Vfs::Off
  447. && _pinState != PinState::AlwaysLocal) {
  448. item->_type = ItemTypeVirtualFile;
  449. if (isVfsWithSuffix())
  450. addVirtualFileSuffix(tmp_path._original);
  451. }
  452. processFileAnalyzeLocalInfo(item, tmp_path, localEntry, serverEntry, dbEntry, _queryServer);
  453. };
  454. // Potential NEW/NEW conflict is handled in AnalyzeLocal
  455. if (localEntry.isValid()) {
  456. postProcessServerNew();
  457. return;
  458. }
  459. // Not in db or locally: either new or a rename
  460. Q_ASSERT(!dbEntry.isValid() && !localEntry.isValid());
  461. // Check for renames (if there is a file with the same file id)
  462. bool done = false;
  463. bool async = false;
  464. // This function will be executed for every candidate
  465. auto renameCandidateProcessing = [&](const OCC::SyncJournalFileRecord &base) {
  466. if (done)
  467. return;
  468. if (!base.isValid())
  469. return;
  470. // Remote rename of a virtual file we have locally scheduled for download.
  471. if (base._type == ItemTypeVirtualFileDownload) {
  472. // We just consider this NEW but mark it for download.
  473. item->_type = ItemTypeVirtualFileDownload;
  474. done = true;
  475. return;
  476. }
  477. // Remote rename targets a file that shall be locally dehydrated.
  478. if (base._type == ItemTypeVirtualFileDehydration) {
  479. // Don't worry about the rename, just consider it DELETE + NEW(virtual)
  480. done = true;
  481. return;
  482. }
  483. // Some things prohibit rename detection entirely.
  484. // Since we don't do the same checks again in reconcile, we can't
  485. // just skip the candidate, but have to give up completely.
  486. if (base.isDirectory() != item->isDirectory()) {
  487. qCInfo(lcDisco, "file types different, not a rename");
  488. done = true;
  489. return;
  490. }
  491. if (!serverEntry.isDirectory && base._etag != serverEntry.etag) {
  492. /* File with different etag, don't do a rename, but download the file again */
  493. qCInfo(lcDisco, "file etag different, not a rename");
  494. done = true;
  495. return;
  496. }
  497. // Now we know there is a sane rename candidate.
  498. QString originalPath = QString::fromUtf8(base._path);
  499. if (_discoveryData->isRenamed(originalPath)) {
  500. qCInfo(lcDisco, "folder already has a rename entry, skipping");
  501. return;
  502. }
  503. /* A remote rename can also mean Encryption Mangled Name.
  504. * if we find one of those in the database, we ignore it.
  505. */
  506. if (!base._e2eMangledName.isEmpty()) {
  507. qCWarning(lcDisco, "Encrypted file can not rename");
  508. done = true;
  509. return;
  510. }
  511. QString originalPathAdjusted = _discoveryData->adjustRenamedPath(originalPath, SyncFileItem::Up);
  512. if (!base.isDirectory()) {
  513. csync_file_stat_t buf;
  514. if (csync_vio_local_stat(_discoveryData->_localDir + originalPathAdjusted, &buf)) {
  515. qCInfo(lcDisco) << "Local file does not exist anymore." << originalPathAdjusted;
  516. return;
  517. }
  518. // NOTE: This prohibits some VFS renames from being detected since
  519. // suffix-file size is different from the db size. That's ok, they'll DELETE+NEW.
  520. if (buf.modtime != base._modtime || buf.size != base._fileSize || buf.type == ItemTypeDirectory) {
  521. qCInfo(lcDisco) << "File has changed locally, not a rename." << originalPath;
  522. return;
  523. }
  524. } else {
  525. if (!QFileInfo(_discoveryData->_localDir + originalPathAdjusted).isDir()) {
  526. qCInfo(lcDisco) << "Local directory does not exist anymore." << originalPathAdjusted;
  527. return;
  528. }
  529. }
  530. // Renames of virtuals are possible
  531. if (base.isVirtualFile()) {
  532. item->_type = ItemTypeVirtualFile;
  533. }
  534. bool wasDeletedOnServer = _discoveryData->findAndCancelDeletedJob(originalPath).first;
  535. auto postProcessRename = [this, item, base, originalPath](PathTuple &path) {
  536. auto adjustedOriginalPath = _discoveryData->adjustRenamedPath(originalPath, SyncFileItem::Up);
  537. _discoveryData->_renamedItemsRemote.insert(originalPath, path._target);
  538. item->_modtime = base._modtime;
  539. item->_inode = base._inode;
  540. item->_instruction = CSYNC_INSTRUCTION_RENAME;
  541. item->_direction = SyncFileItem::Down;
  542. item->_renameTarget = path._target;
  543. item->_file = adjustedOriginalPath;
  544. item->_originalFile = originalPath;
  545. path._original = originalPath;
  546. path._local = adjustedOriginalPath;
  547. qCInfo(lcDisco) << "Rename detected (down) " << item->_file << " -> " << item->_renameTarget;
  548. };
  549. if (wasDeletedOnServer) {
  550. postProcessRename(path);
  551. done = true;
  552. } else {
  553. // we need to make a request to the server to know that the original file is deleted on the server
  554. _pendingAsyncJobs++;
  555. auto job = new RequestEtagJob(_discoveryData->_account, originalPath, this);
  556. connect(job, &RequestEtagJob::finishedWithResult, this, [=](const HttpResult<QString> &etag) {
  557. auto tmp_path = path;
  558. _pendingAsyncJobs--;
  559. QTimer::singleShot(0, _discoveryData, &DiscoveryPhase::scheduleMoreJobs);
  560. if (etag || etag.error().code != 404 ||
  561. // Somehow another item claimed this original path, consider as if it existed
  562. _discoveryData->isRenamed(originalPath)) {
  563. // If the file exist or if there is another error, consider it is a new file.
  564. postProcessServerNew();
  565. return;
  566. }
  567. // The file do not exist, it is a rename
  568. // In case the deleted item was discovered in parallel
  569. _discoveryData->findAndCancelDeletedJob(originalPath);
  570. postProcessRename(tmp_path);
  571. processFileFinalize(item, tmp_path, item->isDirectory(), item->_instruction == CSYNC_INSTRUCTION_RENAME ? NormalQuery : ParentDontExist, _queryServer);
  572. });
  573. job->start();
  574. done = true; // Ideally, if the origin still exist on the server, we should continue searching... but that'd be difficult
  575. async = true;
  576. }
  577. };
  578. if (!_discoveryData->_statedb->getFileRecordsByFileId(serverEntry.fileId, renameCandidateProcessing)) {
  579. dbError();
  580. return;
  581. }
  582. if (async) {
  583. return; // We went async
  584. }
  585. if (item->_instruction == CSYNC_INSTRUCTION_NEW) {
  586. postProcessServerNew();
  587. return;
  588. }
  589. processFileAnalyzeLocalInfo(item, path, localEntry, serverEntry, dbEntry, _queryServer);
  590. }
  591. void ProcessDirectoryJob::processFileAnalyzeLocalInfo(
  592. const SyncFileItemPtr &item, PathTuple path, const LocalInfo &localEntry,
  593. const RemoteInfo &serverEntry, const SyncJournalFileRecord &dbEntry, QueryMode recurseQueryServer)
  594. {
  595. bool noServerEntry = (_queryServer != ParentNotChanged && !serverEntry.isValid())
  596. || (_queryServer == ParentNotChanged && !dbEntry.isValid());
  597. if (noServerEntry)
  598. recurseQueryServer = ParentDontExist;
  599. bool serverModified = item->_instruction == CSYNC_INSTRUCTION_NEW || item->_instruction == CSYNC_INSTRUCTION_SYNC
  600. || item->_instruction == CSYNC_INSTRUCTION_RENAME || item->_instruction == CSYNC_INSTRUCTION_TYPE_CHANGE;
  601. // Decay server modifications to UPDATE_METADATA if the local virtual exists
  602. bool hasLocalVirtual = localEntry.isVirtualFile || (_queryLocal == ParentNotChanged && dbEntry.isVirtualFile());
  603. bool virtualFileDownload = item->_type == ItemTypeVirtualFileDownload;
  604. if (serverModified && !virtualFileDownload && hasLocalVirtual) {
  605. item->_instruction = CSYNC_INSTRUCTION_UPDATE_METADATA;
  606. serverModified = false;
  607. item->_type = ItemTypeVirtualFile;
  608. }
  609. if (dbEntry.isVirtualFile() && !virtualFileDownload)
  610. item->_type = ItemTypeVirtualFile;
  611. _childModified |= serverModified;
  612. auto finalize = [&] {
  613. bool recurse = item->isDirectory() || localEntry.isDirectory || serverEntry.isDirectory;
  614. // Even if we have a local directory: If the remote is a file that's propagated as a
  615. // conflict we don't need to recurse into it. (local c1.owncloud, c1/ ; remote: c1)
  616. if (item->_instruction == CSYNC_INSTRUCTION_CONFLICT && !item->isDirectory())
  617. recurse = false;
  618. if (_queryLocal != NormalQuery && _queryServer != NormalQuery)
  619. recurse = false;
  620. auto recurseQueryLocal = _queryLocal == ParentNotChanged ? ParentNotChanged : localEntry.isDirectory || item->_instruction == CSYNC_INSTRUCTION_RENAME ? NormalQuery : ParentDontExist;
  621. processFileFinalize(item, path, recurse, recurseQueryLocal, recurseQueryServer);
  622. };
  623. if (!localEntry.isValid()) {
  624. if (_queryLocal == ParentNotChanged && dbEntry.isValid()) {
  625. // Not modified locally (ParentNotChanged)
  626. if (noServerEntry) {
  627. // not on the server: Removed on the server, delete locally
  628. item->_instruction = CSYNC_INSTRUCTION_REMOVE;
  629. item->_direction = SyncFileItem::Down;
  630. } else if (dbEntry._type == ItemTypeVirtualFileDehydration) {
  631. // dehydration requested
  632. item->_direction = SyncFileItem::Down;
  633. item->_instruction = CSYNC_INSTRUCTION_SYNC;
  634. item->_type = ItemTypeVirtualFileDehydration;
  635. }
  636. } else if (noServerEntry) {
  637. // Not locally, not on the server. The entry is stale!
  638. qCInfo(lcDisco) << "Stale DB entry";
  639. _discoveryData->_statedb->deleteFileRecord(path._original, true);
  640. return;
  641. } else if (dbEntry._type == ItemTypeVirtualFile && isVfsWithSuffix()) {
  642. // If the virtual file is removed, recreate it.
  643. // This is a precaution since the suffix files don't look like the real ones
  644. // and we don't want users to accidentally delete server data because they
  645. // might not expect that deleting the placeholder will have a remote effect.
  646. item->_instruction = CSYNC_INSTRUCTION_NEW;
  647. item->_direction = SyncFileItem::Down;
  648. item->_type = ItemTypeVirtualFile;
  649. } else if (!serverModified) {
  650. // Removed locally: also remove on the server.
  651. if (!dbEntry._serverHasIgnoredFiles) {
  652. item->_instruction = CSYNC_INSTRUCTION_REMOVE;
  653. item->_direction = SyncFileItem::Up;
  654. }
  655. }
  656. finalize();
  657. return;
  658. }
  659. Q_ASSERT(localEntry.isValid());
  660. item->_inode = localEntry.inode;
  661. if (dbEntry.isValid()) {
  662. bool typeChange = localEntry.isDirectory != dbEntry.isDirectory();
  663. if (!typeChange && localEntry.isVirtualFile) {
  664. if (noServerEntry) {
  665. item->_instruction = CSYNC_INSTRUCTION_REMOVE;
  666. item->_direction = SyncFileItem::Down;
  667. } else if (!dbEntry.isVirtualFile() && isVfsWithSuffix()) {
  668. // If we find what looks to be a spurious "abc.owncloud" the base file "abc"
  669. // might have been renamed to that. Make sure that the base file is not
  670. // deleted from the server.
  671. if (dbEntry._modtime == localEntry.modtime && dbEntry._fileSize == localEntry.size) {
  672. qCInfo(lcDisco) << "Base file was renamed to virtual file:" << item->_file;
  673. item->_direction = SyncFileItem::Down;
  674. item->_instruction = CSYNC_INSTRUCTION_SYNC;
  675. item->_type = ItemTypeVirtualFileDehydration;
  676. addVirtualFileSuffix(item->_file);
  677. item->_renameTarget = item->_file;
  678. } else {
  679. qCInfo(lcDisco) << "Virtual file with non-virtual db entry, ignoring:" << item->_file;
  680. item->_instruction = CSYNC_INSTRUCTION_IGNORE;
  681. }
  682. }
  683. } else if (!typeChange && ((dbEntry._modtime == localEntry.modtime && dbEntry._fileSize == localEntry.size) || localEntry.isDirectory)) {
  684. // Local file unchanged.
  685. if (noServerEntry) {
  686. item->_instruction = CSYNC_INSTRUCTION_REMOVE;
  687. item->_direction = SyncFileItem::Down;
  688. } else if (dbEntry._type == ItemTypeVirtualFileDehydration || localEntry.type == ItemTypeVirtualFileDehydration) {
  689. item->_direction = SyncFileItem::Down;
  690. item->_instruction = CSYNC_INSTRUCTION_SYNC;
  691. item->_type = ItemTypeVirtualFileDehydration;
  692. } else if (!serverModified
  693. && (dbEntry._inode != localEntry.inode
  694. || _discoveryData->_syncOptions._vfs->needsMetadataUpdate(*item))) {
  695. item->_instruction = CSYNC_INSTRUCTION_UPDATE_METADATA;
  696. item->_direction = SyncFileItem::Down;
  697. }
  698. } else if (!typeChange && isVfsWithSuffix()
  699. && dbEntry.isVirtualFile() && !localEntry.isVirtualFile
  700. && dbEntry._inode == localEntry.inode
  701. && dbEntry._modtime == localEntry.modtime
  702. && localEntry.size == 1) {
  703. // A suffix vfs file can be downloaded by renaming it to remove the suffix.
  704. // This check leaks some details of VfsSuffix, particularly the size of placeholders.
  705. item->_direction = SyncFileItem::Down;
  706. if (noServerEntry) {
  707. item->_instruction = CSYNC_INSTRUCTION_REMOVE;
  708. item->_type = ItemTypeFile;
  709. } else {
  710. item->_instruction = CSYNC_INSTRUCTION_SYNC;
  711. item->_type = ItemTypeVirtualFileDownload;
  712. item->_previousSize = 1;
  713. }
  714. } else if (serverModified
  715. || (isVfsWithSuffix() && dbEntry.isVirtualFile())) {
  716. // There's a local change and a server change: Conflict!
  717. // Alternatively, this might be a suffix-file that's virtual in the db but
  718. // not locally. These also become conflicts. For in-place placeholders that's
  719. // not necessary: they could be replaced by real files and should then trigger
  720. // a regular SYNC upwards when there's no server change.
  721. processFileConflict(item, path, localEntry, serverEntry, dbEntry);
  722. } else if (typeChange) {
  723. item->_instruction = CSYNC_INSTRUCTION_TYPE_CHANGE;
  724. item->_direction = SyncFileItem::Up;
  725. item->_checksumHeader.clear();
  726. item->_size = localEntry.size;
  727. item->_modtime = localEntry.modtime;
  728. item->_type = localEntry.isDirectory ? ItemTypeDirectory : ItemTypeFile;
  729. _childModified = true;
  730. } else {
  731. // Local file was changed
  732. item->_instruction = CSYNC_INSTRUCTION_SYNC;
  733. if (noServerEntry) {
  734. // Special case! deleted on server, modified on client, the instruction is then NEW
  735. item->_instruction = CSYNC_INSTRUCTION_NEW;
  736. }
  737. item->_direction = SyncFileItem::Up;
  738. item->_checksumHeader.clear();
  739. item->_size = localEntry.size;
  740. item->_modtime = localEntry.modtime;
  741. _childModified = true;
  742. // Checksum comparison at this stage is only enabled for .eml files,
  743. // check #4754 #4755
  744. bool isEmlFile = path._original.endsWith(QLatin1String(".eml"), Qt::CaseInsensitive);
  745. if (isEmlFile && dbEntry._fileSize == localEntry.size && !dbEntry._checksumHeader.isEmpty()) {
  746. if (computeLocalChecksum(dbEntry._checksumHeader, _discoveryData->_localDir + path._local, item)
  747. && item->_checksumHeader == dbEntry._checksumHeader) {
  748. qCInfo(lcDisco) << "NOTE: Checksums are identical, file did not actually change: " << path._local;
  749. item->_instruction = CSYNC_INSTRUCTION_UPDATE_METADATA;
  750. }
  751. }
  752. }
  753. finalize();
  754. return;
  755. }
  756. Q_ASSERT(!dbEntry.isValid());
  757. if (localEntry.isVirtualFile && !noServerEntry) {
  758. // Somehow there is a missing DB entry while the virtual file already exists.
  759. // The instruction should already be set correctly.
  760. ASSERT(item->_instruction == CSYNC_INSTRUCTION_UPDATE_METADATA);
  761. ASSERT(item->_type == ItemTypeVirtualFile);
  762. finalize();
  763. return;
  764. } else if (serverModified) {
  765. processFileConflict(item, path, localEntry, serverEntry, dbEntry);
  766. finalize();
  767. return;
  768. }
  769. // New local file or rename
  770. item->_instruction = CSYNC_INSTRUCTION_NEW;
  771. item->_direction = SyncFileItem::Up;
  772. item->_checksumHeader.clear();
  773. item->_size = localEntry.size;
  774. item->_modtime = localEntry.modtime;
  775. item->_type = localEntry.isDirectory ? ItemTypeDirectory : localEntry.isVirtualFile ? ItemTypeVirtualFile : ItemTypeFile;
  776. _childModified = true;
  777. auto postProcessLocalNew = [item, localEntry, this]() {
  778. if (localEntry.isVirtualFile) {
  779. // Remove the spurious file if it looks like a placeholder file
  780. // (we know placeholder files contain " ")
  781. if (localEntry.size <= 1) {
  782. qCWarning(lcDisco) << "Wiping virtual file without db entry for" << _currentFolder._local + "/" + localEntry.name;
  783. item->_instruction = CSYNC_INSTRUCTION_REMOVE;
  784. item->_direction = SyncFileItem::Down;
  785. } else {
  786. qCWarning(lcDisco) << "Virtual file without db entry for" << _currentFolder._local << localEntry.name
  787. << "but looks odd, keeping";
  788. item->_instruction = CSYNC_INSTRUCTION_IGNORE;
  789. }
  790. }
  791. };
  792. // Check if it is a move
  793. OCC::SyncJournalFileRecord base;
  794. if (!_discoveryData->_statedb->getFileRecordByInode(localEntry.inode, &base)) {
  795. dbError();
  796. return;
  797. }
  798. const auto originalPath = QString::fromUtf8(base._path);
  799. // Function to gradually check conditions for accepting a move-candidate
  800. auto moveCheck = [&]() {
  801. if (!base.isValid()) {
  802. qCInfo(lcDisco) << "Not a move, no item in db with inode" << localEntry.inode;
  803. return false;
  804. }
  805. if (base.isDirectory() != item->isDirectory()) {
  806. qCInfo(lcDisco) << "Not a move, types don't match" << base._type << item->_type << localEntry.type;
  807. return false;
  808. }
  809. // Directories and virtual files don't need size/mtime equality
  810. if (!localEntry.isDirectory && !base.isVirtualFile()
  811. && (base._modtime != localEntry.modtime || base._fileSize != localEntry.size)) {
  812. qCInfo(lcDisco) << "Not a move, mtime or size differs, "
  813. << "modtime:" << base._modtime << localEntry.modtime << ", "
  814. << "size:" << base._fileSize << localEntry.size;
  815. return false;
  816. }
  817. // The old file must have been deleted.
  818. if (QFile::exists(_discoveryData->_localDir + originalPath)
  819. // Exception: If the rename changes case only (like "foo" -> "Foo") the
  820. // old filename might still point to the same file.
  821. && !(Utility::fsCasePreserving()
  822. && originalPath.compare(path._local, Qt::CaseInsensitive) == 0
  823. && originalPath != path._local)) {
  824. qCInfo(lcDisco) << "Not a move, base file still exists at" << originalPath;
  825. return false;
  826. }
  827. // Verify the checksum where possible
  828. if (!base._checksumHeader.isEmpty() && item->_type == ItemTypeFile && base._type == ItemTypeFile) {
  829. if (computeLocalChecksum(base._checksumHeader, _discoveryData->_localDir + path._original, item)) {
  830. qCInfo(lcDisco) << "checking checksum of potential rename " << path._original << item->_checksumHeader << base._checksumHeader;
  831. if (item->_checksumHeader != base._checksumHeader) {
  832. qCInfo(lcDisco) << "Not a move, checksums differ";
  833. return false;
  834. }
  835. }
  836. }
  837. if (_discoveryData->isRenamed(originalPath)) {
  838. qCInfo(lcDisco) << "Not a move, base path already renamed";
  839. return false;
  840. }
  841. return true;
  842. };
  843. // If it's not a move it's just a local-NEW
  844. if (!moveCheck()) {
  845. postProcessLocalNew();
  846. finalize();
  847. return;
  848. }
  849. // Check local permission if we are allowed to put move the file here
  850. // Technically we should use the permissions from the server, but we'll assume it is the same
  851. auto movePerms = checkMovePermissions(base._remotePerm, originalPath, item->isDirectory());
  852. if (!movePerms.sourceOk || !movePerms.destinationOk) {
  853. qCInfo(lcDisco) << "Move without permission to rename base file, "
  854. << "source:" << movePerms.sourceOk
  855. << ", target:" << movePerms.destinationOk
  856. << ", targetNew:" << movePerms.destinationNewOk;
  857. // If we can create the destination, do that.
  858. // Permission errors on the destination will be handled by checkPermissions later.
  859. postProcessLocalNew();
  860. finalize();
  861. // If the destination upload will work, we're fine with the source deletion.
  862. // If the source deletion can't work, checkPermissions will error.
  863. if (movePerms.destinationNewOk)
  864. return;
  865. // Here we know the new location can't be uploaded: must prevent the source delete.
  866. // Two cases: either the source item was already processed or not.
  867. auto wasDeletedOnClient = _discoveryData->findAndCancelDeletedJob(originalPath);
  868. if (wasDeletedOnClient.first) {
  869. // More complicated. The REMOVE is canceled. Restore will happen next sync.
  870. qCInfo(lcDisco) << "Undid remove instruction on source" << originalPath;
  871. _discoveryData->_statedb->deleteFileRecord(originalPath, true);
  872. _discoveryData->_statedb->schedulePathForRemoteDiscovery(originalPath);
  873. _discoveryData->_anotherSyncNeeded = true;
  874. } else {
  875. // Signal to future checkPermissions() to forbid the REMOVE and set to restore instead
  876. qCInfo(lcDisco) << "Preventing future remove on source" << originalPath;
  877. _discoveryData->_forbiddenDeletes[originalPath + '/'] = true;
  878. }
  879. return;
  880. }
  881. auto wasDeletedOnClient = _discoveryData->findAndCancelDeletedJob(originalPath);
  882. auto processRename = [item, originalPath, base, this](PathTuple &path) {
  883. auto adjustedOriginalPath = _discoveryData->adjustRenamedPath(originalPath, SyncFileItem::Down);
  884. _discoveryData->_renamedItemsLocal.insert(originalPath, path._target);
  885. item->_renameTarget = path._target;
  886. path._server = adjustedOriginalPath;
  887. item->_file = path._server;
  888. path._original = originalPath;
  889. item->_originalFile = path._original;
  890. item->_modtime = base._modtime;
  891. item->_inode = base._inode;
  892. item->_instruction = CSYNC_INSTRUCTION_RENAME;
  893. item->_direction = SyncFileItem::Up;
  894. item->_fileId = base._fileId;
  895. item->_remotePerm = base._remotePerm;
  896. item->_etag = base._etag;
  897. item->_type = base._type;
  898. // Discard any download/dehydrate tags on the base file.
  899. // They could be preserved and honored in a follow-up sync,
  900. // but it complicates handling a lot and will happen rarely.
  901. if (item->_type == ItemTypeVirtualFileDownload)
  902. item->_type = ItemTypeVirtualFile;
  903. if (item->_type == ItemTypeVirtualFileDehydration)
  904. item->_type = ItemTypeFile;
  905. qCInfo(lcDisco) << "Rename detected (up) " << item->_file << " -> " << item->_renameTarget;
  906. };
  907. if (wasDeletedOnClient.first) {
  908. recurseQueryServer = wasDeletedOnClient.second == base._etag ? ParentNotChanged : NormalQuery;
  909. processRename(path);
  910. } else {
  911. // We must query the server to know if the etag has not changed
  912. _pendingAsyncJobs++;
  913. QString serverOriginalPath = _discoveryData->adjustRenamedPath(originalPath, SyncFileItem::Down);
  914. if (base.isVirtualFile() && isVfsWithSuffix())
  915. chopVirtualFileSuffix(serverOriginalPath);
  916. auto job = new RequestEtagJob(_discoveryData->_account, serverOriginalPath, this);
  917. connect(job, &RequestEtagJob::finishedWithResult, this, [=](const HttpResult<QString> &etag) mutable {
  918. if (!etag || (*etag != base._etag && !item->isDirectory()) || _discoveryData->isRenamed(originalPath)) {
  919. qCInfo(lcDisco) << "Can't rename because the etag has changed or the directory is gone" << originalPath;
  920. // Can't be a rename, leave it as a new.
  921. postProcessLocalNew();
  922. } else {
  923. // In case the deleted item was discovered in parallel
  924. _discoveryData->findAndCancelDeletedJob(originalPath);
  925. processRename(path);
  926. recurseQueryServer = *etag == base._etag ? ParentNotChanged : NormalQuery;
  927. }
  928. processFileFinalize(item, path, item->isDirectory(), NormalQuery, recurseQueryServer);
  929. _pendingAsyncJobs--;
  930. QTimer::singleShot(0, _discoveryData, &DiscoveryPhase::scheduleMoreJobs);
  931. });
  932. job->start();
  933. return;
  934. }
  935. finalize();
  936. }
  937. void ProcessDirectoryJob::processFileConflict(const SyncFileItemPtr &item, ProcessDirectoryJob::PathTuple path, const LocalInfo &localEntry, const RemoteInfo &serverEntry, const SyncJournalFileRecord &dbEntry)
  938. {
  939. item->_previousSize = localEntry.size;
  940. item->_previousModtime = localEntry.modtime;
  941. if (serverEntry.isDirectory && localEntry.isDirectory) {
  942. // Folders of the same path are always considered equals
  943. item->_instruction = CSYNC_INSTRUCTION_UPDATE_METADATA;
  944. return;
  945. }
  946. // A conflict with a virtual should lead to virtual file download
  947. if (dbEntry.isVirtualFile() || localEntry.isVirtualFile)
  948. item->_type = ItemTypeVirtualFileDownload;
  949. // If there's no content hash, use heuristics
  950. if (serverEntry.checksumHeader.isEmpty()) {
  951. // If the size or mtime is different, it's definitely a conflict.
  952. bool isConflict = (serverEntry.size != localEntry.size) || (serverEntry.modtime != localEntry.modtime);
  953. // It could be a conflict even if size and mtime match!
  954. //
  955. // In older client versions we always treated these cases as a
  956. // non-conflict. This behavior is preserved in case the server
  957. // doesn't provide a content checksum.
  958. // SO: If there is no checksum, we can have !isConflict here
  959. // even though the files might have different content! This is an
  960. // intentional tradeoff. Downloading and comparing files would
  961. // be technically correct in this situation but leads to too
  962. // much waste.
  963. // In particular this kind of NEW/NEW situation with identical
  964. // sizes and mtimes pops up when the local database is lost for
  965. // whatever reason.
  966. item->_instruction = isConflict ? CSYNC_INSTRUCTION_CONFLICT : CSYNC_INSTRUCTION_UPDATE_METADATA;
  967. item->_direction = isConflict ? SyncFileItem::None : SyncFileItem::Down;
  968. return;
  969. }
  970. // Do we have an UploadInfo for this?
  971. // Maybe the Upload was completed, but the connection was broken just before
  972. // we recieved the etag (Issue #5106)
  973. auto up = _discoveryData->_statedb->getUploadInfo(path._original);
  974. if (up._valid && up._contentChecksum == serverEntry.checksumHeader) {
  975. // Solve the conflict into an upload, or nothing
  976. item->_instruction = up._modtime == localEntry.modtime && up._size == localEntry.size
  977. ? CSYNC_INSTRUCTION_NONE : CSYNC_INSTRUCTION_SYNC;
  978. item->_direction = SyncFileItem::Up;
  979. // Update the etag and other server metadata in the journal already
  980. // (We can't use a typical CSYNC_INSTRUCTION_UPDATE_METADATA because
  981. // we must not store the size/modtime from the file system)
  982. OCC::SyncJournalFileRecord rec;
  983. if (_discoveryData->_statedb->getFileRecord(path._original, &rec)) {
  984. rec._path = path._original.toUtf8();
  985. rec._etag = serverEntry.etag;
  986. rec._fileId = serverEntry.fileId;
  987. rec._modtime = serverEntry.modtime;
  988. rec._type = item->_type;
  989. rec._fileSize = serverEntry.size;
  990. rec._remotePerm = serverEntry.remotePerm;
  991. rec._checksumHeader = serverEntry.checksumHeader;
  992. _discoveryData->_statedb->setFileRecord(rec);
  993. }
  994. return;
  995. }
  996. // Rely on content hash comparisons to optimize away non-conflicts inside the job
  997. item->_instruction = CSYNC_INSTRUCTION_CONFLICT;
  998. item->_direction = SyncFileItem::None;
  999. }
  1000. void ProcessDirectoryJob::processFileFinalize(
  1001. const SyncFileItemPtr &item, PathTuple path, bool recurse,
  1002. QueryMode recurseQueryLocal, QueryMode recurseQueryServer)
  1003. {
  1004. // Adjust target path for virtual-suffix files
  1005. if (isVfsWithSuffix()) {
  1006. if (item->_type == ItemTypeVirtualFile) {
  1007. addVirtualFileSuffix(path._target);
  1008. if (item->_instruction == CSYNC_INSTRUCTION_RENAME)
  1009. addVirtualFileSuffix(item->_renameTarget);
  1010. else
  1011. addVirtualFileSuffix(item->_file);
  1012. }
  1013. if (item->_type == ItemTypeVirtualFileDehydration
  1014. && item->_instruction == CSYNC_INSTRUCTION_SYNC) {
  1015. if (item->_renameTarget.isEmpty()) {
  1016. item->_renameTarget = item->_file;
  1017. addVirtualFileSuffix(item->_renameTarget);
  1018. }
  1019. }
  1020. }
  1021. if (path._original != path._target && (item->_instruction == CSYNC_INSTRUCTION_UPDATE_METADATA || item->_instruction == CSYNC_INSTRUCTION_NONE)) {
  1022. ASSERT(_dirItem && _dirItem->_instruction == CSYNC_INSTRUCTION_RENAME);
  1023. // This is because otherwise subitems are not updated! (ideally renaming a directory could
  1024. // update the database for all items! See PropagateDirectory::slotSubJobsFinished)
  1025. item->_instruction = CSYNC_INSTRUCTION_RENAME;
  1026. item->_renameTarget = path._target;
  1027. item->_direction = _dirItem->_direction;
  1028. }
  1029. qCInfo(lcDisco) << "Discovered" << item->_file << item->_instruction << item->_direction << item->_type;
  1030. if (item->isDirectory() && item->_instruction == CSYNC_INSTRUCTION_SYNC)
  1031. item->_instruction = CSYNC_INSTRUCTION_UPDATE_METADATA;
  1032. bool removed = item->_instruction == CSYNC_INSTRUCTION_REMOVE;
  1033. if (checkPermissions(item)) {
  1034. if (item->_isRestoration && item->isDirectory())
  1035. recurse = true;
  1036. } else {
  1037. recurse = false;
  1038. }
  1039. if (recurse) {
  1040. auto job = new ProcessDirectoryJob(path, item, recurseQueryLocal, recurseQueryServer, this);
  1041. if (removed) {
  1042. job->setParent(_discoveryData);
  1043. _discoveryData->_queuedDeletedDirectories[path._original] = job;
  1044. } else {
  1045. connect(job, &ProcessDirectoryJob::finished, this, &ProcessDirectoryJob::subJobFinished);
  1046. _queuedJobs.push_back(job);
  1047. }
  1048. } else {
  1049. if (removed
  1050. // For the purpose of rename deletion, restored deleted placeholder is as if it was deleted
  1051. || (item->_type == ItemTypeVirtualFile && item->_instruction == CSYNC_INSTRUCTION_NEW)) {
  1052. _discoveryData->_deletedItem[path._original] = item;
  1053. }
  1054. emit _discoveryData->itemDiscovered(item);
  1055. }
  1056. }
  1057. void ProcessDirectoryJob::processBlacklisted(const PathTuple &path, const OCC::LocalInfo &localEntry,
  1058. const SyncJournalFileRecord &dbEntry)
  1059. {
  1060. if (!localEntry.isValid())
  1061. return;
  1062. auto item = SyncFileItem::fromSyncJournalFileRecord(dbEntry);
  1063. item->_file = path._target;
  1064. item->_originalFile = path._original;
  1065. item->_inode = localEntry.inode;
  1066. item->_isSelectiveSync = true;
  1067. if (dbEntry.isValid() && ((dbEntry._modtime == localEntry.modtime && dbEntry._fileSize == localEntry.size) || (localEntry.isDirectory && dbEntry.isDirectory()))) {
  1068. item->_instruction = CSYNC_INSTRUCTION_REMOVE;
  1069. item->_direction = SyncFileItem::Down;
  1070. } else {
  1071. item->_instruction = CSYNC_INSTRUCTION_IGNORE;
  1072. item->_status = SyncFileItem::FileIgnored;
  1073. item->_errorString = tr("Ignored because of the \"choose what to sync\" blacklist");
  1074. _childIgnored = true;
  1075. }
  1076. qCInfo(lcDisco) << "Discovered (blacklisted) " << item->_file << item->_instruction << item->_direction << item->isDirectory();
  1077. if (item->isDirectory() && item->_instruction != CSYNC_INSTRUCTION_IGNORE) {
  1078. auto job = new ProcessDirectoryJob(path, item, NormalQuery, InBlackList, this);
  1079. connect(job, &ProcessDirectoryJob::finished, this, &ProcessDirectoryJob::subJobFinished);
  1080. _queuedJobs.push_back(job);
  1081. } else {
  1082. emit _discoveryData->itemDiscovered(item);
  1083. }
  1084. }
  1085. bool ProcessDirectoryJob::checkPermissions(const OCC::SyncFileItemPtr &item)
  1086. {
  1087. if (item->_direction != SyncFileItem::Up) {
  1088. // Currently we only check server-side permissions
  1089. return true;
  1090. }
  1091. switch (item->_instruction) {
  1092. case CSYNC_INSTRUCTION_TYPE_CHANGE:
  1093. case CSYNC_INSTRUCTION_NEW: {
  1094. const auto perms = !_rootPermissions.isNull() ? _rootPermissions
  1095. : _dirItem ? _dirItem->_remotePerm : _rootPermissions;
  1096. if (perms.isNull()) {
  1097. // No permissions set
  1098. return true;
  1099. } else if (item->isDirectory() && !perms.hasPermission(RemotePermissions::CanAddSubDirectories)) {
  1100. qCWarning(lcDisco) << "checkForPermission: ERROR" << item->_file;
  1101. item->_instruction = CSYNC_INSTRUCTION_ERROR;
  1102. item->_errorString = tr("Not allowed because you don't have permission to add subfolders to that folder");
  1103. return false;
  1104. } else if (!item->isDirectory() && !perms.hasPermission(RemotePermissions::CanAddFile)) {
  1105. qCWarning(lcDisco) << "checkForPermission: ERROR" << item->_file;
  1106. item->_instruction = CSYNC_INSTRUCTION_ERROR;
  1107. item->_errorString = tr("Not allowed because you don't have permission to add files in that folder");
  1108. return false;
  1109. }
  1110. break;
  1111. }
  1112. case CSYNC_INSTRUCTION_SYNC: {
  1113. const auto perms = item->_remotePerm;
  1114. if (perms.isNull()) {
  1115. // No permissions set
  1116. return true;
  1117. }
  1118. if (!perms.hasPermission(RemotePermissions::CanWrite)) {
  1119. item->_instruction = CSYNC_INSTRUCTION_CONFLICT;
  1120. item->_errorString = tr("Not allowed to upload this file because it is read-only on the server, restoring");
  1121. item->_direction = SyncFileItem::Down;
  1122. item->_isRestoration = true;
  1123. qCWarning(lcDisco) << "checkForPermission: RESTORING" << item->_file << item->_errorString;
  1124. // Take the things to write to the db from the "other" node (i.e: info from server).
  1125. // Do a lookup into the csync remote tree to get the metadata we need to restore.
  1126. qSwap(item->_size, item->_previousSize);
  1127. qSwap(item->_modtime, item->_previousModtime);
  1128. return false;
  1129. }
  1130. break;
  1131. }
  1132. case CSYNC_INSTRUCTION_REMOVE: {
  1133. QString fileSlash = item->_file + '/';
  1134. auto forbiddenIt = _discoveryData->_forbiddenDeletes.upperBound(fileSlash);
  1135. if (forbiddenIt != _discoveryData->_forbiddenDeletes.begin())
  1136. forbiddenIt -= 1;
  1137. if (forbiddenIt != _discoveryData->_forbiddenDeletes.end()
  1138. && fileSlash.startsWith(forbiddenIt.key())) {
  1139. item->_instruction = CSYNC_INSTRUCTION_NEW;
  1140. item->_direction = SyncFileItem::Down;
  1141. item->_isRestoration = true;
  1142. item->_errorString = tr("Moved to invalid target, restoring");
  1143. qCWarning(lcDisco) << "checkForPermission: RESTORING" << item->_file << item->_errorString;
  1144. return true; // restore sub items
  1145. }
  1146. const auto perms = item->_remotePerm;
  1147. if (perms.isNull()) {
  1148. // No permissions set
  1149. return true;
  1150. }
  1151. if (!perms.hasPermission(RemotePermissions::CanDelete)) {
  1152. item->_instruction = CSYNC_INSTRUCTION_NEW;
  1153. item->_direction = SyncFileItem::Down;
  1154. item->_isRestoration = true;
  1155. item->_errorString = tr("Not allowed to remove, restoring");
  1156. qCWarning(lcDisco) << "checkForPermission: RESTORING" << item->_file << item->_errorString;
  1157. return true; // (we need to recurse to restore sub items)
  1158. }
  1159. break;
  1160. }
  1161. default:
  1162. break;
  1163. }
  1164. return true;
  1165. }
  1166. auto ProcessDirectoryJob::checkMovePermissions(RemotePermissions srcPerm, const QString &srcPath,
  1167. bool isDirectory)
  1168. -> MovePermissionResult
  1169. {
  1170. auto destPerms = !_rootPermissions.isNull() ? _rootPermissions
  1171. : _dirItem ? _dirItem->_remotePerm : _rootPermissions;
  1172. auto filePerms = srcPerm;
  1173. //true when it is just a rename in the same directory. (not a move)
  1174. bool isRename = srcPath.startsWith(_currentFolder._original)
  1175. && srcPath.lastIndexOf('/') == _currentFolder._original.size();
  1176. // Check if we are allowed to move to the destination.
  1177. bool destinationOK = true;
  1178. bool destinationNewOK = true;
  1179. if (destPerms.isNull()) {
  1180. } else if ((isDirectory && !destPerms.hasPermission(RemotePermissions::CanAddSubDirectories)) ||
  1181. (!isDirectory && !destPerms.hasPermission(RemotePermissions::CanAddFile))) {
  1182. destinationNewOK = false;
  1183. }
  1184. if (!isRename && !destinationNewOK) {
  1185. // no need to check for the destination dir permission for renames
  1186. destinationOK = false;
  1187. }
  1188. // check if we are allowed to move from the source
  1189. bool sourceOK = true;
  1190. if (!filePerms.isNull()
  1191. && ((isRename && !filePerms.hasPermission(RemotePermissions::CanRename))
  1192. || (!isRename && !filePerms.hasPermission(RemotePermissions::CanMove)))) {
  1193. // We are not allowed to move or rename this file
  1194. sourceOK = false;
  1195. }
  1196. return MovePermissionResult{sourceOK, destinationOK, destinationNewOK};
  1197. }
  1198. void ProcessDirectoryJob::subJobFinished()
  1199. {
  1200. auto job = qobject_cast<ProcessDirectoryJob *>(sender());
  1201. ASSERT(job);
  1202. _childIgnored |= job->_childIgnored;
  1203. _childModified |= job->_childModified;
  1204. if (job->_dirItem)
  1205. emit _discoveryData->itemDiscovered(job->_dirItem);
  1206. int count = _runningJobs.removeAll(job);
  1207. ASSERT(count == 1);
  1208. job->deleteLater();
  1209. QTimer::singleShot(0, _discoveryData, &DiscoveryPhase::scheduleMoreJobs);
  1210. }
  1211. int ProcessDirectoryJob::processSubJobs(int nbJobs)
  1212. {
  1213. if (_queuedJobs.empty() && _runningJobs.empty() && _pendingAsyncJobs == 0) {
  1214. _pendingAsyncJobs = -1; // We're finished, we don't want to emit finished again
  1215. if (_dirItem) {
  1216. if (_childModified && _dirItem->_instruction == CSYNC_INSTRUCTION_REMOVE) {
  1217. // re-create directory that has modified contents
  1218. _dirItem->_instruction = CSYNC_INSTRUCTION_NEW;
  1219. _dirItem->_direction = _dirItem->_direction == SyncFileItem::Up ? SyncFileItem::Down : SyncFileItem::Up;
  1220. }
  1221. if (_childModified && _dirItem->_instruction == CSYNC_INSTRUCTION_TYPE_CHANGE && !_dirItem->isDirectory()) {
  1222. // Replacing a directory by a file is a conflict, if the directory had modified children
  1223. _dirItem->_instruction = CSYNC_INSTRUCTION_CONFLICT;
  1224. if (_dirItem->_direction == SyncFileItem::Up) {
  1225. _dirItem->_type = ItemTypeDirectory;
  1226. _dirItem->_direction = SyncFileItem::Down;
  1227. }
  1228. }
  1229. if (_childIgnored && _dirItem->_instruction == CSYNC_INSTRUCTION_REMOVE) {
  1230. // Do not remove a directory that has ignored files
  1231. _dirItem->_instruction = CSYNC_INSTRUCTION_NONE;
  1232. }
  1233. }
  1234. emit finished();
  1235. }
  1236. int started = 0;
  1237. foreach (auto *rj, _runningJobs) {
  1238. started += rj->processSubJobs(nbJobs - started);
  1239. if (started >= nbJobs)
  1240. return started;
  1241. }
  1242. while (started < nbJobs && !_queuedJobs.empty()) {
  1243. auto f = _queuedJobs.front();
  1244. _queuedJobs.pop_front();
  1245. _runningJobs.push_back(f);
  1246. f->start();
  1247. started++;
  1248. }
  1249. return started;
  1250. }
  1251. void ProcessDirectoryJob::dbError()
  1252. {
  1253. _discoveryData->fatalError(tr("Error while reading the database"));
  1254. }
  1255. void ProcessDirectoryJob::addVirtualFileSuffix(QString &str) const
  1256. {
  1257. str.append(_discoveryData->_syncOptions._vfs->fileSuffix());
  1258. }
  1259. bool ProcessDirectoryJob::hasVirtualFileSuffix(const QString &str) const
  1260. {
  1261. if (!isVfsWithSuffix())
  1262. return false;
  1263. return str.endsWith(_discoveryData->_syncOptions._vfs->fileSuffix());
  1264. }
  1265. void ProcessDirectoryJob::chopVirtualFileSuffix(QString &str) const
  1266. {
  1267. if (!isVfsWithSuffix())
  1268. return;
  1269. bool hasSuffix = hasVirtualFileSuffix(str);
  1270. ASSERT(hasSuffix);
  1271. if (hasSuffix)
  1272. str.chop(_discoveryData->_syncOptions._vfs->fileSuffix().size());
  1273. }
  1274. DiscoverySingleDirectoryJob *ProcessDirectoryJob::startAsyncServerQuery()
  1275. {
  1276. auto serverJob = new DiscoverySingleDirectoryJob(_discoveryData->_account,
  1277. _discoveryData->_remoteFolder + _currentFolder._server, this);
  1278. if (!_dirItem)
  1279. serverJob->setIsRootPath(); // query the fingerprint on the root
  1280. connect(serverJob, &DiscoverySingleDirectoryJob::etag, this, &ProcessDirectoryJob::etag);
  1281. _discoveryData->_currentlyActiveJobs++;
  1282. _pendingAsyncJobs++;
  1283. connect(serverJob, &DiscoverySingleDirectoryJob::finished, this, [this, serverJob](const auto &results) {
  1284. _discoveryData->_currentlyActiveJobs--;
  1285. _pendingAsyncJobs--;
  1286. if (results) {
  1287. _serverNormalQueryEntries = *results;
  1288. _serverQueryDone = true;
  1289. if (!serverJob->_dataFingerprint.isEmpty() && _discoveryData->_dataFingerprint.isEmpty())
  1290. _discoveryData->_dataFingerprint = serverJob->_dataFingerprint;
  1291. if (_localQueryDone)
  1292. process();
  1293. } else {
  1294. auto code = results.error().code;
  1295. qCWarning(lcDisco) << "Server error in directory" << _currentFolder._server << code;
  1296. if (_dirItem && code >= 403) {
  1297. // In case of an HTTP error, we ignore that directory
  1298. // 403 Forbidden can be sent by the server if the file firewall is active.
  1299. // A file or directory should be ignored and sync must continue. See #3490
  1300. // The server usually replies with the custom "503 Storage not available"
  1301. // if some path is temporarily unavailable. But in some cases a standard 503
  1302. // is returned too. Thus we can't distinguish the two and will treat any
  1303. // 503 as request to ignore the folder. See #3113 #2884.
  1304. // Similarly, the server might also return 404 or 50x in case of bugs. #7199 #7586
  1305. _dirItem->_instruction = CSYNC_INSTRUCTION_IGNORE;
  1306. _dirItem->_errorString = results.error().message;
  1307. emit finished();
  1308. } else {
  1309. // Fatal for the root job since it has no SyncFileItem, or for the network errors
  1310. emit _discoveryData->fatalError(tr("Server replied with an error while reading directory '%1' : %2")
  1311. .arg(_currentFolder._server, results.error().message));
  1312. }
  1313. }
  1314. });
  1315. connect(serverJob, &DiscoverySingleDirectoryJob::firstDirectoryPermissions, this,
  1316. [this](const RemotePermissions &perms) { _rootPermissions = perms; });
  1317. serverJob->start();
  1318. return serverJob;
  1319. }
  1320. void ProcessDirectoryJob::startAsyncLocalQuery()
  1321. {
  1322. QString localPath = _discoveryData->_localDir + _currentFolder._local;
  1323. auto localJob = new DiscoverySingleLocalDirectoryJob(_discoveryData->_account, localPath, _discoveryData->_syncOptions._vfs.data());
  1324. _discoveryData->_currentlyActiveJobs++;
  1325. _pendingAsyncJobs++;
  1326. connect(localJob, &DiscoverySingleLocalDirectoryJob::itemDiscovered, _discoveryData, &DiscoveryPhase::itemDiscovered);
  1327. connect(localJob, &DiscoverySingleLocalDirectoryJob::childIgnored, this, [this](bool b) {
  1328. _childIgnored = b;
  1329. });
  1330. connect(localJob, &DiscoverySingleLocalDirectoryJob::finishedFatalError, this, [this](const QString &msg) {
  1331. _discoveryData->_currentlyActiveJobs--;
  1332. _pendingAsyncJobs--;
  1333. if (_serverJob)
  1334. _serverJob->abort();
  1335. emit _discoveryData->fatalError(msg);
  1336. });
  1337. connect(localJob, &DiscoverySingleLocalDirectoryJob::finishedNonFatalError, this, [this](const QString &msg) {
  1338. _discoveryData->_currentlyActiveJobs--;
  1339. _pendingAsyncJobs--;
  1340. if (_dirItem) {
  1341. _dirItem->_instruction = CSYNC_INSTRUCTION_IGNORE;
  1342. _dirItem->_errorString = msg;
  1343. emit this->finished();
  1344. } else {
  1345. // Fatal for the root job since it has no SyncFileItem
  1346. emit _discoveryData->fatalError(msg);
  1347. }
  1348. });
  1349. connect(localJob, &DiscoverySingleLocalDirectoryJob::finished, this, [this](const auto &results) {
  1350. _discoveryData->_currentlyActiveJobs--;
  1351. _pendingAsyncJobs--;
  1352. _localNormalQueryEntries = results;
  1353. _localQueryDone = true;
  1354. if (_serverQueryDone)
  1355. this->process();
  1356. });
  1357. QThreadPool *pool = QThreadPool::globalInstance();
  1358. pool->start(localJob); // QThreadPool takes ownership
  1359. }
  1360. bool ProcessDirectoryJob::isVfsWithSuffix() const
  1361. {
  1362. return _discoveryData->_syncOptions._vfs->mode() == Vfs::WithSuffix;
  1363. }
  1364. void ProcessDirectoryJob::computePinState(PinState parentState)
  1365. {
  1366. _pinState = parentState;
  1367. if (_queryLocal != ParentDontExist) {
  1368. if (auto state = _discoveryData->_syncOptions._vfs->pinState(_currentFolder._local)) // ouch! pin local or original?
  1369. _pinState = *state;
  1370. }
  1371. }
  1372. void ProcessDirectoryJob::setupDbPinStateActions(SyncJournalFileRecord &record)
  1373. {
  1374. // Only suffix-vfs uses the db for pin states.
  1375. // Other plugins will set localEntry._type according to the file's pin state.
  1376. if (!isVfsWithSuffix())
  1377. return;
  1378. auto pin = _discoveryData->_statedb->internalPinStates().rawForPath(record._path);
  1379. if (!pin || *pin == PinState::Inherited)
  1380. pin = _pinState;
  1381. // OnlineOnly hydrated files want to be dehydrated
  1382. if (record._type == ItemTypeFile && *pin == PinState::OnlineOnly)
  1383. record._type = ItemTypeVirtualFileDehydration;
  1384. // AlwaysLocal dehydrated files want to be hydrated
  1385. if (record._type == ItemTypeVirtualFile && *pin == PinState::AlwaysLocal)
  1386. record._type = ItemTypeVirtualFileDownload;
  1387. }
  1388. }