Folderwatcher: On linux, fix paths after dir renames #6808
authorChristian Kamm <mail@ckamm.de>
Fri, 12 Oct 2018 09:03:10 +0000 (11:03 +0200)
committerCamila (Rebase PR Action) <hello@camila.codes>
Tue, 24 Nov 2020 16:56:49 +0000 (16:56 +0000)
If a folder was renamed A -> B, the folder watcher for the inode
would be unaware and still report changes for A/foo. Now directory
renames in the watched folders are tracked and paths are updated
accordingly.

src/gui/folderwatcher_linux.cpp
src/gui/folderwatcher_linux.h
test/testfolderwatcher.cpp

index a5215b3b5731b7197ca89066dfc33b0eaa280868..7c207c4f537a8ebe3ea6f1b286574255fcd42bc9 100644 (file)
@@ -31,6 +31,11 @@ FolderWatcherPrivate::FolderWatcherPrivate(FolderWatcher *p, const QString &path
     , _parent(p)
     , _folder(path)
 {
+    _wipePotentialRenamesSoon = new QTimer(this);
+    _wipePotentialRenamesSoon->setInterval(1000);
+    _wipePotentialRenamesSoon->setSingleShot(true);
+    connect(_wipePotentialRenamesSoon, &QTimer::timeout, this, &FolderWatcherPrivate::wipePotentialRenames);
+
     _fd = inotify_init();
     if (_fd != -1) {
         _socket.reset(new QSocketNotifier(_fd, QSocketNotifier::Read));
@@ -89,6 +94,17 @@ void FolderWatcherPrivate::inotifyRegisterPath(const QString &path)
     }
 }
 
+void FolderWatcherPrivate::applyDirectoryRename(const FolderWatcherPrivate::Rename &rename)
+{
+    QString fromSlash = rename.from + "/";
+    qCInfo(lcFolderWatcher) << "Applying rename from" << rename.from << "to" << rename.to;
+    for (auto &watch : _watches) {
+        if (watch == rename.from || watch.startsWith(fromSlash)) {
+            watch = rename.to + watch.mid(rename.from.size());
+        }
+    }
+}
+
 void FolderWatcherPrivate::slotAddFolderRecursive(const QString &path)
 {
     int subdirs = 0;
@@ -124,6 +140,11 @@ void FolderWatcherPrivate::slotAddFolderRecursive(const QString &path)
     }
 }
 
+void FolderWatcherPrivate::wipePotentialRenames()
+{
+    _potentialRenames.clear();
+}
+
 void FolderWatcherPrivate::slotReceivedNotification(int fd)
 {
     int len = 0;
@@ -152,33 +173,44 @@ void FolderWatcherPrivate::slotReceivedNotification(int fd)
         error = errno;
     }
 
-    // reset counter
-    i = 0;
-    // while there are enough events in the buffer
-    while (i + sizeof(struct inotify_event) < static_cast<unsigned int>(len)) {
+    // iterate events in buffer
+    unsigned int ulen = len;
+    for (i = 0; i + sizeof(inotify_event) < ulen; i += sizeof(inotify_event) + (event ? event->len : 0)) {
         // cast an inotify_event
         event = (struct inotify_event *)&buffer[i];
         if (!event) {
             qCDebug(lcFolderWatcher) << "NULL event";
-            i += sizeof(struct inotify_event);
             continue;
         }
 
         // Fire event for the path that was changed.
-        if (event->len > 0 && event->wd > -1) {
-            QByteArray fileName(event->name);
-            if (fileName.startsWith("._sync_")
-                || fileName.startsWith(".csync_journal.db")
-                || fileName.startsWith(".owncloudsync.log")
-                || fileName.startsWith(".sync_")) {
+        if (event->len == 0 || event->wd <= -1)
+            continue;
+        QByteArray fileName(event->name);
+        if (fileName.startsWith("._sync_")
+            || fileName.startsWith(".csync_journal.db")
+            || fileName.startsWith(".owncloudsync.log")
+            || fileName.startsWith(".sync_")) {
+            continue;
+        }
+        const QString p = _watches[event->wd] + '/' + fileName;
+        _parent->changeDetected(p);
+
+        // Collect events to form complete renames where possible
+        // and apply directory renames to the cached paths.
+        if ((event->mask & (IN_MOVED_TO | IN_MOVED_FROM)) && (event->mask & IN_ISDIR) && event->cookie > 0) {
+            auto &rename = _potentialRenames[event->cookie];
+            if (event->mask & IN_MOVED_TO)
+                rename.to = p;
+            if (event->mask & IN_MOVED_FROM)
+                rename.from = p;
+            if (!rename.from.isEmpty() && !rename.to.isEmpty()) {
+                applyDirectoryRename(rename);
+                _potentialRenames.remove(event->cookie);
             } else {
-                const QString p = _watches[event->wd] + '/' + fileName;
-                _parent->changeDetected(p);
+                _wipePotentialRenamesSoon->start();
             }
         }
-
-        // increment counter
-        i += sizeof(struct inotify_event) + event->len;
     }
 }
 
index e44fc421c8856c33d949aa2c41063c0de8201b38..4501f8f3b9cf26c5fa9fede374371e32d031f674 100644 (file)
@@ -23,6 +23,8 @@
 
 #include "folderwatcher.h"
 
+class QTimer;
+
 namespace OCC {
 
 /**
@@ -44,10 +46,22 @@ protected slots:
     void slotReceivedNotification(int fd);
     void slotAddFolderRecursive(const QString &path);
 
+    /// Remove all half-built renames. Called by timer when idle for a bit.
+    void wipePotentialRenames();
+
 protected:
+    struct Rename
+    {
+        QString from;
+        QString to;
+    };
+
     bool findFoldersBelow(const QDir &dir, QStringList &fullList);
     void inotifyRegisterPath(const QString &path);
 
+    /// Adjusts the paths in _watches when directories are renamed.
+    void applyDirectoryRename(const Rename &rename);
+
 private:
     FolderWatcher *_parent;
 
@@ -55,6 +69,25 @@ private:
     QHash<int, QString> _watches;
     QScopedPointer<QSocketNotifier> _socket;
     int _fd;
+
+    /** Maps inotify event cookie to rename data.
+     *
+     * For moves two independent inotify events will be seen and they
+     * can be matched via the event cookie. This field stores partial
+     * information as it is received. When both sides have arrived,
+     * directory moves can be processed with applyDirectoryRename().
+     *
+     * If we don't receive both sides (if something moves away from
+     * the watched folder tree, or into it from an unwatched location)
+     * the _wipePotentialRenamesSoon will eventually discard the
+     * incomplete data.
+     *
+     * These events can even be emitted by different watches if the
+     * directory parent folder changed.
+     */
+    QHash<quint32, Rename> _potentialRenames;
+
+    QTimer *_wipePotentialRenamesSoon;
 };
 }
 
index f515a1d60a68b33f347dcb2fefadefba51facae1..9d44c85cd6ac2893aa56059a6497bd6042c74a03 100644 (file)
@@ -193,6 +193,48 @@ private slots:
         QVERIFY(waitForPathChanged(old_file));
         QVERIFY(waitForPathChanged(new_file));
     }
+
+    void testRenameDirectorySameBase() {
+        QString old_file(_rootPath+"/a1/b1");
+        QString new_file(_rootPath+"/a1/brename");
+        QVERIFY(QFile::exists(old_file));
+        mv(old_file, new_file);
+        QVERIFY(QFile::exists(new_file));
+
+        QVERIFY(waitForPathChanged(old_file));
+        QVERIFY(waitForPathChanged(new_file));
+
+        // Verify that further notifications end up with the correct paths
+
+        QString file(_rootPath+"/a1/brename/c1/random.bin");
+        touch(file);
+        QVERIFY(waitForPathChanged(file));
+
+        QString dir(_rootPath+"/a1/brename/newfolder");
+        mkdir(dir);
+        QVERIFY(waitForPathChanged(dir));
+    }
+
+    void testRenameDirectoryDifferentBase() {
+        QString old_file(_rootPath+"/a1/brename");
+        QString new_file(_rootPath+"/bren");
+        QVERIFY(QFile::exists(old_file));
+        mv(old_file, new_file);
+        QVERIFY(QFile::exists(new_file));
+
+        QVERIFY(waitForPathChanged(old_file));
+        QVERIFY(waitForPathChanged(new_file));
+
+        // Verify that further notifications end up with the correct paths
+
+        QString file(_rootPath+"/bren/c1/random.bin");
+        touch(file);
+        QVERIFY(waitForPathChanged(file));
+
+        QString dir(_rootPath+"/bren/newfolder2");
+        mkdir(dir);
+        QVERIFY(waitForPathChanged(dir));
+    }
 };
 
 #ifdef Q_OS_MAC