diff --git a/src/common/pinstate.h b/src/common/pinstate.h index 053e7062f..44ee12417 100644 --- a/src/common/pinstate.h +++ b/src/common/pinstate.h @@ -73,6 +73,47 @@ enum class PinState { Unspecified = 3, }; +/** A user-facing version of PinState. + * + * PinStates communicate availability intent for an item, but particular + * situations can get complex: An AlwaysLocal folder can have OnlineOnly + * files or directories. + * + * For users this is condensed to a few useful cases. + * + * Note that this is only about *intent*. The file could still be out of date, + * or not have been synced for other reasons, like errors. + */ +enum class VfsItemAvailability { + /** The item and all its subitems are hydrated and pinned AlwaysLocal. + * + * This guarantees that all contents will be kept in sync. + */ + AlwaysLocal, + + /** The item and all its subitems are hydrated. + * + * This may change if the platform or client decide to dehydrate items + * that have Unspecified pin state. + * + * A folder with no file contents will have this availability. + */ + AllHydrated, + + /** There are dehydrated items but the pin state isn't all OnlineOnly. + * + * This would happen if a dehydration happens to a Unspecified item that + * used to be hydrated. + */ + SomeDehydrated, + + /** The item and all its subitems are dehydrated and OnlineOnly. + * + * This guarantees that contents will not take up space. + */ + OnlineOnly, +}; + } #endif diff --git a/src/common/syncjournaldb.cpp b/src/common/syncjournaldb.cpp index e613358a6..b22e041ce 100644 --- a/src/common/syncjournaldb.cpp +++ b/src/common/syncjournaldb.cpp @@ -1320,6 +1320,31 @@ bool SyncJournalDb::updateLocalMetadata(const QString &filename, return _setFileRecordLocalMetadataQuery.exec(); } +Optional<bool> SyncJournalDb::hasDehydratedFiles(const QByteArray &filename) +{ + QMutexLocker locker(&_mutex); + if (!checkConnect()) + return {}; + + auto &query = _countDehydratedFilesQuery; + static_assert(ItemTypeVirtualFile == 4 && ItemTypeVirtualFileDownload == 5, ""); + if (!query.initOrReset(QByteArrayLiteral( + "SELECT count(*) FROM metadata" + " WHERE (" IS_PREFIX_PATH_OR_EQUAL("?1", "path") " OR ?1 == '')" + " AND (type == 4 OR type == 5);"), _db)) { + return {}; + } + + query.bindValue(1, filename); + if (!query.exec()) + return {}; + + if (!query.next().hasData) + return {}; + + return query.intValue(0) > 0; +} + static void toDownloadInfo(SqlQuery &query, SyncJournalDb::DownloadInfo *res) { bool ok = true; @@ -2152,7 +2177,7 @@ Optional<PinState> SyncJournalDb::PinStateInterface::effectiveForPath(const QByt // (it'd be great if paths started with a / and "/" could be the root) " (" IS_PREFIX_PATH_OR_EQUAL("path", "?1") " OR path == '')" " AND pinState is not null AND pinState != 0" - " ORDER BY length(path) DESC;"), + " ORDER BY length(path) DESC LIMIT 1;"), _db->_db)); query.bindValue(1, path); query.exec(); @@ -2167,6 +2192,43 @@ Optional<PinState> SyncJournalDb::PinStateInterface::effectiveForPath(const QByt return static_cast<PinState>(query.intValue(0)); } +Optional<PinState> SyncJournalDb::PinStateInterface::effectiveForPathRecursive(const QByteArray &path) +{ + // Get the item's effective pin state. We'll compare subitem's pin states + // against this. + const auto basePin = effectiveForPath(path); + if (!basePin) + return {}; + + QMutexLocker lock(&_db->_mutex); + if (!_db->checkConnect()) + return {}; + + // Find all the non-inherited pin states below the item + auto &query = _db->_getSubPinsQuery; + ASSERT(query.initOrReset(QByteArrayLiteral( + "SELECT DISTINCT pinState FROM flags WHERE" + " (" IS_PREFIX_PATH_OF("?1", "path") " OR ?1 == '')" + " AND pinState is not null and pinState != 0;"), + _db->_db)); + query.bindValue(1, path); + query.exec(); + + // Check if they are all identical + forever { + auto next = query.next(); + if (!next.ok) + return {}; + if (!next.hasData) + break; + const auto subPin = static_cast<PinState>(query.intValue(0)); + if (subPin != *basePin) + return PinState::Inherited; + } + + return *basePin; +} + void SyncJournalDb::PinStateInterface::setForPath(const QByteArray &path, PinState state) { QMutexLocker lock(&_db->_mutex); diff --git a/src/common/syncjournaldb.h b/src/common/syncjournaldb.h index cf0cf61a3..a68960555 100644 --- a/src/common/syncjournaldb.h +++ b/src/common/syncjournaldb.h @@ -72,6 +72,10 @@ public: const QByteArray &contentChecksumType); bool updateLocalMetadata(const QString &filename, qint64 modtime, qint64 size, quint64 inode); + + /** Returns whether the item or any subitems are dehydrated */ + Optional<bool> hasDehydratedFiles(const QByteArray &filename); + bool exists(); void walCheckpoint(); @@ -284,6 +288,21 @@ public: */ Optional<PinState> effectiveForPath(const QByteArray &path); + /** + * Like effectiveForPath() but also considers subitem pin states. + * + * If the path's pin state and all subitem's pin states are identical + * then that pin state will be returned. + * + * If some subitem's pin state is different from the path's state, + * PinState::Inherited will be returned. Inherited isn't returned in + * any other cases. + * + * It's valid to use the root path "". + * Returns none on db error. + */ + Optional<PinState> effectiveForPathRecursive(const QByteArray &path); + /** * Sets a path's pin state. * @@ -386,6 +405,8 @@ private: SqlQuery _deleteConflictRecordQuery; SqlQuery _getRawPinStateQuery; SqlQuery _getEffectivePinStateQuery; + SqlQuery _getSubPinsQuery; + SqlQuery _countDehydratedFilesQuery; SqlQuery _setPinStateQuery; SqlQuery _wipePinStateQuery; diff --git a/src/common/vfs.cpp b/src/common/vfs.cpp index a40b396a7..5f170daf1 100644 --- a/src/common/vfs.cpp +++ b/src/common/vfs.cpp @@ -79,6 +79,27 @@ Optional<PinState> Vfs::pinStateInDb(const QString &folderPath) return _setupParams.journal->internalPinStates().effectiveForPath(folderPath.toUtf8()); } +Optional<VfsItemAvailability> Vfs::availabilityInDb(const QString &folderPath, const QString &pinPath) +{ + auto pin = _setupParams.journal->internalPinStates().effectiveForPathRecursive(pinPath.toUtf8()); + // not being able to retrieve the pin state isn't too bad + Optional<bool> hasDehydrated = _setupParams.journal->hasDehydratedFiles(folderPath.toUtf8()); + if (!hasDehydrated) + return {}; + + if (*hasDehydrated) { + if (pin && *pin == PinState::OnlineOnly) + return VfsItemAvailability::OnlineOnly; + else + return VfsItemAvailability::SomeDehydrated; + } else { + if (pin && *pin == PinState::AlwaysLocal) + return VfsItemAvailability::AlwaysLocal; + else + return VfsItemAvailability::AllHydrated; + } +} + VfsOff::VfsOff(QObject *parent) : Vfs(parent) { @@ -184,3 +205,22 @@ std::unique_ptr<Vfs> OCC::createVfsFromPlugin(Vfs::Mode mode) qCInfo(lcPlugin) << "Created VFS instance from plugin" << pluginPath; return vfs; } + +QString OCC::vfsItemAvailabilityToString(VfsItemAvailability availability, bool forFolder) +{ + switch(availability) { + case VfsItemAvailability::AlwaysLocal: + return Vfs::tr("Always available locally"); + case VfsItemAvailability::AllHydrated: + return Vfs::tr("Available locally"); + case VfsItemAvailability::SomeDehydrated: + if (forFolder) { + return Vfs::tr("Some available online only"); + } else { + return Vfs::tr("Available online only"); + } + case VfsItemAvailability::OnlineOnly: + return Vfs::tr("Available online only"); + } + ENFORCE(false); +} diff --git a/src/common/vfs.h b/src/common/vfs.h index 2ccd3958d..a8765d793 100644 --- a/src/common/vfs.h +++ b/src/common/vfs.h @@ -188,11 +188,13 @@ public: virtual bool statTypeVirtualFile(csync_file_stat_t *stat, void *stat_data) = 0; /** Sets the pin state for the item at a path. + * + * The pin state is set on the item and for all items below it. * * 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. + * folderPath is relative to the sync folder. Can be "" for root folder. */ virtual bool setPinState(const QString &folderPath, PinState state) = 0; @@ -201,10 +203,19 @@ public: * 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. + * folderPath is relative to the sync folder. Can be "" for root folder. */ virtual Optional<PinState> pinState(const QString &folderPath) = 0; + /** Returns availability status of an item at a path. + * + * The availability is a condensed user-facing version of PinState. See + * VfsItemAvailability for details. + * + * folderPath is relative to the sync folder. Can be "" for root folder. + */ + virtual Optional<VfsItemAvailability> availability(const QString &folderPath) = 0; + public slots: /** Update in-sync state based on SyncFileStatusTracker signal. * @@ -235,6 +246,8 @@ protected: // Db-backed pin state handling. Derived classes may use it to implement pin states. bool setPinStateInDb(const QString &folderPath, PinState state); Optional<PinState> pinStateInDb(const QString &folderPath); + // sadly for virtual files the path in the metadata table can differ from path in 'flags' + Optional<VfsItemAvailability> availabilityInDb(const QString &folderPath, const QString &pinPath); // the parameters passed to start() VfsSetupParams _setupParams; @@ -269,6 +282,7 @@ public: bool setPinState(const QString &, PinState) override { return true; } Optional<PinState> pinState(const QString &) override { return PinState::AlwaysLocal; } + Optional<VfsItemAvailability> availability(const QString &) override { return VfsItemAvailability::AlwaysLocal; } public slots: void fileStatusChanged(const QString &, SyncFileStatus) override {} @@ -286,4 +300,7 @@ OCSYNC_EXPORT Vfs::Mode bestAvailableVfsMode(); /// Create a VFS instance for the mode, returns nullptr on failure. OCSYNC_EXPORT std::unique_ptr<Vfs> createVfsFromPlugin(Vfs::Mode mode); +/// Convert availability to translated string +OCSYNC_EXPORT QString vfsItemAvailabilityToString(VfsItemAvailability availability, bool forFolder); + } // namespace OCC diff --git a/src/gui/accountsettings.cpp b/src/gui/accountsettings.cpp index 69ade7872..05dd56a4d 100644 --- a/src/gui/accountsettings.cpp +++ b/src/gui/accountsettings.cpp @@ -446,14 +446,18 @@ void AccountSettings::slotCustomContextMenuRequested(const QPoint &pos) if (folder->supportsVirtualFiles()) { auto availabilityMenu = menu->addMenu(tr("Availability")); - ac = availabilityMenu->addAction(tr("Local")); - ac->setCheckable(true); - ac->setChecked(!folder->newFilesAreVirtual()); + auto availability = folder->vfs().availability(QString()); + if (availability) { + ac = availabilityMenu->addAction(vfsItemAvailabilityToString(*availability, true)); + ac->setEnabled(false); + } + + ac = availabilityMenu->addAction(tr("Make always available locally")); + ac->setEnabled(!availability || *availability != VfsItemAvailability::AlwaysLocal); connect(ac, &QAction::triggered, this, [this]() { slotSetCurrentFolderAvailability(PinState::AlwaysLocal); }); - ac = availabilityMenu->addAction(tr("Online only")); - ac->setCheckable(true); - ac->setChecked(folder->newFilesAreVirtual()); + ac = availabilityMenu->addAction(tr("Free up local space")); + ac->setEnabled(!availability || *availability != VfsItemAvailability::OnlineOnly); connect(ac, &QAction::triggered, this, [this]() { slotSetCurrentFolderAvailability(PinState::OnlineOnly); }); ac = menu->addAction(tr("Disable virtual file support...")); diff --git a/src/gui/socketapi.cpp b/src/gui/socketapi.cpp index b96619d40..7dcccee26 100644 --- a/src/gui/socketapi.cpp +++ b/src/gui/socketapi.cpp @@ -1042,68 +1042,61 @@ void SocketApi::command_GET_MENU_ITEMS(const QString &argument, OCC::SocketListe if (syncFolder && syncFolder->supportsVirtualFiles() && syncFolder->vfs().socketApiPinStateActionsShown()) { - bool hasAlwaysLocal = false; - bool hasOnlineOnly = false; - bool hasHydratedOnlineOnly = false; - bool hasDehydratedOnlineOnly = false; + ENFORCE(!files.isEmpty()); + + // Determine the combined availability status of the files + auto combined = Optional<VfsItemAvailability>(); + auto merge = [](VfsItemAvailability lhs, VfsItemAvailability rhs) { + if (lhs == rhs) + return lhs; + if (lhs == VfsItemAvailability::SomeDehydrated || rhs == VfsItemAvailability::SomeDehydrated + || lhs == VfsItemAvailability::OnlineOnly || rhs == VfsItemAvailability::OnlineOnly) { + return VfsItemAvailability::SomeDehydrated; + } + return VfsItemAvailability::AllHydrated; + }; + bool isFolderOrMultiple = false; for (const auto &file : files) { auto fileData = FileData::get(file); - auto path = fileData.folderRelativePathNoVfsSuffix(); - auto pinState = syncFolder->vfs().pinState(path); - if (!pinState) { - // db error - hasAlwaysLocal = true; - hasOnlineOnly = true; - } else if (*pinState == PinState::AlwaysLocal) { - hasAlwaysLocal = true; - } else if (*pinState == PinState::OnlineOnly) { - hasOnlineOnly = true; - auto record = fileData.journalRecord(); - if (record._type == ItemTypeFile) - hasHydratedOnlineOnly = true; - if (record.isVirtualFile()) - hasDehydratedOnlineOnly = true; + isFolderOrMultiple = QFileInfo(fileData.localPath).isDir(); + auto availability = syncFolder->vfs().availability(fileData.folderRelativePath); + if (!availability) + availability = VfsItemAvailability::SomeDehydrated; // db error + if (!combined) { + combined = availability; + } else { + combined = merge(*combined, *availability); } } + ENFORCE(combined); + if (files.size() > 1) + isFolderOrMultiple = true; - auto makePinContextMenu = [listener](QString currentState, QString availableLocally, QString onlineOnly) { - listener->sendMessage(QLatin1String("MENU_ITEM:CURRENT_PIN:d:") + currentState); - if (!availableLocally.isEmpty()) - listener->sendMessage(QLatin1String("MENU_ITEM:MAKE_AVAILABLE_LOCALLY::") + availableLocally); - if (!onlineOnly.isEmpty()) - listener->sendMessage(QLatin1String("MENU_ITEM:MAKE_ONLINE_ONLY::") + onlineOnly); + // TODO: Should be a submenu, should use icons + auto makePinContextMenu = [&](bool makeAvailableLocally, bool freeSpace) { + listener->sendMessage(QLatin1String("MENU_ITEM:CURRENT_PIN:d:") + + vfsItemAvailabilityToString(*combined, isFolderOrMultiple)); + listener->sendMessage(QLatin1String("MENU_ITEM:MAKE_AVAILABLE_LOCALLY:") + + (makeAvailableLocally ? QLatin1String(":") : QLatin1String("d:")) + + tr("Make always available locally")); + listener->sendMessage(QLatin1String("MENU_ITEM:MAKE_ONLINE_ONLY:") + + (freeSpace ? QLatin1String(":") : QLatin1String("d:")) + + tr("Free up local space")); }; - // TODO: Should be a submenu, should use menu item checkmarks where available, should use icons - if (hasAlwaysLocal) { - if (!hasOnlineOnly) { - makePinContextMenu( - tr("Currently available locally"), - QString(), - tr("Make available online only")); - } else { // local + online - makePinContextMenu( - tr("Current availability is mixed"), - tr("Make all available locally"), - tr("Make all available online only")); - } - } else if (hasOnlineOnly) { - if (hasDehydratedOnlineOnly && !hasHydratedOnlineOnly) { - makePinContextMenu( - tr("Currently available online only"), - tr("Make available locally"), - QString()); - } else if (hasHydratedOnlineOnly && !hasDehydratedOnlineOnly) { - makePinContextMenu( - tr("Currently available, but marked online only"), - tr("Make available locally"), - tr("Make available online only")); - } else { // hydrated + dehydrated - makePinContextMenu( - tr("Some currently available, all marked online only"), - tr("Make available locally"), - tr("Make available online only")); - } + switch (*combined) { + case VfsItemAvailability::AlwaysLocal: + makePinContextMenu(false, true); + break; + case VfsItemAvailability::AllHydrated: + makePinContextMenu(true, true); + break; + case VfsItemAvailability::SomeDehydrated: + makePinContextMenu(true, true); + break; + case VfsItemAvailability::OnlineOnly: + makePinContextMenu(true, false); + break; } } diff --git a/src/libsync/vfs/suffix/vfs_suffix.cpp b/src/libsync/vfs/suffix/vfs_suffix.cpp index 9860c24b2..0480d02b2 100644 --- a/src/libsync/vfs/suffix/vfs_suffix.cpp +++ b/src/libsync/vfs/suffix/vfs_suffix.cpp @@ -105,4 +105,13 @@ bool VfsSuffix::statTypeVirtualFile(csync_file_stat_t *stat, void *) return false; } +Optional<VfsItemAvailability> VfsSuffix::availability(const QString &folderPath) +{ + const auto suffix = fileSuffix(); + QString pinPath = folderPath; + if (pinPath.endsWith(suffix)) + pinPath.chop(suffix.size()); + return availabilityInDb(folderPath, pinPath); +} + } // namespace OCC diff --git a/src/libsync/vfs/suffix/vfs_suffix.h b/src/libsync/vfs/suffix/vfs_suffix.h index 6c42bcfb4..5aadf5449 100644 --- a/src/libsync/vfs/suffix/vfs_suffix.h +++ b/src/libsync/vfs/suffix/vfs_suffix.h @@ -51,6 +51,7 @@ public: { return setPinStateInDb(folderPath, state); } Optional<PinState> pinState(const QString &folderPath) override { return pinStateInDb(folderPath); } + Optional<VfsItemAvailability> availability(const QString &folderPath) override; public slots: void fileStatusChanged(const QString &, SyncFileStatus) override {} diff --git a/test/testsyncjournaldb.cpp b/test/testsyncjournaldb.cpp index d8d663b9c..fd404559b 100644 --- a/test/testsyncjournaldb.cpp +++ b/test/testsyncjournaldb.cpp @@ -336,6 +336,14 @@ private slots: } return *state; }; + auto getRecursive = [&](const QByteArray &path) -> PinState { + auto state = _db.internalPinStates().effectiveForPathRecursive(path); + if (!state) { + QTest::qFail("couldn't read pin state", __FILE__, __LINE__); + return PinState::Inherited; + } + return *state; + }; auto getRaw = [&](const QByteArray &path) -> PinState { auto state = _db.internalPinStates().rawForPath(path); if (!state) { @@ -370,6 +378,7 @@ private slots: QCOMPARE(list->size(), 4 + 9 + 27); // Baseline direct checks (the fallback for unset root pinstate is AlwaysLocal) + QCOMPARE(get(""), PinState::AlwaysLocal); QCOMPARE(get("local"), PinState::AlwaysLocal); QCOMPARE(get("online"), PinState::OnlineOnly); QCOMPARE(get("inherit"), PinState::AlwaysLocal); @@ -399,6 +408,20 @@ private slots: QCOMPARE(get("online/online/inherit"), PinState::OnlineOnly); QCOMPARE(get("online/online/nonexistant"), PinState::OnlineOnly); + // Spot check the recursive variant + QCOMPARE(getRecursive(""), PinState::Inherited); + QCOMPARE(getRecursive("local"), PinState::Inherited); + QCOMPARE(getRecursive("online"), PinState::Inherited); + QCOMPARE(getRecursive("inherit"), PinState::Inherited); + QCOMPARE(getRecursive("online/local"), PinState::Inherited); + QCOMPARE(getRecursive("online/local/inherit"), PinState::AlwaysLocal); + QCOMPARE(getRecursive("inherit/inherit/inherit"), PinState::AlwaysLocal); + QCOMPARE(getRecursive("inherit/online/inherit"), PinState::OnlineOnly); + QCOMPARE(getRecursive("inherit/online/local"), PinState::AlwaysLocal); + make("local/local/local/local", PinState::AlwaysLocal); + QCOMPARE(getRecursive("local/local/local"), PinState::AlwaysLocal); + QCOMPARE(getRecursive("local/local/local/local"), PinState::AlwaysLocal); + // Check changing the root pin state make("", PinState::OnlineOnly); QCOMPARE(get("local"), PinState::AlwaysLocal); diff --git a/test/testsyncvirtualfiles.cpp b/test/testsyncvirtualfiles.cpp index afb737c6e..86845bc8e 100644 --- a/test/testsyncvirtualfiles.cpp +++ b/test/testsyncvirtualfiles.cpp @@ -1024,6 +1024,66 @@ private slots: QVERIFY(!fakeFolder.currentLocalState().find("A/file2.nextcloud.nextcloud")); cleanup(); } + + void testAvailability() + { + FakeFolder fakeFolder{ FileInfo() }; + auto vfs = setupVfs(fakeFolder); + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + + auto setPin = [&] (const QByteArray &path, PinState state) { + fakeFolder.syncJournal().internalPinStates().setForPath(path, state); + }; + + fakeFolder.remoteModifier().mkdir("local"); + fakeFolder.remoteModifier().mkdir("local/sub"); + fakeFolder.remoteModifier().mkdir("online"); + fakeFolder.remoteModifier().mkdir("online/sub"); + fakeFolder.remoteModifier().mkdir("unspec"); + QVERIFY(fakeFolder.syncOnce()); + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + + setPin("local", PinState::AlwaysLocal); + setPin("online", PinState::OnlineOnly); + setPin("unspec", PinState::Unspecified); + + fakeFolder.remoteModifier().insert("file1"); + fakeFolder.remoteModifier().insert("online/file1"); + fakeFolder.remoteModifier().insert("online/file2"); + fakeFolder.remoteModifier().insert("local/file1"); + fakeFolder.remoteModifier().insert("local/file2"); + fakeFolder.remoteModifier().insert("unspec/file1"); + QVERIFY(fakeFolder.syncOnce()); + + // root is unspecified + QCOMPARE(*vfs->availability("file1"), VfsItemAvailability::AllHydrated); + QCOMPARE(*vfs->availability("local"), VfsItemAvailability::AlwaysLocal); + QCOMPARE(*vfs->availability("local/file1"), VfsItemAvailability::AlwaysLocal); + QCOMPARE(*vfs->availability("online"), VfsItemAvailability::OnlineOnly); + QCOMPARE(*vfs->availability("online/file1.nextcloud"), VfsItemAvailability::OnlineOnly); + QCOMPARE(*vfs->availability("unspec"), VfsItemAvailability::SomeDehydrated); + QCOMPARE(*vfs->availability("unspec/file1.nextcloud"), VfsItemAvailability::SomeDehydrated); + + // Subitem pin states can ruin "pure" availabilities + setPin("local/sub", PinState::OnlineOnly); + QCOMPARE(*vfs->availability("local"), VfsItemAvailability::AllHydrated); + setPin("online/sub", PinState::Unspecified); + QCOMPARE(*vfs->availability("online"), VfsItemAvailability::SomeDehydrated); + + triggerDownload(fakeFolder, "unspec/file1"); + setPin("local/file2", PinState::OnlineOnly); + QVERIFY(fakeFolder.syncOnce()); + + QCOMPARE(*vfs->availability("unspec"), VfsItemAvailability::AllHydrated); + QCOMPARE(*vfs->availability("local"), VfsItemAvailability::SomeDehydrated); + + vfs->setPinState("local", PinState::AlwaysLocal); + vfs->setPinState("online", PinState::OnlineOnly); + QVERIFY(fakeFolder.syncOnce()); + + QCOMPARE(*vfs->availability("online"), VfsItemAvailability::OnlineOnly); + QCOMPARE(*vfs->availability("local"), VfsItemAvailability::AlwaysLocal); + } }; QTEST_GUILESS_MAIN(TestSyncVirtualFiles)