discovery.cpp 95 KB

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