discoveryphase.cpp 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672
  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 "discoveryphase.h"
  15. #include "discovery.h"
  16. #include "helpers.h"
  17. #include "progressdispatcher.h"
  18. #include "account.h"
  19. #include "clientsideencryptionjobs.h"
  20. #include "common/asserts.h"
  21. #include "common/checksums.h"
  22. #include <csync_exclude.h>
  23. #include "vio/csync_vio_local.h"
  24. #include <QLoggingCategory>
  25. #include <QUrl>
  26. #include <QFile>
  27. #include <QFileInfo>
  28. #include <QTextCodec>
  29. #include <cstring>
  30. #include <QDateTime>
  31. namespace OCC {
  32. Q_LOGGING_CATEGORY(lcDiscovery, "nextcloud.sync.discovery", QtInfoMsg)
  33. /* Given a sorted list of paths ending with '/', return whether or not the given path is within one of the paths of the list*/
  34. static bool findPathInList(const QStringList &list, const QString &path)
  35. {
  36. Q_ASSERT(std::is_sorted(list.begin(), list.end()));
  37. if (list.size() == 1 && list.first() == QLatin1String("/")) {
  38. // Special case for the case "/" is there, it matches everything
  39. return true;
  40. }
  41. QString pathSlash = path + QLatin1Char('/');
  42. // Since the list is sorted, we can do a binary search.
  43. // If the path is a prefix of another item or right after in the lexical order.
  44. auto it = std::lower_bound(list.begin(), list.end(), pathSlash);
  45. if (it != list.end() && *it == pathSlash) {
  46. return true;
  47. }
  48. if (it == list.begin()) {
  49. return false;
  50. }
  51. --it;
  52. Q_ASSERT(it->endsWith(QLatin1Char('/'))); // Folder::setSelectiveSyncBlackList makes sure of that
  53. return pathSlash.startsWith(*it);
  54. }
  55. bool DiscoveryPhase::isInSelectiveSyncBlackList(const QString &path) const
  56. {
  57. if (_selectiveSyncBlackList.isEmpty()) {
  58. // If there is no black list, everything is allowed
  59. return false;
  60. }
  61. // Block if it is in the black list
  62. if (findPathInList(_selectiveSyncBlackList, path)) {
  63. return true;
  64. }
  65. return false;
  66. }
  67. void DiscoveryPhase::checkSelectiveSyncNewFolder(const QString &path, RemotePermissions remotePerm,
  68. std::function<void(bool)> callback)
  69. {
  70. if (_syncOptions._confirmExternalStorage && _syncOptions._vfs->mode() == Vfs::Off
  71. && remotePerm.hasPermission(RemotePermissions::IsMounted)) {
  72. // external storage.
  73. /* Note: DiscoverySingleDirectoryJob::directoryListingIteratedSlot make sure that only the
  74. * root of a mounted storage has 'M', all sub entries have 'm' */
  75. // Only allow it if the white list contains exactly this path (not parents)
  76. // We want to ask confirmation for external storage even if the parents where selected
  77. if (_selectiveSyncWhiteList.contains(path + QLatin1Char('/'))) {
  78. return callback(false);
  79. }
  80. emit newBigFolder(path, true);
  81. return callback(true);
  82. }
  83. // If this path or the parent is in the white list, then we do not block this file
  84. if (findPathInList(_selectiveSyncWhiteList, path)) {
  85. return callback(false);
  86. }
  87. auto limit = _syncOptions._newBigFolderSizeLimit;
  88. if (limit < 0 || _syncOptions._vfs->mode() != Vfs::Off) {
  89. // no limit, everything is allowed;
  90. return callback(false);
  91. }
  92. // do a PROPFIND to know the size of this folder
  93. auto propfindJob = new PropfindJob(_account, _remoteFolder + path, this);
  94. propfindJob->setProperties(QList<QByteArray>() << "resourcetype"
  95. << "http://owncloud.org/ns:size");
  96. QObject::connect(propfindJob, &PropfindJob::finishedWithError,
  97. this, [=] { return callback(false); });
  98. QObject::connect(propfindJob, &PropfindJob::result, this, [=](const QVariantMap &values) {
  99. auto result = values.value(QLatin1String("size")).toLongLong();
  100. if (result >= limit) {
  101. // we tell the UI there is a new folder
  102. emit newBigFolder(path, false);
  103. return callback(true);
  104. } else {
  105. // it is not too big, put it in the white list (so we will not do more query for the children)
  106. // and and do not block.
  107. auto p = path;
  108. if (!p.endsWith(QLatin1Char('/')))
  109. p += QLatin1Char('/');
  110. _selectiveSyncWhiteList.insert(
  111. std::upper_bound(_selectiveSyncWhiteList.begin(), _selectiveSyncWhiteList.end(), p),
  112. p);
  113. return callback(false);
  114. }
  115. });
  116. propfindJob->start();
  117. }
  118. /* Given a path on the remote, give the path as it is when the rename is done */
  119. QString DiscoveryPhase::adjustRenamedPath(const QString &original, SyncFileItem::Direction d) const
  120. {
  121. return OCC::adjustRenamedPath(d == SyncFileItem::Down ? _renamedItemsRemote : _renamedItemsLocal, original);
  122. }
  123. QString adjustRenamedPath(const QMap<QString, QString> &renamedItems, const QString &original)
  124. {
  125. int slashPos = original.size();
  126. while ((slashPos = original.lastIndexOf('/', slashPos - 1)) > 0) {
  127. auto it = renamedItems.constFind(original.left(slashPos));
  128. if (it != renamedItems.constEnd()) {
  129. return *it + original.mid(slashPos);
  130. }
  131. }
  132. return original;
  133. }
  134. QPair<bool, QByteArray> DiscoveryPhase::findAndCancelDeletedJob(const QString &originalPath)
  135. {
  136. bool result = false;
  137. QByteArray oldEtag;
  138. auto it = _deletedItem.find(originalPath);
  139. if (it != _deletedItem.end()) {
  140. const SyncInstructions instruction = (*it)->_instruction;
  141. if (instruction == CSYNC_INSTRUCTION_IGNORE && (*it)->_type == ItemTypeVirtualFile) {
  142. // re-creation of virtual files count as a delete
  143. // a file might be in an error state and thus gets marked as CSYNC_INSTRUCTION_IGNORE
  144. // after it was initially marked as CSYNC_INSTRUCTION_REMOVE
  145. // return true, to not trigger any additional actions on that file that could elad to dataloss
  146. result = true;
  147. oldEtag = (*it)->_etag;
  148. } else {
  149. if (!(instruction == CSYNC_INSTRUCTION_REMOVE
  150. // re-creation of virtual files count as a delete
  151. || ((*it)->_type == ItemTypeVirtualFile && instruction == CSYNC_INSTRUCTION_NEW)
  152. || ((*it)->_isRestoration && instruction == CSYNC_INSTRUCTION_NEW)))
  153. {
  154. qCWarning(lcDiscovery) << "ENFORCE(FAILING)" << originalPath;
  155. qCWarning(lcDiscovery) << "instruction == CSYNC_INSTRUCTION_REMOVE" << (instruction == CSYNC_INSTRUCTION_REMOVE);
  156. qCWarning(lcDiscovery) << "((*it)->_type == ItemTypeVirtualFile && instruction == CSYNC_INSTRUCTION_NEW)"
  157. << ((*it)->_type == ItemTypeVirtualFile && instruction == CSYNC_INSTRUCTION_NEW);
  158. qCWarning(lcDiscovery) << "((*it)->_isRestoration && instruction == CSYNC_INSTRUCTION_NEW))"
  159. << ((*it)->_isRestoration && instruction == CSYNC_INSTRUCTION_NEW);
  160. qCWarning(lcDiscovery) << "instruction" << instruction;
  161. qCWarning(lcDiscovery) << "(*it)->_type" << (*it)->_type;
  162. qCWarning(lcDiscovery) << "(*it)->_isRestoration " << (*it)->_isRestoration;
  163. Q_ASSERT(false);
  164. emit addErrorToGui(SyncFileItem::Status::FatalError, tr("Error while canceling deletion of a file"), originalPath, ErrorCategory::GenericError);
  165. emit fatalError(tr("Error while canceling deletion of %1").arg(originalPath), ErrorCategory::GenericError);
  166. }
  167. (*it)->_instruction = CSYNC_INSTRUCTION_NONE;
  168. result = true;
  169. oldEtag = (*it)->_etag;
  170. }
  171. _deletedItem.erase(it);
  172. }
  173. if (auto *otherJob = _queuedDeletedDirectories.take(originalPath)) {
  174. oldEtag = otherJob->_dirItem->_etag;
  175. delete otherJob;
  176. result = true;
  177. }
  178. return { result, oldEtag };
  179. }
  180. void DiscoveryPhase::enqueueDirectoryToDelete(const QString &path, ProcessDirectoryJob* const directoryJob)
  181. {
  182. _queuedDeletedDirectories[path] = directoryJob;
  183. if (directoryJob->_dirItem &&
  184. directoryJob->_dirItem->_isRestoration &&
  185. directoryJob->_dirItem->_direction == SyncFileItem::Down &&
  186. directoryJob->_dirItem->_instruction == CSYNC_INSTRUCTION_NEW) {
  187. _directoryNamesToRestoreOnPropagation.push_back(path);
  188. }
  189. }
  190. void DiscoveryPhase::startJob(ProcessDirectoryJob *job)
  191. {
  192. ENFORCE(!_currentRootJob);
  193. connect(job, &ProcessDirectoryJob::finished, this, [this, job] {
  194. ENFORCE(_currentRootJob == sender());
  195. _currentRootJob = nullptr;
  196. if (job->_dirItem)
  197. emit itemDiscovered(job->_dirItem);
  198. job->deleteLater();
  199. // Once the main job has finished recurse here to execute the remaining
  200. // jobs for queued deleted directories.
  201. if (!_queuedDeletedDirectories.isEmpty()) {
  202. auto nextJob = _queuedDeletedDirectories.take(_queuedDeletedDirectories.firstKey());
  203. startJob(nextJob);
  204. } else {
  205. emit finished();
  206. }
  207. });
  208. _currentRootJob = job;
  209. job->start();
  210. }
  211. void DiscoveryPhase::setSelectiveSyncBlackList(const QStringList &list)
  212. {
  213. _selectiveSyncBlackList = list;
  214. std::sort(_selectiveSyncBlackList.begin(), _selectiveSyncBlackList.end());
  215. }
  216. void DiscoveryPhase::setSelectiveSyncWhiteList(const QStringList &list)
  217. {
  218. _selectiveSyncWhiteList = list;
  219. std::sort(_selectiveSyncWhiteList.begin(), _selectiveSyncWhiteList.end());
  220. }
  221. void DiscoveryPhase::scheduleMoreJobs()
  222. {
  223. auto limit = qMax(1, _syncOptions._parallelNetworkJobs);
  224. if (_currentRootJob && _currentlyActiveJobs < limit) {
  225. _currentRootJob->processSubJobs(limit - _currentlyActiveJobs);
  226. }
  227. }
  228. DiscoverySingleLocalDirectoryJob::DiscoverySingleLocalDirectoryJob(const AccountPtr &account, const QString &localPath, OCC::Vfs *vfs, QObject *parent)
  229. : QObject(parent), QRunnable(), _localPath(localPath), _account(account), _vfs(vfs)
  230. {
  231. qRegisterMetaType<QVector<OCC::LocalInfo> >("QVector<OCC::LocalInfo>");
  232. }
  233. // Use as QRunnable
  234. void DiscoverySingleLocalDirectoryJob::run() {
  235. QString localPath = _localPath;
  236. if (localPath.endsWith('/')) // Happens if _currentFolder._local.isEmpty()
  237. localPath.chop(1);
  238. auto dh = csync_vio_local_opendir(localPath);
  239. if (!dh) {
  240. qCInfo(lcDiscovery) << "Error while opening directory" << (localPath) << errno;
  241. QString errorString = tr("Error while opening directory %1").arg(localPath);
  242. if (errno == EACCES) {
  243. errorString = tr("Directory not accessible on client, permission denied");
  244. emit finishedNonFatalError(errorString);
  245. return;
  246. } else if (errno == ENOENT) {
  247. errorString = tr("Directory not found: %1").arg(localPath);
  248. } else if (errno == ENOTDIR) {
  249. // Not a directory..
  250. // Just consider it is empty
  251. return;
  252. }
  253. emit finishedFatalError(errorString);
  254. return;
  255. }
  256. QVector<LocalInfo> results;
  257. while (true) {
  258. errno = 0;
  259. auto dirent = csync_vio_local_readdir(dh, _vfs);
  260. if (!dirent)
  261. break;
  262. if (dirent->type == ItemTypeSkip)
  263. continue;
  264. LocalInfo i;
  265. static QTextCodec *codec = QTextCodec::codecForName("UTF-8");
  266. ASSERT(codec);
  267. QTextCodec::ConverterState state;
  268. i.name = codec->toUnicode(dirent->path, dirent->path.size(), &state);
  269. if (state.invalidChars > 0 || state.remainingChars > 0) {
  270. emit childIgnored(true);
  271. auto item = SyncFileItemPtr::create();
  272. //item->_file = _currentFolder._target + i.name;
  273. // FIXME ^^ do we really need to use _target or is local fine?
  274. item->_file = _localPath + i.name;
  275. item->_instruction = CSYNC_INSTRUCTION_IGNORE;
  276. item->_status = SyncFileItem::NormalError;
  277. item->_errorString = tr("Filename encoding is not valid");
  278. emit itemDiscovered(item);
  279. continue;
  280. }
  281. i.modtime = dirent->modtime;
  282. i.size = dirent->size;
  283. i.inode = dirent->inode;
  284. i.isDirectory = dirent->type == ItemTypeDirectory;
  285. i.isHidden = dirent->is_hidden;
  286. i.isSymLink = dirent->type == ItemTypeSoftLink;
  287. i.isVirtualFile = dirent->type == ItemTypeVirtualFile || dirent->type == ItemTypeVirtualFileDownload;
  288. i.type = dirent->type;
  289. results.push_back(i);
  290. }
  291. if (errno != 0) {
  292. csync_vio_local_closedir(dh);
  293. // Note: Windows vio converts any error into EACCES
  294. qCWarning(lcDiscovery) << "readdir failed for file in " << localPath << " - errno: " << errno;
  295. emit finishedFatalError(tr("Error while reading directory %1").arg(localPath));
  296. return;
  297. }
  298. errno = 0;
  299. csync_vio_local_closedir(dh);
  300. if (errno != 0) {
  301. qCWarning(lcDiscovery) << "closedir failed for file in " << localPath << " - errno: " << errno;
  302. }
  303. emit finished(results);
  304. }
  305. DiscoverySingleDirectoryJob::DiscoverySingleDirectoryJob(const AccountPtr &account, const QString &path, QObject *parent)
  306. : QObject(parent)
  307. , _subPath(path)
  308. , _account(account)
  309. {
  310. }
  311. void DiscoverySingleDirectoryJob::start()
  312. {
  313. // Start the actual HTTP job
  314. auto *lsColJob = new LsColJob(_account, _subPath, this);
  315. QList<QByteArray> props;
  316. props << "resourcetype"
  317. << "getlastmodified"
  318. << "getcontentlength"
  319. << "getetag"
  320. << "http://owncloud.org/ns:size"
  321. << "http://owncloud.org/ns:id"
  322. << "http://owncloud.org/ns:fileid"
  323. << "http://owncloud.org/ns:downloadURL"
  324. << "http://owncloud.org/ns:dDC"
  325. << "http://owncloud.org/ns:permissions"
  326. << "http://owncloud.org/ns:checksums";
  327. if (_isRootPath)
  328. props << "http://owncloud.org/ns:data-fingerprint";
  329. if (_account->serverVersionInt() >= Account::makeServerVersion(10, 0, 0)) {
  330. // Server older than 10.0 have performances issue if we ask for the share-types on every PROPFIND
  331. props << "http://owncloud.org/ns:share-types";
  332. }
  333. if (_account->capabilities().clientSideEncryptionAvailable()) {
  334. props << "http://nextcloud.org/ns:is-encrypted";
  335. }
  336. if (_account->capabilities().filesLockAvailable()) {
  337. props << "http://nextcloud.org/ns:lock"
  338. << "http://nextcloud.org/ns:lock-owner-displayname"
  339. << "http://nextcloud.org/ns:lock-owner"
  340. << "http://nextcloud.org/ns:lock-owner-type"
  341. << "http://nextcloud.org/ns:lock-owner-editor"
  342. << "http://nextcloud.org/ns:lock-time"
  343. << "http://nextcloud.org/ns:lock-timeout";
  344. }
  345. lsColJob->setProperties(props);
  346. QObject::connect(lsColJob, &LsColJob::directoryListingIterated,
  347. this, &DiscoverySingleDirectoryJob::directoryListingIteratedSlot);
  348. QObject::connect(lsColJob, &LsColJob::finishedWithError, this, &DiscoverySingleDirectoryJob::lsJobFinishedWithErrorSlot);
  349. QObject::connect(lsColJob, &LsColJob::finishedWithoutError, this, &DiscoverySingleDirectoryJob::lsJobFinishedWithoutErrorSlot);
  350. lsColJob->start();
  351. _lsColJob = lsColJob;
  352. }
  353. void DiscoverySingleDirectoryJob::abort()
  354. {
  355. if (_lsColJob && _lsColJob->reply()) {
  356. _lsColJob->reply()->abort();
  357. }
  358. }
  359. bool DiscoverySingleDirectoryJob::isFileDropDetected() const
  360. {
  361. return _isFileDropDetected;
  362. }
  363. bool DiscoverySingleDirectoryJob::encryptedMetadataNeedUpdate() const
  364. {
  365. return _encryptedMetadataNeedUpdate;
  366. }
  367. static void propertyMapToRemoteInfo(const QMap<QString, QString> &map, RemoteInfo &result)
  368. {
  369. for (auto it = map.constBegin(); it != map.constEnd(); ++it) {
  370. QString property = it.key();
  371. QString value = it.value();
  372. if (property == QLatin1String("resourcetype")) {
  373. result.isDirectory = value.contains(QLatin1String("collection"));
  374. } else if (property == QLatin1String("getlastmodified")) {
  375. const auto date = QDateTime::fromString(value, Qt::RFC2822Date);
  376. Q_ASSERT(date.isValid());
  377. result.modtime = 0;
  378. if (date.toSecsSinceEpoch() > 0) {
  379. result.modtime = date.toSecsSinceEpoch();
  380. }
  381. } else if (property == QLatin1String("getcontentlength")) {
  382. // See #4573, sometimes negative size values are returned
  383. bool ok = false;
  384. qlonglong ll = value.toLongLong(&ok);
  385. if (ok && ll >= 0) {
  386. result.size = ll;
  387. } else {
  388. result.size = 0;
  389. }
  390. } else if (property == "getetag") {
  391. result.etag = Utility::normalizeEtag(value.toUtf8());
  392. } else if (property == "id") {
  393. result.fileId = value.toUtf8();
  394. } else if (property == "downloadURL") {
  395. result.directDownloadUrl = value;
  396. } else if (property == "dDC") {
  397. result.directDownloadCookies = value;
  398. } else if (property == "permissions") {
  399. result.remotePerm = RemotePermissions::fromServerString(value);
  400. } else if (property == "checksums") {
  401. result.checksumHeader = findBestChecksum(value.toUtf8());
  402. } else if (property == "share-types" && !value.isEmpty()) {
  403. // Since QMap is sorted, "share-types" is always after "permissions".
  404. if (result.remotePerm.isNull()) {
  405. qWarning() << "Server returned a share type, but no permissions?";
  406. } else {
  407. // S means shared with me.
  408. // But for our purpose, we want to know if the file is shared. It does not matter
  409. // if we are the owner or not.
  410. // Piggy back on the persmission field
  411. result.remotePerm.setPermission(RemotePermissions::IsShared);
  412. result.sharedByMe = true;
  413. }
  414. } else if (property == "is-encrypted" && value == QStringLiteral("1")) {
  415. result._isE2eEncrypted = true;
  416. } else if (property == "lock") {
  417. result.locked = (value == QStringLiteral("1") ? SyncFileItem::LockStatus::LockedItem : SyncFileItem::LockStatus::UnlockedItem);
  418. }
  419. if (property == "lock-owner-displayname") {
  420. result.lockOwnerDisplayName = value;
  421. }
  422. if (property == "lock-owner") {
  423. result.lockOwnerId = value;
  424. }
  425. if (property == "lock-owner-type") {
  426. auto ok = false;
  427. const auto intConvertedValue = value.toULongLong(&ok);
  428. if (ok) {
  429. result.lockOwnerType = static_cast<SyncFileItem::LockOwnerType>(intConvertedValue);
  430. } else {
  431. result.lockOwnerType = SyncFileItem::LockOwnerType::UserLock;
  432. }
  433. }
  434. if (property == "lock-owner-editor") {
  435. result.lockEditorApp = value;
  436. }
  437. if (property == "lock-time") {
  438. auto ok = false;
  439. const auto intConvertedValue = value.toULongLong(&ok);
  440. if (ok) {
  441. result.lockTime = intConvertedValue;
  442. } else {
  443. result.lockTime = 0;
  444. }
  445. }
  446. if (property == "lock-timeout") {
  447. auto ok = false;
  448. const auto intConvertedValue = value.toULongLong(&ok);
  449. if (ok) {
  450. result.lockTimeout = intConvertedValue;
  451. } else {
  452. result.lockTimeout = 0;
  453. }
  454. }
  455. }
  456. if (result.isDirectory && map.contains("size")) {
  457. result.sizeOfFolder = map.value("size").toInt();
  458. }
  459. }
  460. void DiscoverySingleDirectoryJob::directoryListingIteratedSlot(const QString &file, const QMap<QString, QString> &map)
  461. {
  462. if (!_ignoredFirst) {
  463. // The first entry is for the folder itself, we should process it differently.
  464. _ignoredFirst = true;
  465. if (map.contains("permissions")) {
  466. auto perm = RemotePermissions::fromServerString(map.value("permissions"));
  467. emit firstDirectoryPermissions(perm);
  468. _isExternalStorage = perm.hasPermission(RemotePermissions::IsMounted);
  469. }
  470. if (map.contains("data-fingerprint")) {
  471. _dataFingerprint = map.value("data-fingerprint").toUtf8();
  472. if (_dataFingerprint.isEmpty()) {
  473. // Placeholder that means that the server supports the feature even if it did not set one.
  474. _dataFingerprint = "[empty]";
  475. }
  476. }
  477. if (map.contains(QStringLiteral("fileid"))) {
  478. _localFileId = map.value(QStringLiteral("fileid")).toUtf8();
  479. }
  480. if (map.contains("id")) {
  481. _fileId = map.value("id").toUtf8();
  482. }
  483. if (map.contains("is-encrypted") && map.value("is-encrypted") == QStringLiteral("1")) {
  484. _isE2eEncrypted = SyncFileItem::EncryptionStatus::Encrypted;
  485. Q_ASSERT(!_fileId.isEmpty());
  486. }
  487. if (map.contains("size")) {
  488. _size = map.value("size").toInt();
  489. }
  490. } else {
  491. RemoteInfo result;
  492. int slash = file.lastIndexOf('/');
  493. result.name = file.mid(slash + 1);
  494. result.size = -1;
  495. propertyMapToRemoteInfo(map, result);
  496. if (result.isDirectory)
  497. result.size = 0;
  498. if (_isExternalStorage && result.remotePerm.hasPermission(RemotePermissions::IsMounted)) {
  499. /* All the entries in a external storage have 'M' in their permission. However, for all
  500. purposes in the desktop client, we only need to know about the mount points.
  501. So replace the 'M' by a 'm' for every sub entries in an external storage */
  502. result.remotePerm.unsetPermission(RemotePermissions::IsMounted);
  503. result.remotePerm.setPermission(RemotePermissions::IsMountedSub);
  504. }
  505. _results.push_back(std::move(result));
  506. }
  507. //This works in concerto with the RequestEtagJob and the Folder object to check if the remote folder changed.
  508. if (map.contains("getetag")) {
  509. if (_firstEtag.isEmpty()) {
  510. _firstEtag = parseEtag(map.value(QStringLiteral("getetag")).toUtf8()); // for directory itself
  511. }
  512. }
  513. }
  514. void DiscoverySingleDirectoryJob::lsJobFinishedWithoutErrorSlot()
  515. {
  516. if (!_ignoredFirst) {
  517. // This is a sanity check, if we haven't _ignoredFirst then it means we never received any directoryListingIteratedSlot
  518. // which means somehow the server XML was bogus
  519. emit finished(HttpError{ 0, tr("Server error: PROPFIND reply is not XML formatted!") });
  520. deleteLater();
  521. return;
  522. } else if (!_error.isEmpty()) {
  523. emit finished(HttpError{ 0, _error });
  524. deleteLater();
  525. return;
  526. } else if (isE2eEncrypted()) {
  527. emit etag(_firstEtag, QDateTime::fromString(QString::fromUtf8(_lsColJob->responseTimestamp()), Qt::RFC2822Date));
  528. fetchE2eMetadata();
  529. return;
  530. }
  531. emit etag(_firstEtag, QDateTime::fromString(QString::fromUtf8(_lsColJob->responseTimestamp()), Qt::RFC2822Date));
  532. emit finished(_results);
  533. deleteLater();
  534. }
  535. void DiscoverySingleDirectoryJob::lsJobFinishedWithErrorSlot(QNetworkReply *r)
  536. {
  537. const auto contentType = r->header(QNetworkRequest::ContentTypeHeader).toString();
  538. const auto invalidContentType = !contentType.contains("application/xml; charset=utf-8") &&
  539. !contentType.contains("application/xml; charset=\"utf-8\"") &&
  540. !contentType.contains("text/xml; charset=utf-8") &&
  541. !contentType.contains("text/xml; charset=\"utf-8\"");
  542. const auto httpCode = r->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
  543. auto msg = r->errorString();
  544. qCWarning(lcDiscovery) << "LSCOL job error" << r->errorString() << httpCode << r->error();
  545. if (r->error() == QNetworkReply::NoError && invalidContentType) {
  546. msg = tr("Server error: PROPFIND reply is not XML formatted!");
  547. }
  548. emit finished(HttpError{ httpCode, msg });
  549. deleteLater();
  550. }
  551. void DiscoverySingleDirectoryJob::fetchE2eMetadata()
  552. {
  553. const auto job = new GetMetadataApiJob(_account, _localFileId);
  554. connect(job, &GetMetadataApiJob::jsonReceived,
  555. this, &DiscoverySingleDirectoryJob::metadataReceived);
  556. connect(job, &GetMetadataApiJob::error,
  557. this, &DiscoverySingleDirectoryJob::metadataError);
  558. job->start();
  559. }
  560. void DiscoverySingleDirectoryJob::metadataReceived(const QJsonDocument &json, int statusCode)
  561. {
  562. qCDebug(lcDiscovery) << "Metadata received, applying it to the result list";
  563. Q_ASSERT(_subPath.startsWith('/'));
  564. const auto metadata = FolderMetadata(_account,
  565. _isE2eEncrypted == SyncFileItem::EncryptionStatus::EncryptedMigratedV1_2 ? FolderMetadata::RequiredMetadataVersion::Version1_2 : FolderMetadata::RequiredMetadataVersion::Version1,
  566. json.toJson(QJsonDocument::Compact),
  567. statusCode);
  568. _isFileDropDetected = metadata.isFileDropPresent();
  569. _encryptedMetadataNeedUpdate = metadata.encryptedMetadataNeedUpdate();
  570. const auto encryptedFiles = metadata.files();
  571. const auto findEncryptedFile = [=](const QString &name) {
  572. const auto it = std::find_if(std::cbegin(encryptedFiles), std::cend(encryptedFiles), [=](const EncryptedFile &file) {
  573. return file.encryptedFilename == name;
  574. });
  575. if (it == std::cend(encryptedFiles)) {
  576. return Optional<EncryptedFile>();
  577. } else {
  578. return Optional<EncryptedFile>(*it);
  579. }
  580. };
  581. std::transform(std::cbegin(_results), std::cend(_results), std::begin(_results), [=](const RemoteInfo &info) {
  582. auto result = info;
  583. const auto encryptedFileInfo = findEncryptedFile(result.name);
  584. if (encryptedFileInfo) {
  585. result._isE2eEncrypted = true;
  586. result.e2eMangledName = _subPath.mid(1) + QLatin1Char('/') + result.name;
  587. result.name = encryptedFileInfo->originalFilename;
  588. }
  589. return result;
  590. });
  591. emit finished(_results);
  592. deleteLater();
  593. }
  594. void DiscoverySingleDirectoryJob::metadataError(const QByteArray &fileId, int httpReturnCode)
  595. {
  596. qCWarning(lcDiscovery) << "E2EE Metadata job error. Trying to proceed without it." << fileId << httpReturnCode;
  597. emit finished(_results);
  598. deleteLater();
  599. }
  600. }