From dcd5c78c40a730ae2220808cff8ff8d13daa3fd7 Mon Sep 17 00:00:00 2001 From: Klaas Freitag Date: Wed, 20 Aug 2025 16:30:59 +0200 Subject: [PATCH 01/13] Add method to return displayName of PinState enum --- src/libsync/common/pinstate.cpp | 18 ++++++++++++++++++ src/libsync/common/pinstate.h | 2 ++ 2 files changed, 20 insertions(+) diff --git a/src/libsync/common/pinstate.cpp b/src/libsync/common/pinstate.cpp index 6d3570298b..956a70d6fc 100644 --- a/src/libsync/common/pinstate.cpp +++ b/src/libsync/common/pinstate.cpp @@ -19,6 +19,24 @@ using namespace OCC; +template <> +QString Utility::enumToDisplayName(PinState state) +{ + switch (state) { + case PinState::AlwaysLocal: + return QStringLiteral("AlwaysLocal"); + case PinState::Excluded: + return QStringLiteral("Excluded"); + case PinState::Inherited: + return QStringLiteral("Inherited"); + case PinState::OnlineOnly: + return QStringLiteral("OnlineOnly"); + case PinState::Unspecified: + return QStringLiteral("Unspecified"); + } + Q_UNREACHABLE(); +} + template <> QString Utility::enumToDisplayName(VfsItemAvailability availability) { diff --git a/src/libsync/common/pinstate.h b/src/libsync/common/pinstate.h index 48a7ebbb05..dfeb470c7f 100644 --- a/src/libsync/common/pinstate.h +++ b/src/libsync/common/pinstate.h @@ -134,6 +134,8 @@ namespace PinStateEnums { Q_ENUM_NS(VfsItemAvailability) } using namespace PinStateEnums; +template <> +OPENCLOUD_SYNC_EXPORT QString Utility::enumToDisplayName(PinState state); template <> OPENCLOUD_SYNC_EXPORT QString Utility::enumToDisplayName(VfsItemAvailability availability); From cfa88d08b6ee511e81be3147803e4d406bf12e20 Mon Sep 17 00:00:00 2001 From: Klaas Freitag Date: Wed, 20 Aug 2025 16:36:39 +0200 Subject: [PATCH 02/13] Initial commit of an extended file attributes vfs plugin for OpenCloud Initial code was influenced from Nextclouds xattr code, to be enhanced by an openvfs fuse layer for linux and maybe macos --- CMakeLists.txt | 2 +- src/gui/folderwizard/folderwizard.cpp | 7 +- src/libsync/common/vfs.cpp | 6 + src/libsync/common/vfs.h | 2 +- src/plugins/vfs/CMakeLists.txt | 1 + src/plugins/vfs/xattr/CMakeLists.txt | 7 + src/plugins/vfs/xattr/vfs_xattr.cpp | 266 +++++++++++++++++++++++++ src/plugins/vfs/xattr/vfs_xattr.h | 61 ++++++ src/plugins/vfs/xattr/xattrwrapper.cpp | 98 +++++++++ src/plugins/vfs/xattr/xattrwrapper.h | 39 ++++ 10 files changed, 486 insertions(+), 3 deletions(-) create mode 100644 src/plugins/vfs/xattr/CMakeLists.txt create mode 100644 src/plugins/vfs/xattr/vfs_xattr.cpp create mode 100644 src/plugins/vfs/xattr/vfs_xattr.h create mode 100644 src/plugins/vfs/xattr/xattrwrapper.cpp create mode 100644 src/plugins/vfs/xattr/xattrwrapper.h diff --git a/CMakeLists.txt b/CMakeLists.txt index eed6d81ad9..0a93c0364a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -103,7 +103,7 @@ add_feature_info(AppImageUpdate WITH_APPIMAGEUPDATER "Built-in libappimageupdate option(WITH_EXTERNAL_BRANDING "A URL to an external branding repo" "") # specify additional vfs plugins -set(VIRTUAL_FILE_SYSTEM_PLUGINS off cfapi CACHE STRING "Name of internal plugin in src/libsync/vfs or the locations of virtual file plugins") +set(VIRTUAL_FILE_SYSTEM_PLUGINS xattr off cfapi CACHE STRING "Name of internal plugin in src/libsync/vfs or the locations of virtual file plugins") if(APPLE) set( SOCKETAPI_TEAM_IDENTIFIER_PREFIX "" CACHE STRING "SocketApi prefix (including a following dot) that must match the codesign key's TeamIdentifier/Organizational Unit" ) diff --git a/src/gui/folderwizard/folderwizard.cpp b/src/gui/folderwizard/folderwizard.cpp index c414f98078..ce619a9e0c 100644 --- a/src/gui/folderwizard/folderwizard.cpp +++ b/src/gui/folderwizard/folderwizard.cpp @@ -121,7 +121,12 @@ const AccountStatePtr &FolderWizardPrivate::accountState() bool FolderWizardPrivate::useVirtualFiles() const { - return VfsPluginManager::instance().bestAvailableVfsMode() == Vfs::WindowsCfApi; + const auto bavm = VfsPluginManager::instance().bestAvailableVfsMode(); + if (Utility::isWindows()) + return bavm == Vfs::WindowsCfApi; + if (Utility::isLinux()) + return bavm == Vfs::XAttr; + return false; } FolderWizard::FolderWizard(const AccountStatePtr &account, QWidget *parent) diff --git a/src/libsync/common/vfs.cpp b/src/libsync/common/vfs.cpp index e519ad8b2d..e3e87e4eb8 100644 --- a/src/libsync/common/vfs.cpp +++ b/src/libsync/common/vfs.cpp @@ -51,6 +51,8 @@ Optional Vfs::modeFromString(const QString &str) return Off; } else if (str == QLatin1String("cfapi")) { return WindowsCfApi; + } else if (str == QLatin1String("xattr")) { + return XAttr; } return {}; } @@ -65,6 +67,8 @@ QString Utility::enumToString(Vfs::Mode mode) return QStringLiteral("cfapi"); case Vfs::Mode::Off: return QStringLiteral("off"); + case Vfs::Mode::XAttr: + return QStringLiteral("xattr"); } Q_UNREACHABLE(); } @@ -201,6 +205,8 @@ Vfs::Mode OCC::VfsPluginManager::bestAvailableVfsMode() const { if (isVfsPluginAvailable(Vfs::WindowsCfApi)) { return Vfs::WindowsCfApi; + } else if (isVfsPluginAvailable(Vfs::XAttr)) { + return Vfs::XAttr; } else if (isVfsPluginAvailable(Vfs::Off)) { return Vfs::Off; } diff --git a/src/libsync/common/vfs.h b/src/libsync/common/vfs.h index f830e7fcda..e1b510224e 100644 --- a/src/libsync/common/vfs.h +++ b/src/libsync/common/vfs.h @@ -96,7 +96,7 @@ class OPENCLOUD_SYNC_EXPORT Vfs : public QObject * Currently plugins and modes are one-to-one but that's not required. * The raw integer values are used in Qml */ - enum Mode : uint8_t { Off = 0, WindowsCfApi = 1 }; + enum Mode : uint8_t { Off = 0, WindowsCfApi = 1, XAttr = 2 }; Q_ENUM(Mode) enum class ConvertToPlaceholderResult : uint8_t { Ok, Locked }; Q_ENUM(ConvertToPlaceholderResult) diff --git a/src/plugins/vfs/CMakeLists.txt b/src/plugins/vfs/CMakeLists.txt index 6a2935120a..b835455ea1 100644 --- a/src/plugins/vfs/CMakeLists.txt +++ b/src/plugins/vfs/CMakeLists.txt @@ -5,6 +5,7 @@ include(OCAddVfsPlugin) foreach(vfsPlugin ${VIRTUAL_FILE_SYSTEM_PLUGINS}) + message("checking vfs ${vfsPlugin}") set(vfsPluginPath ${vfsPlugin}) set(vfsPluginPathOut ${vfsPlugin}) get_filename_component(vfsPluginName ${vfsPlugin} NAME) diff --git a/src/plugins/vfs/xattr/CMakeLists.txt b/src/plugins/vfs/xattr/CMakeLists.txt new file mode 100644 index 0000000000..3312b4c2a1 --- /dev/null +++ b/src/plugins/vfs/xattr/CMakeLists.txt @@ -0,0 +1,7 @@ + add_vfs_plugin(NAME xattr + SRC + xattrwrapper.cpp + vfs_xattr.cpp + ) + + diff --git a/src/plugins/vfs/xattr/vfs_xattr.cpp b/src/plugins/vfs/xattr/vfs_xattr.cpp new file mode 100644 index 0000000000..755ba941ae --- /dev/null +++ b/src/plugins/vfs/xattr/vfs_xattr.cpp @@ -0,0 +1,266 @@ +/* + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2025 OpenCloud GmbH and OpenCloud contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "vfs_xattr.h" + +#include "syncfileitem.h" +#include "filesystem.h" +#include "common/syncjournaldb.h" +#include "xattrwrapper.h" + +#include +#include +#include + +Q_LOGGING_CATEGORY(lcVfsXAttr, "sync.vfs.xattr", QtInfoMsg) + +namespace xattr { +// using namespace XAttrWrapper; +} + +namespace OCC { + +VfsXAttr::VfsXAttr(QObject *parent) + : Vfs(parent) +{ +} + +VfsXAttr::~VfsXAttr() = default; + +Vfs::Mode VfsXAttr::mode() const +{ + return XAttr; +} + +void VfsXAttr::startImpl(const VfsSetupParams &) +{ + qCDebug(lcVfsXAttr(), "Start XAttr VFS"); + + Q_EMIT started(); +} + +void VfsXAttr::stop() +{ +} + +void VfsXAttr::unregisterFolder() +{ +} + +bool VfsXAttr::socketApiPinStateActionsShown() const +{ + return true; +} + + + +OCC::Result VfsXAttr::updateMetadata(const SyncFileItem &syncItem, const QString &filePath, const QString &replacesFile) +{ + const auto localPath = QDir::toNativeSeparators(filePath); + const auto replacesPath = QDir::toNativeSeparators(replacesFile); + + qCDebug(lcVfsXAttr()) << localPath; + + if (syncItem._type == ItemTypeVirtualFileDehydration) { + // FIXME: Error handling + dehydratePlaceholder(syncItem); + } else { + XAttrWrapper::PlaceHolderAttribs attribs = XAttrWrapper::placeHolderAttributes(localPath); + + if (attribs.itsMe()) { // checks if there are placeholder Attribs at all + FileSystem::setModTime(localPath, syncItem._modtime); + + XAttrWrapper::addPlaceholderAttribute(localPath, "user.openvfs.fsize", QByteArray::number(syncItem._size)); + XAttrWrapper::addPlaceholderAttribute(localPath, "user.openvfs.state", "dehydrated"); + XAttrWrapper::addPlaceholderAttribute(localPath, "user.openvfs.fileid", syncItem._fileId); + XAttrWrapper::addPlaceholderAttribute(localPath, "user.openvfs.etag", syncItem._etag.toUtf8()); + + } else { + // FIXME use fileItem as parameter + return convertToPlaceholder(localPath, syncItem._modtime, syncItem._size, syncItem._fileId, replacesPath); + } + } + + return {OCC::Vfs::ConvertToPlaceholderResult::Ok}; +} + +Result VfsXAttr::createPlaceholder(const SyncFileItem &item) +{ + if (item._modtime <= 0) { + return {tr("Error updating metadata due to invalid modification time")}; + } + + const auto path = QDir::toNativeSeparators(params().filesystemPath + item.localName()); + + qCDebug(lcVfsXAttr()) << path; + + QFile file(path); + // FIXME: Check to not overwrite an existing file + // if (file.exists() && file.size() > 1 + // && !FileSystem::verifyFileUnchanged(path, item._size, item._modtime)) { + // return QStringLiteral("Cannot create a placeholder because a file with the placeholder name already exist"); + // } + + if (!file.open(QFile::ReadWrite | QFile::Truncate)) { + return file.errorString(); + } + + file.write(" "); + file.close(); + + /* + * Only write the state and the executor, the rest is added in the updateMetadata() method + */ + XAttrWrapper::addPlaceholderAttribute(path, "user.openvfs.state", "dehydrated"); + return {}; +} + +OCC::Result VfsXAttr::dehydratePlaceholder(const SyncFileItem &item) +{ + /* + * const auto path = QDir::toNativeSeparators(params().filesystemPath + item.localName()); + * + * QFile file(path); + * + * if (!file.remove()) { + * return QStringLiteral("Couldn't remove the original file to dehydrate"); + * } + */ + auto r = createPlaceholder(item); + if (!r) { + return r; + } + + // Ensure the pin state isn't contradictory + const auto pin = pinState(item.localName()); + if (pin && *pin == PinState::AlwaysLocal) { + setPinState(item._renameTarget, PinState::Unspecified); + } + return {}; +} + +OCC::Result VfsXAttr::convertToPlaceholder( + const QString &path, time_t modtime, qint64 size, const QByteArray &fileId, const QString &replacesPath) +{ + Q_UNUSED(modtime) + Q_UNUSED(size) + Q_UNUSED(fileId) + Q_UNUSED(replacesPath) + + // Nothing necessary - no idea why, taken from previews... + qCDebug(lcVfsXAttr()) << "empty function returning ok, DOUBLECHECK" << path ; + return {ConvertToPlaceholderResult::Ok}; +} + +bool VfsXAttr::needsMetadataUpdate(const SyncFileItem &) +{ + qCDebug(lcVfsXAttr()) << "returns false by default DOUBLECHECK"; + return false; +} + +bool VfsXAttr::isDehydratedPlaceholder(const QString &filePath) +{ + const auto fi = QFileInfo(filePath); + return fi.exists() && + XAttrWrapper::hasPlaceholderAttributes(filePath); +} + +LocalInfo VfsXAttr::statTypeVirtualFile(const std::filesystem::directory_entry &path, ItemType type) +{ + if (type == ItemTypeFile) { + const QString p = QString::fromUtf8(path.path().c_str()); //FIXME? + qCDebug(lcVfsXAttr()) << p; + + if (XAttrWrapper::hasPlaceholderAttributes(p)) { + // const auto shouldDownload = pin && (*pin == PinState::AlwaysLocal); + bool shouldDownload{false}; + if (shouldDownload) { + type = ItemTypeVirtualFileDownload; + } else { + type = ItemTypeVirtualFile; + } + } else { + const auto shouldDehydrate = false; // pin && (*pin == PinState::OnlineOnly); + if (shouldDehydrate) { + type = ItemTypeVirtualFileDehydration; + } + } + } + + return LocalInfo(path, type); +} + +bool VfsXAttr::setPinState(const QString &folderPath, PinState state) +{ + qCDebug(lcVfsXAttr()) << folderPath << state; + auto stateStr = Utility::enumToDisplayName(state); + auto res = XAttrWrapper::addPlaceholderAttribute(folderPath, "user.openvfs.pinstate", stateStr.toUtf8()); + if (res) { + qCDebug(lcVfsXAttr()) << "Failed to set pin state"; + return false; + } + return true; +} + +Optional VfsXAttr::pinState(const QString &folderPath) +{ + qCDebug(lcVfsXAttr()) << folderPath; + + XAttrWrapper::PlaceHolderAttribs attribs = XAttrWrapper::placeHolderAttributes(folderPath); + + const QString pin = QString::fromUtf8(attribs.pinState()); + PinState pState; + if (pin == Utility::enumToDisplayName(PinState::AlwaysLocal)) { + pState = PinState::AlwaysLocal; + } else if (pin == Utility::enumToDisplayName(PinState::Excluded)) { + pState = PinState::Excluded; + } else if (pin == Utility::enumToDisplayName(PinState::Inherited)) { + pState = PinState::Inherited; + } else if (pin == Utility::enumToDisplayName(PinState::OnlineOnly)) { + pState = PinState::OnlineOnly; + } else if (pin == Utility::enumToDisplayName(PinState::Unspecified)) { + pState = PinState::Unspecified; + } + + return pState; +} + +Vfs::AvailabilityResult VfsXAttr::availability(const QString &folderPath) +{ + qCDebug(lcVfsXAttr()) << folderPath; + + const auto basePinState = pinState(folderPath); + if (basePinState) { + switch (*basePinState) { + case OCC::PinState::AlwaysLocal: + return VfsItemAvailability::AlwaysLocal; + break; + case OCC::PinState::Inherited: + break; + case OCC::PinState::OnlineOnly: + return VfsItemAvailability::OnlineOnly; + break; + case OCC::PinState::Unspecified: + break; + case OCC::PinState::Excluded: + break; + }; + return VfsItemAvailability::Mixed; + } else { + return AvailabilityError::NoSuchItem; + } +} + +void VfsXAttr::fileStatusChanged(const QString& systemFileName, SyncFileStatus fileStatus) +{ + qCDebug(lcVfsXAttr()) << systemFileName << fileStatus; + + if (fileStatus.tag() == SyncFileStatus::StatusExcluded) { + setPinState(systemFileName, PinState::Excluded); + } +} + +} // namespace OCC diff --git a/src/plugins/vfs/xattr/vfs_xattr.h b/src/plugins/vfs/xattr/vfs_xattr.h new file mode 100644 index 0000000000..1561720de6 --- /dev/null +++ b/src/plugins/vfs/xattr/vfs_xattr.h @@ -0,0 +1,61 @@ +/* + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2025 OpenCloud GmbH and OpenCloud contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ +#pragma once + +#include +#include + +#include "common/vfs.h" +#include "common/plugin.h" + +namespace OCC { + +class VfsXAttr : public Vfs +{ + Q_OBJECT + +public: + explicit VfsXAttr(QObject *parent = nullptr); + ~VfsXAttr() override; + + [[nodiscard]] Mode mode() const override; + + void stop() override; + void unregisterFolder() override; + + [[nodiscard]] bool socketApiPinStateActionsShown() const override; + + Result updateMetadata(const SyncFileItem &syncItem, const QString &filePath, const QString &replacesFile) override; + // [[nodiscard]] bool isPlaceHolderInSync(const QString &filePath) const override { Q_UNUSED(filePath) return true; } + + Result createPlaceholder(const SyncFileItem &item) override; + OCC::Result dehydratePlaceholder(const SyncFileItem &item); + OCC::Result convertToPlaceholder( + const QString &path, time_t modtime, qint64 size, const QByteArray &fileId, const QString &replacesPath); + + bool needsMetadataUpdate(const SyncFileItem &item) override; + bool isDehydratedPlaceholder(const QString &filePath) override; + LocalInfo statTypeVirtualFile(const std::filesystem::directory_entry &path, ItemType type) override; + + bool setPinState(const QString &folderPath, PinState state) override; + Optional pinState(const QString &folderPath) override; + AvailabilityResult availability(const QString &folderPath) override; + +public Q_SLOTS: + void fileStatusChanged(const QString &systemFileName, OCC::SyncFileStatus fileStatus) override; + +protected: + void startImpl(const VfsSetupParams ¶ms) override; +}; + +class XattrVfsPluginFactory : public QObject, public DefaultPluginFactory +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "eu.opencloud.PluginFactory" FILE "libsync/common/vfspluginmetadata.json") + Q_INTERFACES(OCC::PluginFactory) +}; + +} // namespace OCC diff --git a/src/plugins/vfs/xattr/xattrwrapper.cpp b/src/plugins/vfs/xattr/xattrwrapper.cpp new file mode 100644 index 0000000000..3530343e5d --- /dev/null +++ b/src/plugins/vfs/xattr/xattrwrapper.cpp @@ -0,0 +1,98 @@ +/* + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2025 OpenCloud GmbH and OpenCloud contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "xattrwrapper.h" +#include "common/result.h" +#include "config.h" + +#include +#include + + + +Q_LOGGING_CATEGORY(lcXAttrWrapper, "sync.vfs.xattr.wrapper", QtInfoMsg) + +namespace { +constexpr auto hydrateExecAttributeName = "user.openvfs.hydrate_exec"; + +OCC::Optional xattrGet(const QByteArray &path, const QByteArray &name) +{ + constexpr auto bufferSize = 256; + QByteArray result; + result.resize(bufferSize); + const auto count = getxattr(path.constData(), name.constData(), result.data(), bufferSize); + if (count >= 0) { + result.resize(static_cast(count) - 1); + return result; + } else { + return {}; + } +} + +bool xattrSet(const QByteArray &path, const QByteArray &name, const QByteArray &value) +{ + const auto returnCode = setxattr(path.constData(), name.constData(), value.constData(), value.size() + 1, 0); + return returnCode == 0; +} + +} + +namespace XAttrWrapper { + +PlaceHolderAttribs placeHolderAttributes(const QString& path) +{ + PlaceHolderAttribs attribs; + + // lambda to handle the Optional return val of xattrGet + auto xattr = [](const QByteArray& p, const QByteArray& name) { + const auto value = xattrGet(p, name); + if (value) { + return *value; + } else { + return QByteArray(); + } + }; + + const auto p = path.toUtf8(); + + attribs._executor = xattr(p, hydrateExecAttributeName); + attribs._etag = QString::fromUtf8(xattr(p, "user.openvfs.etag")); + attribs._fileId = xattr(p, "user.openvfs.fileid"); + + const QByteArray& tt = xattr(p, "user.openvfs.modtime"); + attribs._modtime = tt.toLongLong(); + attribs._size = xattr(p, "user.openvfs.fsize").toLongLong(); + attribs._pinState = xattr(p, "user.openvfs.pinstate"); + + return attribs; +} + + +bool hasPlaceholderAttributes(const QString &path) +{ + const PlaceHolderAttribs attribs = placeHolderAttributes(path); + + // Only pretend to have attribs if they are from us... + return attribs.itsMe(); +} + +OCC::Result addPlaceholderAttribute(const QString &path, const QByteArray& name, const QByteArray& value) +{ + auto success = xattrSet(path.toUtf8(), hydrateExecAttributeName, APPLICATION_EXECUTABLE); + if (!success) { + return QStringLiteral("Failed to set the extended attribute hydrateExec"); + } + + if (!name.isEmpty()) { + success = xattrSet(path.toUtf8(), name, value); + if (!success) { + return QStringLiteral("Failed to set the extended attribute"); + } + } + + return {}; +} +} diff --git a/src/plugins/vfs/xattr/xattrwrapper.h b/src/plugins/vfs/xattr/xattrwrapper.h new file mode 100644 index 0000000000..4063266fcd --- /dev/null +++ b/src/plugins/vfs/xattr/xattrwrapper.h @@ -0,0 +1,39 @@ +/* + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2025 OpenCloud GmbH and OpenCloud contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ +#pragma once + +#include + +#include "config.h" +#include "common/result.h" + +namespace XAttrWrapper +{ +struct PlaceHolderAttribs { +public: + qint64 size() const { return _size; } + QByteArray fileId() const { return _fileId; } + time_t modTime() const {return _modtime; } + QString eTag() const { return _etag; } + QByteArray pinState() const { return _pinState; } + + bool itsMe() const { return !_executor.isEmpty() && _executor == QByteArrayLiteral(APPLICATION_EXECUTABLE);} + + qint64 _size; + QByteArray _fileId; + time_t _modtime; + QString _etag; + QByteArray _executor; + QByteArray _pinState; + +}; + +PlaceHolderAttribs placeHolderAttributes(const QString& path); +bool hasPlaceholderAttributes(const QString &path); + +OCC::Result addPlaceholderAttribute(const QString &path, const QByteArray &name = {}, const QByteArray &val = {}); + +} From f4e451d35826995b833232ef09b0fd17d9f3c5a2 Mon Sep 17 00:00:00 2001 From: Klaas Freitag Date: Thu, 21 Aug 2025 11:39:54 +0200 Subject: [PATCH 03/13] Minor fixes, squash up --- src/plugins/vfs/xattr/vfs_xattr.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/plugins/vfs/xattr/vfs_xattr.cpp b/src/plugins/vfs/xattr/vfs_xattr.cpp index 755ba941ae..754a0f4279 100644 --- a/src/plugins/vfs/xattr/vfs_xattr.cpp +++ b/src/plugins/vfs/xattr/vfs_xattr.cpp @@ -73,6 +73,7 @@ OCC::Result VfsXAttr::updateMetad if (attribs.itsMe()) { // checks if there are placeholder Attribs at all FileSystem::setModTime(localPath, syncItem._modtime); + // FIXME only write attribs if they're different XAttrWrapper::addPlaceholderAttribute(localPath, "user.openvfs.fsize", QByteArray::number(syncItem._size)); XAttrWrapper::addPlaceholderAttribute(localPath, "user.openvfs.state", "dehydrated"); XAttrWrapper::addPlaceholderAttribute(localPath, "user.openvfs.fileid", syncItem._fileId); @@ -198,7 +199,7 @@ bool VfsXAttr::setPinState(const QString &folderPath, PinState state) qCDebug(lcVfsXAttr()) << folderPath << state; auto stateStr = Utility::enumToDisplayName(state); auto res = XAttrWrapper::addPlaceholderAttribute(folderPath, "user.openvfs.pinstate", stateStr.toUtf8()); - if (res) { + if (!res) { qCDebug(lcVfsXAttr()) << "Failed to set pin state"; return false; } From f0b2e6b4b84785b87b7d0d9dddc6431e432e9f97 Mon Sep 17 00:00:00 2001 From: Klaas Freitag Date: Thu, 21 Aug 2025 11:40:22 +0200 Subject: [PATCH 04/13] Ignore the OpenCloud log in the linux folderwatcher on low level --- src/gui/folderwatcher_linux.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/gui/folderwatcher_linux.cpp b/src/gui/folderwatcher_linux.cpp index 0751e7cf52..bd6bd60cdc 100644 --- a/src/gui/folderwatcher_linux.cpp +++ b/src/gui/folderwatcher_linux.cpp @@ -173,7 +173,8 @@ void FolderWatcherPrivate::slotReceivedNotification(int fd) // Filter out journal changes - redundant with filtering in FolderWatcher::pathIsIgnored. if (fileName.startsWith("._sync_") || fileName.startsWith(".csync_journal.db") - || fileName.startsWith(".sync_")) { + || fileName.startsWith(".sync_") + || fileName == QByteArrayLiteral(".OpenCloudSync.log") ) { continue; } From 4547d31e2ea6fa630c541a9a331ec721fcc62921 Mon Sep 17 00:00:00 2001 From: Klaas Freitag Date: Fri, 5 Sep 2025 15:43:20 +0200 Subject: [PATCH 05/13] Make folderwatcher listen on changes on xattr changes This can be used to trigger vfs actions by setting an xattr --- src/gui/folder.cpp | 7 +++++++ src/gui/folder.h | 3 ++- src/gui/folderwatcher.cpp | 33 ++++++++++++++++++++++++++++++++- src/gui/folderwatcher.h | 8 +++++++- src/gui/folderwatcher_linux.cpp | 9 ++++++++- 5 files changed, 56 insertions(+), 4 deletions(-) diff --git a/src/gui/folder.cpp b/src/gui/folder.cpp index 4424be96f9..2a47bd447d 100644 --- a/src/gui/folder.cpp +++ b/src/gui/folder.cpp @@ -604,6 +604,10 @@ void Folder::slotWatchedPathsChanged(const QSet &paths, ChangeReason re Q_ASSERT(FileSystem::isChildPathOf(path, this->path())); const QString relativePath = path.mid(this->path().size()); + if (reason == ChangeReason::XAttr) { + // Changes to the extended file attributes. Maybe the VFS has something to do with it + _vfs->handleXAttrChange(paths); + } if (reason == ChangeReason::UnLock) { journalDb()->wipeErrorBlacklistEntry(relativePath, SyncJournalErrorBlacklistRecord::Category::LocalSoftError); @@ -1116,6 +1120,9 @@ void Folder::registerFolderWatcher() _folderWatcher.reset(new FolderWatcher(this)); connect(_folderWatcher.data(), &FolderWatcher::pathChanged, this, [this](const QSet &paths) { slotWatchedPathsChanged(paths, Folder::ChangeReason::Other); }); + connect(_folderWatcher.data(), &FolderWatcher::xattrChanged, this, + [this](const QSet &paths) { slotWatchedPathsChanged(paths, Folder::ChangeReason::XAttr); }); + connect(_folderWatcher.data(), &FolderWatcher::changesDetected, this, [this] { // don't set to not yet started if a sync is already running if (!isSyncRunning()) { diff --git a/src/gui/folder.h b/src/gui/folder.h index fac94a4459..69e899b07a 100644 --- a/src/gui/folder.h +++ b/src/gui/folder.h @@ -66,7 +66,8 @@ class OPENCLOUD_GUI_EXPORT Folder : public QObject public: enum class ChangeReason { Other, - UnLock + UnLock, + XAttr }; Q_ENUM(ChangeReason) diff --git a/src/gui/folderwatcher.cpp b/src/gui/folderwatcher.cpp index 31768f7445..70d1610153 100644 --- a/src/gui/folderwatcher.cpp +++ b/src/gui/folderwatcher.cpp @@ -48,8 +48,14 @@ FolderWatcher::FolderWatcher(Folder *folder) _timer.setInterval(notificationTimeoutC); _timer.setSingleShot(true); connect(&_timer, &QTimer::timeout, this, [this] { - auto paths = popChangeSet(); + _timer.stop(); + auto paths = std::move(_changeSet); Q_ASSERT(!paths.empty()); + if (_xattrChangeSet.size() > 0) { + auto xattrChangesPaths = std::move(_xattrChangeSet); + qCInfo(lcFolderWatcher) << u"Detected XAttr changes in paths:" << paths; + Q_EMIT xattrChanged(xattrChangesPaths); + } if (!paths.isEmpty()) { qCInfo(lcFolderWatcher) << u"Detected changes in paths:" << paths; Q_EMIT pathChanged(paths); @@ -176,6 +182,31 @@ void FolderWatcher::addChanges(QSet &&paths) } } +void FolderWatcher::addXAttrChanges(QSet && paths) +{ + auto it = paths.cbegin(); + while (it != paths.cend()) { + // we cause a file change from time to time to check whether the folder watcher works as expected + if (!_testNotificationPath.isEmpty() && Utility::fileNamesEqual(*it, _testNotificationPath)) { + _testNotificationPath.clear(); + } + if (pathIsIgnored(*it)) { + it = paths.erase(it); + } else { + ++it; + } + } + if (!paths.isEmpty()) { + _xattrChangeSet.unite(paths); + if (!_timer.isActive()) { + _timer.start(); + // promote that we will report changes once _timer times out + // not needed for the changes of xattr probably + // Q_EMIT changesDetected(); + } + } +} + QSet FolderWatcher::popChangeSet() { // stop the timer as we pop all queued changes diff --git a/src/gui/folderwatcher.h b/src/gui/folderwatcher.h index 1aaed61fbc..7f14f266f3 100644 --- a/src/gui/folderwatcher.h +++ b/src/gui/folderwatcher.h @@ -80,15 +80,19 @@ class OPENCLOUD_GUI_EXPORT FolderWatcher : public QObject /// For testing linux behavior only int testLinuxWatchCount() const; + // pop the accumulated changes QSet popChangeSet(); - Q_SIGNALS: /** Emitted when one of the watched directories or one * of the contained files is changed. */ void pathChanged(const QSet &path); + /** Emitted when an extended file attribute changed on one + * of the files in the list */ + void xattrChanged(const QSet &path); + /** * We detected a file change, this signal can be used to trigger the prepareSync state */ @@ -115,11 +119,13 @@ private Q_SLOTS: protected: // called from the implementations to indicate a change in path void addChanges(QSet &&paths); + void addXAttrChanges(QSet &&paths); private: QScopedPointer _d; QTimer _timer; QSet _changeSet; + QSet _xattrChangeSet; Folder *_folder; bool _isReliable = true; diff --git a/src/gui/folderwatcher_linux.cpp b/src/gui/folderwatcher_linux.cpp index bd6bd60cdc..ce14b5fd73 100644 --- a/src/gui/folderwatcher_linux.cpp +++ b/src/gui/folderwatcher_linux.cpp @@ -149,7 +149,7 @@ void FolderWatcherPrivate::slotReceivedNotification(int fd) } } - QSet paths; + QSet paths, xattrPaths; // iterate over events in buffer struct inotify_event *event = nullptr; for (size_t bytePosition = 0; // start at the beginning of the buffer @@ -189,10 +189,17 @@ void FolderWatcherPrivate::slotReceivedNotification(int fd) if (event->mask & (IN_MOVED_FROM | IN_DELETE)) { removeFoldersBelow(p); } + if (event->mask & (IN_ATTRIB)) { + xattrPaths.insert(p); + } } if (!paths.isEmpty()) { _parent->addChanges(std::move(paths)); } + if (!xattrPaths.isEmpty()) { + _parent->addXAttrChanges(std::move(xattrPaths)); + } + } void FolderWatcherPrivate::removeFoldersBelow(const QString &path) From 205d08f107a834908bc894e3f7195257702da408 Mon Sep 17 00:00:00 2001 From: Klaas Freitag Date: Fri, 5 Sep 2025 15:46:00 +0200 Subject: [PATCH 06/13] Refactor vfs: Move commonly used code from cfapi to vfs Used by both the xattr and cfapi vfs --- src/libsync/common/CMakeLists.txt | 1 + .../cfapi => libsync/common}/hydrationjob.cpp | 31 ++- .../cfapi => libsync/common}/hydrationjob.h | 22 +- src/libsync/common/vfs.cpp | 144 +++++++++- src/libsync/common/vfs.h | 26 ++ src/plugins/vfs/cfapi/CMakeLists.txt | 1 - src/plugins/vfs/cfapi/cfapiwrapper.cpp | 15 +- src/plugins/vfs/cfapi/cfapiwrapper.h | 10 - src/plugins/vfs/cfapi/vfs_cfapi.cpp | 124 +-------- src/plugins/vfs/cfapi/vfs_cfapi.h | 15 +- src/plugins/vfs/off/vfs_off.cpp | 5 + src/plugins/vfs/off/vfs_off.h | 1 + src/plugins/vfs/xattr/vfs_xattr.cpp | 246 +++++++++++++++++- src/plugins/vfs/xattr/vfs_xattr.h | 4 + src/plugins/vfs/xattr/xattrwrapper.cpp | 35 ++- src/plugins/vfs/xattr/xattrwrapper.h | 2 + 16 files changed, 482 insertions(+), 200 deletions(-) rename src/{plugins/vfs/cfapi => libsync/common}/hydrationjob.cpp (84%) rename src/{plugins/vfs/cfapi => libsync/common}/hydrationjob.h (84%) diff --git a/src/libsync/common/CMakeLists.txt b/src/libsync/common/CMakeLists.txt index 9832e0652e..494f09f97a 100644 --- a/src/libsync/common/CMakeLists.txt +++ b/src/libsync/common/CMakeLists.txt @@ -12,6 +12,7 @@ target_sources(libsync PRIVATE utility.cpp remotepermissions.cpp vfs.cpp + hydrationjob.cpp pinstate.cpp plugin.cpp restartmanager.cpp diff --git a/src/plugins/vfs/cfapi/hydrationjob.cpp b/src/libsync/common/hydrationjob.cpp similarity index 84% rename from src/plugins/vfs/cfapi/hydrationjob.cpp rename to src/libsync/common/hydrationjob.cpp index fad56cbd1e..040dc64a93 100644 --- a/src/plugins/vfs/cfapi/hydrationjob.cpp +++ b/src/libsync/common/hydrationjob.cpp @@ -3,10 +3,7 @@ * SPDX-License-Identifier: GPL-2.0-or-later */ -#include "plugins/vfs/cfapi/hydrationjob.h" - -#include "plugins/vfs/cfapi/cfapiwrapper.h" -#include "plugins/vfs/cfapi/vfs_cfapi.h" +#include "hydrationjob.h" #include "libsync/common/syncjournaldb.h" #include "libsync/filesystem.h" @@ -19,7 +16,7 @@ using namespace Qt::Literals::StringLiterals; Q_LOGGING_CATEGORY(lcHydration, "sync.vfs.hydrationjob", QtDebugMsg) -OCC::HydrationJob::HydrationJob(const CfApiWrapper::CallBackContext &context) +OCC::HydrationJob::HydrationJob(const CallBackContext &context) : QObject(context.vfs) , _context(context) { @@ -103,7 +100,7 @@ OCC::HydrationJob::Status OCC::HydrationJob::status() const return _status; } -const OCC::CfApiWrapper::CallBackContext OCC::HydrationJob::context() const +const OCC::CallBackContext OCC::HydrationJob::context() const { return _context; } @@ -132,21 +129,22 @@ void OCC::HydrationJob::start() Q_ASSERT(_localRoot.endsWith('/'_L1)); const auto startServer = [this](const QString &serverName) -> QLocalServer * { + QLocalServer::removeServer(serverName); const auto server = new QLocalServer(this); const auto listenResult = server->listen(serverName); if (!listenResult) { - qCCritical(lcHydration) << u"Couldn't get server to listen" << serverName << _localRoot << _context; + //qCCritical(lcHydration) << u"Couldn't get server to listen" << serverName << _localRoot << _context; if (!_isCancelled) { emitFinished(Status::Error); } return nullptr; } - qCInfo(lcHydration) << u"Server ready, waiting for connections" << serverName << _localRoot << _context; + // qCInfo(lcHydration) << u"Server ready, waiting for connections" << serverName << _localRoot << _context; return server; }; // Start cancellation server - _signalServer = startServer(_context.requestHexId() + u":cancellation"_s); + _signalServer = startServer(_context.requestHexId() + QStringLiteral(":cancellation")); Q_ASSERT(_signalServer); if (!_signalServer) { return; @@ -209,7 +207,7 @@ void OCC::HydrationJob::onCancellationServerNewConnection() { Q_ASSERT(!_signalSocket); - qCInfo(lcHydration) << u"Got new connection on cancellation server" << _context; + // qCInfo(lcHydration) << u"Got new connection on cancellation server" << _context; _signalSocket = _signalServer->nextPendingConnection(); } @@ -220,7 +218,7 @@ void OCC::HydrationJob::onNewConnection() handleNewConnection(); } -void OCC::HydrationJob::finalize(OCC::VfsCfApi *vfs) +void OCC::HydrationJob::finalize(OCC::Vfs *vfs) { auto item = SyncFileItem::fromSyncJournalFileRecord(_record); if (_isCancelled) { @@ -246,10 +244,10 @@ void OCC::HydrationJob::finalize(OCC::VfsCfApi *vfs) FileSystem::getInode(FileSystem::toFilesystemPath(localFilePathAbs()), &item->_inode); const auto result = _journal->setFileRecord(SyncJournalFileRecord::fromSyncFileItem(*item)); if (!result) { - qCWarning(lcHydration) << u"Error when setting the file record to the database" << _context << result.error(); + // qCWarning(lcHydration) << u"Error when setting the file record to the database" << _context << result.error(); } } else { - qCWarning(lcHydration) << u"Hydration succeeded but the file appears to be moved" << _context; + // qCWarning(lcHydration) << u"Hydration succeeded but the file appears to be moved" << _context; } } @@ -271,9 +269,9 @@ void OCC::HydrationJob::onGetFinished() } } if (!_errorString.isEmpty()) { - qCInfo(lcHydration) << u"GETFileJob finished" << _context << _errorCode << _statusCode << _errorString; + // qCInfo(lcHydration) << u"GETFileJob finished" << _context << _errorCode << _statusCode << _errorString; } else { - qCInfo(lcHydration) << u"GETFileJob finished" << _context; + // qCInfo(lcHydration) << u"GETFileJob finished" << _context; } if (_isCancelled) { _errorCode = QNetworkReply::NoError; @@ -292,7 +290,8 @@ void OCC::HydrationJob::onGetFinished() void OCC::HydrationJob::handleNewConnection() { - qCInfo(lcHydration) << u"Got new connection starting GETFileJob" << _context; + // FIXME: Fix all the loggings which require the operator<< from vfs.cpp for CallBackContext + // qCInfo(lcHydration) << u"Got new connection starting GETFileJob" << _context; _transferDataSocket = _transferDataServer->nextPendingConnection(); _job = new GETFileJob(_account, _remoteSyncRootPath, _remoteFilePathRel, _transferDataSocket, {}, {}, 0, this); _job->setExpectedContentLength(_record.size()); diff --git a/src/plugins/vfs/cfapi/hydrationjob.h b/src/libsync/common/hydrationjob.h similarity index 84% rename from src/plugins/vfs/cfapi/hydrationjob.h rename to src/libsync/common/hydrationjob.h index baf6330262..9aa7510e7c 100644 --- a/src/plugins/vfs/cfapi/hydrationjob.h +++ b/src/libsync/common/hydrationjob.h @@ -4,7 +4,6 @@ */ #pragma once -#include "cfapiwrapper.h" #include "libsync/account.h" #include "libsync/common/syncjournalfilerecord.h" @@ -16,7 +15,18 @@ class QLocalSocket; namespace OCC { class GETFileJob; class SyncJournalDb; -class VfsCfApi; +class Vfs; + +struct CallBackContext +{ + OCC::Vfs *vfs; + QString path; + int64_t requestId; + QByteArray fileId; + QMap extraArgs; + + inline QString requestHexId() const { return QString::number(requestId, 16); } +}; // TODO: check checksums class HydrationJob : public QObject @@ -30,7 +40,7 @@ class HydrationJob : public QObject }; Q_ENUM(Status) - explicit HydrationJob(const CfApiWrapper::CallBackContext &context); + explicit HydrationJob(const CallBackContext &context); ~HydrationJob() override; @@ -58,7 +68,7 @@ class HydrationJob : public QObject Status status() const; - const CfApiWrapper::CallBackContext context() const; + const CallBackContext context() const; [[nodiscard]] int errorCode() const; [[nodiscard]] int statusCode() const; @@ -66,7 +76,7 @@ class HydrationJob : public QObject void start(); void cancel(); - void finalize(OCC::VfsCfApi *vfs); + void finalize(OCC::Vfs *vfs); Q_SIGNALS: void finished(HydrationJob *job); @@ -89,7 +99,7 @@ class HydrationJob : public QObject SyncJournalDb *_journal = nullptr; bool _isCancelled = false; - CfApiWrapper::CallBackContext _context; + OCC::CallBackContext _context; QString _remoteFilePathRel; SyncJournalFileRecord _record; diff --git a/src/libsync/common/vfs.cpp b/src/libsync/common/vfs.cpp index e3e87e4eb8..11a3ecef59 100644 --- a/src/libsync/common/vfs.cpp +++ b/src/libsync/common/vfs.cpp @@ -21,6 +21,7 @@ #include "common/version.h" #include "plugin.h" #include "syncjournaldb.h" +#include "syncfileitem.h" #include #include @@ -35,9 +36,29 @@ using namespace Qt::Literals::StringLiterals; Q_LOGGING_CATEGORY(lcVfs, "sync.vfs", QtInfoMsg) +QDebug operator<<(QDebug debug, const OCC::CallBackContext &context) +{ + QDebugStateSaver saver(debug); + debug.setAutoInsertSpaces(false); + debug << u"cfapiCallback(" << context.path << u", " << context.requestHexId(); + for (const auto &[k, v] : context.extraArgs.asKeyValueRange()) { + debug << u", "; + debug.noquote() << k << u"="; + debug.quote() << v; + }; + debug << u")"; + return debug.maybeSpace(); +} + +class OCC::VfsApiPrivate +{ +public: + QMap hydrationJobs; +}; Vfs::Vfs(QObject *parent) : QObject(parent) + , d(new VfsApiPrivate) { } @@ -114,7 +135,6 @@ void Vfs::start(const VfsSetupParams ¶ms) startImpl(this->params()); } - void Vfs::wipeDehydratedVirtualFiles() { if (mode() == Vfs::Mode::Off) { @@ -148,6 +168,128 @@ void Vfs::wipeDehydratedVirtualFiles() // But hydrated placeholders may still be around. } +// ======================= Hydration ================ + +HydrationJob *Vfs::findHydrationJob(int64_t requestId) const +{ + // Find matching hydration job for request id + return d->hydrationJobs.value(requestId); +} + +void Vfs::cancelHydration(const OCC::CallBackContext &context) +{ + // Find matching hydration job for request id + const auto hydrationJob = findHydrationJob(context.requestId); + // If found, cancel it + if (hydrationJob) { + qCInfo(lcVfs) << u"Cancel hydration" << hydrationJob->context(); + hydrationJob->cancel(); + } +} + +void Vfs::requestHydration(const OCC::CallBackContext &context, qint64 requestedFileSize) +{ + qCInfo(lcVfs) << u"Received request to hydrate" << context.path << context.fileId; + const auto root = QDir::toNativeSeparators(params().filesystemPath); + Q_ASSERT(context.path.startsWith(root)); + + + // Set in the database that we should download the file + SyncJournalFileRecord record; + params().journal->getFileRecordsByFileId(context.fileId, [&record](const auto &r) { + Q_ASSERT(!record.isValid()); + record = r; + }); + if (!record.isValid()) { + qCInfo(lcVfs) << u"Couldn't hydrate, did not find file in db"; + Q_ASSERT(false); // how did we end up here if it's not a cloud file + Q_EMIT hydrationRequestFailed(context.requestId); + Q_EMIT needSync(); + return; + } + + bool isNotVirtualFileFailure = false; + if (!record.isVirtualFile()) { + if (isDehydratedPlaceholder(context.path)) { + qCWarning(lcVfs) << u"Hydration requested for a placeholder file that is incorrectly not marked as a virtual file in the local database. " + u"Attempting to correct this inconsistency..."; + auto item = SyncFileItem::fromSyncJournalFileRecord(record); + item->_type = ItemTypeVirtualFileDownload; + isNotVirtualFileFailure = !params().journal->setFileRecord(SyncJournalFileRecord::fromSyncFileItem(*item)); + } else { + isNotVirtualFileFailure = true; + } + } + if (requestedFileSize != record.size()) { + // we are out of sync + qCWarning(lcVfs) << u"The db size and the placeholder meta data are out of sync, request resync"; + Q_ASSERT(false); // this should not happen + Q_EMIT hydrationRequestFailed(context.requestId); + Q_EMIT needSync(); + return; + } + + if (isNotVirtualFileFailure) { + qCWarning(lcVfs) << u"Couldn't hydrate, the file is not virtual"; + Q_ASSERT(false); // this should not happen + Q_EMIT hydrationRequestFailed(context.requestId); + Q_EMIT needSync(); + return; + } + + // All good, let's hydrate now + scheduleHydrationJob(context, std::move(record)); +} + +void Vfs::scheduleHydrationJob(const OCC::CallBackContext &context, SyncJournalFileRecord &&record) +{ + // after a local move, the remotePath and the targetPath might not match + if (findHydrationJob(context.requestId)) { + qCWarning(lcVfs) << u"The OS submitted again a hydration request which is already on-going" << context; + Q_EMIT hydrationRequestFailed(context.requestId); + return; + } + Q_ASSERT(!std::any_of(std::cbegin(d->hydrationJobs), std::cend(d->hydrationJobs), + [=](HydrationJob *job) { return job->requestId() == context.requestId || job->localFilePathAbs() == context.path; })); + auto job = new HydrationJob(context); + job->setAccount(params().account); + job->setRemoteSyncRootPath(params().baseUrl()); + job->setLocalRoot(params().filesystemPath); + job->setJournal(params().journal); + job->setRemoteFilePathRel(record.path()); + job->setRecord(std::move(record)); + connect(job, &HydrationJob::finished, this, &Vfs::onHydrationJobFinished); + d->hydrationJobs.insert(context.requestId, job); + job->start(); + Q_EMIT hydrationRequestReady(context.requestId); +} + +void Vfs::onHydrationJobFinished(HydrationJob *job) +{ + Q_ASSERT(findHydrationJob(job->requestId())); + qCInfo(lcVfs) << u"Hydration job finished" << job->requestId() << job->localFilePathAbs() << job->status(); + Q_EMIT hydrationRequestFinished(job->requestId()); + if (!job->errorString().isEmpty()) { + qCWarning(lcVfs) << job->errorString(); + } +} + +HydrationJob::Status Vfs::finalizeHydrationJob(int64_t requestId) +{ + // Find matching hydration job for request id + if (const auto hydrationJob = findHydrationJob(requestId)) { + qCDebug(lcVfs) << u"Finalize hydration job" << hydrationJob->context(); + hydrationJob->finalize(this); + d->hydrationJobs.take(hydrationJob->requestId()); + hydrationJob->deleteLater(); + return hydrationJob->status(); + } + qCCritical(lcVfs) << u"Failed to finalize hydration job" << requestId << u". Job not found."; + return HydrationJob::Status::Error; +} + +// =============================================================== + Q_LOGGING_CATEGORY(lcPlugin, "sync.plugins", QtInfoMsg) OCC::VfsPluginManager *OCC::VfsPluginManager::_instance = nullptr; diff --git a/src/libsync/common/vfs.h b/src/libsync/common/vfs.h index e1b510224e..21ffd51da3 100644 --- a/src/libsync/common/vfs.h +++ b/src/libsync/common/vfs.h @@ -21,6 +21,7 @@ #include "result.h" #include "syncfilestatus.h" #include "utility.h" +#include "hydrationjob.h" #include #include @@ -36,6 +37,7 @@ class Account; class SyncJournalDb; class SyncFileItem; class SyncEngine; +class VfsApiPrivate; /** Collection of parameters for initializing a Vfs instance. */ struct OPENCLOUD_SYNC_EXPORT VfsSetupParams @@ -202,7 +204,25 @@ class OPENCLOUD_SYNC_EXPORT Vfs : public QObject */ void wipeDehydratedVirtualFiles(); + /** + * Any type of change to the extended file attributes was detected by the + * folderwatcher. The vfs might want react to that. + */ + virtual bool handleXAttrChange(const QSet &) = 0; + + // === Hydration + HydrationJob *findHydrationJob(int64_t requestId) const; + void cancelHydration(const OCC::CallBackContext &context); + + void requestHydration(const OCC::CallBackContext &context, qint64 requestedFileSize); + + void scheduleHydrationJob(const OCC::CallBackContext &context, SyncJournalFileRecord &&record); + public Q_SLOTS: + void onHydrationJobFinished(HydrationJob *job); + HydrationJob::Status finalizeHydrationJob(int64_t requestId); + + /** Update in-sync state based on SyncFileStatusTracker signal. * * For some vfs plugins the icons aren't based on SocketAPI but rather on data shared @@ -221,6 +241,11 @@ public Q_SLOTS: /// The vfs plugin detected that the meta data are out of sync and requests a sync with the server void needSync(); +Q_SIGNALS: + void hydrationRequestReady(int64_t requestId); + void hydrationRequestFailed(int64_t requestId); + void hydrationRequestFinished(int64_t requestId); + protected: /** Update placeholder metadata during discovery. * @@ -244,6 +269,7 @@ public Q_SLOTS: private: // the parameters passed to start() std::unique_ptr _setupParams; + QScopedPointer d; friend class OwncloudPropagator; }; diff --git a/src/plugins/vfs/cfapi/CMakeLists.txt b/src/plugins/vfs/cfapi/CMakeLists.txt index ba00122505..cac580a1e4 100644 --- a/src/plugins/vfs/cfapi/CMakeLists.txt +++ b/src/plugins/vfs/cfapi/CMakeLists.txt @@ -2,7 +2,6 @@ if (WIN32) add_vfs_plugin(NAME cfapi SRC cfapiwrapper.cpp - hydrationjob.cpp vfs_cfapi.cpp LIBS cldapi diff --git a/src/plugins/vfs/cfapi/cfapiwrapper.cpp b/src/plugins/vfs/cfapi/cfapiwrapper.cpp index bd0c5e99c7..e214e499d5 100644 --- a/src/plugins/vfs/cfapi/cfapiwrapper.cpp +++ b/src/plugins/vfs/cfapi/cfapiwrapper.cpp @@ -311,6 +311,7 @@ void CALLBACK cfApiFetchDataCallback(const CF_CALLBACK_INFO *callbackInfo, const if (hydrationJobResult != OCC::HydrationJob::Status::Success) { sendTransferError(); } + } OCC::Result updatePlaceholderState( @@ -826,16 +827,4 @@ bool OCC::CfApiWrapper::isPlaceHolderInSync(const QString &filePath) return true; } -QDebug operator<<(QDebug debug, const OCC::CfApiWrapper::CallBackContext &context) -{ - QDebugStateSaver saver(debug); - debug.setAutoInsertSpaces(false); - debug << u"cfapiCallback(" << context.path << u", " << context.requestHexId(); - for (const auto &[k, v] : context.extraArgs.asKeyValueRange()) { - debug << u", "; - debug.noquote() << k << u"="; - debug.quote() << v; - }; - debug << u")"; - return debug.maybeSpace(); -} + diff --git a/src/plugins/vfs/cfapi/cfapiwrapper.h b/src/plugins/vfs/cfapi/cfapiwrapper.h index 293b3ea3da..a1d8775cd0 100644 --- a/src/plugins/vfs/cfapi/cfapiwrapper.h +++ b/src/plugins/vfs/cfapi/cfapiwrapper.h @@ -22,16 +22,6 @@ namespace OCC { class VfsCfApi; namespace CfApiWrapper { - struct CallBackContext - { - OCC::VfsCfApi *vfs; - QString path; - int64_t requestId; - QByteArray fileId; - QMap extraArgs; - - inline QString requestHexId() const { return QString::number(requestId, 16); } - }; template class PlaceHolderInfo diff --git a/src/plugins/vfs/cfapi/vfs_cfapi.cpp b/src/plugins/vfs/cfapi/vfs_cfapi.cpp index 90a43cbad1..996ee6df0d 100644 --- a/src/plugins/vfs/cfapi/vfs_cfapi.cpp +++ b/src/plugins/vfs/cfapi/vfs_cfapi.cpp @@ -103,7 +103,6 @@ namespace OCC { class VfsCfApiPrivate { public: - QMap hydrationJobs; CF_CONNECTION_KEY connectionKey = {}; }; @@ -187,6 +186,11 @@ Result VfsCfApi::updateMetadata(const } } +bool VfsCfApi::handleXAttrChange(const QSet &) +{ + return false; +} + Result VfsCfApi::createPlaceholder(const SyncFileItem &item) { Q_ASSERT(params().filesystemPath.endsWith('/'_L1)); @@ -290,77 +294,6 @@ Vfs::AvailabilityResult VfsCfApi::availability(const QString &folderPath) } } -HydrationJob *VfsCfApi::findHydrationJob(int64_t requestId) const -{ - // Find matching hydration job for request id - return d->hydrationJobs.value(requestId); -} - -void VfsCfApi::cancelHydration(const OCC::CfApiWrapper::CallBackContext &context) -{ - // Find matching hydration job for request id - const auto hydrationJob = findHydrationJob(context.requestId); - // If found, cancel it - if (hydrationJob) { - qCInfo(lcCfApi) << u"Cancel hydration" << hydrationJob->context(); - hydrationJob->cancel(); - } -} - -void VfsCfApi::requestHydration(const OCC::CfApiWrapper::CallBackContext &context, qint64 requestedFileSize) -{ - qCInfo(lcCfApi) << u"Received request to hydrate" << context; - const auto root = QDir::toNativeSeparators(params().filesystemPath); - Q_ASSERT(context.path.startsWith(root)); - - - // Set in the database that we should download the file - SyncJournalFileRecord record; - params().journal->getFileRecordsByFileId(context.fileId, [&record](const auto &r) { - Q_ASSERT(!record.isValid()); - record = r; - }); - if (!record.isValid()) { - qCInfo(lcCfApi) << u"Couldn't hydrate, did not find file in db"; - Q_ASSERT(false); // how did we end up here if it's not a cloud file - Q_EMIT hydrationRequestFailed(context.requestId); - Q_EMIT needSync(); - return; - } - - bool isNotVirtualFileFailure = false; - if (!record.isVirtualFile()) { - if (isDehydratedPlaceholder(context.path)) { - qCWarning(lcCfApi) << u"Hydration requested for a placeholder file that is incorrectly not marked as a virtual file in the local database. " - u"Attempting to correct this inconsistency..."; - auto item = SyncFileItem::fromSyncJournalFileRecord(record); - item->_type = ItemTypeVirtualFileDownload; - isNotVirtualFileFailure = !params().journal->setFileRecord(SyncJournalFileRecord::fromSyncFileItem(*item)); - } else { - isNotVirtualFileFailure = true; - } - } - if (requestedFileSize != record.size()) { - // we are out of sync - qCWarning(lcCfApi) << u"The db size and the placeholder meta data are out of sync, request resync"; - Q_ASSERT(false); // this should not happen - Q_EMIT hydrationRequestFailed(context.requestId); - Q_EMIT needSync(); - return; - } - - if (isNotVirtualFileFailure) { - qCWarning(lcCfApi) << u"Couldn't hydrate, the file is not virtual"; - Q_ASSERT(false); // this should not happen - Q_EMIT hydrationRequestFailed(context.requestId); - Q_EMIT needSync(); - return; - } - - // All good, let's hydrate now - scheduleHydrationJob(context, std::move(record)); -} - void VfsCfApi::fileStatusChanged(const QString &systemFileName, SyncFileStatus fileStatus) { if (!QFileInfo::exists(systemFileName)) { @@ -380,52 +313,5 @@ void VfsCfApi::fileStatusChanged(const QString &systemFileName, SyncFileStatus f } } -void VfsCfApi::scheduleHydrationJob(const OCC::CfApiWrapper::CallBackContext &context, SyncJournalFileRecord &&record) -{ - // after a local move, the remotePath and the targetPath might not match - if (findHydrationJob(context.requestId)) { - qCWarning(lcCfApi) << u"The OS submitted again a hydration request which is already on-going" << context; - Q_EMIT hydrationRequestFailed(context.requestId); - return; - } - Q_ASSERT(!std::any_of(std::cbegin(d->hydrationJobs), std::cend(d->hydrationJobs), - [=](HydrationJob *job) { return job->requestId() == context.requestId || job->localFilePathAbs() == context.path; })); - auto job = new HydrationJob(context); - job->setAccount(params().account); - job->setRemoteSyncRootPath(params().baseUrl()); - job->setLocalRoot(params().filesystemPath); - job->setJournal(params().journal); - job->setRemoteFilePathRel(record.path()); - job->setRecord(std::move(record)); - connect(job, &HydrationJob::finished, this, &VfsCfApi::onHydrationJobFinished); - d->hydrationJobs.insert(context.requestId, job); - job->start(); - Q_EMIT hydrationRequestReady(context.requestId); -} - -void VfsCfApi::onHydrationJobFinished(HydrationJob *job) -{ - Q_ASSERT(findHydrationJob(job->requestId())); - qCInfo(lcCfApi) << u"Hydration job finished" << job->requestId() << job->localFilePathAbs() << job->status(); - Q_EMIT hydrationRequestFinished(job->requestId()); - if (!job->errorString().isEmpty()) { - qCWarning(lcCfApi) << job->errorString(); - } -} - -HydrationJob::Status VfsCfApi::finalizeHydrationJob(int64_t requestId) -{ - // Find matching hydration job for request id - if (const auto hydrationJob = findHydrationJob(requestId)) { - qCDebug(lcCfApi) << u"Finalize hydration job" << hydrationJob->context(); - hydrationJob->finalize(this); - d->hydrationJobs.take(hydrationJob->requestId()); - hydrationJob->deleteLater(); - return hydrationJob->status(); - } - qCCritical(lcCfApi) << u"Failed to finalize hydration job" << requestId << u". Job not found."; - return HydrationJob::Status::Error; -} - } // namespace OCC diff --git a/src/plugins/vfs/cfapi/vfs_cfapi.h b/src/plugins/vfs/cfapi/vfs_cfapi.h index 263656f23b..8309813ebd 100644 --- a/src/plugins/vfs/cfapi/vfs_cfapi.h +++ b/src/plugins/vfs/cfapi/vfs_cfapi.h @@ -40,31 +40,18 @@ class VfsCfApi : public Vfs bool setPinState(const QString &folderPath, PinState state) override; Optional pinState(const QString &folderPath) override; AvailabilityResult availability(const QString &folderPath) override; - - void cancelHydration(const OCC::CfApiWrapper::CallBackContext &context); - - HydrationJob::Status finalizeHydrationJob(int64_t requestId); + bool handleXAttrChange(const QSet &) override; LocalInfo statTypeVirtualFile(const std::filesystem::directory_entry &path, ItemType type) override; public Q_SLOTS: - void requestHydration(const CfApiWrapper::CallBackContext &context, qint64 requestedFileSize); void fileStatusChanged(const QString &systemFileName, OCC::SyncFileStatus fileStatus) override; -Q_SIGNALS: - void hydrationRequestReady(int64_t requestId); - void hydrationRequestFailed(int64_t requestId); - void hydrationRequestFinished(int64_t requestId); - protected: Result updateMetadata(const SyncFileItem &, const QString &, const QString &) override; void startImpl(const VfsSetupParams ¶ms) override; private: - void scheduleHydrationJob(const OCC::CfApiWrapper::CallBackContext &context, SyncJournalFileRecord &&record); - void onHydrationJobFinished(HydrationJob *job); - HydrationJob *findHydrationJob(int64_t requestId) const; - QScopedPointer d; }; diff --git a/src/plugins/vfs/off/vfs_off.cpp b/src/plugins/vfs/off/vfs_off.cpp index f9d448f5f4..64217bc042 100644 --- a/src/plugins/vfs/off/vfs_off.cpp +++ b/src/plugins/vfs/off/vfs_off.cpp @@ -60,6 +60,11 @@ bool VfsOff::setPinState(const QString &, PinState) return true; } +bool VfsOff::handleXAttrChange(const QSet &) +{ + return false; +} + Optional VfsOff::pinState(const QString &) { return PinState::AlwaysLocal; diff --git a/src/plugins/vfs/off/vfs_off.h b/src/plugins/vfs/off/vfs_off.h index cae2b74327..6aa625d5bf 100644 --- a/src/plugins/vfs/off/vfs_off.h +++ b/src/plugins/vfs/off/vfs_off.h @@ -44,6 +44,7 @@ class VfsOff : public Vfs bool setPinState(const QString &, PinState) override; Optional pinState(const QString &) override; AvailabilityResult availability(const QString &) override; + bool handleXAttrChange(const QSet &) override; LocalInfo statTypeVirtualFile(const std::filesystem::directory_entry &path, ItemType type) override; diff --git a/src/plugins/vfs/xattr/vfs_xattr.cpp b/src/plugins/vfs/xattr/vfs_xattr.cpp index 754a0f4279..0796bfe97a 100644 --- a/src/plugins/vfs/xattr/vfs_xattr.cpp +++ b/src/plugins/vfs/xattr/vfs_xattr.cpp @@ -13,10 +13,34 @@ #include #include +#include #include Q_LOGGING_CATEGORY(lcVfsXAttr, "sync.vfs.xattr", QtInfoMsg) +QDebug operator<<(QDebug debug, const OCC::CallBackContext &context) +{ + QDebugStateSaver saver(debug); + debug.setAutoInsertSpaces(false); + debug << u"cfapiCallback(" << context.path << u", " << context.requestHexId(); + for (const auto &[k, v] : context.extraArgs.asKeyValueRange()) { + debug << u", "; + debug.noquote() << k << u"="; + debug.quote() << v; + }; + debug << u")"; + return debug.maybeSpace(); +} + +namespace { +std::atomic id{1}; + +int requestId() { + return id++; +} + +} + namespace xattr { // using namespace XAttrWrapper; } @@ -156,6 +180,213 @@ OCC::Result VfsXAttr::convertToPlaceho return {ConvertToPlaceholderResult::Ok}; } +bool VfsXAttr::handleAction(const QString& path, const XAttrWrapper::PlaceHolderAttribs& attribs) +{ + bool re{false}; + + const auto sendTransferError = [=] { + qCWarning(lcVfsXAttr) << u"Transfer ERROR detected"; + }; + + const auto sendTransferInfo = [=](qint64 size) { + qCInfo(lcVfsXAttr) << u"Received" << size << u"bytes"; + }; + + if (attribs.action() == QByteArrayLiteral("hydrate")) { + qCInfo(lcVfsXAttr) << u"Received request to hydrate"; + const auto root = QDir::toNativeSeparators(params().filesystemPath); + Q_ASSERT(path.startsWith(root)); + + SyncJournalFileRecord record; + params().journal->getFileRecordsByFileId(attribs.fileId(), [&record](const auto &r) { + Q_ASSERT(!record.isValid()); + record = r; + }); + if (!record.isValid()) { + qCInfo(lcVfsXAttr) << u"Couldn't hydrate, did not find file in db"; + Q_ASSERT(false); // how did we end up here if it's not a cloud file + Q_EMIT hydrationRequestFailed(-1); + Q_EMIT needSync(); + return false; + } + + bool isNotVirtualFileFailure = false; + if (!record.isVirtualFile()) { + if (isDehydratedPlaceholder(path)) { + qCWarning(lcVfsXAttr) << u"Hydration requested for a placeholder file that is incorrectly not marked as a virtual file in the local database. " + u"Attempting to correct this inconsistency..."; + auto item = SyncFileItem::fromSyncJournalFileRecord(record); + item->_type = ItemTypeVirtualFileDownload; + isNotVirtualFileFailure = !params().journal->setFileRecord(SyncJournalFileRecord::fromSyncFileItem(*item)); + } else { + isNotVirtualFileFailure = true; + } + } + if (isNotVirtualFileFailure) { + qCWarning(lcVfsXAttr) << u"Couldn't hydrate, the file is not virtual"; + Q_ASSERT(false); // this should not happen + Q_EMIT hydrationRequestFailed(-1); + Q_EMIT needSync(); + return false; + } + + OCC::CallBackContext context{.vfs = this, + .path = path, + .requestId = requestId(), + .fileId = attribs.fileId(), + .extraArgs = {}}; + // .extraArgs = std::move(extraArgs)}; + // All good, let's hydrate now + + const auto invokeResult = QMetaObject::invokeMethod(context.vfs, [=] + { + context.vfs->requestHydration(context, attribs.size()); + }, Qt::QueuedConnection); + if (!invokeResult) { + // qCCritical(lcVfsXAttr) << u"Failed to trigger hydration for" << context; + sendTransferError(); + return false; + } + + qCDebug(lcVfsXAttr) << u"Successfully triggered hydration for" << context; + + // Block and wait for vfs to signal back the hydration is ready + bool hydrationRequestResult = false; + QEventLoop loop; + QObject::connect(context.vfs, &OCC::Vfs::hydrationRequestReady, &loop, [&](int64_t id) { + if (context.requestId == id) { + hydrationRequestResult = true; + qCDebug(lcVfsXAttr) << u"Hydration request ready for" << context; + loop.quit(); + } + }); + QObject::connect(context.vfs, &OCC::Vfs::hydrationRequestFailed, &loop, [&](int64_t id) { + if (context.requestId == id) { + hydrationRequestResult = false; + qCWarning(lcVfsXAttr) << u"Hydration request failed for" << context; + loop.quit(); + } + }); + + qCDebug(lcVfsXAttr) << u"Starting event loop 1"; + loop.exec(); + QObject::disconnect(context.vfs, nullptr, &loop, nullptr); // Ensure we properly cancel hydration on server errors + + qCInfo(lcVfsXAttr) << u"VFS replied for hydration of" << context << u"status was:" << hydrationRequestResult; + if (!hydrationRequestResult) { + qCCritical(lcVfsXAttr) << u"Failed to trigger hydration for" << context; + sendTransferError(); + return false; + } + + QLocalSocket socket; + socket.connectToServer(context.requestHexId()); + const auto connectResult = socket.waitForConnected(); + if (!connectResult) { + qCWarning(lcVfsXAttr) << u"Couldn't connect the socket" << context << socket.error() << socket.errorString(); + sendTransferError(); + return false; + } + + QLocalSocket signalSocket; + const QString signalSocketName = context.requestHexId() + QStringLiteral(":cancellation"); + signalSocket.connectToServer(signalSocketName); + const auto cancellationSocketConnectResult = signalSocket.waitForConnected(); + if (!cancellationSocketConnectResult) { + qCWarning(lcVfsXAttr) << u"Couldn't connect the socket" << signalSocketName << signalSocket.error() << signalSocket.errorString(); + sendTransferError(); + return false; + } + + auto hydrationRequestCancelled = false; + QObject::connect(&signalSocket, &QLocalSocket::readyRead, &loop, [&] { + hydrationRequestCancelled = true; + qCCritical(lcVfsXAttr) << u"Hydration canceled for " << context; + }); + + // Create a save file to store received data into + QSaveFile targetFile(context.path); + if (!targetFile.open(QFile::WriteOnly | QFile::Truncate)) { + sendTransferError(); + } + + QObject::connect(&socket, &QLocalSocket::readyRead, &loop, [&] { + if (hydrationRequestCancelled) { + qCDebug(lcVfsXAttr) << u"Don't transfer data because request" << context << u"was cancelled"; + return; + } + + const auto receivedData = socket.readAll(); + if (receivedData.isEmpty()) { + qCWarning(lcVfsXAttr) << u"Unexpected empty data received" << context; + sendTransferError(); + loop.quit(); + return; + } + + // Put received data to target file + targetFile.write(receivedData); + sendTransferInfo(receivedData.size()); + }); + + QObject::connect(context.vfs, &OCC::Vfs::hydrationRequestFinished, this, [&](int64_t id) { + qDebug(lcVfsXAttr) << u"Hydration finished for request" << id << "IN MY THREAD"; + if (context.requestId == id) { + const auto receivedData = socket.readAll(); + targetFile.write(receivedData); + sendTransferInfo(receivedData.size()); + } + targetFile.commit(); + loop.quit(); + }); + + QObject::connect(context.vfs, &OCC::Vfs::hydrationRequestFinished, &loop, [&](int64_t id) { + qDebug(lcVfsXAttr) << u"Hydration finished for request" << id << "IN LOOP"; + if (context.requestId == id) { + const auto receivedData = socket.readAll(); + targetFile.write(receivedData); + sendTransferInfo(receivedData.size()); + } + targetFile.commit(); + loop.quit(); + }); + + qCDebug(lcVfsXAttr) << u"Starting event loop 2"; + loop.exec(); + + OCC::HydrationJob::Status hydrationJobResult = OCC::HydrationJob::Status::Error; + const auto invokeFinalizeResult = QMetaObject::invokeMethod( + context.vfs, [&hydrationJobResult, &context] { + hydrationJobResult = context.vfs->finalizeHydrationJob(context.requestId); + } /* , Qt::BlockingQueuedConnection */); + if (!invokeFinalizeResult) { + qCritical(lcVfsXAttr) << u"Failed to finalize hydration job for" << context; + } + + if (hydrationJobResult != OCC::HydrationJob::Status::Success) { + sendTransferError(); + } + } + + return re; +} + +// if an extended attribute was changed. +bool VfsXAttr::handleXAttrChange(const QSet &paths) +{ + bool re{false}; + + for(const auto &p : paths) { + // check if an "action xattr" was set + const XAttrWrapper::PlaceHolderAttribs attribs = XAttrWrapper::placeHolderAttributes(p); + if (!attribs.action().isEmpty()) { + handleAction(p, attribs); + } + } + + return re; +} + bool VfsXAttr::needsMetadataUpdate(const SyncFileItem &) { qCDebug(lcVfsXAttr()) << "returns false by default DOUBLECHECK"; @@ -213,12 +444,13 @@ Optional VfsXAttr::pinState(const QString &folderPath) XAttrWrapper::PlaceHolderAttribs attribs = XAttrWrapper::placeHolderAttributes(folderPath); const QString pin = QString::fromUtf8(attribs.pinState()); - PinState pState; + PinState pState{PinState::Unspecified}; + if (pin == Utility::enumToDisplayName(PinState::AlwaysLocal)) { pState = PinState::AlwaysLocal; } else if (pin == Utility::enumToDisplayName(PinState::Excluded)) { pState = PinState::Excluded; - } else if (pin == Utility::enumToDisplayName(PinState::Inherited)) { + } else if (pin.isEmpty() || pin == Utility::enumToDisplayName(PinState::Inherited)) { pState = PinState::Inherited; } else if (pin == Utility::enumToDisplayName(PinState::OnlineOnly)) { pState = PinState::OnlineOnly; @@ -234,6 +466,7 @@ Vfs::AvailabilityResult VfsXAttr::availability(const QString &folderPath) qCDebug(lcVfsXAttr()) << folderPath; const auto basePinState = pinState(folderPath); + if (basePinState) { switch (*basePinState) { case OCC::PinState::AlwaysLocal: @@ -257,11 +490,12 @@ Vfs::AvailabilityResult VfsXAttr::availability(const QString &folderPath) void VfsXAttr::fileStatusChanged(const QString& systemFileName, SyncFileStatus fileStatus) { - qCDebug(lcVfsXAttr()) << systemFileName << fileStatus; - if (fileStatus.tag() == SyncFileStatus::StatusExcluded) { - setPinState(systemFileName, PinState::Excluded); - } + setPinState(systemFileName, PinState::Excluded); + return; + } + + qCDebug(lcVfsXAttr()) << systemFileName << fileStatus; } } // namespace OCC diff --git a/src/plugins/vfs/xattr/vfs_xattr.h b/src/plugins/vfs/xattr/vfs_xattr.h index 1561720de6..e4a1e8085a 100644 --- a/src/plugins/vfs/xattr/vfs_xattr.h +++ b/src/plugins/vfs/xattr/vfs_xattr.h @@ -11,6 +11,8 @@ #include "common/vfs.h" #include "common/plugin.h" +#include "xattrwrapper.h" + namespace OCC { class VfsXAttr : public Vfs @@ -36,6 +38,8 @@ class VfsXAttr : public Vfs OCC::Result convertToPlaceholder( const QString &path, time_t modtime, qint64 size, const QByteArray &fileId, const QString &replacesPath); + bool handleAction(const QString& path, const XAttrWrapper::PlaceHolderAttribs &attribs); + bool handleXAttrChange(const QSet &) override; bool needsMetadataUpdate(const SyncFileItem &item) override; bool isDehydratedPlaceholder(const QString &filePath) override; LocalInfo statTypeVirtualFile(const std::filesystem::directory_entry &path, ItemType type) override; diff --git a/src/plugins/vfs/xattr/xattrwrapper.cpp b/src/plugins/vfs/xattr/xattrwrapper.cpp index 3530343e5d..d44ada01a3 100644 --- a/src/plugins/vfs/xattr/xattrwrapper.cpp +++ b/src/plugins/vfs/xattr/xattrwrapper.cpp @@ -20,12 +20,17 @@ constexpr auto hydrateExecAttributeName = "user.openvfs.hydrate_exec"; OCC::Optional xattrGet(const QByteArray &path, const QByteArray &name) { - constexpr auto bufferSize = 256; - QByteArray result; - result.resize(bufferSize); - const auto count = getxattr(path.constData(), name.constData(), result.data(), bufferSize); - if (count >= 0) { - result.resize(static_cast(count) - 1); + QByteArray result(512, Qt::Initialization::Uninitialized); + auto count = getxattr(path.constData(), name.constData(), result.data(), result.size()); + if (count > 0) { + // xattr is special. It does not store C-Strings, but blobs. + // So it needs to be checked, if a trailing \0 was added when writing + // (as this software does) or not as the standard setfattr-tool + // the following will handle both cases correctly. + if (result[count-1] == '\0') { + count--; + } + result.truncate(count); return result; } else { return {}; @@ -34,7 +39,7 @@ OCC::Optional xattrGet(const QByteArray &path, const QByteArray &nam bool xattrSet(const QByteArray &path, const QByteArray &name, const QByteArray &value) { - const auto returnCode = setxattr(path.constData(), name.constData(), value.constData(), value.size() + 1, 0); + const auto returnCode = setxattr(path.constData(), name.constData(), value.constData(), value.size()+1, 0); return returnCode == 0; } @@ -48,13 +53,13 @@ PlaceHolderAttribs placeHolderAttributes(const QString& path) // lambda to handle the Optional return val of xattrGet auto xattr = [](const QByteArray& p, const QByteArray& name) { - const auto value = xattrGet(p, name); - if (value) { - return *value; - } else { - return QByteArray(); - } - }; + const auto value = xattrGet(p, name); + if (value) { + return *value; + } else { + return QByteArray(); + } + }; const auto p = path.toUtf8(); @@ -64,6 +69,8 @@ PlaceHolderAttribs placeHolderAttributes(const QString& path) const QByteArray& tt = xattr(p, "user.openvfs.modtime"); attribs._modtime = tt.toLongLong(); + + attribs._action = xattr(p, "user.openvfs.action"); attribs._size = xattr(p, "user.openvfs.fsize").toLongLong(); attribs._pinState = xattr(p, "user.openvfs.pinstate"); diff --git a/src/plugins/vfs/xattr/xattrwrapper.h b/src/plugins/vfs/xattr/xattrwrapper.h index 4063266fcd..3c93bdb6c6 100644 --- a/src/plugins/vfs/xattr/xattrwrapper.h +++ b/src/plugins/vfs/xattr/xattrwrapper.h @@ -19,6 +19,7 @@ struct PlaceHolderAttribs { time_t modTime() const {return _modtime; } QString eTag() const { return _etag; } QByteArray pinState() const { return _pinState; } + QByteArray action() const { return _action; } bool itsMe() const { return !_executor.isEmpty() && _executor == QByteArrayLiteral(APPLICATION_EXECUTABLE);} @@ -28,6 +29,7 @@ struct PlaceHolderAttribs { QString _etag; QByteArray _executor; QByteArray _pinState; + QByteArray _action; }; From 87985655862547b0fc051ad4a00dc7e094a2e62c Mon Sep 17 00:00:00 2001 From: Klaas Freitag Date: Fri, 12 Sep 2025 11:09:29 +0200 Subject: [PATCH 07/13] Add HYDRATE_FILE job stub to socketapi --- src/gui/socketapi/socketapi.cpp | 19 ++++++++++++++++++- src/gui/socketapi/socketapi.h | 3 +++ src/plugins/vfs/xattr/vfs_xattr.cpp | 3 +-- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/gui/socketapi/socketapi.cpp b/src/gui/socketapi/socketapi.cpp index 4a66e70054..b33c828514 100644 --- a/src/gui/socketapi/socketapi.cpp +++ b/src/gui/socketapi/socketapi.cpp @@ -158,6 +158,13 @@ SocketApi::SocketApi(QObject *parent) unregisterAccount(accountState->account()); } }); + + const QJsonObject args { { QStringLiteral("size"), 5 } }; + const QJsonObject obj { { QStringLiteral("id"), QString::number(17) }, { QStringLiteral("arguments"), args } }; + const auto json = QJsonDocument(obj).toJson(QJsonDocument::Indented); + + qCDebug(lcSocketApi) << u"JSON:" << json; + } SocketApi::~SocketApi() @@ -283,7 +290,7 @@ void SocketApi::slotReadSocket() } } else if (command.startsWith(QLatin1String("V2/"))) { QJsonParseError error; - const auto json = QJsonDocument::fromJson(argument.toUtf8(), &error).object(); + const auto json = QJsonDocument::fromJson(argument.trimmed().toUtf8(), &error).object(); if (error.error != QJsonParseError::NoError) { qCWarning(lcSocketApi()) << u"Invalid json" << argument << error.errorString(); listener->sendError(error.errorString()); @@ -709,6 +716,16 @@ void SocketApi::command_V2_GET_CLIENT_ICON(const QSharedPointer job->success({ { QStringLiteral("png"), QString::fromUtf8(data) } }); } +void SocketApi::command_V2_HYDRATE_FILE(const QSharedPointer &job) const +{ + OC_ASSERT(job); + const auto &arguments = job->arguments(); + + hydrate_the_file ähnlich zu VfsXattr::handleAction. + + job->success({ {QStringLiteral("hydration"), QStringLiteral("STARTED") } }); +} + void SocketApi::emailPrivateLink(const QUrl &link) { Utility::openEmailComposer( diff --git a/src/gui/socketapi/socketapi.h b/src/gui/socketapi/socketapi.h index 0113d70933..3084597905 100644 --- a/src/gui/socketapi/socketapi.h +++ b/src/gui/socketapi/socketapi.h @@ -120,6 +120,9 @@ private Q_SLOTS: Q_INVOKABLE void command_OPEN_APP_LINK(const QString &localFile, SocketListener *listener); // External sync Q_INVOKABLE void command_V2_LIST_ACCOUNTS(const QSharedPointer &job) const; + // VFS + Q_INVOKABLE void command_V2_HYDRATE_FILE(const QSharedPointer &job) const; + // Sends the id and the client icon as PNG image (base64 encoded) in Json key "png" // e.g. { "id" : "1", "arguments" : { "png" : "hswehs343dj8..." } } or an error message in key "error" diff --git a/src/plugins/vfs/xattr/vfs_xattr.cpp b/src/plugins/vfs/xattr/vfs_xattr.cpp index 0796bfe97a..882057799f 100644 --- a/src/plugins/vfs/xattr/vfs_xattr.cpp +++ b/src/plugins/vfs/xattr/vfs_xattr.cpp @@ -238,8 +238,7 @@ bool VfsXAttr::handleAction(const QString& path, const XAttrWrapper::PlaceHolder // .extraArgs = std::move(extraArgs)}; // All good, let's hydrate now - const auto invokeResult = QMetaObject::invokeMethod(context.vfs, [=] - { + const auto invokeResult = QMetaObject::invokeMethod(context.vfs, [=] { context.vfs->requestHydration(context, attribs.size()); }, Qt::QueuedConnection); if (!invokeResult) { From 652dc31ea77a322155a23ec3017365793e5502dc Mon Sep 17 00:00:00 2001 From: Hannah von Reth Date: Fri, 12 Sep 2025 17:01:06 +0200 Subject: [PATCH 08/13] Fix include --- src/libsync/common/hydrationjob.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libsync/common/hydrationjob.cpp b/src/libsync/common/hydrationjob.cpp index 040dc64a93..b5ad0a522a 100644 --- a/src/libsync/common/hydrationjob.cpp +++ b/src/libsync/common/hydrationjob.cpp @@ -6,6 +6,7 @@ #include "hydrationjob.h" #include "libsync/common/syncjournaldb.h" +#include "libsync/common/vfs.h" #include "libsync/filesystem.h" #include "libsync/networkjobs/getfilejob.h" From 1d2f072c2a0be8b3e2d2f0c35bb636b16067b5e9 Mon Sep 17 00:00:00 2001 From: Klaas Freitag Date: Fri, 12 Sep 2025 18:23:42 +0200 Subject: [PATCH 09/13] Remove the extra queue for xattr changes again. Xattr changes now are handled just like any other detected change to files. --- src/gui/folder.cpp | 6 -- src/gui/folderwatcher.cpp | 30 ------ src/gui/folderwatcher.h | 2 - src/gui/folderwatcher_linux.cpp | 9 +- src/gui/socketapi/socketapi.cpp | 7 +- src/libsync/common/vfs.h | 6 -- src/plugins/vfs/off/vfs_off.cpp | 5 - src/plugins/vfs/off/vfs_off.h | 1 - src/plugins/vfs/xattr/vfs_xattr.cpp | 140 +--------------------------- src/plugins/vfs/xattr/vfs_xattr.h | 1 - 10 files changed, 9 insertions(+), 198 deletions(-) diff --git a/src/gui/folder.cpp b/src/gui/folder.cpp index 2a47bd447d..64fb1c3b1b 100644 --- a/src/gui/folder.cpp +++ b/src/gui/folder.cpp @@ -604,10 +604,6 @@ void Folder::slotWatchedPathsChanged(const QSet &paths, ChangeReason re Q_ASSERT(FileSystem::isChildPathOf(path, this->path())); const QString relativePath = path.mid(this->path().size()); - if (reason == ChangeReason::XAttr) { - // Changes to the extended file attributes. Maybe the VFS has something to do with it - _vfs->handleXAttrChange(paths); - } if (reason == ChangeReason::UnLock) { journalDb()->wipeErrorBlacklistEntry(relativePath, SyncJournalErrorBlacklistRecord::Category::LocalSoftError); @@ -1120,8 +1116,6 @@ void Folder::registerFolderWatcher() _folderWatcher.reset(new FolderWatcher(this)); connect(_folderWatcher.data(), &FolderWatcher::pathChanged, this, [this](const QSet &paths) { slotWatchedPathsChanged(paths, Folder::ChangeReason::Other); }); - connect(_folderWatcher.data(), &FolderWatcher::xattrChanged, this, - [this](const QSet &paths) { slotWatchedPathsChanged(paths, Folder::ChangeReason::XAttr); }); connect(_folderWatcher.data(), &FolderWatcher::changesDetected, this, [this] { // don't set to not yet started if a sync is already running diff --git a/src/gui/folderwatcher.cpp b/src/gui/folderwatcher.cpp index 70d1610153..53e8ba2778 100644 --- a/src/gui/folderwatcher.cpp +++ b/src/gui/folderwatcher.cpp @@ -51,11 +51,6 @@ FolderWatcher::FolderWatcher(Folder *folder) _timer.stop(); auto paths = std::move(_changeSet); Q_ASSERT(!paths.empty()); - if (_xattrChangeSet.size() > 0) { - auto xattrChangesPaths = std::move(_xattrChangeSet); - qCInfo(lcFolderWatcher) << u"Detected XAttr changes in paths:" << paths; - Q_EMIT xattrChanged(xattrChangesPaths); - } if (!paths.isEmpty()) { qCInfo(lcFolderWatcher) << u"Detected changes in paths:" << paths; Q_EMIT pathChanged(paths); @@ -182,31 +177,6 @@ void FolderWatcher::addChanges(QSet &&paths) } } -void FolderWatcher::addXAttrChanges(QSet && paths) -{ - auto it = paths.cbegin(); - while (it != paths.cend()) { - // we cause a file change from time to time to check whether the folder watcher works as expected - if (!_testNotificationPath.isEmpty() && Utility::fileNamesEqual(*it, _testNotificationPath)) { - _testNotificationPath.clear(); - } - if (pathIsIgnored(*it)) { - it = paths.erase(it); - } else { - ++it; - } - } - if (!paths.isEmpty()) { - _xattrChangeSet.unite(paths); - if (!_timer.isActive()) { - _timer.start(); - // promote that we will report changes once _timer times out - // not needed for the changes of xattr probably - // Q_EMIT changesDetected(); - } - } -} - QSet FolderWatcher::popChangeSet() { // stop the timer as we pop all queued changes diff --git a/src/gui/folderwatcher.h b/src/gui/folderwatcher.h index 7f14f266f3..9953960397 100644 --- a/src/gui/folderwatcher.h +++ b/src/gui/folderwatcher.h @@ -119,13 +119,11 @@ private Q_SLOTS: protected: // called from the implementations to indicate a change in path void addChanges(QSet &&paths); - void addXAttrChanges(QSet &&paths); private: QScopedPointer _d; QTimer _timer; QSet _changeSet; - QSet _xattrChangeSet; Folder *_folder; bool _isReliable = true; diff --git a/src/gui/folderwatcher_linux.cpp b/src/gui/folderwatcher_linux.cpp index ce14b5fd73..bd6bd60cdc 100644 --- a/src/gui/folderwatcher_linux.cpp +++ b/src/gui/folderwatcher_linux.cpp @@ -149,7 +149,7 @@ void FolderWatcherPrivate::slotReceivedNotification(int fd) } } - QSet paths, xattrPaths; + QSet paths; // iterate over events in buffer struct inotify_event *event = nullptr; for (size_t bytePosition = 0; // start at the beginning of the buffer @@ -189,17 +189,10 @@ void FolderWatcherPrivate::slotReceivedNotification(int fd) if (event->mask & (IN_MOVED_FROM | IN_DELETE)) { removeFoldersBelow(p); } - if (event->mask & (IN_ATTRIB)) { - xattrPaths.insert(p); - } } if (!paths.isEmpty()) { _parent->addChanges(std::move(paths)); } - if (!xattrPaths.isEmpty()) { - _parent->addXAttrChanges(std::move(xattrPaths)); - } - } void FolderWatcherPrivate::removeFoldersBelow(const QString &path) diff --git a/src/gui/socketapi/socketapi.cpp b/src/gui/socketapi/socketapi.cpp index b33c828514..f7425e68c5 100644 --- a/src/gui/socketapi/socketapi.cpp +++ b/src/gui/socketapi/socketapi.cpp @@ -721,8 +721,13 @@ void SocketApi::command_V2_HYDRATE_FILE(const QSharedPointer &jo OC_ASSERT(job); const auto &arguments = job->arguments(); - hydrate_the_file ähnlich zu VfsXattr::handleAction. + const auto &file = arguments[QStringLiteral("file")].toString(); + auto fileData = FileData::get(file); + + if (fileData.folder) { + // call the getfile job to hydrate + } job->success({ {QStringLiteral("hydration"), QStringLiteral("STARTED") } }); } diff --git a/src/libsync/common/vfs.h b/src/libsync/common/vfs.h index 21ffd51da3..2170ebd958 100644 --- a/src/libsync/common/vfs.h +++ b/src/libsync/common/vfs.h @@ -204,12 +204,6 @@ class OPENCLOUD_SYNC_EXPORT Vfs : public QObject */ void wipeDehydratedVirtualFiles(); - /** - * Any type of change to the extended file attributes was detected by the - * folderwatcher. The vfs might want react to that. - */ - virtual bool handleXAttrChange(const QSet &) = 0; - // === Hydration HydrationJob *findHydrationJob(int64_t requestId) const; void cancelHydration(const OCC::CallBackContext &context); diff --git a/src/plugins/vfs/off/vfs_off.cpp b/src/plugins/vfs/off/vfs_off.cpp index 64217bc042..f9d448f5f4 100644 --- a/src/plugins/vfs/off/vfs_off.cpp +++ b/src/plugins/vfs/off/vfs_off.cpp @@ -60,11 +60,6 @@ bool VfsOff::setPinState(const QString &, PinState) return true; } -bool VfsOff::handleXAttrChange(const QSet &) -{ - return false; -} - Optional VfsOff::pinState(const QString &) { return PinState::AlwaysLocal; diff --git a/src/plugins/vfs/off/vfs_off.h b/src/plugins/vfs/off/vfs_off.h index 6aa625d5bf..cae2b74327 100644 --- a/src/plugins/vfs/off/vfs_off.h +++ b/src/plugins/vfs/off/vfs_off.h @@ -44,7 +44,6 @@ class VfsOff : public Vfs bool setPinState(const QString &, PinState) override; Optional pinState(const QString &) override; AvailabilityResult availability(const QString &) override; - bool handleXAttrChange(const QSet &) override; LocalInfo statTypeVirtualFile(const std::filesystem::directory_entry &path, ItemType type) override; diff --git a/src/plugins/vfs/xattr/vfs_xattr.cpp b/src/plugins/vfs/xattr/vfs_xattr.cpp index 882057799f..1913c36f03 100644 --- a/src/plugins/vfs/xattr/vfs_xattr.cpp +++ b/src/plugins/vfs/xattr/vfs_xattr.cpp @@ -238,129 +238,9 @@ bool VfsXAttr::handleAction(const QString& path, const XAttrWrapper::PlaceHolder // .extraArgs = std::move(extraArgs)}; // All good, let's hydrate now - const auto invokeResult = QMetaObject::invokeMethod(context.vfs, [=] { - context.vfs->requestHydration(context, attribs.size()); - }, Qt::QueuedConnection); - if (!invokeResult) { - // qCCritical(lcVfsXAttr) << u"Failed to trigger hydration for" << context; - sendTransferError(); - return false; - } - - qCDebug(lcVfsXAttr) << u"Successfully triggered hydration for" << context; - - // Block and wait for vfs to signal back the hydration is ready - bool hydrationRequestResult = false; - QEventLoop loop; - QObject::connect(context.vfs, &OCC::Vfs::hydrationRequestReady, &loop, [&](int64_t id) { - if (context.requestId == id) { - hydrationRequestResult = true; - qCDebug(lcVfsXAttr) << u"Hydration request ready for" << context; - loop.quit(); - } - }); - QObject::connect(context.vfs, &OCC::Vfs::hydrationRequestFailed, &loop, [&](int64_t id) { - if (context.requestId == id) { - hydrationRequestResult = false; - qCWarning(lcVfsXAttr) << u"Hydration request failed for" << context; - loop.quit(); - } - }); - - qCDebug(lcVfsXAttr) << u"Starting event loop 1"; - loop.exec(); - QObject::disconnect(context.vfs, nullptr, &loop, nullptr); // Ensure we properly cancel hydration on server errors - - qCInfo(lcVfsXAttr) << u"VFS replied for hydration of" << context << u"status was:" << hydrationRequestResult; - if (!hydrationRequestResult) { - qCCritical(lcVfsXAttr) << u"Failed to trigger hydration for" << context; - sendTransferError(); - return false; - } - - QLocalSocket socket; - socket.connectToServer(context.requestHexId()); - const auto connectResult = socket.waitForConnected(); - if (!connectResult) { - qCWarning(lcVfsXAttr) << u"Couldn't connect the socket" << context << socket.error() << socket.errorString(); - sendTransferError(); - return false; - } - - QLocalSocket signalSocket; - const QString signalSocketName = context.requestHexId() + QStringLiteral(":cancellation"); - signalSocket.connectToServer(signalSocketName); - const auto cancellationSocketConnectResult = signalSocket.waitForConnected(); - if (!cancellationSocketConnectResult) { - qCWarning(lcVfsXAttr) << u"Couldn't connect the socket" << signalSocketName << signalSocket.error() << signalSocket.errorString(); - sendTransferError(); - return false; - } - - auto hydrationRequestCancelled = false; - QObject::connect(&signalSocket, &QLocalSocket::readyRead, &loop, [&] { - hydrationRequestCancelled = true; - qCCritical(lcVfsXAttr) << u"Hydration canceled for " << context; - }); - - // Create a save file to store received data into - QSaveFile targetFile(context.path); - if (!targetFile.open(QFile::WriteOnly | QFile::Truncate)) { - sendTransferError(); - } - - QObject::connect(&socket, &QLocalSocket::readyRead, &loop, [&] { - if (hydrationRequestCancelled) { - qCDebug(lcVfsXAttr) << u"Don't transfer data because request" << context << u"was cancelled"; - return; - } + sendTransferInfo(0); - const auto receivedData = socket.readAll(); - if (receivedData.isEmpty()) { - qCWarning(lcVfsXAttr) << u"Unexpected empty data received" << context; - sendTransferError(); - loop.quit(); - return; - } - - // Put received data to target file - targetFile.write(receivedData); - sendTransferInfo(receivedData.size()); - }); - - QObject::connect(context.vfs, &OCC::Vfs::hydrationRequestFinished, this, [&](int64_t id) { - qDebug(lcVfsXAttr) << u"Hydration finished for request" << id << "IN MY THREAD"; - if (context.requestId == id) { - const auto receivedData = socket.readAll(); - targetFile.write(receivedData); - sendTransferInfo(receivedData.size()); - } - targetFile.commit(); - loop.quit(); - }); - - QObject::connect(context.vfs, &OCC::Vfs::hydrationRequestFinished, &loop, [&](int64_t id) { - qDebug(lcVfsXAttr) << u"Hydration finished for request" << id << "IN LOOP"; - if (context.requestId == id) { - const auto receivedData = socket.readAll(); - targetFile.write(receivedData); - sendTransferInfo(receivedData.size()); - } - targetFile.commit(); - loop.quit(); - }); - - qCDebug(lcVfsXAttr) << u"Starting event loop 2"; - loop.exec(); - - OCC::HydrationJob::Status hydrationJobResult = OCC::HydrationJob::Status::Error; - const auto invokeFinalizeResult = QMetaObject::invokeMethod( - context.vfs, [&hydrationJobResult, &context] { - hydrationJobResult = context.vfs->finalizeHydrationJob(context.requestId); - } /* , Qt::BlockingQueuedConnection */); - if (!invokeFinalizeResult) { - qCritical(lcVfsXAttr) << u"Failed to finalize hydration job for" << context; - } + OCC::HydrationJob::Status hydrationJobResult = OCC::HydrationJob::Status::Success; if (hydrationJobResult != OCC::HydrationJob::Status::Success) { sendTransferError(); @@ -370,22 +250,6 @@ bool VfsXAttr::handleAction(const QString& path, const XAttrWrapper::PlaceHolder return re; } -// if an extended attribute was changed. -bool VfsXAttr::handleXAttrChange(const QSet &paths) -{ - bool re{false}; - - for(const auto &p : paths) { - // check if an "action xattr" was set - const XAttrWrapper::PlaceHolderAttribs attribs = XAttrWrapper::placeHolderAttributes(p); - if (!attribs.action().isEmpty()) { - handleAction(p, attribs); - } - } - - return re; -} - bool VfsXAttr::needsMetadataUpdate(const SyncFileItem &) { qCDebug(lcVfsXAttr()) << "returns false by default DOUBLECHECK"; diff --git a/src/plugins/vfs/xattr/vfs_xattr.h b/src/plugins/vfs/xattr/vfs_xattr.h index e4a1e8085a..afbf4b44bf 100644 --- a/src/plugins/vfs/xattr/vfs_xattr.h +++ b/src/plugins/vfs/xattr/vfs_xattr.h @@ -39,7 +39,6 @@ class VfsXAttr : public Vfs const QString &path, time_t modtime, qint64 size, const QByteArray &fileId, const QString &replacesPath); bool handleAction(const QString& path, const XAttrWrapper::PlaceHolderAttribs &attribs); - bool handleXAttrChange(const QSet &) override; bool needsMetadataUpdate(const SyncFileItem &item) override; bool isDehydratedPlaceholder(const QString &filePath) override; LocalInfo statTypeVirtualFile(const std::filesystem::directory_entry &path, ItemType type) override; From ab08502d926e25783e84025411e7b476eb3d594a Mon Sep 17 00:00:00 2001 From: Klaas Freitag Date: Mon, 15 Sep 2025 14:56:36 +0200 Subject: [PATCH 10/13] make setupParams protected rather than private allow derived vfs classes to access the setup parameter struct --- src/libsync/common/vfs.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libsync/common/vfs.h b/src/libsync/common/vfs.h index 2170ebd958..5ec8499ae1 100644 --- a/src/libsync/common/vfs.h +++ b/src/libsync/common/vfs.h @@ -260,9 +260,9 @@ public Q_SLOTS: */ virtual void startImpl(const VfsSetupParams ¶ms) = 0; + std::unique_ptr _setupParams; private: // the parameters passed to start() - std::unique_ptr _setupParams; QScopedPointer d; friend class OwncloudPropagator; From d55bb7406a7ff08e4b306c5226f7b483d2d7b1b0 Mon Sep 17 00:00:00 2001 From: Klaas Freitag Date: Mon, 15 Sep 2025 15:01:38 +0200 Subject: [PATCH 11/13] Drop extra file xattr_wrapper. Move its contents to vfs_xattr Cleaned namespace usage --- src/plugins/vfs/xattr/CMakeLists.txt | 1 - src/plugins/vfs/xattr/vfs_xattr.cpp | 112 ++++++++++++++++++++++--- src/plugins/vfs/xattr/vfs_xattr.h | 35 +++++++- src/plugins/vfs/xattr/xattrwrapper.cpp | 105 ----------------------- src/plugins/vfs/xattr/xattrwrapper.h | 41 --------- 5 files changed, 132 insertions(+), 162 deletions(-) delete mode 100644 src/plugins/vfs/xattr/xattrwrapper.cpp delete mode 100644 src/plugins/vfs/xattr/xattrwrapper.h diff --git a/src/plugins/vfs/xattr/CMakeLists.txt b/src/plugins/vfs/xattr/CMakeLists.txt index 3312b4c2a1..c035267321 100644 --- a/src/plugins/vfs/xattr/CMakeLists.txt +++ b/src/plugins/vfs/xattr/CMakeLists.txt @@ -1,6 +1,5 @@ add_vfs_plugin(NAME xattr SRC - xattrwrapper.cpp vfs_xattr.cpp ) diff --git a/src/plugins/vfs/xattr/vfs_xattr.cpp b/src/plugins/vfs/xattr/vfs_xattr.cpp index 1913c36f03..e0bc6601ff 100644 --- a/src/plugins/vfs/xattr/vfs_xattr.cpp +++ b/src/plugins/vfs/xattr/vfs_xattr.cpp @@ -9,13 +9,14 @@ #include "syncfileitem.h" #include "filesystem.h" #include "common/syncjournaldb.h" -#include "xattrwrapper.h" #include #include #include #include +#include + Q_LOGGING_CATEGORY(lcVfsXAttr, "sync.vfs.xattr", QtInfoMsg) QDebug operator<<(QDebug debug, const OCC::CallBackContext &context) @@ -41,12 +42,96 @@ int requestId() { } +Q_LOGGING_CATEGORY(lcXAttrWrapper, "sync.vfs.xattr.wrapper", QtInfoMsg) + namespace xattr { -// using namespace XAttrWrapper; +constexpr auto hydrateExecAttributeName = "user.openvfs.hydrate_exec"; + +OCC::Optional get(const QByteArray &path, const QByteArray &name) +{ + QByteArray result(512, Qt::Initialization::Uninitialized); + auto count = getxattr(path.constData(), name.constData(), result.data(), result.size()); + if (count > 0) { + // xattr is special. It does not store C-Strings, but blobs. + // So it needs to be checked, if a trailing \0 was added when writing + // (as this software does) or not as the standard setfattr-tool + // the following will handle both cases correctly. + if (result[count-1] == '\0') { + count--; + } + result.truncate(count); + return result; + } else { + return {}; + } +} + +bool set(const QByteArray &path, const QByteArray &name, const QByteArray &value) +{ + const auto returnCode = setxattr(path.constData(), name.constData(), value.constData(), value.size()+1, 0); + return returnCode == 0; +} + +PlaceHolderAttribs placeHolderAttributes(const QString& path) +{ + PlaceHolderAttribs attribs; + + // lambda to handle the Optional return val of xattrGet + auto xattr = [](const QByteArray& p, const QByteArray& name) { + const auto value = xattr::get(p, name); + if (value) { + return *value; + } else { + return QByteArray(); + } + }; + + const auto p = path.toUtf8(); + + attribs._executor = xattr(p, hydrateExecAttributeName); + attribs._etag = QString::fromUtf8(xattr(p, "user.openvfs.etag")); + attribs._fileId = xattr(p, "user.openvfs.fileid"); + + const QByteArray& tt = xattr(p, "user.openvfs.modtime"); + attribs._modtime = tt.toLongLong(); + + attribs._action = xattr(p, "user.openvfs.action"); + attribs._size = xattr(p, "user.openvfs.fsize").toLongLong(); + attribs._pinState = xattr(p, "user.openvfs.pinstate"); + + return attribs; +} + +bool hasPlaceholderAttributes(const QString &path) +{ + const PlaceHolderAttribs attribs = placeHolderAttributes(path); + + // Only pretend to have attribs if they are from us... + return attribs.itsMe(); +} + +OCC::Result addPlaceholderAttribute(const QString &path, const QByteArray& name, const QByteArray& value) +{ + auto success = xattr::set(path.toUtf8(), hydrateExecAttributeName, APPLICATION_EXECUTABLE); + if (!success) { + return QStringLiteral("Failed to set the extended attribute hydrateExec"); + } + + if (!name.isEmpty()) { + success = xattr::set(path.toUtf8(), name, value); + if (!success) { + return QStringLiteral("Failed to set the extended attribute"); + } + } + + return {}; +} } namespace OCC { +using namespace xattr; + VfsXAttr::VfsXAttr(QObject *parent) : Vfs(parent) { @@ -92,16 +177,17 @@ OCC::Result VfsXAttr::updateMetad // FIXME: Error handling dehydratePlaceholder(syncItem); } else { - XAttrWrapper::PlaceHolderAttribs attribs = XAttrWrapper::placeHolderAttributes(localPath); + PlaceHolderAttribs attribs = placeHolderAttributes(localPath); if (attribs.itsMe()) { // checks if there are placeholder Attribs at all FileSystem::setModTime(localPath, syncItem._modtime); // FIXME only write attribs if they're different - XAttrWrapper::addPlaceholderAttribute(localPath, "user.openvfs.fsize", QByteArray::number(syncItem._size)); - XAttrWrapper::addPlaceholderAttribute(localPath, "user.openvfs.state", "dehydrated"); - XAttrWrapper::addPlaceholderAttribute(localPath, "user.openvfs.fileid", syncItem._fileId); - XAttrWrapper::addPlaceholderAttribute(localPath, "user.openvfs.etag", syncItem._etag.toUtf8()); + addPlaceholderAttribute(localPath, "user.openvfs.owner", _setupParams->account->uuid().toByteArray(QUuid::WithoutBraces)); + addPlaceholderAttribute(localPath, "user.openvfs.fsize", QByteArray::number(syncItem._size)); + addPlaceholderAttribute(localPath, "user.openvfs.state", "dehydrated"); + addPlaceholderAttribute(localPath, "user.openvfs.fileid", syncItem._fileId); + addPlaceholderAttribute(localPath, "user.openvfs.etag", syncItem._etag.toUtf8()); } else { // FIXME use fileItem as parameter @@ -139,7 +225,7 @@ Result VfsXAttr::createPlaceholder(const SyncFileItem &item) /* * Only write the state and the executor, the rest is added in the updateMetadata() method */ - XAttrWrapper::addPlaceholderAttribute(path, "user.openvfs.state", "dehydrated"); + addPlaceholderAttribute(path, "user.openvfs.state", "dehydrated"); return {}; } @@ -180,7 +266,7 @@ OCC::Result VfsXAttr::convertToPlaceho return {ConvertToPlaceholderResult::Ok}; } -bool VfsXAttr::handleAction(const QString& path, const XAttrWrapper::PlaceHolderAttribs& attribs) +bool VfsXAttr::handleAction(const QString& path, const PlaceHolderAttribs& attribs) { bool re{false}; @@ -260,7 +346,7 @@ bool VfsXAttr::isDehydratedPlaceholder(const QString &filePath) { const auto fi = QFileInfo(filePath); return fi.exists() && - XAttrWrapper::hasPlaceholderAttributes(filePath); + hasPlaceholderAttributes(filePath); } LocalInfo VfsXAttr::statTypeVirtualFile(const std::filesystem::directory_entry &path, ItemType type) @@ -269,7 +355,7 @@ LocalInfo VfsXAttr::statTypeVirtualFile(const std::filesystem::directory_entry & const QString p = QString::fromUtf8(path.path().c_str()); //FIXME? qCDebug(lcVfsXAttr()) << p; - if (XAttrWrapper::hasPlaceholderAttributes(p)) { + if (hasPlaceholderAttributes(p)) { // const auto shouldDownload = pin && (*pin == PinState::AlwaysLocal); bool shouldDownload{false}; if (shouldDownload) { @@ -292,7 +378,7 @@ bool VfsXAttr::setPinState(const QString &folderPath, PinState state) { qCDebug(lcVfsXAttr()) << folderPath << state; auto stateStr = Utility::enumToDisplayName(state); - auto res = XAttrWrapper::addPlaceholderAttribute(folderPath, "user.openvfs.pinstate", stateStr.toUtf8()); + auto res = addPlaceholderAttribute(folderPath, "user.openvfs.pinstate", stateStr.toUtf8()); if (!res) { qCDebug(lcVfsXAttr()) << "Failed to set pin state"; return false; @@ -304,7 +390,7 @@ Optional VfsXAttr::pinState(const QString &folderPath) { qCDebug(lcVfsXAttr()) << folderPath; - XAttrWrapper::PlaceHolderAttribs attribs = XAttrWrapper::placeHolderAttributes(folderPath); + PlaceHolderAttribs attribs = placeHolderAttributes(folderPath); const QString pin = QString::fromUtf8(attribs.pinState()); PinState pState{PinState::Unspecified}; diff --git a/src/plugins/vfs/xattr/vfs_xattr.h b/src/plugins/vfs/xattr/vfs_xattr.h index afbf4b44bf..3933b1796b 100644 --- a/src/plugins/vfs/xattr/vfs_xattr.h +++ b/src/plugins/vfs/xattr/vfs_xattr.h @@ -10,11 +10,42 @@ #include "common/vfs.h" #include "common/plugin.h" +#include "common/result.h" -#include "xattrwrapper.h" +#include "config.h" + +namespace xattr { +struct PlaceHolderAttribs { +public: + qint64 size() const { return _size; } + QByteArray fileId() const { return _fileId; } + time_t modTime() const {return _modtime; } + QString eTag() const { return _etag; } + QByteArray pinState() const { return _pinState; } + QByteArray action() const { return _action; } + + bool itsMe() const { return !_executor.isEmpty() && _executor == QByteArrayLiteral(APPLICATION_EXECUTABLE);} + + qint64 _size; + QByteArray _fileId; + time_t _modtime; + QString _etag; + QByteArray _executor; + QByteArray _pinState; + QByteArray _action; + +}; + +PlaceHolderAttribs placeHolderAttributes(const QString& path); +bool hasPlaceholderAttributes(const QString &path); + +OCC::Result addPlaceholderAttribute(const QString &path, const QByteArray &name = {}, const QByteArray &val = {}); +} namespace OCC { +using namespace xattr; + class VfsXAttr : public Vfs { Q_OBJECT @@ -38,7 +69,7 @@ class VfsXAttr : public Vfs OCC::Result convertToPlaceholder( const QString &path, time_t modtime, qint64 size, const QByteArray &fileId, const QString &replacesPath); - bool handleAction(const QString& path, const XAttrWrapper::PlaceHolderAttribs &attribs); + bool handleAction(const QString& path, const PlaceHolderAttribs &attribs); bool needsMetadataUpdate(const SyncFileItem &item) override; bool isDehydratedPlaceholder(const QString &filePath) override; LocalInfo statTypeVirtualFile(const std::filesystem::directory_entry &path, ItemType type) override; diff --git a/src/plugins/vfs/xattr/xattrwrapper.cpp b/src/plugins/vfs/xattr/xattrwrapper.cpp deleted file mode 100644 index d44ada01a3..0000000000 --- a/src/plugins/vfs/xattr/xattrwrapper.cpp +++ /dev/null @@ -1,105 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors - * SPDX-FileCopyrightText: 2025 OpenCloud GmbH and OpenCloud contributors - * SPDX-License-Identifier: GPL-2.0-or-later - */ - -#include "xattrwrapper.h" -#include "common/result.h" -#include "config.h" - -#include -#include - - - -Q_LOGGING_CATEGORY(lcXAttrWrapper, "sync.vfs.xattr.wrapper", QtInfoMsg) - -namespace { -constexpr auto hydrateExecAttributeName = "user.openvfs.hydrate_exec"; - -OCC::Optional xattrGet(const QByteArray &path, const QByteArray &name) -{ - QByteArray result(512, Qt::Initialization::Uninitialized); - auto count = getxattr(path.constData(), name.constData(), result.data(), result.size()); - if (count > 0) { - // xattr is special. It does not store C-Strings, but blobs. - // So it needs to be checked, if a trailing \0 was added when writing - // (as this software does) or not as the standard setfattr-tool - // the following will handle both cases correctly. - if (result[count-1] == '\0') { - count--; - } - result.truncate(count); - return result; - } else { - return {}; - } -} - -bool xattrSet(const QByteArray &path, const QByteArray &name, const QByteArray &value) -{ - const auto returnCode = setxattr(path.constData(), name.constData(), value.constData(), value.size()+1, 0); - return returnCode == 0; -} - -} - -namespace XAttrWrapper { - -PlaceHolderAttribs placeHolderAttributes(const QString& path) -{ - PlaceHolderAttribs attribs; - - // lambda to handle the Optional return val of xattrGet - auto xattr = [](const QByteArray& p, const QByteArray& name) { - const auto value = xattrGet(p, name); - if (value) { - return *value; - } else { - return QByteArray(); - } - }; - - const auto p = path.toUtf8(); - - attribs._executor = xattr(p, hydrateExecAttributeName); - attribs._etag = QString::fromUtf8(xattr(p, "user.openvfs.etag")); - attribs._fileId = xattr(p, "user.openvfs.fileid"); - - const QByteArray& tt = xattr(p, "user.openvfs.modtime"); - attribs._modtime = tt.toLongLong(); - - attribs._action = xattr(p, "user.openvfs.action"); - attribs._size = xattr(p, "user.openvfs.fsize").toLongLong(); - attribs._pinState = xattr(p, "user.openvfs.pinstate"); - - return attribs; -} - - -bool hasPlaceholderAttributes(const QString &path) -{ - const PlaceHolderAttribs attribs = placeHolderAttributes(path); - - // Only pretend to have attribs if they are from us... - return attribs.itsMe(); -} - -OCC::Result addPlaceholderAttribute(const QString &path, const QByteArray& name, const QByteArray& value) -{ - auto success = xattrSet(path.toUtf8(), hydrateExecAttributeName, APPLICATION_EXECUTABLE); - if (!success) { - return QStringLiteral("Failed to set the extended attribute hydrateExec"); - } - - if (!name.isEmpty()) { - success = xattrSet(path.toUtf8(), name, value); - if (!success) { - return QStringLiteral("Failed to set the extended attribute"); - } - } - - return {}; -} -} diff --git a/src/plugins/vfs/xattr/xattrwrapper.h b/src/plugins/vfs/xattr/xattrwrapper.h deleted file mode 100644 index 3c93bdb6c6..0000000000 --- a/src/plugins/vfs/xattr/xattrwrapper.h +++ /dev/null @@ -1,41 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors - * SPDX-FileCopyrightText: 2025 OpenCloud GmbH and OpenCloud contributors - * SPDX-License-Identifier: GPL-2.0-or-later - */ -#pragma once - -#include - -#include "config.h" -#include "common/result.h" - -namespace XAttrWrapper -{ -struct PlaceHolderAttribs { -public: - qint64 size() const { return _size; } - QByteArray fileId() const { return _fileId; } - time_t modTime() const {return _modtime; } - QString eTag() const { return _etag; } - QByteArray pinState() const { return _pinState; } - QByteArray action() const { return _action; } - - bool itsMe() const { return !_executor.isEmpty() && _executor == QByteArrayLiteral(APPLICATION_EXECUTABLE);} - - qint64 _size; - QByteArray _fileId; - time_t _modtime; - QString _etag; - QByteArray _executor; - QByteArray _pinState; - QByteArray _action; - -}; - -PlaceHolderAttribs placeHolderAttributes(const QString& path); -bool hasPlaceholderAttributes(const QString &path); - -OCC::Result addPlaceholderAttribute(const QString &path, const QByteArray &name = {}, const QByteArray &val = {}); - -} From 155200c06fdd38f9e6a3af302d486c2119974907 Mon Sep 17 00:00:00 2001 From: Klaas Freitag Date: Fri, 19 Sep 2025 15:24:35 +0200 Subject: [PATCH 12/13] Clean the xattr persisting, fix owner handling --- src/plugins/vfs/xattr/vfs_xattr.cpp | 176 ++++++++++++++++------------ src/plugins/vfs/xattr/vfs_xattr.h | 25 ++-- 2 files changed, 118 insertions(+), 83 deletions(-) diff --git a/src/plugins/vfs/xattr/vfs_xattr.cpp b/src/plugins/vfs/xattr/vfs_xattr.cpp index e0bc6601ff..f0a0f69979 100644 --- a/src/plugins/vfs/xattr/vfs_xattr.cpp +++ b/src/plugins/vfs/xattr/vfs_xattr.cpp @@ -42,10 +42,15 @@ int requestId() { } -Q_LOGGING_CATEGORY(lcXAttrWrapper, "sync.vfs.xattr.wrapper", QtInfoMsg) - namespace xattr { -constexpr auto hydrateExecAttributeName = "user.openvfs.hydrate_exec"; +constexpr auto ownerXAttrName = "user.openvfs.owner"; +constexpr auto etagXAttrName = "user.openvfs.etag"; +constexpr auto fileidXAttrName = "user.openvfs.fileid"; +constexpr auto modtimeXAttrName = "user.openvfs.modtime"; +constexpr auto fileSizeXAttrName = "user.openvfs.fsize"; +constexpr auto actionXAttrName = "user.openvfs.action"; +constexpr auto stateXAttrName = "user.openvfs.state"; +constexpr auto pinstateXAttrName = "user.openvfs.pinstate"; OCC::Optional get(const QByteArray &path, const QByteArray &name) { @@ -71,61 +76,6 @@ bool set(const QByteArray &path, const QByteArray &name, const QByteArray &value const auto returnCode = setxattr(path.constData(), name.constData(), value.constData(), value.size()+1, 0); return returnCode == 0; } - -PlaceHolderAttribs placeHolderAttributes(const QString& path) -{ - PlaceHolderAttribs attribs; - - // lambda to handle the Optional return val of xattrGet - auto xattr = [](const QByteArray& p, const QByteArray& name) { - const auto value = xattr::get(p, name); - if (value) { - return *value; - } else { - return QByteArray(); - } - }; - - const auto p = path.toUtf8(); - - attribs._executor = xattr(p, hydrateExecAttributeName); - attribs._etag = QString::fromUtf8(xattr(p, "user.openvfs.etag")); - attribs._fileId = xattr(p, "user.openvfs.fileid"); - - const QByteArray& tt = xattr(p, "user.openvfs.modtime"); - attribs._modtime = tt.toLongLong(); - - attribs._action = xattr(p, "user.openvfs.action"); - attribs._size = xattr(p, "user.openvfs.fsize").toLongLong(); - attribs._pinState = xattr(p, "user.openvfs.pinstate"); - - return attribs; -} - -bool hasPlaceholderAttributes(const QString &path) -{ - const PlaceHolderAttribs attribs = placeHolderAttributes(path); - - // Only pretend to have attribs if they are from us... - return attribs.itsMe(); -} - -OCC::Result addPlaceholderAttribute(const QString &path, const QByteArray& name, const QByteArray& value) -{ - auto success = xattr::set(path.toUtf8(), hydrateExecAttributeName, APPLICATION_EXECUTABLE); - if (!success) { - return QStringLiteral("Failed to set the extended attribute hydrateExec"); - } - - if (!name.isEmpty()) { - success = xattr::set(path.toUtf8(), name, value); - if (!success) { - return QStringLiteral("Failed to set the extended attribute"); - } - } - - return {}; -} } namespace OCC { @@ -164,7 +114,79 @@ bool VfsXAttr::socketApiPinStateActionsShown() const return true; } +QByteArray VfsXAttr::xattrOwnerString() const +{ + auto s = QByteArray(APPLICATION_EXECUTABLE); + s.append(":"); + s.append(_setupParams->account->uuid().toByteArray(QUuid::WithoutBraces)); + return s; +} + +PlaceHolderAttribs VfsXAttr::placeHolderAttributes(const QString& path) +{ + PlaceHolderAttribs attribs; + + // lambda to handle the Optional return val of xattrGet + auto xattr = [](const QByteArray& p, const QByteArray& name) { + const auto value = xattr::get(p, name); + if (value) { + return *value; + } else { + return QByteArray(); + } + }; + + const auto p = path.toUtf8(); + + attribs._owner = xattr(p, ownerXAttrName); + if (attribs._owner.isEmpty()) { + // lets claim it + attribs._owner = xattrOwnerString(); + } else { + if (attribs._owner != xattrOwnerString()) { + qCDebug(lcVfsXAttr) << "XAttributes not from our instance"; + attribs._owner.clear(); + return attribs; + } + } + + attribs._etag = QString::fromUtf8(xattr(p, etagXAttrName)); + attribs._fileId = xattr(p, fileidXAttrName); + + const QByteArray& tt = xattr(p, modtimeXAttrName); + attribs._modtime = tt.toLongLong(); + + attribs._action = xattr(p, actionXAttrName); + attribs._size = xattr(p, fileSizeXAttrName).toLongLong(); + attribs._state = xattr(p, stateXAttrName); + attribs._pinState = xattr(p, pinstateXAttrName); + + return attribs; +} + +OCC::Result VfsXAttr::addPlaceholderAttribute(const QString &path, const QByteArray& name, const QByteArray& value) +{ + const PlaceHolderAttribs attribs = placeHolderAttributes(path); + + if (! attribs.validOwner()) { + return QStringLiteral("Can not overwrite attributes - not our placeholder"); + } + + // FIXME: this always sets the name, can be optimized + auto success = xattr::set(path.toUtf8(), ownerXAttrName, xattrOwnerString()); + if (!success) { + return QStringLiteral("Failed to set the extended attribute for owner"); + } + if (!name.isEmpty()) { + auto success = xattr::set(path.toUtf8(), name, value); + if (!success) { + return QStringLiteral("Failed to set the extended attribute"); + } + } + + return {}; +} OCC::Result VfsXAttr::updateMetadata(const SyncFileItem &syncItem, const QString &filePath, const QString &replacesFile) { @@ -179,16 +201,14 @@ OCC::Result VfsXAttr::updateMetad } else { PlaceHolderAttribs attribs = placeHolderAttributes(localPath); - if (attribs.itsMe()) { // checks if there are placeholder Attribs at all + if (attribs.validOwner() && attribs.state().isEmpty()) { // No status FileSystem::setModTime(localPath, syncItem._modtime); - // FIXME only write attribs if they're different - addPlaceholderAttribute(localPath, "user.openvfs.owner", _setupParams->account->uuid().toByteArray(QUuid::WithoutBraces)); - addPlaceholderAttribute(localPath, "user.openvfs.fsize", QByteArray::number(syncItem._size)); - addPlaceholderAttribute(localPath, "user.openvfs.state", "dehydrated"); - addPlaceholderAttribute(localPath, "user.openvfs.fileid", syncItem._fileId); - addPlaceholderAttribute(localPath, "user.openvfs.etag", syncItem._etag.toUtf8()); - + // FIXME only write attribs if they're different, and/or all together + addPlaceholderAttribute(localPath, fileSizeXAttrName, QByteArray::number(syncItem._size)); + addPlaceholderAttribute(localPath, stateXAttrName, "dehydrated"); + addPlaceholderAttribute(localPath, fileidXAttrName, syncItem._fileId); + addPlaceholderAttribute(localPath, etagXAttrName, syncItem._etag.toUtf8()); } else { // FIXME use fileItem as parameter return convertToPlaceholder(localPath, syncItem._modtime, syncItem._size, syncItem._fileId, replacesPath); @@ -262,7 +282,7 @@ OCC::Result VfsXAttr::convertToPlaceho Q_UNUSED(replacesPath) // Nothing necessary - no idea why, taken from previews... - qCDebug(lcVfsXAttr()) << "empty function returning ok, DOUBLECHECK" << path ; + qCDebug(lcVfsXAttr) << "empty function returning ok, DOUBLECHECK" << path ; return {ConvertToPlaceholderResult::Ok}; } @@ -345,8 +365,12 @@ bool VfsXAttr::needsMetadataUpdate(const SyncFileItem &) bool VfsXAttr::isDehydratedPlaceholder(const QString &filePath) { const auto fi = QFileInfo(filePath); - return fi.exists() && - hasPlaceholderAttributes(filePath); + if (fi.exists()) { + const auto attribs = placeHolderAttributes(filePath); + return (attribs.validOwner() && + attribs.state() == QByteArrayLiteral("dehydrated")); + } + return false; } LocalInfo VfsXAttr::statTypeVirtualFile(const std::filesystem::directory_entry &path, ItemType type) @@ -355,19 +379,19 @@ LocalInfo VfsXAttr::statTypeVirtualFile(const std::filesystem::directory_entry & const QString p = QString::fromUtf8(path.path().c_str()); //FIXME? qCDebug(lcVfsXAttr()) << p; - if (hasPlaceholderAttributes(p)) { - // const auto shouldDownload = pin && (*pin == PinState::AlwaysLocal); + auto attribs = placeHolderAttributes(p); + if (attribs.validOwner()) { bool shouldDownload{false}; + if (attribs.pinState() == QByteArrayLiteral("alwayslocal")) { + shouldDownload = true; + } + + // const auto shouldDownload = pin && (*pin == PinState::AlwaysLocal); if (shouldDownload) { type = ItemTypeVirtualFileDownload; } else { type = ItemTypeVirtualFile; } - } else { - const auto shouldDehydrate = false; // pin && (*pin == PinState::OnlineOnly); - if (shouldDehydrate) { - type = ItemTypeVirtualFileDehydration; - } } } diff --git a/src/plugins/vfs/xattr/vfs_xattr.h b/src/plugins/vfs/xattr/vfs_xattr.h index 3933b1796b..4d733ede9e 100644 --- a/src/plugins/vfs/xattr/vfs_xattr.h +++ b/src/plugins/vfs/xattr/vfs_xattr.h @@ -15,6 +15,7 @@ #include "config.h" namespace xattr { + struct PlaceHolderAttribs { public: qint64 size() const { return _size; } @@ -23,23 +24,27 @@ struct PlaceHolderAttribs { QString eTag() const { return _etag; } QByteArray pinState() const { return _pinState; } QByteArray action() const { return _action; } + QByteArray state() const { return _state; } + QByteArray owner() const { return _owner; } + + // the owner must not be empty but set to the ownerString, that consists + // of the app name and an instance ID + // If no xattrs are set at all, the method @placeHolderAttributes sets it + // to our name and claims the space - bool itsMe() const { return !_executor.isEmpty() && _executor == QByteArrayLiteral(APPLICATION_EXECUTABLE);} + // Always check if we're the valid owner before accessing the xattrs. + bool validOwner() const { return !_owner.isEmpty(); } qint64 _size; QByteArray _fileId; time_t _modtime; QString _etag; - QByteArray _executor; + QByteArray _owner; QByteArray _pinState; QByteArray _action; + QByteArray _state; }; - -PlaceHolderAttribs placeHolderAttributes(const QString& path); -bool hasPlaceholderAttributes(const QString &path); - -OCC::Result addPlaceholderAttribute(const QString &path, const QByteArray &name = {}, const QByteArray &val = {}); } namespace OCC { @@ -83,6 +88,12 @@ public Q_SLOTS: protected: void startImpl(const VfsSetupParams ¶ms) override; + +private: + QByteArray xattrOwnerString() const; + PlaceHolderAttribs placeHolderAttributes(const QString& path); + OCC::Result addPlaceholderAttribute(const QString &path, const QByteArray &name = {}, const QByteArray &val = {}); + }; class XattrVfsPluginFactory : public QObject, public DefaultPluginFactory From ddc5254a76c8c3063ed559454ce625c81ba7c4d6 Mon Sep 17 00:00:00 2001 From: Klaas Freitag Date: Wed, 24 Sep 2025 16:32:19 +0200 Subject: [PATCH 13/13] More code completions and cleanups in vfs_xattr --- src/plugins/vfs/xattr/vfs_xattr.cpp | 78 ++++++++++++++--------------- src/plugins/vfs/xattr/vfs_xattr.h | 1 - 2 files changed, 37 insertions(+), 42 deletions(-) diff --git a/src/plugins/vfs/xattr/vfs_xattr.cpp b/src/plugins/vfs/xattr/vfs_xattr.cpp index f0a0f69979..2993939ef9 100644 --- a/src/plugins/vfs/xattr/vfs_xattr.cpp +++ b/src/plugins/vfs/xattr/vfs_xattr.cpp @@ -195,13 +195,25 @@ OCC::Result VfsXAttr::updateMetad qCDebug(lcVfsXAttr()) << localPath; - if (syncItem._type == ItemTypeVirtualFileDehydration) { + PlaceHolderAttribs attribs = placeHolderAttributes(localPath); + OCC::Vfs::ConvertToPlaceholderResult res{OCC::Vfs::ConvertToPlaceholderResult::Ok}; + + if (attribs.validOwner() && attribs.state().isEmpty()) { // No status + // There is no state, so it is a normal, hydrated file + } + + if (syncItem._type == ItemTypeVirtualFileDehydration) { // + addPlaceholderAttribute(localPath, actionXAttrName, "dehydrate"); // FIXME: Error handling - dehydratePlaceholder(syncItem); - } else { - PlaceHolderAttribs attribs = placeHolderAttributes(localPath); + auto r = createPlaceholder(syncItem); + if (!r) { + res = OCC::Vfs::ConvertToPlaceholderResult::Locked; + } - if (attribs.validOwner() && attribs.state().isEmpty()) { // No status + } else if (syncItem._type == ItemTypeVirtualFileDownload) { + addPlaceholderAttribute(localPath, actionXAttrName, "hydrate"); + // start to download? FIXME + } else if (syncItem._type == ItemTypeVirtualFile) { FileSystem::setModTime(localPath, syncItem._modtime); // FIXME only write attribs if they're different, and/or all together @@ -209,13 +221,13 @@ OCC::Result VfsXAttr::updateMetad addPlaceholderAttribute(localPath, stateXAttrName, "dehydrated"); addPlaceholderAttribute(localPath, fileidXAttrName, syncItem._fileId); addPlaceholderAttribute(localPath, etagXAttrName, syncItem._etag.toUtf8()); - } else { - // FIXME use fileItem as parameter - return convertToPlaceholder(localPath, syncItem._modtime, syncItem._size, syncItem._fileId, replacesPath); - } + } else { + // FIXME anything to check for other types? + qCDebug(lcVfsXAttr) << "Unexpected syncItem Type"; } - return {OCC::Vfs::ConvertToPlaceholderResult::Ok}; + // FIXME Errorhandling + return res; } Result VfsXAttr::createPlaceholder(const SyncFileItem &item) @@ -245,25 +257,8 @@ Result VfsXAttr::createPlaceholder(const SyncFileItem &item) /* * Only write the state and the executor, the rest is added in the updateMetadata() method */ - addPlaceholderAttribute(path, "user.openvfs.state", "dehydrated"); - return {}; -} + addPlaceholderAttribute(path, stateXAttrName, "dehydrated"); -OCC::Result VfsXAttr::dehydratePlaceholder(const SyncFileItem &item) -{ - /* - * const auto path = QDir::toNativeSeparators(params().filesystemPath + item.localName()); - * - * QFile file(path); - * - * if (!file.remove()) { - * return QStringLiteral("Couldn't remove the original file to dehydrate"); - * } - */ - auto r = createPlaceholder(item); - if (!r) { - return r; - } // Ensure the pin state isn't contradictory const auto pin = pinState(item.localName()); @@ -273,6 +268,7 @@ OCC::Result VfsXAttr::dehydratePlaceholder(const SyncFileItem &it return {}; } + OCC::Result VfsXAttr::convertToPlaceholder( const QString &path, time_t modtime, qint64 size, const QByteArray &fileId, const QString &replacesPath) { @@ -402,7 +398,7 @@ bool VfsXAttr::setPinState(const QString &folderPath, PinState state) { qCDebug(lcVfsXAttr()) << folderPath << state; auto stateStr = Utility::enumToDisplayName(state); - auto res = addPlaceholderAttribute(folderPath, "user.openvfs.pinstate", stateStr.toUtf8()); + auto res = addPlaceholderAttribute(folderPath, pinstateXAttrName, stateStr.toUtf8()); if (!res) { qCDebug(lcVfsXAttr()) << "Failed to set pin state"; return false; @@ -416,19 +412,19 @@ Optional VfsXAttr::pinState(const QString &folderPath) PlaceHolderAttribs attribs = placeHolderAttributes(folderPath); - const QString pin = QString::fromUtf8(attribs.pinState()); PinState pState{PinState::Unspecified}; - - if (pin == Utility::enumToDisplayName(PinState::AlwaysLocal)) { - pState = PinState::AlwaysLocal; - } else if (pin == Utility::enumToDisplayName(PinState::Excluded)) { - pState = PinState::Excluded; - } else if (pin.isEmpty() || pin == Utility::enumToDisplayName(PinState::Inherited)) { - pState = PinState::Inherited; - } else if (pin == Utility::enumToDisplayName(PinState::OnlineOnly)) { - pState = PinState::OnlineOnly; - } else if (pin == Utility::enumToDisplayName(PinState::Unspecified)) { - pState = PinState::Unspecified; + if (attribs.validOwner()) { + const QString pin = QString::fromUtf8(attribs.pinState()); + + if (pin == Utility::enumToDisplayName(PinState::AlwaysLocal)) { + pState = PinState::AlwaysLocal; + } else if (pin == Utility::enumToDisplayName(PinState::Excluded)) { + pState = PinState::Excluded; + } else if (pin.isEmpty() || pin == Utility::enumToDisplayName(PinState::Inherited)) { + pState = PinState::Inherited; + } else if (pin == Utility::enumToDisplayName(PinState::OnlineOnly)) { + pState = PinState::OnlineOnly; + } } return pState; diff --git a/src/plugins/vfs/xattr/vfs_xattr.h b/src/plugins/vfs/xattr/vfs_xattr.h index 4d733ede9e..9ffa393521 100644 --- a/src/plugins/vfs/xattr/vfs_xattr.h +++ b/src/plugins/vfs/xattr/vfs_xattr.h @@ -70,7 +70,6 @@ class VfsXAttr : public Vfs // [[nodiscard]] bool isPlaceHolderInSync(const QString &filePath) const override { Q_UNUSED(filePath) return true; } Result createPlaceholder(const SyncFileItem &item) override; - OCC::Result dehydratePlaceholder(const SyncFileItem &item); OCC::Result convertToPlaceholder( const QString &path, time_t modtime, qint64 size, const QByteArray &fileId, const QString &replacesPath);