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 
15 #pragma once
16 
17 #include <QObject>
18 #include <QElapsedTimer>
19 #include <QStringList>
20 #include <csync.h>
21 #include <QMap>
22 #include <QSet>
23 #include "networkjobs.h"
24 #include <QMutex>
25 #include <QWaitCondition>
26 #include <QLinkedList>
27 #include <QRunnable>
28 #include <deque>
29 #include "syncoptions.h"
30 #include "syncfileitem.h"
31 
32 class ExcludedFiles;
33 
34 namespace OCC {
35 
36 enum class LocalDiscoveryStyle {
37     FilesystemOnly, //< read all local data from the filesystem
38     DatabaseAndFilesystem, //< read from the db, except for listed paths
39 };
40 
41 
42 class Account;
43 class SyncJournalDb;
44 class ProcessDirectoryJob;
45 
46 /**
47  * Represent all the meta-data about a file in the server
48  */
49 struct RemoteInfo
50 {
51     /** FileName of the entry (this does not contains any directory or path, just the plain name */
52     QString name;
53     QByteArray etag;
54     QByteArray fileId;
55     QByteArray checksumHeader;
56     OCC::RemotePermissions remotePerm;
57     time_t modtime = 0;
58     int64_t size = 0;
59     bool isDirectory = false;
isValidRemoteInfo60     bool isValid() const { return !name.isNull(); }
61 
62     QString directDownloadUrl;
63     QString directDownloadCookies;
64 };
65 
66 struct LocalInfo
67 {
68     /** FileName of the entry (this does not contains any directory or path, just the plain name */
69     QString name;
70     time_t modtime = 0;
71     int64_t size = 0;
72     uint64_t inode = 0;
73     ItemType type = ItemTypeSkip;
74     bool isDirectory = false;
75     bool isHidden = false;
76     bool isVirtualFile = false;
77     bool isSymLink = false;
isValidLocalInfo78     bool isValid() const { return !name.isNull(); }
79 };
80 
81 /**
82  * @brief Run list on a local directory and process the results for Discovery
83  *
84  * @ingroup libsync
85  */
86 class DiscoverySingleLocalDirectoryJob : public QObject, public QRunnable
87 {
88     Q_OBJECT
89 public:
90     explicit DiscoverySingleLocalDirectoryJob(const AccountPtr &account, const QString &localPath, OCC::Vfs *vfs, QObject *parent = nullptr);
91 
92     void run() override;
93 signals:
94     void finished(QVector<LocalInfo> result);
95     void finishedFatalError(QString errorString);
96     void finishedNonFatalError(QString errorString);
97 
98     void itemDiscovered(SyncFileItemPtr item);
99     void childIgnored(bool b);
100 private slots:
101 private:
102     QString _localPath;
103     AccountPtr _account;
104     OCC::Vfs* _vfs;
105 public:
106 };
107 
108 
109 /**
110  * @brief Run a PROPFIND on a directory and process the results for Discovery
111  *
112  * @ingroup libsync
113  */
114 class DiscoverySingleDirectoryJob : public QObject
115 {
116     Q_OBJECT
117 public:
118     explicit DiscoverySingleDirectoryJob(const AccountPtr &account, const QString &path, QObject *parent = nullptr);
119     // Specify that this is the root and we need to check the data-fingerprint
setIsRootPath()120     void setIsRootPath() { _isRootPath = true; }
121     void start();
122     void abort();
123 
124     // This is not actually a network job, it is just a job
125 signals:
126     void firstDirectoryPermissions(RemotePermissions);
127     void etag(const QString &);
128     void finished(const HttpResult<QVector<RemoteInfo>> &result);
129 
130 private slots:
131     void directoryListingIteratedSlot(const QString &, const QMap<QString, QString> &);
132     void lsJobFinishedWithoutErrorSlot();
133     void lsJobFinishedWithErrorSlot(QNetworkReply *);
134 
135 private:
136     QVector<RemoteInfo> _results;
137     QString _subPath;
138     QString _firstEtag;
139     AccountPtr _account;
140     // The first result is for the directory itself and need to be ignored.
141     // This flag is true if it was already ignored.
142     bool _ignoredFirst;
143     // Set to true if this is the root path and we need to check the data-fingerprint
144     bool _isRootPath;
145     // If this directory is an external storage (The first item has 'M' in its permission)
146     bool _isExternalStorage;
147     // If set, the discovery will finish with an error
148     QString _error;
149     QPointer<LsColJob> _lsColJob;
150 
151 public:
152     QByteArray _dataFingerprint;
153 };
154 
155 class DiscoveryPhase : public QObject
156 {
157     Q_OBJECT
158 
159     friend class ProcessDirectoryJob;
160 
161     QPointer<ProcessDirectoryJob> _currentRootJob;
162 
163     /** Maps the db-path of a deleted item to its SyncFileItem.
164      *
165      * If it turns out the item was renamed after all, the instruction
166      * can be changed. See findAndCancelDeletedJob(). Note that
167      * itemDiscovered() will already have been emitted for the item.
168      */
169     QMap<QString, SyncFileItemPtr> _deletedItem;
170 
171     /** Maps the db-path of a deleted folder to its queued job.
172      *
173      * If a folder is deleted and must be recursed into, its job isn't
174      * executed immediately. Instead it's queued here and only run
175      * once the rest of the discovery has finished and we are certain
176      * that the folder wasn't just renamed. This avoids running the
177      * discovery on contents in the old location of renamed folders.
178      *
179      * See findAndCancelDeletedJob().
180      */
181     QMap<QString, ProcessDirectoryJob *> _queuedDeletedDirectories;
182 
183     // map source (original path) -> destinations (current server or local path)
184     QMap<QString, QString> _renamedItemsRemote;
185     QMap<QString, QString> _renamedItemsLocal;
186 
187     // set of paths that should not be removed even though they are removed locally:
188     // there was a move to an invalid destination and now the source should be restored
189     //
190     // This applies recursively to subdirectories.
191     // All entries should have a trailing slash (even files), so lookup with
192     // lowerBound() is reliable.
193     //
194     // The value of this map doesn't matter.
195     QMap<QString, bool> _forbiddenDeletes;
196 
197     /** Returns whether the db-path has been renamed locally or on the remote.
198      *
199      * Useful for avoiding processing of items that have already been claimed in
200      * a rename (would otherwise be discovered as deletions).
201      */
isRenamed(const QString & p)202     bool isRenamed(const QString &p) const { return _renamedItemsLocal.contains(p) || _renamedItemsRemote.contains(p); }
203 
204     int _currentlyActiveJobs = 0;
205 
206     // both must contain a sorted list
207     QStringList _selectiveSyncBlackList;
208     QStringList _selectiveSyncWhiteList;
209 
210     void scheduleMoreJobs();
211 
212     bool isInSelectiveSyncBlackList(const QString &path) const;
213 
214     // Check if the new folder should be deselected or not.
215     // May be async. "Return" via the callback, true if the item is blacklisted
216     void checkSelectiveSyncNewFolder(const QString &path, RemotePermissions rp,
217         std::function<void(bool)> callback);
218 
219     /** Given an original path, return the target path obtained when renaming is done.
220      *
221      * Note that it only considers parent directory renames. So if A/B got renamed to C/D,
222      * checking A/B/file would yield C/D/file, but checking A/B would yield A/B.
223      */
224     QString adjustRenamedPath(const QString &original, SyncFileItem::Direction) const;
225 
226     /** If the db-path is scheduled for deletion, abort it.
227      *
228      * Check if there is already a job to delete that item:
229      * If that's not the case, return { false, QByteArray() }.
230      * If there is such a job, cancel that job and return true and the old etag.
231      *
232      * Used when having detected a rename: The rename source may have been
233      * discovered before and would have looked like a delete.
234      *
235      * See _deletedItem and _queuedDeletedDirectories.
236      */
237     QPair<bool, QByteArray> findAndCancelDeletedJob(const QString &originalPath);
238 
239 public:
240     // input
241     QString _localDir; // absolute path to the local directory. ends with '/'
242     QString _remoteFolder; // remote folder, ends with '/'
243     SyncJournalDb *_statedb;
244     AccountPtr _account;
245     SyncOptions _syncOptions;
246     ExcludedFiles *_excludes;
247     QRegExp _invalidFilenameRx; // FIXME: maybe move in ExcludedFiles
248     QStringList _serverBlacklistedFiles; // The blacklist from the capabilities
249     bool _ignoreHiddenFiles = false;
250     std::function<bool(const QString &)> _shouldDiscoverLocaly;
251 
252     void startJob(ProcessDirectoryJob *);
253 
254     void setSelectiveSyncBlackList(const QStringList &list);
255     void setSelectiveSyncWhiteList(const QStringList &list);
256 
257     // output
258     QByteArray _dataFingerprint;
259     bool _anotherSyncNeeded = false;
260 
261 signals:
262     void fatalError(const QString &errorString);
263     void itemDiscovered(const SyncFileItemPtr &item);
264     void finished();
265 
266     // A new folder was discovered and was not synced because of the confirmation feature
267     void newBigFolder(const QString &folder, bool isExternal);
268 
269     /** For excluded items that don't show up in itemDiscovered()
270       *
271       * The path is relative to the sync folder, similar to item->_file
272       */
273     void silentlyExcluded(const QString &folderPath);
274 };
275 
276 /// Implementation of DiscoveryPhase::adjustRenamedPath
277 QString adjustRenamedPath(const QMap<QString, QString> &renamedItems, const QString &original);
278 }
279