discoveryphase.cpp 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495
  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 "account.h"
  17. #include "common/asserts.h"
  18. #include "common/checksums.h"
  19. #include <csync_exclude.h>
  20. #include "vio/csync_vio_local.h"
  21. #include <QLoggingCategory>
  22. #include <QUrl>
  23. #include <QFile>
  24. #include <QFileInfo>
  25. #include <QTextCodec>
  26. #include <cstring>
  27. namespace OCC {
  28. Q_LOGGING_CATEGORY(lcDiscovery, "nextcloud.sync.discovery", QtInfoMsg)
  29. /* Given a sorted list of paths ending with '/', return whether or not the given path is within one of the paths of the list*/
  30. static bool findPathInList(const QStringList &list, const QString &path)
  31. {
  32. Q_ASSERT(std::is_sorted(list.begin(), list.end()));
  33. if (list.size() == 1 && list.first() == QLatin1String("/")) {
  34. // Special case for the case "/" is there, it matches everything
  35. return true;
  36. }
  37. QString pathSlash = path + QLatin1Char('/');
  38. // Since the list is sorted, we can do a binary search.
  39. // If the path is a prefix of another item or right after in the lexical order.
  40. auto it = std::lower_bound(list.begin(), list.end(), pathSlash);
  41. if (it != list.end() && *it == pathSlash) {
  42. return true;
  43. }
  44. if (it == list.begin()) {
  45. return false;
  46. }
  47. --it;
  48. Q_ASSERT(it->endsWith(QLatin1Char('/'))); // Folder::setSelectiveSyncBlackList makes sure of that
  49. return pathSlash.startsWith(*it);
  50. }
  51. bool DiscoveryPhase::isInSelectiveSyncBlackList(const QString &path) const
  52. {
  53. if (_selectiveSyncBlackList.isEmpty()) {
  54. // If there is no black list, everything is allowed
  55. return false;
  56. }
  57. // Block if it is in the black list
  58. if (findPathInList(_selectiveSyncBlackList, path)) {
  59. return true;
  60. }
  61. return false;
  62. }
  63. void DiscoveryPhase::checkSelectiveSyncNewFolder(const QString &path, RemotePermissions remotePerm,
  64. std::function<void(bool)> callback)
  65. {
  66. if (_syncOptions._confirmExternalStorage && _syncOptions._vfs->mode() == Vfs::Off
  67. && remotePerm.hasPermission(RemotePermissions::IsMounted)) {
  68. // external storage.
  69. /* Note: DiscoverySingleDirectoryJob::directoryListingIteratedSlot make sure that only the
  70. * root of a mounted storage has 'M', all sub entries have 'm' */
  71. // Only allow it if the white list contains exactly this path (not parents)
  72. // We want to ask confirmation for external storage even if the parents where selected
  73. if (_selectiveSyncWhiteList.contains(path + QLatin1Char('/'))) {
  74. return callback(false);
  75. }
  76. emit newBigFolder(path, true);
  77. return callback(true);
  78. }
  79. // If this path or the parent is in the white list, then we do not block this file
  80. if (findPathInList(_selectiveSyncWhiteList, path)) {
  81. return callback(false);
  82. }
  83. auto limit = _syncOptions._newBigFolderSizeLimit;
  84. if (limit < 0 || _syncOptions._vfs->mode() != Vfs::Off) {
  85. // no limit, everything is allowed;
  86. return callback(false);
  87. }
  88. // do a PROPFIND to know the size of this folder
  89. auto propfindJob = new PropfindJob(_account, _remoteFolder + path, this);
  90. propfindJob->setProperties(QList<QByteArray>() << "resourcetype"
  91. << "http://owncloud.org/ns:size");
  92. QObject::connect(propfindJob, &PropfindJob::finishedWithError,
  93. this, [=] { return callback(false); });
  94. QObject::connect(propfindJob, &PropfindJob::result, this, [=](const QVariantMap &values) {
  95. auto result = values.value(QLatin1String("size")).toLongLong();
  96. if (result >= limit) {
  97. // we tell the UI there is a new folder
  98. emit newBigFolder(path, false);
  99. return callback(true);
  100. } else {
  101. // it is not too big, put it in the white list (so we will not do more query for the children)
  102. // and and do not block.
  103. auto p = path;
  104. if (!p.endsWith(QLatin1Char('/')))
  105. p += QLatin1Char('/');
  106. _selectiveSyncWhiteList.insert(
  107. std::upper_bound(_selectiveSyncWhiteList.begin(), _selectiveSyncWhiteList.end(), p),
  108. p);
  109. return callback(false);
  110. }
  111. });
  112. propfindJob->start();
  113. }
  114. /* Given a path on the remote, give the path as it is when the rename is done */
  115. QString DiscoveryPhase::adjustRenamedPath(const QString &original, SyncFileItem::Direction d) const
  116. {
  117. return OCC::adjustRenamedPath(d == SyncFileItem::Down ? _renamedItemsRemote : _renamedItemsLocal, original);
  118. }
  119. QString adjustRenamedPath(const QMap<QString, QString> renamedItems, const QString original)
  120. {
  121. int slashPos = original.size();
  122. while ((slashPos = original.lastIndexOf('/', slashPos - 1)) > 0) {
  123. auto it = renamedItems.constFind(original.left(slashPos));
  124. if (it != renamedItems.constEnd()) {
  125. return *it + original.mid(slashPos);
  126. }
  127. }
  128. return original;
  129. }
  130. QPair<bool, QByteArray> DiscoveryPhase::findAndCancelDeletedJob(const QString &originalPath)
  131. {
  132. bool result = false;
  133. QByteArray oldEtag;
  134. auto it = _deletedItem.find(originalPath);
  135. if (it != _deletedItem.end()) {
  136. const csync_instructions_e instruction = (*it)->_instruction;
  137. if (instruction == CSYNC_INSTRUCTION_IGNORE && (*it)->_type == ItemTypeVirtualFile) {
  138. // re-creation of virtual files count as a delete
  139. // a file might be in an error state and thus gets marked as CSYNC_INSTRUCTION_IGNORE
  140. // after it was initially marked as CSYNC_INSTRUCTION_REMOVE
  141. // return true, to not trigger any additional actions on that file that could elad to dataloss
  142. result = true;
  143. oldEtag = (*it)->_etag;
  144. } else {
  145. ENFORCE(instruction == CSYNC_INSTRUCTION_REMOVE
  146. // re-creation of virtual files count as a delete
  147. || ((*it)->_type == ItemTypeVirtualFile && instruction == CSYNC_INSTRUCTION_NEW)
  148. || ((*it)->_isRestoration && instruction == CSYNC_INSTRUCTION_NEW)
  149. );
  150. (*it)->_instruction = CSYNC_INSTRUCTION_NONE;
  151. result = true;
  152. oldEtag = (*it)->_etag;
  153. }
  154. _deletedItem.erase(it);
  155. }
  156. if (auto *otherJob = _queuedDeletedDirectories.take(originalPath)) {
  157. oldEtag = otherJob->_dirItem->_etag;
  158. delete otherJob;
  159. result = true;
  160. }
  161. return { result, oldEtag };
  162. }
  163. void DiscoveryPhase::startJob(ProcessDirectoryJob *job)
  164. {
  165. ENFORCE(!_currentRootJob);
  166. connect(job, &ProcessDirectoryJob::finished, this, [this, job] {
  167. ENFORCE(_currentRootJob == sender());
  168. _currentRootJob = nullptr;
  169. if (job->_dirItem)
  170. emit itemDiscovered(job->_dirItem);
  171. job->deleteLater();
  172. // Once the main job has finished recurse here to execute the remaining
  173. // jobs for queued deleted directories.
  174. if (!_queuedDeletedDirectories.isEmpty()) {
  175. auto nextJob = _queuedDeletedDirectories.take(_queuedDeletedDirectories.firstKey());
  176. startJob(nextJob);
  177. } else {
  178. emit finished();
  179. }
  180. });
  181. _currentRootJob = job;
  182. job->start();
  183. }
  184. void DiscoveryPhase::setSelectiveSyncBlackList(const QStringList &list)
  185. {
  186. _selectiveSyncBlackList = list;
  187. std::sort(_selectiveSyncBlackList.begin(), _selectiveSyncBlackList.end());
  188. }
  189. void DiscoveryPhase::setSelectiveSyncWhiteList(const QStringList &list)
  190. {
  191. _selectiveSyncWhiteList = list;
  192. std::sort(_selectiveSyncWhiteList.begin(), _selectiveSyncWhiteList.end());
  193. }
  194. void DiscoveryPhase::scheduleMoreJobs()
  195. {
  196. auto limit = qMax(1, _syncOptions._parallelNetworkJobs);
  197. if (_currentRootJob && _currentlyActiveJobs < limit) {
  198. _currentRootJob->processSubJobs(limit - _currentlyActiveJobs);
  199. }
  200. }
  201. DiscoverySingleLocalDirectoryJob::DiscoverySingleLocalDirectoryJob(const AccountPtr &account, const QString &localPath, OCC::Vfs *vfs, QObject *parent)
  202. : QObject(parent), QRunnable(), _localPath(localPath), _account(account), _vfs(vfs)
  203. {
  204. qRegisterMetaType<QVector<LocalInfo> >("QVector<LocalInfo>");
  205. }
  206. // Use as QRunnable
  207. void DiscoverySingleLocalDirectoryJob::run() {
  208. QString localPath = _localPath;
  209. if (localPath.endsWith('/')) // Happens if _currentFolder._local.isEmpty()
  210. localPath.chop(1);
  211. auto dh = csync_vio_local_opendir(localPath);
  212. if (!dh) {
  213. qCInfo(lcDiscovery) << "Error while opening directory" << (localPath) << errno;
  214. QString errorString = tr("Error while opening directory %1").arg(localPath);
  215. if (errno == EACCES) {
  216. errorString = tr("Directory not accessible on client, permission denied");
  217. emit finishedNonFatalError(errorString);
  218. return;
  219. } else if (errno == ENOENT) {
  220. errorString = tr("Directory not found: %1").arg(localPath);
  221. } else if (errno == ENOTDIR) {
  222. // Not a directory..
  223. // Just consider it is empty
  224. return;
  225. }
  226. emit finishedFatalError(errorString);
  227. return;
  228. }
  229. QVector<LocalInfo> results;
  230. while (true) {
  231. errno = 0;
  232. auto dirent = csync_vio_local_readdir(dh, _vfs);
  233. if (!dirent)
  234. break;
  235. if (dirent->type == ItemTypeSkip)
  236. continue;
  237. LocalInfo i;
  238. static QTextCodec *codec = QTextCodec::codecForName("UTF-8");
  239. ASSERT(codec);
  240. QTextCodec::ConverterState state;
  241. i.name = codec->toUnicode(dirent->path, dirent->path.size(), &state);
  242. if (state.invalidChars > 0 || state.remainingChars > 0) {
  243. emit childIgnored(true);
  244. auto item = SyncFileItemPtr::create();
  245. //item->_file = _currentFolder._target + i.name;
  246. // FIXME ^^ do we really need to use _target or is local fine?
  247. item->_file = _localPath + i.name;
  248. item->_instruction = CSYNC_INSTRUCTION_IGNORE;
  249. item->_status = SyncFileItem::NormalError;
  250. item->_errorString = tr("Filename encoding is not valid");
  251. emit itemDiscovered(item);
  252. continue;
  253. }
  254. i.modtime = dirent->modtime;
  255. i.size = dirent->size;
  256. i.inode = dirent->inode;
  257. i.isDirectory = dirent->type == ItemTypeDirectory;
  258. i.isHidden = dirent->is_hidden;
  259. i.isSymLink = dirent->type == ItemTypeSoftLink;
  260. i.isVirtualFile = dirent->type == ItemTypeVirtualFile || dirent->type == ItemTypeVirtualFileDownload;
  261. i.type = dirent->type;
  262. results.push_back(i);
  263. }
  264. if (errno != 0) {
  265. csync_vio_local_closedir(dh);
  266. // Note: Windows vio converts any error into EACCES
  267. qCWarning(lcDiscovery) << "readdir failed for file in " << localPath << " - errno: " << errno;
  268. emit finishedFatalError(tr("Error while reading directory %1").arg(localPath));
  269. return;
  270. }
  271. errno = 0;
  272. csync_vio_local_closedir(dh);
  273. if (errno != 0) {
  274. qCWarning(lcDiscovery) << "closedir failed for file in " << localPath << " - errno: " << errno;
  275. }
  276. emit finished(results);
  277. }
  278. DiscoverySingleDirectoryJob::DiscoverySingleDirectoryJob(const AccountPtr &account, const QString &path, QObject *parent)
  279. : QObject(parent)
  280. , _subPath(path)
  281. , _account(account)
  282. , _ignoredFirst(false)
  283. , _isRootPath(false)
  284. , _isExternalStorage(false)
  285. {
  286. }
  287. void DiscoverySingleDirectoryJob::start()
  288. {
  289. // Start the actual HTTP job
  290. auto *lsColJob = new LsColJob(_account, _subPath, this);
  291. QList<QByteArray> props;
  292. props << "resourcetype"
  293. << "getlastmodified"
  294. << "getcontentlength"
  295. << "getetag"
  296. << "http://owncloud.org/ns:id"
  297. << "http://owncloud.org/ns:downloadURL"
  298. << "http://owncloud.org/ns:dDC"
  299. << "http://owncloud.org/ns:permissions"
  300. << "http://owncloud.org/ns:checksums";
  301. if (_isRootPath)
  302. props << "http://owncloud.org/ns:data-fingerprint";
  303. if (_account->serverVersionInt() >= Account::makeServerVersion(10, 0, 0)) {
  304. // Server older than 10.0 have performances issue if we ask for the share-types on every PROPFIND
  305. props << "http://owncloud.org/ns:share-types";
  306. }
  307. lsColJob->setProperties(props);
  308. QObject::connect(lsColJob, &LsColJob::directoryListingIterated,
  309. this, &DiscoverySingleDirectoryJob::directoryListingIteratedSlot);
  310. QObject::connect(lsColJob, &LsColJob::finishedWithError, this, &DiscoverySingleDirectoryJob::lsJobFinishedWithErrorSlot);
  311. QObject::connect(lsColJob, &LsColJob::finishedWithoutError, this, &DiscoverySingleDirectoryJob::lsJobFinishedWithoutErrorSlot);
  312. lsColJob->start();
  313. _lsColJob = lsColJob;
  314. }
  315. void DiscoverySingleDirectoryJob::abort()
  316. {
  317. if (_lsColJob && _lsColJob->reply()) {
  318. _lsColJob->reply()->abort();
  319. }
  320. }
  321. static void propertyMapToRemoteInfo(const QMap<QString, QString> &map, RemoteInfo &result)
  322. {
  323. for (auto it = map.constBegin(); it != map.constEnd(); ++it) {
  324. QString property = it.key();
  325. QString value = it.value();
  326. if (property == "resourcetype") {
  327. result.isDirectory = value.contains("collection");
  328. } else if (property == "getlastmodified") {
  329. result.modtime = oc_httpdate_parse(value.toUtf8());
  330. } else if (property == "getcontentlength") {
  331. // See #4573, sometimes negative size values are returned
  332. bool ok = false;
  333. qlonglong ll = value.toLongLong(&ok);
  334. if (ok && ll >= 0) {
  335. result.size = ll;
  336. } else {
  337. result.size = 0;
  338. }
  339. } else if (property == "getetag") {
  340. result.etag = Utility::normalizeEtag(value.toUtf8());
  341. } else if (property == "id") {
  342. result.fileId = value.toUtf8();
  343. } else if (property == "downloadURL") {
  344. result.directDownloadUrl = value;
  345. } else if (property == "dDC") {
  346. result.directDownloadCookies = value;
  347. } else if (property == "permissions") {
  348. result.remotePerm = RemotePermissions::fromServerString(value);
  349. } else if (property == "checksums") {
  350. result.checksumHeader = findBestChecksum(value.toUtf8());
  351. } else if (property == "share-types" && !value.isEmpty()) {
  352. // Since QMap is sorted, "share-types" is always after "permissions".
  353. if (result.remotePerm.isNull()) {
  354. qWarning() << "Server returned a share type, but no permissions?";
  355. } else {
  356. // S means shared with me.
  357. // But for our purpose, we want to know if the file is shared. It does not matter
  358. // if we are the owner or not.
  359. // Piggy back on the persmission field
  360. result.remotePerm.setPermission(RemotePermissions::IsShared);
  361. }
  362. }
  363. }
  364. }
  365. void DiscoverySingleDirectoryJob::directoryListingIteratedSlot(QString file, const QMap<QString, QString> &map)
  366. {
  367. if (!_ignoredFirst) {
  368. // The first entry is for the folder itself, we should process it differently.
  369. _ignoredFirst = true;
  370. if (map.contains("permissions")) {
  371. auto perm = RemotePermissions::fromServerString(map.value("permissions"));
  372. emit firstDirectoryPermissions(perm);
  373. _isExternalStorage = perm.hasPermission(RemotePermissions::IsMounted);
  374. }
  375. if (map.contains("data-fingerprint")) {
  376. _dataFingerprint = map.value("data-fingerprint").toUtf8();
  377. if (_dataFingerprint.isEmpty()) {
  378. // Placeholder that means that the server supports the feature even if it did not set one.
  379. _dataFingerprint = "[empty]";
  380. }
  381. }
  382. } else {
  383. RemoteInfo result;
  384. int slash = file.lastIndexOf('/');
  385. result.name = file.mid(slash + 1);
  386. result.size = -1;
  387. propertyMapToRemoteInfo(map, result);
  388. if (result.isDirectory)
  389. result.size = 0;
  390. if (_isExternalStorage && result.remotePerm.hasPermission(RemotePermissions::IsMounted)) {
  391. /* All the entries in a external storage have 'M' in their permission. However, for all
  392. purposes in the desktop client, we only need to know about the mount points.
  393. So replace the 'M' by a 'm' for every sub entries in an external storage */
  394. result.remotePerm.unsetPermission(RemotePermissions::IsMounted);
  395. result.remotePerm.setPermission(RemotePermissions::IsMountedSub);
  396. }
  397. QStringRef fileRef(&file);
  398. int slashPos = file.lastIndexOf(QLatin1Char('/'));
  399. if (slashPos > -1) {
  400. fileRef = file.midRef(slashPos + 1);
  401. }
  402. _results.push_back(std::move(result));
  403. }
  404. //This works in concerto with the RequestEtagJob and the Folder object to check if the remote folder changed.
  405. if (map.contains("getetag")) {
  406. if (_firstEtag.isEmpty()) {
  407. _firstEtag = parseEtag(map.value("getetag").toUtf8()); // for directory itself
  408. }
  409. }
  410. }
  411. void DiscoverySingleDirectoryJob::lsJobFinishedWithoutErrorSlot()
  412. {
  413. if (!_ignoredFirst) {
  414. // This is a sanity check, if we haven't _ignoredFirst then it means we never received any directoryListingIteratedSlot
  415. // which means somehow the server XML was bogus
  416. emit finished(HttpError{ 0, tr("Server error: PROPFIND reply is not XML formatted!") });
  417. deleteLater();
  418. return;
  419. } else if (!_error.isEmpty()) {
  420. emit finished(HttpError{ 0, _error });
  421. deleteLater();
  422. return;
  423. }
  424. emit etag(_firstEtag);
  425. emit finished(_results);
  426. deleteLater();
  427. }
  428. void DiscoverySingleDirectoryJob::lsJobFinishedWithErrorSlot(QNetworkReply *r)
  429. {
  430. QString contentType = r->header(QNetworkRequest::ContentTypeHeader).toString();
  431. int httpCode = r->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
  432. QString httpReason = r->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString();
  433. QString msg = r->errorString();
  434. qCWarning(lcDiscovery) << "LSCOL job error" << r->errorString() << httpCode << r->error();
  435. if (r->error() == QNetworkReply::NoError
  436. && !contentType.contains("application/xml; charset=utf-8")) {
  437. msg = tr("Server error: PROPFIND reply is not XML formatted!");
  438. }
  439. emit finished(HttpError{ httpCode, msg });
  440. deleteLater();
  441. }
  442. }