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
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;
// (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();
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);
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();
*/
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.
*
SqlQuery _deleteConflictRecordQuery;
SqlQuery _getRawPinStateQuery;
SqlQuery _getEffectivePinStateQuery;
+ SqlQuery _getSubPinsQuery;
+ SqlQuery _countDehydratedFilesQuery;
SqlQuery _setPinStateQuery;
SqlQuery _wipePinStateQuery;
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)
{
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);
+}
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;
* 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.
*
// 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;
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 {}
/// 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
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..."));
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);
}
}
-
- 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);
+ ENFORCE(combined);
+ if (files.size() > 1)
+ isFolderOrMultiple = true;
+
+ // 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;
}
}
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
{ 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 {}
}
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) {
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);
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);
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)