From 446002bcb4e70e85047ec6371ef166d61dbf3885 Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Mon, 26 Nov 2018 13:40:51 +0100 Subject: [PATCH] vfs: Introduce PinState db storage #6815 The idea is to allow folders (and later maybe files?) to be - pinned to be available locally - pinned to be online only - inherit their pin from the parent Where this pinning only controls the default for new files. Subfolders may have a different pin state, and contained files may be hydrated or dehydrated based on user actions. This value is stored in a new 'flags' table. The idea is to store data there that doesn't necessarily exist for each metadata entry. The selective sync state could be migrated to this table. --- src/common/syncjournaldb.cpp | 51 ++++++++++++++++++++++++++++++++ src/common/syncjournaldb.h | 33 +++++++++++++++++++++ test/testsyncjournaldb.cpp | 56 ++++++++++++++++++++++++++++++++++++ 3 files changed, 140 insertions(+) diff --git a/src/common/syncjournaldb.cpp b/src/common/syncjournaldb.cpp index 4d8f3374fc..111f6366e7 100644 --- a/src/common/syncjournaldb.cpp +++ b/src/common/syncjournaldb.cpp @@ -463,6 +463,15 @@ bool SyncJournalDb::checkConnect() return sqlFail("Create table datafingerprint", createQuery); } + // create the flags table. + createQuery.prepare("CREATE TABLE IF NOT EXISTS flags (" + "path TEXT PRIMARY KEY," + "pinState INTEGER" + ");"); + if (!createQuery.exec()) { + return sqlFail("Create table flags", createQuery); + } + // create the conflicts table. createQuery.prepare("CREATE TABLE IF NOT EXISTS conflicts(" "path TEXT PRIMARY KEY," @@ -1981,6 +1990,48 @@ void SyncJournalDb::markVirtualFileForDownloadRecursively(const QByteArray &path query.exec(); } +PinState SyncJournalDb::pinStateForPath(const QByteArray &path) +{ + QMutexLocker lock(&_mutex); + if (!checkConnect()) + return PinState::Unspecified; + + auto &query = _getPinStateQuery; + ASSERT(query.initOrReset(QByteArrayLiteral( + "SELECT pinState FROM flags WHERE" + " " IS_PREFIX_PATH_OR_EQUAL("path", "?1") + " AND pinState is not null AND pinState != 0" + " ORDER BY length(path) DESC;"), + _db)); + query.bindValue(1, path); + query.exec(); + + if (!query.next()) + return PinState::Unspecified; + + return static_cast(query.intValue(0)); +} + +void SyncJournalDb::setPinStateForPath(const QByteArray &path, PinState state) +{ + QMutexLocker lock(&_mutex); + if (!checkConnect()) + return; + + auto &query = _setPinStateQuery; + ASSERT(query.initOrReset(QByteArrayLiteral( + // If we had sqlite >=3.24.0 everywhere this could be an upsert, + // making further flags columns easy + //"INSERT INTO flags(path, pinState) VALUES(?1, ?2)" + //" ON CONFLICT(path) DO UPDATE SET pinState=?2;"), + // Simple version that doesn't work nicely with multiple columns: + "INSERT OR REPLACE INTO flags(path, pinState) VALUES(?1, ?2);"), + _db)); + query.bindValue(1, path); + query.bindValue(2, static_cast(state)); + query.exec(); +} + void SyncJournalDb::commit(const QString &context, bool startTrans) { QMutexLocker lock(&_mutex); diff --git a/src/common/syncjournaldb.h b/src/common/syncjournaldb.h index 1f10bd2458..bf626358bd 100644 --- a/src/common/syncjournaldb.h +++ b/src/common/syncjournaldb.h @@ -32,6 +32,24 @@ 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 Unspecified. + * + * 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) + Unspecified = 0, + /// Download file and keep it updated. + AlwaysLocal = 1, + /// File shall be virtual locally. + OnlineOnly = 2, +}; + /** * @brief Class that handles the sync database * @@ -253,6 +271,19 @@ public: */ void markVirtualFileForDownloadRecursively(const QByteArray &path); + /** + * Gets the PinState for the path. + * + * If the exact path has no entry or has an unspecified state, + * the state is inherited through the parent. + */ + PinState pinStateForPath(const QByteArray &path); + + /** + * Sets a path's pin state. + */ + void setPinStateForPath(const QByteArray &path, PinState state); + /** * Only used for auto-test: * when positive, will decrease the counter for every database operation. @@ -316,6 +347,8 @@ private: SqlQuery _getConflictRecordQuery; SqlQuery _setConflictRecordQuery; SqlQuery _deleteConflictRecordQuery; + SqlQuery _getPinStateQuery; + SqlQuery _setPinStateQuery; /* Storing etags to these folders, or their parent folders, is filtered out. * diff --git a/test/testsyncjournaldb.cpp b/test/testsyncjournaldb.cpp index 79bd29fe99..cbc82f660a 100644 --- a/test/testsyncjournaldb.cpp +++ b/test/testsyncjournaldb.cpp @@ -327,6 +327,62 @@ private slots: QVERIFY(checkElements()); } + void testPinState() + { + auto make = [&](const QByteArray &path, PinState state) { + _db.setPinStateForPath(path, state); + }; + auto get = [&](const QByteArray &path) { + return _db.pinStateForPath(path); + }; + + // Make a thrice-nested setup + make("local", PinState::AlwaysLocal); + make("online", PinState::OnlineOnly); + make("unspec", PinState::Unspecified); + for (auto base : {"local/", "online/", "unspec/"}) { + make(QByteArray(base) + "unspec", PinState::Unspecified); + make(QByteArray(base) + "local", PinState::AlwaysLocal); + make(QByteArray(base) + "online", PinState::OnlineOnly); + + for (auto base2 : {"local/", "online/", "unspec/"}) { + make(QByteArray(base) + base2 + "/unspec", PinState::Unspecified); + make(QByteArray(base) + base2 + "/local", PinState::AlwaysLocal); + make(QByteArray(base) + base2 + "/online", PinState::OnlineOnly); + } + } + + // Baseline direct checks + QCOMPARE(get("local"), PinState::AlwaysLocal); + QCOMPARE(get("online"), PinState::OnlineOnly); + QCOMPARE(get("unspec"), PinState::Unspecified); + QCOMPARE(get("nonexistant"), PinState::Unspecified); + QCOMPARE(get("online/local"), PinState::AlwaysLocal); + QCOMPARE(get("local/online"), PinState::OnlineOnly); + QCOMPARE(get("unspec/local"), PinState::AlwaysLocal); + QCOMPARE(get("unspec/online"), PinState::OnlineOnly); + QCOMPARE(get("unspec/unspec"), PinState::Unspecified); + QCOMPARE(get("unspec/nonexistant"), PinState::Unspecified); + + // Inheriting checks, level 1 + QCOMPARE(get("local/unspec"), PinState::AlwaysLocal); + QCOMPARE(get("local/nonexistant"), PinState::AlwaysLocal); + QCOMPARE(get("online/unspec"), PinState::OnlineOnly); + QCOMPARE(get("online/nonexistant"), PinState::OnlineOnly); + + // Inheriting checks, level 2 + QCOMPARE(get("local/unspec/unspec"), PinState::AlwaysLocal); + QCOMPARE(get("local/local/unspec"), PinState::AlwaysLocal); + QCOMPARE(get("local/local/nonexistant"), PinState::AlwaysLocal); + QCOMPARE(get("local/online/unspec"), PinState::OnlineOnly); + QCOMPARE(get("local/online/nonexistant"), PinState::OnlineOnly); + QCOMPARE(get("online/unspec/unspec"), PinState::OnlineOnly); + QCOMPARE(get("online/local/unspec"), PinState::AlwaysLocal); + QCOMPARE(get("online/local/nonexistant"), PinState::AlwaysLocal); + QCOMPARE(get("online/online/unspec"), PinState::OnlineOnly); + QCOMPARE(get("online/online/nonexistant"), PinState::OnlineOnly); + } + private: SyncJournalDb _db; };