1
0
mirror of https://github.com/chylex/Nextcloud-Desktop.git synced 2025-04-07 04:15:44 +02:00

Vfs: Add 'availability', a simplified, user-facing pin state

The idea is that the user's question is "is this folder's data available
offline?" and not "does this folder have AlwaysLocal pin state?".
The the answers to the two questions can differ: an always-local
folder can have subitems that are not always-local and are dehydrated.

The new availability enum intends to describe the answer to the user's
actual question and can be derived from pin states. If pin states aren't
stored in the database the way of calculating availability will depend
on the vfs plugin.
This commit is contained in:
Christian Kamm 2019-04-03 10:53:04 +02:00 committed by Kevin Ottens
parent 590db28541
commit dcf34316fd
No known key found for this signature in database
GPG Key ID: 074BBBCB8DECC9E2
11 changed files with 335 additions and 64 deletions

View File

@ -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

View File

@ -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);

View File

@ -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;

View File

@ -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);
}

View File

@ -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

View File

@ -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..."));

View File

@ -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;
}
}

View File

@ -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

View File

@ -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 {}

View File

@ -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);

View File

@ -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)