discovery.cpp 63 KB

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