From: Christian Kamm Date: Fri, 12 Oct 2018 09:03:10 +0000 (+0200) Subject: Folderwatcher: On linux, fix paths after dir renames #6808 X-Git-Tag: archive/raspbian/3.16.7-1_deb13u1+rpi1~1^2~12^2~22^2~47^2~6 X-Git-Url: https://dgit.raspbian.org/?a=commitdiff_plain;h=351bada5522dffad89665650d30d63b64d0c7e8d;p=nextcloud-desktop.git Folderwatcher: On linux, fix paths after dir renames #6808 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. --- diff --git a/src/gui/folderwatcher_linux.cpp b/src/gui/folderwatcher_linux.cpp index a5215b3b5..7c207c4f5 100644 --- a/src/gui/folderwatcher_linux.cpp +++ b/src/gui/folderwatcher_linux.cpp @@ -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(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; } } diff --git a/src/gui/folderwatcher_linux.h b/src/gui/folderwatcher_linux.h index e44fc421c..4501f8f3b 100644 --- a/src/gui/folderwatcher_linux.h +++ b/src/gui/folderwatcher_linux.h @@ -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 _watches; QScopedPointer _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 _potentialRenames; + + QTimer *_wipePotentialRenamesSoon; }; } diff --git a/test/testfolderwatcher.cpp b/test/testfolderwatcher.cpp index f515a1d60..9d44c85cd 100644 --- a/test/testfolderwatcher.cpp +++ b/test/testfolderwatcher.cpp @@ -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