Pin state updates
authorChristian Kamm <mail@ckamm.de>
Wed, 23 Jan 2019 14:12:02 +0000 (15:12 +0100)
committerKevin Ottens <kevin.ottens@nextcloud.com>
Tue, 15 Dec 2020 09:58:38 +0000 (10:58 +0100)
- unspecified and inherited are different
- move enum to header in common/
- access through Vfs instead of directly in Journal

15 files changed:
src/common/pinstate.h [new file with mode: 0644]
src/common/syncjournaldb.h
src/common/vfs.cpp
src/common/vfs.h
src/gui/folder.cpp
src/gui/folder.h
src/gui/folderwatcher_win.cpp
src/gui/socketapi.cpp
src/libsync/discovery.cpp
src/libsync/discovery.h
src/libsync/syncengine.cpp
src/libsync/vfs/suffix/vfs_suffix.cpp
src/libsync/vfs/suffix/vfs_suffix.h
test/syncenginetestutils.h
test/testsyncvirtualfiles.cpp

diff --git a/src/common/pinstate.h b/src/common/pinstate.h
new file mode 100644 (file)
index 0000000..5fb2671
--- /dev/null
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) by Christian Kamm <mail@ckamm.de>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+
+#ifndef PINSTATE_H
+#define PINSTATE_H
+
+#include "ocsynclib.h"
+
+namespace OCC {
+
+/** Determines whether items should be available locally permanently or not
+ *
+ * The idea is that files and folders can be marked with the user intent
+ * on availability.
+ *
+ * The Inherited state is used for resetting a pin state to what its
+ * parent path would do.
+ *
+ * The pin state of a directory usually only matters for the initial pin and
+ * hydration state of new remote files. It's perfectly possible for a
+ * AlwaysLocal directory to have only OnlineOnly items. (though setting pin
+ * states is usually done recursively, so one'd need to set the folder to
+ * pinned and then each contained item to unpinned)
+ *
+ * Note: This enum intentionally mimics CF_PIN_STATE of Windows cfapi.
+ */
+enum class PinState {
+    /** The pin state is derived from the state of the parent folder.
+     *
+     * For example new remote files start out in this state, following
+     * the state of their parent folder.
+     *
+     * This state is used purely for resetting pin states to their derived
+     * value. The effective state for an item will never be "Inherited".
+     */
+    Inherited = 0,
+
+    /** The file shall be available and up to date locally.
+     *
+     * Also known as "pinned". Pinned dehydrated files shall be hydrated
+     * as soon as possible.
+     */
+    AlwaysLocal = 1,
+
+    /** File shall be a dehydrated placeholder, filled on demand.
+     *
+     * Also known as "unpinned". Unpinned hydrated files shall be dehydrated
+     * as soon as possible.
+     *
+     * If a unpinned file becomes hydrated its pin state changes to unspecified.
+     */
+    OnlineOnly = 2,
+
+    /** The user hasn't made a decision. The client or platform may hydrate or
+     * dehydrate as they see fit.
+     *
+     * New remote files in unspecified directories start unspecified, and
+     * dehydrated (which is an arbitrary decision).
+     */
+    Unspecified = 3,
+};
+
+}
+
+#endif
index 0e280f8cb56519fa5782f9228f435dc964456048..2062581c1c1afdf786ad8c462f212da95aebc168 100644 (file)
 #include "common/ownsql.h"
 #include "common/syncjournalfilerecord.h"
 #include "common/result.h"
+#include "common/pinstate.h"
 
 namespace OCC {
 class SyncJournalFileRecord;
 
-/** Determines whether files should be available locally or not
- *
- * For new remote files the file's PinState is calculated by looking for
- * the closest parent folder that isn't Inherited.
- *
- * TODO: It seems to make sense to also store per-file PinStates.
- * Maybe these could communicate intent, similar to ItemTypeVirtualFileDownload
- * and ...FileDehydrate?
- */
-enum class PinState {
-    /// Inherit the PinState of the parent directory (default)
-    Inherited = 0,
-    /// Download file and keep it updated.
-    AlwaysLocal = 1,
-    /// File shall be virtual locally.
-    OnlineOnly = 2,
-};
-
 /**
  * @brief Class that handles the sync database
  *
index 4f3b6fadcdf26a37c81b38ee70fdc581ae96756d..7e712a65472db2bad0dddf671ff5f6b7d901eb82 100644 (file)
@@ -19,6 +19,7 @@
 #include "vfs.h"
 #include "plugin.h"
 #include "version.h"
+#include "syncjournaldb.h"
 
 #include <QPluginLoader>
 #include <QLoggingCategory>
@@ -59,11 +60,34 @@ Optional<Vfs::Mode> Vfs::modeFromString(const QString &str)
     return {};
 }
 
-VfsOff::VfsOff(QObject *parent)
+VfsDefaults::VfsDefaults(QObject *parent)
     : Vfs(parent)
 {
 }
 
+void VfsDefaults::start(const VfsSetupParams &params)
+{
+    _setupParams = params;
+}
+
+bool VfsDefaults::setPinState(const QString &folderPath, PinState state)
+{
+    auto path = folderPath.toUtf8();
+    _setupParams.journal->wipePinStateForPathAndBelow(path);
+    _setupParams.journal->setPinStateForPath(path, state);
+    return true;
+}
+
+Optional<PinState> VfsDefaults::getPinState(const QString &folderPath)
+{
+    return _setupParams.journal->effectivePinStateForPath(folderPath.toUtf8());
+}
+
+VfsOff::VfsOff(QObject *parent)
+    : VfsDefaults(parent)
+{
+}
+
 VfsOff::~VfsOff() = default;
 
 static QString modeToPluginName(Vfs::Mode mode)
index 9db7edc9ccf1cffd49fea6fd34729383277cc9db..35e7b86ace6af7cf2f6ef76d368936255612124a 100644 (file)
@@ -22,6 +22,7 @@
 #include "ocsynclib.h"
 #include "result.h"
 #include "syncfilestatus.h"
+#include "pinstate.h"
 
 typedef struct csync_file_stat_s csync_file_stat_t;
 
@@ -48,7 +49,7 @@ struct OCSYNC_EXPORT VfsSetupParams
      *
      * Note: The journal must live at least until the Vfs::stop() call.
      */
-    SyncJournalDb *journal;
+    SyncJournalDb *journal = nullptr;
 
     /// Strings potentially passed on to the platform
     QString providerName;
@@ -101,14 +102,14 @@ public:
     virtual QString fileSuffix() const = 0;
 
 
-    /// Must be called at least once before start(). May make sense to merge with start().
-    virtual void registerFolder(const VfsSetupParams &params) = 0;
-
     /** Initializes interaction with the VFS provider.
      *
      * For example, the VFS provider might monitor files to be able to start a file
      * hydration (download of a file's remote contents) when the user wants to open
      * it.
+     *
+     * Usually some registration needs to be done with the backend. This function
+     * should take care of it if necessary.
      */
     virtual void start(const VfsSetupParams &params) = 0;
 
@@ -160,6 +161,24 @@ public:
      */
     virtual bool statTypeVirtualFile(csync_file_stat_t *stat, void *stat_data) = 0;
 
+    /** Sets the pin state for the item at a path.
+     *
+     * Usually this would forward to setting the pin state flag in the db table,
+     * but some vfs plugins will store the pin state in file attributes instead.
+     *
+     * folderPath is relative to the sync folder.
+     */
+    virtual bool setPinState(const QString &folderPath, PinState state) = 0;
+
+    /** Returns the pin state of an item at a path.
+     *
+     * Usually backed by the db's effectivePinState() function but some vfs
+     * plugins will override it to retrieve the state from elsewhere.
+     *
+     * folderPath is relative to the sync folder.
+     */
+    virtual Optional<PinState> getPinState(const QString &folderPath) = 0;
+
 public slots:
     /** Update in-sync state based on SyncFileStatusTracker signal.
      *
@@ -176,8 +195,27 @@ signals:
     void doneHydrating();
 };
 
+class OCSYNC_EXPORT VfsDefaults : public Vfs
+{
+public:
+    explicit VfsDefaults(QObject* parent = nullptr);
+
+    // stores the params
+    void start(const VfsSetupParams &params) override;
+
+    // use the journal to back the pinstates
+    bool setPinState(const QString &folderPath, PinState state) override;
+    Optional<PinState> getPinState(const QString &folderPath) override;
+
+    // access initial setup data
+    const VfsSetupParams &params() const { return _setupParams; }
+
+protected:
+    VfsSetupParams _setupParams;
+};
+
 /// Implementation of Vfs for Vfs::Off mode - does nothing
-class OCSYNC_EXPORT VfsOff : public Vfs
+class OCSYNC_EXPORT VfsOff : public VfsDefaults
 {
     Q_OBJECT
 
@@ -189,12 +227,9 @@ public:
 
     QString fileSuffix() const override { return QString(); }
 
-    void registerFolder(const VfsSetupParams &) override {}
-    void start(const VfsSetupParams &) override {}
     void stop() override {}
     void unregisterFolder() override {}
 
-
     bool isHydrating() const override { return false; }
 
     bool updateMetadata(const QString &, time_t, quint64, const QByteArray &, QString *) override { return true; }
index f4a8107a13256d9c9f8280f75039219cbe0de3b4..0fb13ec1301aaa4160763a13500e970129f0553e 100644 (file)
@@ -487,7 +487,6 @@ void Folder::startVfs()
     connect(&_engine->syncFileStatusTracker(), &SyncFileStatusTracker::fileStatusChanged,
             _vfs.data(), &Vfs::fileStatusChanged);
 
-    _vfs->registerFolder(vfsParams); // Do this always?
     _vfs->start(vfsParams);
 }
 
index 0eb45286a4632025dd4303a28711a8e8592fbc69..ab395b04985ea569415871892155ef6ef1bb48f4 100644 (file)
@@ -205,6 +205,7 @@ public:
     // Used by the Socket API
     SyncJournalDb *journalDb() { return &_journal; }
     SyncEngine &syncEngine() { return *_engine; }
+    Vfs &vfs() { return *_vfs; }
 
     RequestEtagJob *etagJob() { return _requestEtagJob; }
     std::chrono::milliseconds msecSinceLastSync() const { return std::chrono::milliseconds(_timeSinceLastSyncDone.elapsed()); }
index 23175f73eefd615cc018f29ae2db97b42380ce66..9e184c32c1d9215d888a2f1c30241de4151b1145 100644 (file)
@@ -69,7 +69,10 @@ void WatcherThread::watchChanges(size_t fileNotifyBufferSize,
         SecureZeroMemory(pFileNotifyBuffer, fileNotifyBufferSize);
         if (!ReadDirectoryChangesW(_directory, (LPVOID)pFileNotifyBuffer,
                 OCC::Utility::convertSizeToDWORD(fileNotifyBufferSize), true,
-                FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME | FILE_NOTIFY_CHANGE_LAST_WRITE,
+                FILE_NOTIFY_CHANGE_FILE_NAME
+                | FILE_NOTIFY_CHANGE_DIR_NAME
+                | FILE_NOTIFY_CHANGE_LAST_WRITE
+                | FILE_NOTIFY_CHANGE_ATTRIBUTES, // attributes are for vfs pin state changes
                 &dwBytesReturned,
                 &overlapped,
                 nullptr)) {
index a420332115fc577b0a9db1c96b1798de8816e981..e4f44f639b408d9bf9017454c279b544b5f93822 100644 (file)
@@ -700,9 +700,8 @@ void SocketApi::command_MAKE_AVAILABLE_LOCALLY(const QString &filesArg, SocketLi
             continue;
 
         // Update the pin state on all items
-        auto pinPath = data.folderRelativePathNoVfsSuffix().toUtf8();
-        data.folder->journalDb()->wipePinStateForPathAndBelow(pinPath);
-        data.folder->journalDb()->setPinStateForPath(pinPath, PinState::AlwaysLocal);
+        auto pinPath = data.folderRelativePathNoVfsSuffix();
+        data.folder->vfs().setPinState(pinPath, PinState::AlwaysLocal);
 
         // Trigger the recursive download
         data.folder->downloadVirtualFile(data.folderRelativePath);
@@ -720,9 +719,8 @@ void SocketApi::command_MAKE_ONLINE_ONLY(const QString &filesArg, SocketListener
             continue;
 
         // Update the pin state on all items
-        auto pinPath = data.folderRelativePathNoVfsSuffix().toUtf8();
-        data.folder->journalDb()->wipePinStateForPathAndBelow(pinPath);
-        data.folder->journalDb()->setPinStateForPath(pinPath, PinState::OnlineOnly);
+        auto pinPath = data.folderRelativePathNoVfsSuffix();
+        data.folder->vfs().setPinState(pinPath, PinState::OnlineOnly);
 
         // Trigger recursive dehydration
         data.folder->dehydrateFile(data.folderRelativePath);
@@ -1021,7 +1019,7 @@ void SocketApi::command_GET_MENU_ITEMS(const QString &argument, OCC::SocketListe
         for (const auto &file : files) {
             auto fileData = FileData::get(file);
             auto path = fileData.folderRelativePathNoVfsSuffix();
-            auto pinState = syncFolder->journalDb()->effectivePinStateForPath(path.toUtf8());
+            auto pinState = syncFolder->vfs().getPinState(path);
             if (!pinState) {
                 // db error
                 hasAlwaysLocal = true;
index 04e50292c0c78a95b16b3fe775af013fc5293e04..286c72fc5eca439a9360517e7aac1738c9e57220 100644 (file)
@@ -426,14 +426,10 @@ void ProcessDirectoryJob::processFileAnalyzeRemoteInfo(
         }
         // Turn new remote files into virtual files if the option is enabled.
         auto &opts = _discoveryData->_syncOptions;
-        if (!directoryPinState()) {
-            dbError();
-            return;
-        }
         if (!localEntry.isValid()
             && item->_type == ItemTypeFile
             && opts._vfs->mode() != Vfs::Off
-            && *directoryPinState() == PinState::OnlineOnly) {
+            && _pinState != PinState::AlwaysLocal) {
             item->_type = ItemTypeVirtualFile;
             if (isVfsWithSuffix())
                 addVirtualFileSuffix(path._original);
@@ -989,8 +985,7 @@ void ProcessDirectoryJob::processFileFinalize(
     if (!checkPermissions(item))
         recurse = false;
     if (recurse) {
-        auto job = new ProcessDirectoryJob(item, recurseQueryLocal, recurseQueryServer, _discoveryData, this);
-        job->_currentFolder = path;
+        auto job = new ProcessDirectoryJob(path, item, recurseQueryLocal, recurseQueryServer, this);
         if (item->_instruction == CSYNC_INSTRUCTION_REMOVE) {
             job->setParent(_discoveryData);
             _discoveryData->_queuedDeletedDirectories[path._original] = job;
@@ -1031,8 +1026,7 @@ void ProcessDirectoryJob::processBlacklisted(const PathTuple &path, const OCC::L
     qCInfo(lcDisco) << "Discovered (blacklisted) " << item->_file << item->_instruction << item->_direction << item->isDirectory();
 
     if (item->isDirectory() && item->_instruction != CSYNC_INSTRUCTION_IGNORE) {
-        auto job = new ProcessDirectoryJob(item, NormalQuery, InBlackList, _discoveryData, this);
-        job->_currentFolder = path;
+        auto job = new ProcessDirectoryJob(path, item, NormalQuery, InBlackList, this);
         connect(job, &ProcessDirectoryJob::finished, this, &ProcessDirectoryJob::subJobFinished);
         _queuedJobs.push_back(job);
     } else {
@@ -1357,19 +1351,18 @@ bool ProcessDirectoryJob::runLocalQuery()
     return true;
 }
 
-Optional<PinState> ProcessDirectoryJob::directoryPinState()
+bool ProcessDirectoryJob::isVfsWithSuffix() const
 {
-    if (!_pinStateCache) {
-        _pinStateCache = _discoveryData->_statedb->effectivePinStateForPath(
-                _currentFolder._original.toUtf8());
-        // don't cache db errors, just retry next time
-    }
-    return _pinStateCache;
+    return _discoveryData->_syncOptions._vfs->mode() == Vfs::WithSuffix;
 }
 
-bool ProcessDirectoryJob::isVfsWithSuffix() const
+void ProcessDirectoryJob::computePinState(PinState parentState)
 {
-    return _discoveryData->_syncOptions._vfs->mode() == Vfs::WithSuffix;
+    _pinState = parentState;
+    if (_queryLocal != ParentDontExist) {
+        if (auto state = _discoveryData->_syncOptions._vfs->getPinState(_currentFolder._local)) // ouch! pin local or original?
+            _pinState = *state;
+    }
 }
 
 }
index bfb6856a357276a9ee21706c5b7c54a2dbd9c447..6cc059bbd94e56c4adf185c922bac77e33e6c3dc 100644 (file)
@@ -48,6 +48,8 @@ class SyncJournalDb;
 class ProcessDirectoryJob : public QObject
 {
     Q_OBJECT
+
+    struct PathTuple;
 public:
     enum QueryMode {
         NormalQuery,
@@ -56,14 +58,30 @@ public:
         InBlackList // Do not query this folder because it is in the blacklist (remote entries only)
     };
     Q_ENUM(QueryMode)
-    explicit ProcessDirectoryJob(const SyncFileItemPtr &dirItem, QueryMode queryLocal, QueryMode queryServer,
-        DiscoveryPhase *data, QObject *parent)
+
+    /** For creating the root job
+     *
+     * The base pin state is used if the root dir's pin state can't be retrieved.
+     */
+    explicit ProcessDirectoryJob(DiscoveryPhase *data, PinState basePinState, QObject *parent)
+        : QObject(parent)
+        , _discoveryData(data)
+    {
+        computePinState(basePinState);
+    }
+
+    /// For creating subjobs
+    explicit ProcessDirectoryJob(const PathTuple &path, const SyncFileItemPtr &dirItem,
+        QueryMode queryLocal, QueryMode queryServer,
+        ProcessDirectoryJob *parent)
         : QObject(parent)
         , _dirItem(dirItem)
         , _queryServer(queryServer)
         , _queryLocal(queryLocal)
-        , _discoveryData(data)
+        , _discoveryData(parent->_discoveryData)
+        , _currentFolder(path)
     {
+        computePinState(parent->_pinState);
     }
 
     void start();
@@ -180,11 +198,15 @@ private:
       */
     bool runLocalQuery();
 
-    /** Retrieve and cache directory pin state */
-    Optional<PinState> directoryPinState();
+    /** Sets _pinState
+     *
+     * If the folder exists locally its state is retrieved, otherwise the
+     * parent's pin state is inherited.
+     */
+    void computePinState(PinState parentState);
 
-    QueryMode _queryServer;
-    QueryMode _queryLocal;
+    QueryMode _queryServer = QueryMode::NormalQuery;
+    QueryMode _queryLocal = QueryMode::NormalQuery;
 
     // Holds entries that resulted from a NormalQuery
     QVector<RemoteInfo> _serverNormalQueryEntries;
@@ -222,7 +244,7 @@ private:
     PathTuple _currentFolder;
     bool _childModified = false; // the directory contains modified item what would prevent deletion
     bool _childIgnored = false; // The directory contains ignored item that would prevent deletion
-    Optional<PinState> _pinStateCache; // The directories pin-state, once retrieved, see directoryPinState()
+    PinState _pinState = PinState::Unspecified; // The directory's pin-state, see setParentPinState()
 
 signals:
     void finished();
index c48b75a5638f33bce7d44f58b99c008bac22e5c4..1cc29d8a6032e55ccb6c6f2a6cd86512384e6498 100644 (file)
@@ -637,8 +637,8 @@ void SyncEngine::slotStartDiscovery()
     connect(_discoveryPhase.data(), &DiscoveryPhase::silentlyExcluded,
         _syncFileStatusTracker.data(), &SyncFileStatusTracker::slotAddSilentlyExcluded);
 
-    auto discoveryJob = new ProcessDirectoryJob(SyncFileItemPtr(), ProcessDirectoryJob::NormalQuery, ProcessDirectoryJob::NormalQuery,
-        _discoveryPhase.data(), _discoveryPhase.data());
+    auto discoveryJob = new ProcessDirectoryJob(
+        _discoveryPhase.data(), PinState::AlwaysLocal, _discoveryPhase.data());
     _discoveryPhase->startJob(discoveryJob);
     connect(discoveryJob, &ProcessDirectoryJob::etag, this, &SyncEngine::slotRootEtagReceived);
 }
index 60d753a1b0b790387a512e174009c4e12edfb4d4..518decf70a938879f319a8d904d6cd89e6d15e00 100644 (file)
@@ -22,7 +22,7 @@
 namespace OCC {
 
 VfsSuffix::VfsSuffix(QObject *parent)
-    : Vfs(parent)
+    : VfsDefaults(parent)
 {
 }
 
@@ -40,14 +40,6 @@ QString VfsSuffix::fileSuffix() const
     return QStringLiteral(APPLICATION_DOTVIRTUALFILE_SUFFIX);
 }
 
-void VfsSuffix::registerFolder(const VfsSetupParams &)
-{
-}
-
-void VfsSuffix::start(const VfsSetupParams &)
-{
-}
-
 void VfsSuffix::stop()
 {
 }
index 2949533aa6cd865e8c59c6b5000c8ee9adfb4f57..9c2a2cc08b36134e87bf1cbd4f448d85f3e09d10 100644 (file)
@@ -21,7 +21,7 @@
 
 namespace OCC {
 
-class VfsSuffix : public Vfs
+class VfsSuffix : public VfsDefaults
 {
     Q_OBJECT
 
@@ -32,8 +32,6 @@ public:
     Mode mode() const override;
     QString fileSuffix() const override;
 
-    void registerFolder(const VfsSetupParams &params) override;
-    void start(const VfsSetupParams &params) override;
     void stop() override;
     void unregisterFolder() override;
 
index d60d6dc3a4d1b0300c59a9693fdd2fcf65ca2764..f3663f8ba6737066c5921a80bb091b07ef06d2c7 100644 (file)
@@ -12,6 +12,7 @@
 #include "filesystem.h"
 #include "syncengine.h"
 #include "common/syncjournaldb.h"
+#include "common/vfs.h"
 #include "csync_exclude.h"
 
 #include <QDir>
@@ -925,12 +926,41 @@ public:
         // Ignore temporary files from the download. (This is in the default exclude list, but we don't load it)
         _syncEngine->excludedFiles().addManualExclude("]*.~*");
 
+        // Ensure we have a valid VfsOff instance "running"
+        switchToVfs(_syncEngine->syncOptions()._vfs);
+
         // A new folder will update the local file state database on first sync.
         // To have a state matching what users will encounter, we have to a sync
         // using an identical local/remote file tree first.
         syncOnce();
     }
 
+    void switchToVfs(QSharedPointer<OCC::Vfs> vfs)
+    {
+        auto opts = _syncEngine->syncOptions();
+
+        opts._vfs->stop();
+        QObject::disconnect(_syncEngine.get(), 0, opts._vfs.data(), 0);
+
+        opts._vfs = vfs;
+        _syncEngine->setSyncOptions(opts);
+
+        OCC::VfsSetupParams vfsParams;
+        vfsParams.filesystemPath = localPath();
+        vfsParams.remotePath = "";
+        vfsParams.account = _account;
+        vfsParams.journal = _journalDb.get();
+        vfsParams.providerName = "OC-TEST";
+        vfsParams.providerVersion = "0.1";
+        vfsParams.enableShellIntegration = false;
+        QObject::connect(_syncEngine.get(), &QObject::destroyed, vfs.data(), [vfs]() {
+            vfs->stop();
+            vfs->unregisterFolder();
+        });
+
+        vfs->start(vfsParams);
+    }
+
     OCC::AccountPtr account() const { return _account; }
     OCC::SyncEngine &syncEngine() const { return *_syncEngine; }
     OCC::SyncJournalDb &syncJournal() const { return *_journalDb; }
index f07c4e59a65bde6114feb63ee26e3c77d8788f08..c8e8c2d6aedf8860c96260d5d742b6a7debedf47 100644 (file)
@@ -59,12 +59,15 @@ void markForDehydration(FakeFolder &folder, const QByteArray &path)
     journal.avoidReadFromDbOnNextSync(record._path);
 }
 
-SyncOptions vfsSyncOptions(FakeFolder &fakeFolder)
+QSharedPointer<Vfs> setupVfs(FakeFolder &folder)
 {
-    SyncOptions options;
-    options._vfs.reset(createVfsFromPlugin(Vfs::WithSuffix).release());
-    fakeFolder.syncJournal().setPinStateForPath("", PinState::OnlineOnly);
-    return options;
+    auto suffixVfs = QSharedPointer<Vfs>(createVfsFromPlugin(Vfs::WithSuffix).release());
+    folder.switchToVfs(suffixVfs);
+
+    // Using this directly doesn't recursively unpin everything
+    folder.syncJournal().setPinStateForPath("", PinState::OnlineOnly);
+
+    return suffixVfs;
 }
 
 class TestSyncVirtualFiles : public QObject
@@ -85,7 +88,7 @@ private slots:
         QFETCH(bool, doLocalDiscovery);
 
         FakeFolder fakeFolder{ FileInfo() };
-        fakeFolder.syncEngine().setSyncOptions(vfsSyncOptions(fakeFolder));
+        setupVfs(fakeFolder);
         QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
         QSignalSpy completeSpy(&fakeFolder.syncEngine(), SIGNAL(itemCompleted(const SyncFileItemPtr &)));
 
@@ -206,7 +209,7 @@ private slots:
     void testVirtualFileConflict()
     {
         FakeFolder fakeFolder{ FileInfo() };
-        fakeFolder.syncEngine().setSyncOptions(vfsSyncOptions(fakeFolder));
+        setupVfs(fakeFolder);
         QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
         QSignalSpy completeSpy(&fakeFolder.syncEngine(), SIGNAL(itemCompleted(const SyncFileItemPtr &)));
 
@@ -277,7 +280,7 @@ private slots:
     void testWithNormalSync()
     {
         FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
-        fakeFolder.syncEngine().setSyncOptions(vfsSyncOptions(fakeFolder));
+        setupVfs(fakeFolder);
         QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
         QSignalSpy completeSpy(&fakeFolder.syncEngine(), SIGNAL(itemCompleted(const SyncFileItemPtr &)));
 
@@ -313,7 +316,7 @@ private slots:
     void testVirtualFileDownload()
     {
         FakeFolder fakeFolder{ FileInfo() };
-        fakeFolder.syncEngine().setSyncOptions(vfsSyncOptions(fakeFolder));
+        setupVfs(fakeFolder);
         QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
         QSignalSpy completeSpy(&fakeFolder.syncEngine(), SIGNAL(itemCompleted(const SyncFileItemPtr &)));
 
@@ -381,7 +384,7 @@ private slots:
     void testVirtualFileDownloadResume()
     {
         FakeFolder fakeFolder{ FileInfo() };
-        fakeFolder.syncEngine().setSyncOptions(vfsSyncOptions(fakeFolder));
+        setupVfs(fakeFolder);
         QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
         QSignalSpy completeSpy(&fakeFolder.syncEngine(), SIGNAL(itemCompleted(const SyncFileItemPtr &)));
 
@@ -422,8 +425,7 @@ private slots:
     void testNewFilesNotVirtual()
     {
         FakeFolder fakeFolder{ FileInfo() };
-        SyncOptions syncOptions = vfsSyncOptions(fakeFolder);
-        fakeFolder.syncEngine().setSyncOptions(syncOptions);
+        setupVfs(fakeFolder);
         QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
 
         fakeFolder.remoteModifier().mkdir("A");
@@ -443,7 +445,7 @@ private slots:
     void testDownloadRecursive()
     {
         FakeFolder fakeFolder{ FileInfo() };
-        fakeFolder.syncEngine().setSyncOptions(vfsSyncOptions(fakeFolder));
+        setupVfs(fakeFolder);
         QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
 
         // Create a virtual file for remote files
@@ -540,7 +542,7 @@ private slots:
     void testRenameToVirtual()
     {
         FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
-        fakeFolder.syncEngine().setSyncOptions(vfsSyncOptions(fakeFolder));
+        setupVfs(fakeFolder);
         QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
         QSignalSpy completeSpy(&fakeFolder.syncEngine(), SIGNAL(itemCompleted(const SyncFileItemPtr &)));
 
@@ -578,7 +580,7 @@ private slots:
     void testRenameVirtual()
     {
         FakeFolder fakeFolder{ FileInfo() };
-        fakeFolder.syncEngine().setSyncOptions(vfsSyncOptions(fakeFolder));
+        setupVfs(fakeFolder);
         QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
         QSignalSpy completeSpy(&fakeFolder.syncEngine(), SIGNAL(itemCompleted(const SyncFileItemPtr &)));
 
@@ -620,7 +622,7 @@ private slots:
     void testSyncDehydration()
     {
         FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
-        fakeFolder.syncEngine().setSyncOptions(vfsSyncOptions(fakeFolder));
+        setupVfs(fakeFolder);
 
         QVERIFY(fakeFolder.syncOnce());
         QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
@@ -697,7 +699,7 @@ private slots:
     void testWipeVirtualSuffixFiles()
     {
         FakeFolder fakeFolder{ FileInfo{} };
-        fakeFolder.syncEngine().setSyncOptions(vfsSyncOptions(fakeFolder));
+        setupVfs(fakeFolder);
 
         // Create a suffix-vfs baseline
 
@@ -733,7 +735,7 @@ private slots:
         QVERIFY(fakeFolder.currentLocalState().find("A/a3.nextcloud"));
         QVERIFY(!fakeFolder.currentLocalState().find("A/B/b1.nextcloud"));
 
-        fakeFolder.syncEngine().setSyncOptions(SyncOptions{});
+        fakeFolder.switchToVfs(QSharedPointer<Vfs>(new VfsOff));
         QVERIFY(fakeFolder.syncOnce());
         QVERIFY(fakeFolder.currentRemoteState().find("A/a3.nextcloud")); // regular upload
         QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
@@ -742,7 +744,7 @@ private slots:
     void testNewVirtuals()
     {
         FakeFolder fakeFolder{ FileInfo() };
-        fakeFolder.syncEngine().setSyncOptions(vfsSyncOptions(fakeFolder));
+        setupVfs(fakeFolder);
         QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
 
         auto setPin = [&] (const QByteArray &path, PinState state) {
@@ -757,6 +759,7 @@ private slots:
 
         setPin("local", PinState::AlwaysLocal);
         setPin("online", PinState::OnlineOnly);
+        setPin("unspec", PinState::Unspecified);
 
         // Test 1: root is OnlineOnly
         fakeFolder.remoteModifier().insert("file1");
@@ -782,7 +785,7 @@ private slots:
         QVERIFY(fakeFolder.currentLocalState().find("file2"));
         QVERIFY(fakeFolder.currentLocalState().find("online/file2.nextcloud"));
         QVERIFY(fakeFolder.currentLocalState().find("local/file2"));
-        QVERIFY(fakeFolder.currentLocalState().find("unspec/file2"));
+        QVERIFY(fakeFolder.currentLocalState().find("unspec/file2.nextcloud"));
 
         // file1 is unchanged
         QVERIFY(fakeFolder.currentLocalState().find("file1.nextcloud"));
@@ -810,7 +813,7 @@ private slots:
         cleanup();
 
         // Enable suffix vfs
-        fakeFolder.syncEngine().setSyncOptions(vfsSyncOptions(fakeFolder));
+        setupVfs(fakeFolder);
 
         // Local changes of suffixed file do nothing
         fakeFolder.localModifier().appendByte("A/file1.nextcloud");