From 9d51220dab68fdc00a1ea89367f217c3c7716670 Mon Sep 17 00:00:00 2001 From: Klaas Freitag Date: Thu, 25 Sep 2025 15:54:02 +0200 Subject: [PATCH 1/9] Add XAttr based plugin for VFS for Linux and later Mac --- CMakeLists.txt | 2 +- src/libsync/common/pinstate.cpp | 20 ++ src/libsync/common/pinstate.h | 2 + src/libsync/vfs/vfs.cpp | 7 + src/libsync/vfs/vfs.h | 4 +- src/plugins/vfs/xattr/CMakeLists.txt | 4 + src/plugins/vfs/xattr/vfs_xattr.cpp | 390 +++++++++++++++++++++++++++ src/plugins/vfs/xattr/vfs_xattr.h | 104 +++++++ 8 files changed, 530 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 diff --git a/CMakeLists.txt b/CMakeLists.txt index d645f23555..03cfaf70a0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -101,7 +101,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 off cfapi xattr 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/libsync/common/pinstate.cpp b/src/libsync/common/pinstate.cpp index 6d3570298b..0f61426fc8 100644 --- a/src/libsync/common/pinstate.cpp +++ b/src/libsync/common/pinstate.cpp @@ -19,6 +19,26 @@ using namespace OCC; + +template <> +QString Utility::enumToDisplayName(PinState pState) +{ + switch (pState) { + 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..ed5f4d866a 100644 --- a/src/libsync/common/pinstate.h +++ b/src/libsync/common/pinstate.h @@ -137,6 +137,8 @@ using namespace PinStateEnums; template <> OPENCLOUD_SYNC_EXPORT QString Utility::enumToDisplayName(VfsItemAvailability availability); +template <> +OPENCLOUD_SYNC_EXPORT QString Utility::enumToDisplayName(PinState pState); } #endif diff --git a/src/libsync/vfs/vfs.cpp b/src/libsync/vfs/vfs.cpp index 225a2bf43c..b12976f0f1 100644 --- a/src/libsync/vfs/vfs.cpp +++ b/src/libsync/vfs/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(); } @@ -146,6 +150,7 @@ void Vfs::wipeDehydratedVirtualFiles() QFuture> Vfs::hydrateFile(const QByteArray &, const QString &) { + Q_UNUSED(fileId) // nothing to do return QtFuture::makeReadyValueFuture(Result{}); } @@ -207,6 +212,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/vfs/vfs.h b/src/libsync/vfs/vfs.h index 4404cc7fe1..b3f321d8a6 100644 --- a/src/libsync/vfs/vfs.h +++ b/src/libsync/vfs/vfs.h @@ -97,7 +97,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) @@ -252,9 +252,9 @@ public Q_SLOTS: */ virtual void startImpl(const VfsSetupParams ¶ms) = 0; -private: // the parameters passed to start() std::unique_ptr _setupParams; +private: friend class OwncloudPropagator; }; diff --git a/src/plugins/vfs/xattr/CMakeLists.txt b/src/plugins/vfs/xattr/CMakeLists.txt new file mode 100644 index 0000000000..c71f400ae3 --- /dev/null +++ b/src/plugins/vfs/xattr/CMakeLists.txt @@ -0,0 +1,4 @@ + add_vfs_plugin(NAME xattr + SRC + 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..675f71226f --- /dev/null +++ b/src/plugins/vfs/xattr/vfs_xattr.cpp @@ -0,0 +1,390 @@ +/* + * 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 "account.h" + +#include +#include +#include +#include +#include + +#include + +Q_LOGGING_CATEGORY(lcVfsXAttr, "sync.vfs.xattr", QtInfoMsg) + +namespace { + +#if 0 +std::atomic id{1}; + +int requestId() { + return id++; +} +#endif +} + +namespace xattr { +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) +{ + 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; +} +} + +namespace OCC { + +using namespace xattr; + +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; +} + +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) +{ + const auto localPath = QDir::toNativeSeparators(filePath); + const auto replacesPath = QDir::toNativeSeparators(replacesFile); + + qCDebug(lcVfsXAttr()) << localPath; + + 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 + auto r = createPlaceholder(syncItem); + if (!r) { + res = OCC::Vfs::ConvertToPlaceholderResult::Locked; + } + + } 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 + addPlaceholderAttribute(localPath, fileSizeXAttrName, QByteArray::number(syncItem._size)); + addPlaceholderAttribute(localPath, stateXAttrName, Utility::enumToDisplayName(PinState::OnlineOnly).toUtf8()); + addPlaceholderAttribute(localPath, fileidXAttrName, syncItem._fileId); + addPlaceholderAttribute(localPath, etagXAttrName, syncItem._etag.toUtf8()); + } else { + // FIXME anything to check for other types? + qCDebug(lcVfsXAttr) << "Unexpected syncItem Type"; + } + + // FIXME Errorhandling + return res; +} + +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 + */ + addPlaceholderAttribute(path, stateXAttrName, Utility::enumToDisplayName(PinState::OnlineOnly).toUtf8()); + + + // 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); + if (fi.exists()) { + const auto attribs = placeHolderAttributes(filePath); + return (attribs.validOwner() && + attribs.state() == Utility::enumToDisplayName(PinState::OnlineOnly).toUtf8()); + } + return false; +} + +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; + + auto attribs = placeHolderAttributes(p); + if (attribs.validOwner()) { + bool shouldDownload{false}; + if (attribs.pinState() == Utility::enumToDisplayName(PinState::AlwaysLocal).toUtf8()) { + shouldDownload = true; + } + + // const auto shouldDownload = pin && (*pin == PinState::AlwaysLocal); + if (shouldDownload) { + type = ItemTypeVirtualFileDownload; + } else { + type = ItemTypeVirtualFile; + } + } + } + + return LocalInfo(path, type); +} + +bool VfsXAttr::setPinState(const QString &folderPath, PinState state) +{ + qCDebug(lcVfsXAttr()) << folderPath << state; + auto stateStr = Utility::enumToDisplayName(state); + auto res = addPlaceholderAttribute(folderPath, pinstateXAttrName, stateStr.toUtf8()); + if (!res) { + qCDebug(lcVfsXAttr()) << "Failed to set pin state"; + return false; + } + return true; +} + +Optional VfsXAttr::pinState(const QString &folderPath) +{ + qCDebug(lcVfsXAttr()) << folderPath; + + PlaceHolderAttribs attribs = placeHolderAttributes(folderPath); + + PinState 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; +} + +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) +{ + if (fileStatus.tag() == SyncFileStatus::StatusExcluded) { + 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 new file mode 100644 index 0000000000..885ed2d0e2 --- /dev/null +++ b/src/plugins/vfs/xattr/vfs_xattr.h @@ -0,0 +1,104 @@ +/* + * 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 "vfs/vfs.h" +#include "common/plugin.h" +#include "common/result.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; } + 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 + + // 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 _owner; + QByteArray _pinState; + QByteArray _action; + QByteArray _state; + +}; +} + +namespace OCC { + +using namespace xattr; + +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 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; + +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 +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "eu.opencloud.PluginFactory" FILE "libsync/common/vfspluginmetadata.json") + Q_INTERFACES(OCC::PluginFactory) +}; + +} // namespace OCC From 961dfc1eafd4ec38c7951fdc0a13826c35514805 Mon Sep 17 00:00:00 2001 From: Klaas Freitag Date: Fri, 26 Sep 2025 17:25:29 +0200 Subject: [PATCH 2/9] Use HydrationJob class in socketapi --- src/gui/socketapi/socketapi.cpp | 31 ++- src/libsync/vfs/hydrationjob.cpp | 10 + src/libsync/vfs/hydrationjob.h | 7 + src/libsync/vfs/vfs.cpp | 5 +- src/libsync/vfs/vfs.h | 3 +- src/plugins/vfs/xattr/vfs_xattr.cpp | 381 ++++++++++++++++++---------- src/plugins/vfs/xattr/vfs_xattr.h | 45 ++-- 7 files changed, 314 insertions(+), 168 deletions(-) diff --git a/src/gui/socketapi/socketapi.cpp b/src/gui/socketapi/socketapi.cpp index b142a1bf9d..4d7c153364 100644 --- a/src/gui/socketapi/socketapi.cpp +++ b/src/gui/socketapi/socketapi.cpp @@ -32,6 +32,7 @@ #include "syncengine.h" #include "syncfileitem.h" #include "theme.h" +#include "vfs/hydrationjob.h" #include #include @@ -476,7 +477,9 @@ void SocketApi::command_SHARE(const QString &localFile, SocketListener *listener void SocketApi::command_VERSION(const QString &, SocketListener *listener) { - listener->sendMessage(QStringLiteral("VERSION:%1:%2").arg(OCC::Version::versionWithBuildNumber().toString(), QStringLiteral(MIRALL_SOCKET_API_VERSION))); + listener->sendMessage(QStringLiteral("VERSION:%1:%2:%3").arg(OCC::Version::versionWithBuildNumber().toString(), + QStringLiteral(MIRALL_SOCKET_API_VERSION), + QString::number(qApp->applicationPid()))); } void SocketApi::command_SHARE_MENU_TITLE(const QString &, SocketListener *listener) @@ -665,23 +668,27 @@ void SocketApi::command_V2_HYDRATE_FILE(const QSharedPointer &jo { const auto &arguments = job->arguments(); - const QString targetPath = arguments[QStringLiteral("file")].toString(); const QByteArray fileId = arguments[QStringLiteral("fileId")].toString().toUtf8(); + const QString targetPath = arguments[QStringLiteral("file")].toString(); auto fileData = FileData::get(targetPath); if (fileData.folder) { - auto watcher = new QFutureWatcher>(); - connect(watcher, &QFutureWatcher>::finished, this, [job, watcher] { - const auto resut = watcher->result>(); - watcher->deleteLater(); - if (!resut) { - job->success({{QStringLiteral("status"), QStringLiteral("ERROR")}, {QStringLiteral("error"), resut.error()}}); - } else { + HydrationJob *hydJob = fileData.folder->vfs().hydrateFile(fileId, targetPath); + + if (hydJob) { + connect(hydJob, &HydrationJob::finished, this, [job, hydJob] { job->success({{QStringLiteral("status"), QStringLiteral("OK")}}); - } - }); - watcher->setFuture(fileData.folder->vfs().hydrateFile(fileId, targetPath)); + hydJob->deleteLater(); + }); + connect(hydJob, &HydrationJob::error, this, [job, hydJob](const QString& err) { + job->success({{QStringLiteral("status"), QStringLiteral("ERROR")}, {QStringLiteral("error"), err}}); + hydJob->deleteLater(); + }); + hydJob->start(); + } else { + qCDebug(lcSocketApi) << "Hydration job for" << fileId << "already running"; + } } else { job->failure(QStringLiteral("cannot hydrate unknown file")); } diff --git a/src/libsync/vfs/hydrationjob.cpp b/src/libsync/vfs/hydrationjob.cpp index a7b4a9c0b5..b6188ca309 100644 --- a/src/libsync/vfs/hydrationjob.cpp +++ b/src/libsync/vfs/hydrationjob.cpp @@ -17,6 +17,16 @@ HydrationJob::HydrationJob(Vfs *vfs, const QByteArray &fileId, std::unique_ptrparams().journal->getFileRecordsByFileId(_fileId, [this](const SyncJournalFileRecord &record) { diff --git a/src/libsync/vfs/hydrationjob.h b/src/libsync/vfs/hydrationjob.h index 68516d9c88..bcb52dc79e 100644 --- a/src/libsync/vfs/hydrationjob.h +++ b/src/libsync/vfs/hydrationjob.h @@ -22,10 +22,16 @@ class OPENCLOUD_SYNC_EXPORT HydrationJob : public QObject void start(); void abort(); + // In case the device to write to is a file, it can be passed here to the result slots + void setTargetFile(const QString& fileName); + QString targetFileName() const; + Vfs *vfs() const; SyncJournalFileRecord record() const; + QByteArray fileId() const { return _fileId; } + Q_SIGNALS: void finished(); void error(const QString &error); @@ -34,6 +40,7 @@ class OPENCLOUD_SYNC_EXPORT HydrationJob : public QObject Vfs *_vfs; QByteArray _fileId; std::unique_ptr _device; + QString _fileName; SyncJournalFileRecord _record; GETFileJob *_job = nullptr; }; diff --git a/src/libsync/vfs/vfs.cpp b/src/libsync/vfs/vfs.cpp index b12976f0f1..c63491501a 100644 --- a/src/libsync/vfs/vfs.cpp +++ b/src/libsync/vfs/vfs.cpp @@ -148,11 +148,10 @@ void Vfs::wipeDehydratedVirtualFiles() // But hydrated placeholders may still be around. } -QFuture> Vfs::hydrateFile(const QByteArray &, const QString &) +HydrationJob* Vfs::hydrateFile(const QByteArray&, const QString&) { - Q_UNUSED(fileId) // nothing to do - return QtFuture::makeReadyValueFuture(Result{}); + return nullptr; } Q_LOGGING_CATEGORY(lcPlugin, "sync.plugins", QtInfoMsg) diff --git a/src/libsync/vfs/vfs.h b/src/libsync/vfs/vfs.h index b3f321d8a6..7bc590d45d 100644 --- a/src/libsync/vfs/vfs.h +++ b/src/libsync/vfs/vfs.h @@ -37,6 +37,7 @@ class Account; class SyncJournalDb; class SyncFileItem; class SyncEngine; +class HydrationJob; /** Collection of parameters for initializing a Vfs instance. */ struct OPENCLOUD_SYNC_EXPORT VfsSetupParams @@ -211,7 +212,7 @@ class OPENCLOUD_SYNC_EXPORT Vfs : public QObject * * Returns a QFuture void if successful and QFuture QString if an error occurs. */ - [[nodiscard]] virtual QFuture> hydrateFile(const QByteArray &fileId, const QString &targetPath); + [[nodiscard]] virtual HydrationJob* hydrateFile(const QByteArray &fileId, const QString &targetPath); public Q_SLOTS: /** Update in-sync state based on SyncFileStatusTracker signal. diff --git a/src/plugins/vfs/xattr/vfs_xattr.cpp b/src/plugins/vfs/xattr/vfs_xattr.cpp index 675f71226f..b3496373ac 100644 --- a/src/plugins/vfs/xattr/vfs_xattr.cpp +++ b/src/plugins/vfs/xattr/vfs_xattr.cpp @@ -10,6 +10,7 @@ #include "filesystem.h" #include "common/syncjournaldb.h" #include "account.h" +#include "vfs/hydrationjob.h" #include #include @@ -33,19 +34,19 @@ int requestId() { } namespace xattr { -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) +const QString ownerXAttrName = QStringLiteral("user.openvfs.owner"); +const QString etagXAttrName = QStringLiteral("user.openvfs.etag"); +const QString fileidXAttrName = QStringLiteral("user.openvfs.fileid"); +const QString modtimeXAttrName = QStringLiteral("user.openvfs.modtime"); +const QString fileSizeXAttrName = QStringLiteral("user.openvfs.fsize"); +const QString actionXAttrName = QStringLiteral("user.openvfs.action"); +const QString stateXAttrName = QStringLiteral("user.openvfs.state"); +const QString pinstateXAttrName = QStringLiteral("user.openvfs.pinstate"); + +OCC::Optional get(const QString &path, const QString& name) { QByteArray result(512, Qt::Initialization::Uninitialized); - auto count = getxattr(path.constData(), name.constData(), result.data(), result.size()); + auto count = getxattr(path.toUtf8().constData(), name.toUtf8().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 @@ -61,11 +62,19 @@ OCC::Optional get(const QByteArray &path, const QByteArray &name) } } -bool set(const QByteArray &path, const QByteArray &name, const QByteArray &value) +bool set(const QString &path, const QString &name, const QString &value) { - const auto returnCode = setxattr(path.constData(), name.constData(), value.constData(), value.size()+1, 0); + const auto returnCode = setxattr(path.toUtf8().constData(), name.toUtf8().constData(), + value.toUtf8().constData(), value.toUtf8().size()+1, 0); return returnCode == 0; } + +bool remove(const QString &path, const QString &name) +{ + const auto returnCode = removexattr(path.toUtf8().constData(), name.toUtf8().constData()); + return returnCode == 0; +} + } namespace OCC { @@ -84,11 +93,45 @@ Vfs::Mode VfsXAttr::mode() const return XAttr; } -void VfsXAttr::startImpl(const VfsSetupParams &) +QString VfsXAttr::xattrOwnerString() const +{ + auto s = QByteArray(APPLICATION_EXECUTABLE); + s.append(":"); + s.append(_setupParams->account->uuid().toByteArray(QUuid::WithoutBraces)); + return QString::fromUtf8(s); +} + +void VfsXAttr::startImpl(const VfsSetupParams ¶ms) { qCDebug(lcVfsXAttr(), "Start XAttr VFS"); - Q_EMIT started(); + // Lets claim the sync root directory for us + const QString& path = params.filesystemPath; + + auto owner = xattr::get(path, ownerXAttrName); + QString err; + + if (!owner) { + // set the owner to opencloud to claim it + if (!xattr::set(path, ownerXAttrName, xattrOwnerString() )) { + err = QStringLiteral("Unable to claim sync root for vfs"); + return; + } + } else { + // owner is set. See if it is us + const auto o = QString::fromUtf8(*owner); + if (o == xattrOwnerString()) { + // all good + } else { + qCDebug(lcVfsXAttr) << "Root-FS has a different owner" << o << "Not our vfs!"; + err = QStringLiteral("VFS path claimed by other cloud, check your setup"); + return; + } + } + if (err.isEmpty()) + Q_EMIT started(); + else + Q_EMIT error(err); } void VfsXAttr::stop() @@ -104,72 +147,38 @@ 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) { + auto xattr = [](const QString& p, const QString& name) { const auto value = xattr::get(p, name); if (value) { - return *value; + return QString::fromUtf8(*value); } else { - return QByteArray(); + return QString(); } }; - 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 = xattr(path, etagXAttrName); + attribs._fileId = xattr(path, fileidXAttrName); - attribs._etag = QString::fromUtf8(xattr(p, etagXAttrName)); - attribs._fileId = xattr(p, fileidXAttrName); - - const QByteArray& tt = xattr(p, modtimeXAttrName); + const QString tt = xattr(path, 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); + attribs._action = xattr(path, actionXAttrName); + attribs._size = xattr(path, fileSizeXAttrName).toLongLong(); + attribs._state = xattr(path, stateXAttrName); + attribs._pinState = xattr(path, pinstateXAttrName); return attribs; } -OCC::Result VfsXAttr::addPlaceholderAttribute(const QString &path, const QByteArray& name, const QByteArray& value) +OCC::Result VfsXAttr::addPlaceholderAttribute(const QString &path, const QString& name, const QString& 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); + auto success = xattr::set(path, name, value); if (!success) { return QStringLiteral("Failed to set the extended attribute"); } @@ -183,17 +192,13 @@ OCC::Result VfsXAttr::updateMetad const auto localPath = QDir::toNativeSeparators(filePath); const auto replacesPath = QDir::toNativeSeparators(replacesFile); - qCDebug(lcVfsXAttr()) << localPath; + qCDebug(lcVfsXAttr) << localPath; 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"); + addPlaceholderAttribute(localPath, actionXAttrName, QStringLiteral("dehydrate")); // FIXME: Error handling auto r = createPlaceholder(syncItem); if (!r) { @@ -201,16 +206,17 @@ OCC::Result VfsXAttr::updateMetad } } else if (syncItem._type == ItemTypeVirtualFileDownload) { - addPlaceholderAttribute(localPath, actionXAttrName, "hydrate"); + addPlaceholderAttribute(localPath, actionXAttrName, QStringLiteral("hydrate")); + qCDebug(lcVfsXAttr) << "FIXME: Do we need to download here?"; // 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 - addPlaceholderAttribute(localPath, fileSizeXAttrName, QByteArray::number(syncItem._size)); - addPlaceholderAttribute(localPath, stateXAttrName, Utility::enumToDisplayName(PinState::OnlineOnly).toUtf8()); - addPlaceholderAttribute(localPath, fileidXAttrName, syncItem._fileId); - addPlaceholderAttribute(localPath, etagXAttrName, syncItem._etag.toUtf8()); + addPlaceholderAttribute(localPath, fileSizeXAttrName, QString::number(syncItem._size)); + addPlaceholderAttribute(localPath, stateXAttrName, QStringLiteral("virtual")); + addPlaceholderAttribute(localPath, fileidXAttrName, QString::fromUtf8(syncItem._fileId)); + addPlaceholderAttribute(localPath, etagXAttrName, syncItem._etag); } else { // FIXME anything to check for other types? qCDebug(lcVfsXAttr) << "Unexpected syncItem Type"; @@ -220,6 +226,61 @@ OCC::Result VfsXAttr::updateMetad return res; } +void VfsXAttr::slotHydrateJobFinished() +{ + HydrationJob *hydration = qobject_cast(sender()); + + const QString targetPath = hydration ->targetFileName(); + Q_ASSERT(!targetPath.isEmpty()); + + qCInfo(lcVfsXAttr) << u"Hydration Job finished for" << targetPath; + + if (QFileInfo::exists(targetPath)) { + auto item = OCC::SyncFileItem::fromSyncJournalFileRecord(hydration->record()); + // the file is now downloaded + item->_type = ItemTypeFile; + FileSystem::getInode(targetPath, &item->_inode); + + // set the xattrs + // the file is not virtual any more, remove the xattrs. No state xattr means local available data + bool ok{true}; + ok = xattr::remove(targetPath, stateXAttrName); + if (!ok) { + qCInfo(lcVfsXAttr) << u"Removing extended file attribute state failed for" << targetPath; + } + ok = ok && xattr::remove(targetPath, actionXAttrName); + if (!ok) { + qCInfo(lcVfsXAttr) << u"Removing extended file attribute action failed for" << targetPath; + } + + if (ok) { + time_t modtime = item->_modtime; + qCInfo(lcVfsXAttr) << u"Setting hydrated file's modtime to" << modtime; + + if (!FileSystem::setModTime(targetPath, modtime)) { + qCInfo(lcVfsXAttr) << u"Failed to set the mod time of the hydrated file" << targetPath; + // What can be done in this error condition + ok = false; + } + } + + if (ok) { + // Update the client sync journal database if the file modifications have been successful + const auto result = this->params().journal->setFileRecord(SyncJournalFileRecord::fromSyncFileItem(*item)); + if (!result) { + qCWarning(lcVfsXAttr) << u"Error when setting the file record to the database" << result.error(); + } else { + qCInfo(lcVfsXAttr) << u"Hydration succeeded" << targetPath; + } + } + } else { + qCWarning(lcVfsXAttr) << u"Hydration succeeded but the file appears to be moved" << targetPath; + } + + hydration->deleteLater(); + this->_hydrationJobs.remove(hydration->fileId()); +} + Result VfsXAttr::createPlaceholder(const SyncFileItem &item) { if (item._modtime <= 0) { @@ -231,85 +292,105 @@ Result VfsXAttr::createPlaceholder(const SyncFileItem &item) 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.exists() + && FileSystem::fileChanged(FileSystem::toFilesystemPath(path), FileSystem::FileChangedInfo::fromSyncFileItem(&item))) { + 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 - */ - addPlaceholderAttribute(path, stateXAttrName, Utility::enumToDisplayName(PinState::OnlineOnly).toUtf8()); + xattr::remove(path, actionXAttrName); // remove the action xattr + // FIXME only write attribs if they're different, and/or all together + addPlaceholderAttribute(path, fileSizeXAttrName, QString::number(item._size)); + addPlaceholderAttribute(path, stateXAttrName, QStringLiteral("virtual")); + addPlaceholderAttribute(path, fileidXAttrName, QString::fromUtf8(item._fileId)); + addPlaceholderAttribute(path, etagXAttrName, item._etag); + FileSystem::setModTime(path, item._modtime); // Ensure the pin state isn't contradictory - const auto pin = pinState(item.localName()); + const auto pin = pinState(path); 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) +HydrationJob* VfsXAttr::hydrateFile(const QByteArray &fileId, const QString &targetPath) { - 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}; + qCInfo(lcVfsXAttr) << u"Requesting hydration for" << fileId; + if (_hydrationJobs.contains(fileId)) { + qCWarning(lcVfsXAttr) << u"Ignoring hydration request for running hydration for fileId" << fileId; + return {}; + } + + HydrationJob *hydration = new HydrationJob(this, fileId, std::make_unique(targetPath), nullptr); + hydration->setTargetFile(targetPath); + _hydrationJobs.insert(fileId, hydration); + + // set an action attrib + addPlaceholderAttribute(targetPath, actionXAttrName, QStringLiteral("hydrate")); + + connect(hydration, &HydrationJob::finished, this, &VfsXAttr::slotHydrateJobFinished); + + connect(hydration, &HydrationJob::error, this, [this, hydration](const QString &error) { + qCWarning(lcVfsXAttr) << u"Hydration failed" << error; + this->_hydrationJobs.remove(hydration->fileId()); + hydration->deleteLater(); + }); + + return hydration; } -bool VfsXAttr::needsMetadataUpdate(const SyncFileItem &) +bool VfsXAttr::needsMetadataUpdate(const SyncFileItem &item) { - qCDebug(lcVfsXAttr()) << "returns false by default DOUBLECHECK"; - return false; + // return true if file exists + const auto path = item.localName(); + QFileInfo fi{path}; + + // FIXME: Unsure about this implementation + bool re{false}; + if (fi.exists()) { + re = true; + } + qCDebug(lcVfsXAttr()) << "returning" << re; + return re; } bool VfsXAttr::isDehydratedPlaceholder(const QString &filePath) { const auto fi = QFileInfo(filePath); + bool re{false}; if (fi.exists()) { const auto attribs = placeHolderAttributes(filePath); - return (attribs.validOwner() && - attribs.state() == Utility::enumToDisplayName(PinState::OnlineOnly).toUtf8()); + re = (attribs.state() == QStringLiteral("virtual")); } - return false; + return re; } LocalInfo VfsXAttr::statTypeVirtualFile(const std::filesystem::directory_entry &path, ItemType type) { + const QString p = FileSystem::fromFilesystemPath(path.path()); if (type == ItemTypeFile) { - const QString p = QString::fromUtf8(path.path().c_str()); //FIXME? - qCDebug(lcVfsXAttr()) << p; auto attribs = placeHolderAttributes(p); - if (attribs.validOwner()) { - bool shouldDownload{false}; - if (attribs.pinState() == Utility::enumToDisplayName(PinState::AlwaysLocal).toUtf8()) { - shouldDownload = true; - } - - // const auto shouldDownload = pin && (*pin == PinState::AlwaysLocal); - if (shouldDownload) { + if (attribs.state() == QStringLiteral("virtual")) { + type = ItemTypeVirtualFile; + if (attribs.pinState() == pinStateToString(PinState::AlwaysLocal)) { type = ItemTypeVirtualFileDownload; - } else { - type = ItemTypeVirtualFile; + } + } else { + if (attribs.pinState() == pinStateToString(PinState::OnlineOnly)) { + type = ItemTypeVirtualFileDehydration; } } } + qCDebug(lcVfsXAttr()) << p << Utility::enumToString(type); return LocalInfo(path, type); } @@ -317,11 +398,13 @@ LocalInfo VfsXAttr::statTypeVirtualFile(const std::filesystem::directory_entry & bool VfsXAttr::setPinState(const QString &folderPath, PinState state) { qCDebug(lcVfsXAttr()) << folderPath << state; - auto stateStr = Utility::enumToDisplayName(state); - auto res = addPlaceholderAttribute(folderPath, pinstateXAttrName, stateStr.toUtf8()); - if (!res) { - qCDebug(lcVfsXAttr()) << "Failed to set pin state"; - return false; + + if (state == PinState::AlwaysLocal || state == PinState::OnlineOnly || state == PinState::Excluded) { + auto stateStr = pinStateToString(state); + addPlaceholderAttribute(folderPath, pinstateXAttrName, stateStr); + } else { + qCDebug(lcVfsXAttr) << "Do not set Pinstate" << pinStateToString(state) << ", remove pinstate xattr"; + xattr::remove(folderPath, pinstateXAttrName); } return true; } @@ -332,19 +415,17 @@ Optional VfsXAttr::pinState(const QString &folderPath) PlaceHolderAttribs attribs = placeHolderAttributes(folderPath); - PinState 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; - } + PinState pState{PinState::Unspecified}; // the default if no owner or state is set + const QString pin = attribs.pinState(); + + if (pin == pinStateToString(PinState::AlwaysLocal)) { + pState = PinState::AlwaysLocal; + } else if (pin == pinStateToString(PinState::Excluded)) { + pState = PinState::Excluded; + } else if (pin.isEmpty() || pin == pinStateToString(PinState::Inherited)) { + pState = PinState::Inherited; + } else if (pin == pinStateToString(PinState::OnlineOnly)) { + pState = PinState::OnlineOnly; } return pState; @@ -387,4 +468,44 @@ void VfsXAttr::fileStatusChanged(const QString& systemFileName, SyncFileStatus f qCDebug(lcVfsXAttr()) << systemFileName << fileStatus; } +QString VfsXAttr::pinStateToString(PinState pState) const +{ + QString re; + switch (pState) { + case OCC::PinState::AlwaysLocal: + re = QStringLiteral("alwayslocal"); + break; + case OCC::PinState::Inherited: + re = QStringLiteral("interited"); + break; + case OCC::PinState::OnlineOnly: + re = QStringLiteral("onlineonly"); + break; + case OCC::PinState::Unspecified: + re = QStringLiteral("unspecified"); + break; + case OCC::PinState::Excluded: + re = QStringLiteral("excluded"); + break; + }; + return re; +} + +PinState VfsXAttr::stringToPinState(const QString& str) const +{ + PinState p{PinState::Unspecified}; + if (str.isEmpty() || str == QStringLiteral("unspecified")) { + p = PinState::Unspecified; + } else if( str == QStringLiteral("alwayslocal")) { + p = PinState::AlwaysLocal; + } else if( str == QStringLiteral("inherited")) { + p = PinState::Inherited; + } else if( str == QStringLiteral("unspecified")) { + p = PinState::Unspecified; + } else if( str == QStringLiteral("excluded")) { + p = PinState::Excluded; + } + return p; +} + } // namespace OCC diff --git a/src/plugins/vfs/xattr/vfs_xattr.h b/src/plugins/vfs/xattr/vfs_xattr.h index 885ed2d0e2..8076b52ec7 100644 --- a/src/plugins/vfs/xattr/vfs_xattr.h +++ b/src/plugins/vfs/xattr/vfs_xattr.h @@ -19,35 +19,26 @@ namespace xattr { struct PlaceHolderAttribs { public: qint64 size() const { return _size; } - QByteArray fileId() const { return _fileId; } + QString 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; } - 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 - - // Always check if we're the valid owner before accessing the xattrs. - bool validOwner() const { return !_owner.isEmpty(); } + QString pinState() const { return _pinState; } + QString action() const { return _action; } + QString state() const { return _state; } qint64 _size; - QByteArray _fileId; + QString _fileId; time_t _modtime; QString _etag; - QByteArray _owner; - QByteArray _pinState; - QByteArray _action; - QByteArray _state; + QString _pinState; + QString _action; + QString _state; }; } namespace OCC { +class HydrationJob; using namespace xattr; @@ -70,8 +61,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 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; @@ -81,17 +70,29 @@ class VfsXAttr : public Vfs Optional pinState(const QString &folderPath) override; AvailabilityResult availability(const QString &folderPath) override; + HydrationJob* hydrateFile(const QByteArray &fileId, const QString& targetPath) override; + + QString pinStateToString(PinState) const; + PinState stringToPinState(const QString&) const; + +Q_SIGNALS: + void finished(Result); + public Q_SLOTS: void fileStatusChanged(const QString &systemFileName, OCC::SyncFileStatus fileStatus) override; + void slotHydrateJobFinished(); + protected: void startImpl(const VfsSetupParams ¶ms) override; private: - QByteArray xattrOwnerString() const; + QString xattrOwnerString() const; PlaceHolderAttribs placeHolderAttributes(const QString& path); - OCC::Result addPlaceholderAttribute(const QString &path, const QByteArray &name = {}, const QByteArray &val = {}); + OCC::Result addPlaceholderAttribute(const QString &path, const QString &name = {}, const QString &val = {}); + OCC::Result removePlaceHolderAttributes(const QString& path); + QMap _hydrationJobs; }; class XattrVfsPluginFactory : public QObject, public DefaultPluginFactory From b376f10d975d6f54f3488060ad37b5428d1d6386 Mon Sep 17 00:00:00 2001 From: Klaas Freitag Date: Mon, 27 Oct 2025 13:56:03 +0100 Subject: [PATCH 3/9] Log improvements in xattr vfs and fix xattr read method --- src/gui/socketapi/socketapi.cpp | 4 +-- src/plugins/vfs/xattr/vfs_xattr.cpp | 55 ++++++++++++++++------------- 2 files changed, 33 insertions(+), 26 deletions(-) diff --git a/src/gui/socketapi/socketapi.cpp b/src/gui/socketapi/socketapi.cpp index 4d7c153364..222dcec9fa 100644 --- a/src/gui/socketapi/socketapi.cpp +++ b/src/gui/socketapi/socketapi.cpp @@ -550,7 +550,7 @@ void SocketApi::command_MAKE_AVAILABLE_LOCALLY(const QString &filesArg, SocketLi continue; // Update the pin state on all items - std::ignore = data.folder->vfs().setPinState(data.folderRelativePath, PinState::AlwaysLocal); + std::ignore = data.folder->vfs().setPinState(data.localPath, PinState::AlwaysLocal); // Trigger sync data.folder->schedulePathForLocalDiscovery(data.folderRelativePath); @@ -569,7 +569,7 @@ void SocketApi::command_MAKE_ONLINE_ONLY(const QString &filesArg, SocketListener continue; // Update the pin state on all items - std::ignore = data.folder->vfs().setPinState(data.folderRelativePath, PinState::OnlineOnly); + std::ignore = data.folder->vfs().setPinState(data.localPath, PinState::OnlineOnly); // Trigger sync data.folder->schedulePathForLocalDiscovery(data.folderRelativePath); diff --git a/src/plugins/vfs/xattr/vfs_xattr.cpp b/src/plugins/vfs/xattr/vfs_xattr.cpp index b3496373ac..1a562259b5 100644 --- a/src/plugins/vfs/xattr/vfs_xattr.cpp +++ b/src/plugins/vfs/xattr/vfs_xattr.cpp @@ -65,7 +65,7 @@ OCC::Optional get(const QString &path, const QString& name) bool set(const QString &path, const QString &name, const QString &value) { const auto returnCode = setxattr(path.toUtf8().constData(), name.toUtf8().constData(), - value.toUtf8().constData(), value.toUtf8().size()+1, 0); + value.toUtf8().constData(), value.toUtf8().size(), 0); return returnCode == 0; } @@ -179,6 +179,7 @@ OCC::Result VfsXAttr::addPlaceholderAttribute(const QString &path { if (!name.isEmpty()) { auto success = xattr::set(path, name, value); + // Q_ASSERT(success); if (!success) { return QStringLiteral("Failed to set the extended attribute"); } @@ -192,37 +193,39 @@ OCC::Result VfsXAttr::updateMetad const auto localPath = QDir::toNativeSeparators(filePath); const auto replacesPath = QDir::toNativeSeparators(replacesFile); - qCDebug(lcVfsXAttr) << localPath; + qCDebug(lcVfsXAttr) << localPath << syncItem._type; - PlaceHolderAttribs attribs = placeHolderAttributes(localPath); + // PlaceHolderAttribs attribs = placeHolderAttributes(localPath); OCC::Vfs::ConvertToPlaceholderResult res{OCC::Vfs::ConvertToPlaceholderResult::Ok}; if (syncItem._type == ItemTypeVirtualFileDehydration) { // - addPlaceholderAttribute(localPath, actionXAttrName, QStringLiteral("dehydrate")); // FIXME: Error handling auto r = createPlaceholder(syncItem); if (!r) { res = OCC::Vfs::ConvertToPlaceholderResult::Locked; } - + addPlaceholderAttribute(localPath, actionXAttrName, QStringLiteral("dehydrate")); + addPlaceholderAttribute(localPath, stateXAttrName, QStringLiteral("virtual")); } else if (syncItem._type == ItemTypeVirtualFileDownload) { addPlaceholderAttribute(localPath, actionXAttrName, QStringLiteral("hydrate")); - qCDebug(lcVfsXAttr) << "FIXME: Do we need to download here?"; - // start to download? FIXME + // file gets downloaded and becomes a normal file, the xattr gets removed + xattr::remove(localPath, stateXAttrName); } else if (syncItem._type == ItemTypeVirtualFile) { - FileSystem::setModTime(localPath, syncItem._modtime); - - // FIXME only write attribs if they're different, and/or all together - addPlaceholderAttribute(localPath, fileSizeXAttrName, QString::number(syncItem._size)); - addPlaceholderAttribute(localPath, stateXAttrName, QStringLiteral("virtual")); - addPlaceholderAttribute(localPath, fileidXAttrName, QString::fromUtf8(syncItem._fileId)); - addPlaceholderAttribute(localPath, etagXAttrName, syncItem._etag); + qCDebug(lcVfsXAttr) << "updateMetadata for virtual file " << syncItem._type; + addPlaceholderAttribute(localPath, stateXAttrName, QStringLiteral("virtual")); } else { - // FIXME anything to check for other types? - qCDebug(lcVfsXAttr) << "Unexpected syncItem Type"; + qCDebug(lcVfsXAttr) << "Unexpected syncItem Type" << syncItem._type; } - // FIXME Errorhandling + FileSystem::setModTime(localPath, syncItem._modtime); + + addPlaceholderAttribute(localPath, fileSizeXAttrName, QString::number(syncItem._size)); + addPlaceholderAttribute(localPath, fileidXAttrName, QString::fromUtf8(syncItem._fileId)); + addPlaceholderAttribute(localPath, etagXAttrName, syncItem._etag); + + // remove the action marker again + xattr::remove(localPath, actionXAttrName); + return res; } @@ -293,7 +296,8 @@ Result VfsXAttr::createPlaceholder(const SyncFileItem &item) QFile file(path); if (file.exists() - && FileSystem::fileChanged(FileSystem::toFilesystemPath(path), FileSystem::FileChangedInfo::fromSyncFileItem(&item))) { + && FileSystem::fileChanged(FileSystem::toFilesystemPath(path), + FileSystem::FileChangedInfo::fromSyncFileItem(&item))) { return QStringLiteral("Cannot create a placeholder because a file with the placeholder name already exist"); } @@ -411,7 +415,6 @@ bool VfsXAttr::setPinState(const QString &folderPath, PinState state) Optional VfsXAttr::pinState(const QString &folderPath) { - qCDebug(lcVfsXAttr()) << folderPath; PlaceHolderAttribs attribs = placeHolderAttributes(folderPath); @@ -427,35 +430,39 @@ Optional VfsXAttr::pinState(const QString &folderPath) } else if (pin == pinStateToString(PinState::OnlineOnly)) { pState = PinState::OnlineOnly; } + qCDebug(lcVfsXAttr()) << folderPath << pState; return pState; } Vfs::AvailabilityResult VfsXAttr::availability(const QString &folderPath) { - qCDebug(lcVfsXAttr()) << folderPath; const auto basePinState = pinState(folderPath); + Vfs::AvailabilityResult res {VfsItemAvailability::Mixed}; if (basePinState) { switch (*basePinState) { case OCC::PinState::AlwaysLocal: - return VfsItemAvailability::AlwaysLocal; + res = VfsItemAvailability::AlwaysLocal; break; case OCC::PinState::Inherited: break; case OCC::PinState::OnlineOnly: - return VfsItemAvailability::OnlineOnly; + res = VfsItemAvailability::OnlineOnly; break; case OCC::PinState::Unspecified: break; case OCC::PinState::Excluded: break; }; - return VfsItemAvailability::Mixed; + res = VfsItemAvailability::Mixed; } else { - return AvailabilityError::NoSuchItem; + res = AvailabilityError::NoSuchItem; } + qCDebug(lcVfsXAttr()) << folderPath << res.get(); + + return res; } void VfsXAttr::fileStatusChanged(const QString& systemFileName, SyncFileStatus fileStatus) From ec9f5c55d28f5ddcafbaee116bcaf6e16a1b6ab6 Mon Sep 17 00:00:00 2001 From: Klaas Freitag Date: Fri, 31 Oct 2025 16:01:16 +0100 Subject: [PATCH 4/9] SocketAPI: Add virtual file status and pinstate to file status msg --- src/gui/socketapi/socketapi.cpp | 18 ++++++++++++++++++ src/plugins/vfs/xattr/vfs_xattr.cpp | 7 ++++++- src/resources/fonticon.h | 8 +++++--- 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/gui/socketapi/socketapi.cpp b/src/gui/socketapi/socketapi.cpp index 222dcec9fa..5a9b3d5e9b 100644 --- a/src/gui/socketapi/socketapi.cpp +++ b/src/gui/socketapi/socketapi.cpp @@ -428,6 +428,24 @@ void SocketApi::command_RETRIEVE_FILE_STATUS(const QString &argument, SocketList listener->registerMonitoredDirectory(qHash(directory)); statusString = fileData.syncFileStatus().toSocketAPIString(); + +#ifdef Q_OS_LINUX + // append vfs status in case... + QString vfsStatus; + const auto fileType = fileData.journalRecord().type(); + if (fileType == ItemTypeVirtualFileDownload || fileType == ItemTypeVirtualFile) { + vfsStatus = QStringLiteral("+VIRT"); + } + const auto pState = fileData.folder->vfs().pinState(argument); + if (pState) { + if (*pState == PinState::AlwaysLocal) { + vfsStatus += QStringLiteral("+AL"); + } else if (*pState == PinState::OnlineOnly) { + vfsStatus += QStringLiteral("+OO"); + } + } + statusString.append(vfsStatus); +#endif } const QString message = QStringLiteral("STATUS:") % statusString % QLatin1Char(':') % QDir::toNativeSeparators(argument); diff --git a/src/plugins/vfs/xattr/vfs_xattr.cpp b/src/plugins/vfs/xattr/vfs_xattr.cpp index 1a562259b5..bfa6970a79 100644 --- a/src/plugins/vfs/xattr/vfs_xattr.cpp +++ b/src/plugins/vfs/xattr/vfs_xattr.cpp @@ -242,7 +242,12 @@ void VfsXAttr::slotHydrateJobFinished() auto item = OCC::SyncFileItem::fromSyncJournalFileRecord(hydration->record()); // the file is now downloaded item->_type = ItemTypeFile; - FileSystem::getInode(targetPath, &item->_inode); + + if (auto inode = FileSystem::getInode(FileSystem::toFilesystemPath(targetPath))) { + item->_inode = inode.value(); + } else { + qCWarning(lcVfsXAttr) << u"Failed to get inode for" << targetPath; + } // set the xattrs // the file is not virtual any more, remove the xattrs. No state xattr means local available data diff --git a/src/resources/fonticon.h b/src/resources/fonticon.h index 388526efc1..c1b7594e5f 100644 --- a/src/resources/fonticon.h +++ b/src/resources/fonticon.h @@ -13,6 +13,7 @@ class OPENCLOUD_RESOURCES_EXPORT FontIcon : public QIcon { Q_GADGET QML_VALUE_TYPE(fontIcon) + public: enum class DefaultGlyphes : char16_t { Question = u'', @@ -20,13 +21,13 @@ class OPENCLOUD_RESOURCES_EXPORT FontIcon : public QIcon Info = u'' }; - Q_ENUM(DefaultGlyphes); + Q_ENUM(DefaultGlyphes) enum class FontFamily : uint8_t { FontAwesome, RemixIcon, }; - Q_ENUM(FontFamily); + Q_ENUM(FontFamily) enum class Size : uint8_t { // fullsized icon @@ -34,7 +35,8 @@ class OPENCLOUD_RESOURCES_EXPORT FontIcon : public QIcon // hafl sized, centered icon Half }; - Q_ENUM(Size); + Q_ENUM(Size) + FontIcon(); // defaults to fontawesoem FontIcon(QChar glyphe, Size size = Size::Normal, const QColor &color = {}); From 3c1d1181a8b8d9af3f8b0c3c7aaa852ff2b2c2ac Mon Sep 17 00:00:00 2001 From: Klaas Freitag Date: Thu, 20 Nov 2025 21:47:22 +0100 Subject: [PATCH 5/9] Return XAttr vfs plugin for Linux and Mac --- src/gui/folderwizard/folderwizard.cpp | 5 + src/gui/guiutility.cpp | 6 +- src/libsync/CMakeLists.txt | 16 +-- src/libsync/filesystem.cpp | 80 +++--------- src/libsync/filesystem.h | 6 +- src/libsync/xattr.cpp | 68 +++++++++++ src/libsync/xattr.h | 23 ++++ src/plugins/vfs/xattr/CMakeLists.txt | 2 + src/plugins/vfs/xattr/vfs_xattr.cpp | 167 +++++++++----------------- src/plugins/vfs/xattr/vfs_xattr.h | 2 +- test/testutility.cpp | 4 +- 11 files changed, 190 insertions(+), 189 deletions(-) create mode 100644 src/libsync/xattr.cpp create mode 100644 src/libsync/xattr.h diff --git a/src/gui/folderwizard/folderwizard.cpp b/src/gui/folderwizard/folderwizard.cpp index c414f98078..b9356d6976 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 { +#ifdef Q_OS_WIN return VfsPluginManager::instance().bestAvailableVfsMode() == Vfs::WindowsCfApi; +#elif defined(Q_OS_UNIX) && !defined(Q_OS_MAC) + return VfsPluginManager::instance().bestAvailableVfsMode() == Vfs::XAttr; +#endif + return false; } FolderWizard::FolderWizard(const AccountStatePtr &account, QWidget *parent) diff --git a/src/gui/guiutility.cpp b/src/gui/guiutility.cpp index 7215aa3b1a..fc545c6121 100644 --- a/src/gui/guiutility.cpp +++ b/src/gui/guiutility.cpp @@ -100,7 +100,7 @@ void Utility::markDirectoryAsSyncRoot(const QString &path, const QUuid &accountU Q_ASSERT(getDirectorySyncRootMarkings(path).first.isEmpty()); Q_ASSERT(getDirectorySyncRootMarkings(path).second.isNull()); - auto result1 = FileSystem::Tags::set(path, dirTag(), Theme::instance()->orgDomainName().toUtf8()); + auto result1 = FileSystem::Tags::set(path, dirTag(), Theme::instance()->orgDomainName()); if (!result1) { qCWarning(lcGuiUtility) << QStringLiteral("Failed to set tag on »%1«: %2").arg(path, result1.error()) #ifdef Q_OS_WIN @@ -110,7 +110,7 @@ void Utility::markDirectoryAsSyncRoot(const QString &path, const QUuid &accountU return; } - auto result2 = FileSystem::Tags::set(path, uuidTag(), accountUuid.toString().toUtf8()); + auto result2 = FileSystem::Tags::set(path, uuidTag(), accountUuid.toString()); if (!result2) { qCWarning(lcGuiUtility) << QStringLiteral("Failed to set tag on »%1«: %2").arg(path, result2.error()) #ifdef Q_OS_WIN @@ -127,7 +127,7 @@ std::pair Utility::getDirectorySyncRootMarkings(const QString &p auto existingUuidTag = FileSystem::Tags::get(path, uuidTag()); if (existingDirTag.has_value() && existingUuidTag.has_value()) { - return {QString::fromUtf8(existingDirTag.value()), QUuid::fromString(QString::fromUtf8(existingUuidTag.value()))}; + return {existingDirTag.value(), QUuid::fromString(existingUuidTag.value())}; } return {}; diff --git a/src/libsync/CMakeLists.txt b/src/libsync/CMakeLists.txt index d5478d3757..b72a2aa6cb 100644 --- a/src/libsync/CMakeLists.txt +++ b/src/libsync/CMakeLists.txt @@ -7,7 +7,8 @@ set_package_properties(LibreGraphAPI PROPERTIES configure_file(config.h.in ${CMAKE_CURRENT_BINARY_DIR}/config.h) -set(libsync_SRCS + +add_library(libsync SHARED account.cpp bandwidthmanager.cpp capabilities.cpp @@ -45,8 +46,9 @@ set(libsync_SRCS syncoptions.cpp theme.cpp - creds/credentialmanager.cpp creds/abstractcredentials.cpp + creds/credentialmanager.cpp + creds/httpcredentials.cpp creds/oauth.cpp creds/jwt.cpp creds/webfinger.cpp @@ -65,17 +67,16 @@ set(libsync_SRCS ) if(WIN32) - list(APPEND libsync_SRCS platform_win.cpp) + target_sources(libsync PRIVATE platform_win.cpp) elseif(UNIX) + target_sources(libsync PRIVATE xattr.cpp) if (APPLE) - list(APPEND libsync_SRCS platform_mac.mm) + target_sources(libsync PRIVATE platform_mac.mm) else() - list(APPEND libsync_SRCS platform_unix.cpp) + target_sources(libsync PRIVATE platform_unix.cpp) endif() endif() -set(libsync_SRCS ${libsync_SRCS} creds/httpcredentials.cpp) - # These headers are installed for libopencloudsync to be used by 3rd party apps INSTALL( FILES @@ -85,7 +86,6 @@ INSTALL( DESTINATION ${KDE_INSTALL_INCLUDEDIR}/${APPLICATION_SHORTNAME}/libsync ) -add_library(libsync SHARED ${libsync_SRCS}) set_target_properties(libsync PROPERTIES EXPORT_NAME SyncCore) target_link_libraries(libsync diff --git a/src/libsync/filesystem.cpp b/src/libsync/filesystem.cpp index e27854a314..b2acd2f98d 100644 --- a/src/libsync/filesystem.cpp +++ b/src/libsync/filesystem.cpp @@ -17,6 +17,7 @@ #include "common/asserts.h" #include "common/utility.h" #include "libsync/discoveryinfo.h" +#include "libsync/xattr.h" #include #include @@ -28,10 +29,6 @@ #include -#if defined(Q_OS_MAC) || defined(Q_OS_LINUX) -#include -#endif - #ifdef Q_OS_WIN32 #include "common/utility_win.h" #include @@ -250,52 +247,25 @@ std::optional FileSystem::getInode(const std::filesystem::path &filena return info.inode(); } -namespace { - -#ifdef Q_OS_LINUX - Q_ALWAYS_INLINE ssize_t getxattr(const char *path, const char *name, void *value, size_t size, u_int32_t, int) - { - return ::getxattr(path, name, value, size); - } - - Q_ALWAYS_INLINE int setxattr(const char *path, const char *name, const void *value, size_t size, u_int32_t, int) - { - return ::setxattr(path, name, value, size, 0); - } - - Q_ALWAYS_INLINE int removexattr(const char *path, const char *name, int) - { - return ::removexattr(path, name); - } -#endif // Q_OS_LINUX - -} // anonymous namespace - -std::optional FileSystem::Tags::get(const QString &path, const QString &key) +std::optional FileSystem::Tags::get(const QString &path, const QString &key) { #if defined(Q_OS_MAC) || defined(Q_OS_LINUX) QString platformKey = key; if (Utility::isLinux()) { platformKey = QStringLiteral("user.") + platformKey; } - - QByteArray value(MaxValueSize + 1, '\0'); // Add a NUL character to terminate a string - auto size = getxattr(path.toUtf8().constData(), platformKey.toUtf8().constData(), value.data(), MaxValueSize, 0, 0); - if (size != -1) { - value.truncate(size); - return value; - } + return Xattr::getxattr(toFilesystemPath(path), platformKey); #elif defined(Q_OS_WIN) QFile file(QStringLiteral("%1:%2").arg(path, key)); if (file.open(QIODevice::ReadOnly)) { - return file.readAll(); + return QString::fromUtf8(file.readAll()); } #endif // Q_OS_MAC || Q_OS_LINUX return {}; } -OCC::Result FileSystem::Tags::set(const QString &path, const QString &key, const QByteArray &value) +OCC::Result FileSystem::Tags::set(const QString &path, const QString &key, const QString &value) { OC_ASSERT(value.size() < MaxValueSize) @@ -304,30 +274,25 @@ OCC::Result FileSystem::Tags::set(const QString &path, const QStr if (Utility::isLinux()) { platformKey = QStringLiteral("user.") + platformKey; } - - auto result = setxattr(path.toUtf8().constData(), platformKey.toUtf8().constData(), value.constData(), value.size(), 0, 0); - if (result != 0) { - return QString::fromUtf8(strerror(errno)); - } - - return {}; + return Xattr::setxattr(toFilesystemPath(path), platformKey, value); #elif defined(Q_OS_WIN) QFile file(QStringLiteral("%1:%2").arg(path, key)); if (!file.open(QIODevice::WriteOnly)) { return file.errorString(); } - auto bytesWritten = file.write(value); - if (bytesWritten != value.size()) { - return QStringLiteral("wrote %1 out of %2 bytes").arg(QString::number(bytesWritten), QString::number(value.size())); + const auto data = value.toUtf8(); + auto bytesWritten = file.write(data); + if (bytesWritten != data.size()) { + return QStringLiteral("wrote %1 out of %2 bytes").arg(QString::number(bytesWritten), QString::number(data.size())); } return {}; #else - return QStringLiteral("function not implemented"); + return u"Not implemented"_s; #endif // Q_OS_MAC || Q_OS_LINUX } -bool FileSystem::Tags::remove(const QString &path, const QString &key) +OCC::Result FileSystem::Tags::remove(const QString &path, const QString &key) { #if defined(Q_OS_MAC) || defined(Q_OS_LINUX) QString platformKey = key; @@ -335,31 +300,18 @@ bool FileSystem::Tags::remove(const QString &path, const QString &key) platformKey = QStringLiteral("user.%1").arg(platformKey); } - auto result = removexattr(path.toUtf8().constData(), platformKey.toUtf8().constData(), 0); - if (result == 0) { - return true; - } -#ifdef Q_OS_MAC - if (errno == ENOATTR) { -#else - if (errno == ENODATA) { -#endif - qCWarning(lcFileSystem) << u"Failed to remove tag" << key << u"from" << path << u"tag doesn't exist"; - return true; - } - qCWarning(lcFileSystem) << u"Failed to remove tag" << key << u"from" << path << u":" << strerror(errno); - return false; + return Xattr::removexattr(toFilesystemPath(path), platformKey); #elif defined(Q_OS_WIN) const auto fsPath = toFilesystemPath(u"%1:%2"_s.arg(path, key)); std::error_code fileError; std::filesystem::remove(fsPath, fileError); if (fileError) { qCWarning(lcFileSystem) << u"Failed to remove tag" << key << u"from" << path << u":" << fsPath << u"error:" << fileError.message(); - return false; + return QString::fromStdString(fileError.message()); } - return true; + return {}; #else - return false; + return u"Not implemented"_s; #endif // Q_OS_MAC || Q_OS_LINUX } } // namespace OCC diff --git a/src/libsync/filesystem.h b/src/libsync/filesystem.h index c45497d801..0a9ce0d75e 100644 --- a/src/libsync/filesystem.h +++ b/src/libsync/filesystem.h @@ -128,9 +128,9 @@ namespace FileSystem { bool OPENCLOUD_SYNC_EXPORT removeRecursively(const QString &path, RemoveEntryList *success, RemoveEntryList *locked, RemoveErrorList *errors); namespace Tags { - std::optional OPENCLOUD_SYNC_EXPORT get(const QString &path, const QString &key); - OCC::Result OPENCLOUD_SYNC_EXPORT set(const QString &path, const QString &key, const QByteArray &value); - bool OPENCLOUD_SYNC_EXPORT remove(const QString &path, const QString &key); + std::optional OPENCLOUD_SYNC_EXPORT get(const QString &path, const QString &key); + OCC::Result OPENCLOUD_SYNC_EXPORT set(const QString &path, const QString &key, const QString &value); + OCC::Result OPENCLOUD_SYNC_EXPORT remove(const QString &path, const QString &key); } } diff --git a/src/libsync/xattr.cpp b/src/libsync/xattr.cpp new file mode 100644 index 0000000000..07a8cb808e --- /dev/null +++ b/src/libsync/xattr.cpp @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2025 Hannah von Reth + +#include "xattr.h" + +#include + +#include "common/filesystembase.h" + +namespace OCC { +namespace FileSystem { + std::optional Xattr::getxattr(const std::filesystem::path &path, const QString &name) + { + QByteArray value; + ssize_t res = 0; + do { + value.resize(value.size() + 255); +#ifdef Q_OS_MAC + res = ::getxattr(path.c_str(), name.toUtf8().constData(), value.data(), value.size(), 0, XATTR_NOFOLLOW); +#else + res = ::lgetxattr(path.c_str(), name.toUtf8().constData(), value.data(), value.size()); +#endif + } while (res == -1 && errno == ERANGE); + if (res > 0) { + value.resize(res); + return QString::fromUtf8(value); + } else { + return {}; + } + } + + Result Xattr::setxattr(const std::filesystem::path &path, const QString &name, const QString &value) + { + const auto data = value.toUtf8(); +#ifdef Q_OS_MAC + const auto result = ::setxattr(path.c_str(), name.toUtf8().constData(), data.constData(), data.size(), 0, XATTR_NOFOLLOW); +#else + const auto result = ::lsetxattr(path.c_str(), name.toUtf8().constData(), data.constData(), data.size(), 0); +#endif + if (result != 0) { + return QString::fromUtf8(strerror(errno)); + } + return {}; + } + + Result Xattr::removexattr(const std::filesystem::path &path, const QString &name) + { +#ifdef Q_OS_MAC + const auto result = ::removexattr(path.c_str(), name.toUtf8().constData(), 0); +#else + const auto result = ::lremovexattr(path.c_str(), name.toUtf8().constData()); +#endif + +#ifdef Q_OS_MAC + if (errno == ENOATTR) { +#else + if (errno == ENODATA) { +#endif + qCWarning(lcFileSystem) << u"Failed to remove tag" << name << u"from" << path.native() << u"tag doesn't exist"; + return {}; + } + if (result != 0) { + return QString::fromUtf8(strerror(errno)); + } + return {}; + } +} +} diff --git a/src/libsync/xattr.h b/src/libsync/xattr.h new file mode 100644 index 0000000000..a99358fb1d --- /dev/null +++ b/src/libsync/xattr.h @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2025 Hannah von Reth + +#pragma once + +#include "libsync/common/result.h" +#include "libsync/opencloudsynclib.h" + +#include +#include + +#include + +namespace OCC { +namespace FileSystem { + namespace Xattr { + OPENCLOUD_SYNC_EXPORT std::optional getxattr(const std::filesystem::path &path, const QString &name); + OPENCLOUD_SYNC_EXPORT Result setxattr(const std::filesystem::path &path, const QString &name, const QString &value); + + OPENCLOUD_SYNC_EXPORT Result removexattr(const std::filesystem::path &path, const QString &name); + } +} +} \ No newline at end of file diff --git a/src/plugins/vfs/xattr/CMakeLists.txt b/src/plugins/vfs/xattr/CMakeLists.txt index c71f400ae3..3e1df3a3bb 100644 --- a/src/plugins/vfs/xattr/CMakeLists.txt +++ b/src/plugins/vfs/xattr/CMakeLists.txt @@ -1,4 +1,6 @@ +if (NOT WIN32) add_vfs_plugin(NAME xattr SRC vfs_xattr.cpp ) +endif() diff --git a/src/plugins/vfs/xattr/vfs_xattr.cpp b/src/plugins/vfs/xattr/vfs_xattr.cpp index bfa6970a79..a7238df834 100644 --- a/src/plugins/vfs/xattr/vfs_xattr.cpp +++ b/src/plugins/vfs/xattr/vfs_xattr.cpp @@ -6,10 +6,11 @@ #include "vfs_xattr.h" -#include "syncfileitem.h" -#include "filesystem.h" -#include "common/syncjournaldb.h" #include "account.h" +#include "common/syncjournaldb.h" +#include "filesystem.h" +#include "libsync/xattr.h" +#include "syncfileitem.h" #include "vfs/hydrationjob.h" #include @@ -18,22 +19,10 @@ #include #include -#include - Q_LOGGING_CATEGORY(lcVfsXAttr, "sync.vfs.xattr", QtInfoMsg) -namespace { - -#if 0 -std::atomic id{1}; -int requestId() { - return id++; -} -#endif -} - -namespace xattr { +namespace { const QString ownerXAttrName = QStringLiteral("user.openvfs.owner"); const QString etagXAttrName = QStringLiteral("user.openvfs.etag"); const QString fileidXAttrName = QStringLiteral("user.openvfs.fileid"); @@ -42,45 +31,10 @@ const QString fileSizeXAttrName = QStringLiteral("user.openvfs.fsize"); const QString actionXAttrName = QStringLiteral("user.openvfs.action"); const QString stateXAttrName = QStringLiteral("user.openvfs.state"); const QString pinstateXAttrName = QStringLiteral("user.openvfs.pinstate"); - -OCC::Optional get(const QString &path, const QString& name) -{ - QByteArray result(512, Qt::Initialization::Uninitialized); - auto count = getxattr(path.toUtf8().constData(), name.toUtf8().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 QString &path, const QString &name, const QString &value) -{ - const auto returnCode = setxattr(path.toUtf8().constData(), name.toUtf8().constData(), - value.toUtf8().constData(), value.toUtf8().size(), 0); - return returnCode == 0; -} - -bool remove(const QString &path, const QString &name) -{ - const auto returnCode = removexattr(path.toUtf8().constData(), name.toUtf8().constData()); - return returnCode == 0; -} - } namespace OCC { -using namespace xattr; - VfsXAttr::VfsXAttr(QObject *parent) : Vfs(parent) { @@ -103,28 +57,27 @@ QString VfsXAttr::xattrOwnerString() const void VfsXAttr::startImpl(const VfsSetupParams ¶ms) { - qCDebug(lcVfsXAttr(), "Start XAttr VFS"); + qCDebug(lcVfsXAttr, "Start XAttr VFS"); // Lets claim the sync root directory for us - const QString& path = params.filesystemPath; + const auto path = FileSystem::toFilesystemPath(params.filesystemPath); - auto owner = xattr::get(path, ownerXAttrName); + auto owner = FileSystem::Xattr::getxattr(path, ownerXAttrName); QString err; if (!owner) { // set the owner to opencloud to claim it - if (!xattr::set(path, ownerXAttrName, xattrOwnerString() )) { - err = QStringLiteral("Unable to claim sync root for vfs"); + if (!FileSystem::Xattr::setxattr(path, ownerXAttrName, xattrOwnerString() )) { + err = Vfs::tr("Unable to claim the sync root for files on demand"); return; } } else { // owner is set. See if it is us - const auto o = QString::fromUtf8(*owner); - if (o == xattrOwnerString()) { + if (owner.value() == xattrOwnerString()) { // all good } else { - qCDebug(lcVfsXAttr) << "Root-FS has a different owner" << o << "Not our vfs!"; - err = QStringLiteral("VFS path claimed by other cloud, check your setup"); + qCDebug(lcVfsXAttr) << "Root-FS has a different owner" << owner.value() << "Not our vfs!"; + err = Vfs::tr("The sync path is claimed by a different cloud, please check your setup"); return; } } @@ -150,27 +103,20 @@ bool VfsXAttr::socketApiPinStateActionsShown() const PlaceHolderAttribs VfsXAttr::placeHolderAttributes(const QString& path) { PlaceHolderAttribs attribs; + const auto fPath = FileSystem::toFilesystemPath(path); // lambda to handle the Optional return val of xattrGet - auto xattr = [](const QString& p, const QString& name) { - const auto value = xattr::get(p, name); - if (value) { - return QString::fromUtf8(*value); - } else { - return QString(); - } - }; - attribs._etag = xattr(path, etagXAttrName); - attribs._fileId = xattr(path, fileidXAttrName); + attribs._etag = FileSystem::Xattr::getxattr(fPath, etagXAttrName).value_or(QString()); + attribs._fileId = FileSystem::Xattr::getxattr(fPath, fileidXAttrName).value_or(QString()); - const QString tt = xattr(path, modtimeXAttrName); + const QString tt = FileSystem::Xattr::getxattr(fPath, modtimeXAttrName).value_or(QString()); attribs._modtime = tt.toLongLong(); - attribs._action = xattr(path, actionXAttrName); - attribs._size = xattr(path, fileSizeXAttrName).toLongLong(); - attribs._state = xattr(path, stateXAttrName); - attribs._pinState = xattr(path, pinstateXAttrName); + attribs._action = FileSystem::Xattr::getxattr(fPath, actionXAttrName).value_or(QString()); + attribs._size = FileSystem::Xattr::getxattr(fPath, fileSizeXAttrName).value_or(QString()).toLongLong(); + attribs._state = FileSystem::Xattr::getxattr(fPath, stateXAttrName).value_or(QString()); + attribs._pinState = FileSystem::Xattr::getxattr(fPath, pinstateXAttrName).value_or(QString()); return attribs; } @@ -178,10 +124,10 @@ PlaceHolderAttribs VfsXAttr::placeHolderAttributes(const QString& path) OCC::Result VfsXAttr::addPlaceholderAttribute(const QString &path, const QString& name, const QString& value) { if (!name.isEmpty()) { - auto success = xattr::set(path, name, value); + auto success = FileSystem::Xattr::setxattr(FileSystem::toFilesystemPath(path), name, value); // Q_ASSERT(success); if (!success) { - return QStringLiteral("Failed to set the extended attribute"); + return Vfs::tr("Failed to set the extended file attribute"); } } @@ -190,8 +136,8 @@ OCC::Result VfsXAttr::addPlaceholderAttribute(const QString &path 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); + Q_UNUSED(replacesFile); + const auto localPath = FileSystem::toFilesystemPath(filePath); qCDebug(lcVfsXAttr) << localPath << syncItem._type; @@ -204,27 +150,35 @@ OCC::Result VfsXAttr::updateMetad if (!r) { res = OCC::Vfs::ConvertToPlaceholderResult::Locked; } - addPlaceholderAttribute(localPath, actionXAttrName, QStringLiteral("dehydrate")); - addPlaceholderAttribute(localPath, stateXAttrName, QStringLiteral("virtual")); + addPlaceholderAttribute(filePath, actionXAttrName, QStringLiteral("dehydrate")); + addPlaceholderAttribute(filePath, stateXAttrName, QStringLiteral("virtual")); + addPlaceholderAttribute(filePath, fileSizeXAttrName, QString::number(syncItem._size)); } else if (syncItem._type == ItemTypeVirtualFileDownload) { - addPlaceholderAttribute(localPath, actionXAttrName, QStringLiteral("hydrate")); + addPlaceholderAttribute(filePath, actionXAttrName, QStringLiteral("hydrate")); // file gets downloaded and becomes a normal file, the xattr gets removed - xattr::remove(localPath, stateXAttrName); + FileSystem::Xattr::removexattr(localPath, stateXAttrName); + FileSystem::Xattr::removexattr(localPath, fileSizeXAttrName); } else if (syncItem._type == ItemTypeVirtualFile) { qCDebug(lcVfsXAttr) << "updateMetadata for virtual file " << syncItem._type; - addPlaceholderAttribute(localPath, stateXAttrName, QStringLiteral("virtual")); + addPlaceholderAttribute(filePath, stateXAttrName, QStringLiteral("virtual")); + addPlaceholderAttribute(filePath, fileSizeXAttrName, QString::number(syncItem._size)); + } else if (syncItem._type == ItemTypeFile) { + qCDebug(lcVfsXAttr) << "updateMetadata for normal file " << syncItem._type; + FileSystem::Xattr::removexattr(localPath, fileSizeXAttrName); + } else if (syncItem._type == ItemTypeDirectory) { + qCDebug(lcVfsXAttr) << "updateMetadata for directory" << syncItem._type; } else { qCDebug(lcVfsXAttr) << "Unexpected syncItem Type" << syncItem._type; + Q_UNREACHABLE(); } FileSystem::setModTime(localPath, syncItem._modtime); - addPlaceholderAttribute(localPath, fileSizeXAttrName, QString::number(syncItem._size)); - addPlaceholderAttribute(localPath, fileidXAttrName, QString::fromUtf8(syncItem._fileId)); - addPlaceholderAttribute(localPath, etagXAttrName, syncItem._etag); + addPlaceholderAttribute(filePath, fileidXAttrName, QString::fromUtf8(syncItem._fileId)); + addPlaceholderAttribute(filePath, etagXAttrName, syncItem._etag); // remove the action marker again - xattr::remove(localPath, actionXAttrName); + FileSystem::Xattr::removexattr(localPath, actionXAttrName); return res; } @@ -233,17 +187,17 @@ void VfsXAttr::slotHydrateJobFinished() { HydrationJob *hydration = qobject_cast(sender()); - const QString targetPath = hydration ->targetFileName(); - Q_ASSERT(!targetPath.isEmpty()); + const auto targetPath = FileSystem::toFilesystemPath(hydration->targetFileName()); + Q_ASSERT(!targetPath.empty()); qCInfo(lcVfsXAttr) << u"Hydration Job finished for" << targetPath; - if (QFileInfo::exists(targetPath)) { + if (std::filesystem::exists(targetPath)) { auto item = OCC::SyncFileItem::fromSyncJournalFileRecord(hydration->record()); // the file is now downloaded item->_type = ItemTypeFile; - if (auto inode = FileSystem::getInode(FileSystem::toFilesystemPath(targetPath))) { + if (auto inode = FileSystem::getInode(targetPath)) { item->_inode = inode.value(); } else { qCWarning(lcVfsXAttr) << u"Failed to get inode for" << targetPath; @@ -252,11 +206,12 @@ void VfsXAttr::slotHydrateJobFinished() // set the xattrs // the file is not virtual any more, remove the xattrs. No state xattr means local available data bool ok{true}; - ok = xattr::remove(targetPath, stateXAttrName); + ok = ok && FileSystem::Xattr::removexattr(targetPath, stateXAttrName); if (!ok) { qCInfo(lcVfsXAttr) << u"Removing extended file attribute state failed for" << targetPath; } - ok = ok && xattr::remove(targetPath, actionXAttrName); + ok = ok && FileSystem::Xattr::removexattr(targetPath, actionXAttrName); + ok = ok && FileSystem::Xattr::removexattr(targetPath, fileSizeXAttrName); if (!ok) { qCInfo(lcVfsXAttr) << u"Removing extended file attribute action failed for" << targetPath; } @@ -291,19 +246,15 @@ void VfsXAttr::slotHydrateJobFinished() 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; + qCDebug(lcVfsXAttr) << path; QFile file(path); if (file.exists() && FileSystem::fileChanged(FileSystem::toFilesystemPath(path), FileSystem::FileChangedInfo::fromSyncFileItem(&item))) { - return QStringLiteral("Cannot create a placeholder because a file with the placeholder name already exist"); + return Vfs::tr("Cannot create a placeholder because a file with the placeholder name already exist"); } if (!file.open(QFile::ReadWrite | QFile::Truncate)) { @@ -312,7 +263,7 @@ Result VfsXAttr::createPlaceholder(const SyncFileItem &item) file.write(""); file.close(); - xattr::remove(path, actionXAttrName); // remove the action xattr + FileSystem::Xattr::removexattr(FileSystem::toFilesystemPath(path), actionXAttrName); // remove the action xattr // FIXME only write attribs if they're different, and/or all together addPlaceholderAttribute(path, fileSizeXAttrName, QString::number(item._size)); @@ -367,7 +318,7 @@ bool VfsXAttr::needsMetadataUpdate(const SyncFileItem &item) if (fi.exists()) { re = true; } - qCDebug(lcVfsXAttr()) << "returning" << re; + qCDebug(lcVfsXAttr) << "returning" << re; return re; } @@ -399,21 +350,21 @@ LocalInfo VfsXAttr::statTypeVirtualFile(const std::filesystem::directory_entry & } } } - qCDebug(lcVfsXAttr()) << p << Utility::enumToString(type); + qCDebug(lcVfsXAttr) << p << Utility::enumToString(type); return LocalInfo(path, type); } bool VfsXAttr::setPinState(const QString &folderPath, PinState state) { - qCDebug(lcVfsXAttr()) << folderPath << state; + qCDebug(lcVfsXAttr) << folderPath << state; if (state == PinState::AlwaysLocal || state == PinState::OnlineOnly || state == PinState::Excluded) { auto stateStr = pinStateToString(state); addPlaceholderAttribute(folderPath, pinstateXAttrName, stateStr); } else { qCDebug(lcVfsXAttr) << "Do not set Pinstate" << pinStateToString(state) << ", remove pinstate xattr"; - xattr::remove(folderPath, pinstateXAttrName); + FileSystem::Xattr::removexattr(FileSystem::toFilesystemPath(folderPath), pinstateXAttrName); } return true; } @@ -435,7 +386,7 @@ Optional VfsXAttr::pinState(const QString &folderPath) } else if (pin == pinStateToString(PinState::OnlineOnly)) { pState = PinState::OnlineOnly; } - qCDebug(lcVfsXAttr()) << folderPath << pState; + qCDebug(lcVfsXAttr) << folderPath << pState; return pState; } @@ -465,7 +416,7 @@ Vfs::AvailabilityResult VfsXAttr::availability(const QString &folderPath) } else { res = AvailabilityError::NoSuchItem; } - qCDebug(lcVfsXAttr()) << folderPath << res.get(); + qCDebug(lcVfsXAttr) << folderPath << res.get(); return res; } @@ -477,7 +428,7 @@ void VfsXAttr::fileStatusChanged(const QString& systemFileName, SyncFileStatus f return; } - qCDebug(lcVfsXAttr()) << systemFileName << fileStatus; + qCDebug(lcVfsXAttr) << systemFileName << fileStatus; } QString VfsXAttr::pinStateToString(PinState pState) const diff --git a/src/plugins/vfs/xattr/vfs_xattr.h b/src/plugins/vfs/xattr/vfs_xattr.h index 8076b52ec7..19d8b5e21f 100644 --- a/src/plugins/vfs/xattr/vfs_xattr.h +++ b/src/plugins/vfs/xattr/vfs_xattr.h @@ -98,7 +98,7 @@ public Q_SLOTS: class XattrVfsPluginFactory : public QObject, public DefaultPluginFactory { Q_OBJECT - Q_PLUGIN_METADATA(IID "eu.opencloud.PluginFactory" FILE "libsync/common/vfspluginmetadata.json") + Q_PLUGIN_METADATA(IID "eu.opencloud.PluginFactory" FILE "libsync/vfs/vfspluginmetadata.json") Q_INTERFACES(OCC::PluginFactory) }; diff --git a/test/testutility.cpp b/test/testutility.cpp index 63ba0616a7..1b7ec35d96 100644 --- a/test/testutility.cpp +++ b/test/testutility.cpp @@ -315,7 +315,7 @@ private Q_SLOTS: const auto fn = tempFile.fileName(); const QString testKey = QStringLiteral("testKey"); - const QByteArray testValue("testValue"); + const auto testValue(u"testValue"_s); QVERIFY(!Tags::get(fn, testKey).has_value()); QVERIFY(Tags::set(fn, testKey, testValue)); @@ -333,7 +333,7 @@ private Q_SLOTS: const auto fn = tempDir.path(); const QString testKey = QStringLiteral("testKey"); - const QByteArray testValue("testValue"); + const QString testValue(u"testValue"_s); QVERIFY(!Tags::get(fn, testKey).has_value()); QVERIFY(Tags::set(fn, testKey, testValue)); From 7504eae8612706f96a793873c9b71c5d8ebfaf94 Mon Sep 17 00:00:00 2001 From: Klaas Freitag Date: Thu, 22 Jan 2026 08:53:29 +0100 Subject: [PATCH 6/9] Call setPinState with relative path and expand that in there Keep compatibility with the interface semantics. --- src/gui/socketapi/socketapi.cpp | 4 ++-- src/plugins/vfs/xattr/vfs_xattr.cpp | 8 +++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/gui/socketapi/socketapi.cpp b/src/gui/socketapi/socketapi.cpp index 5a9b3d5e9b..c71d8f0672 100644 --- a/src/gui/socketapi/socketapi.cpp +++ b/src/gui/socketapi/socketapi.cpp @@ -568,7 +568,7 @@ void SocketApi::command_MAKE_AVAILABLE_LOCALLY(const QString &filesArg, SocketLi continue; // Update the pin state on all items - std::ignore = data.folder->vfs().setPinState(data.localPath, PinState::AlwaysLocal); + std::ignore = data.folder->vfs().setPinState(data.folderRelativePath, PinState::AlwaysLocal); // Trigger sync data.folder->schedulePathForLocalDiscovery(data.folderRelativePath); @@ -587,7 +587,7 @@ void SocketApi::command_MAKE_ONLINE_ONLY(const QString &filesArg, SocketListener continue; // Update the pin state on all items - std::ignore = data.folder->vfs().setPinState(data.localPath, PinState::OnlineOnly); + std::ignore = data.folder->vfs().setPinState(data.folderRelativePath, PinState::OnlineOnly); // Trigger sync data.folder->schedulePathForLocalDiscovery(data.folderRelativePath); diff --git a/src/plugins/vfs/xattr/vfs_xattr.cpp b/src/plugins/vfs/xattr/vfs_xattr.cpp index a7238df834..491f6161c5 100644 --- a/src/plugins/vfs/xattr/vfs_xattr.cpp +++ b/src/plugins/vfs/xattr/vfs_xattr.cpp @@ -355,16 +355,18 @@ LocalInfo VfsXAttr::statTypeVirtualFile(const std::filesystem::directory_entry & return LocalInfo(path, type); } +// expects a relative path bool VfsXAttr::setPinState(const QString &folderPath, PinState state) { - qCDebug(lcVfsXAttr) << folderPath << state; + const auto localPath = QDir::toNativeSeparators(params().filesystemPath + folderPath); + qCDebug(lcVfsXAttr) << localPath << state; if (state == PinState::AlwaysLocal || state == PinState::OnlineOnly || state == PinState::Excluded) { auto stateStr = pinStateToString(state); - addPlaceholderAttribute(folderPath, pinstateXAttrName, stateStr); + addPlaceholderAttribute(localPath, pinstateXAttrName, stateStr); } else { qCDebug(lcVfsXAttr) << "Do not set Pinstate" << pinStateToString(state) << ", remove pinstate xattr"; - FileSystem::Xattr::removexattr(FileSystem::toFilesystemPath(folderPath), pinstateXAttrName); + FileSystem::Xattr::removexattr(FileSystem::toFilesystemPath(localPath), pinstateXAttrName); } return true; } From de997f0a195103224820903481ca5b9cb55fc42e Mon Sep 17 00:00:00 2001 From: Klaas Freitag Date: Thu, 22 Jan 2026 09:01:16 +0100 Subject: [PATCH 7/9] Remove enumToDisplayName(PinState) --- src/libsync/common/pinstate.cpp | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/src/libsync/common/pinstate.cpp b/src/libsync/common/pinstate.cpp index 0f61426fc8..6d3570298b 100644 --- a/src/libsync/common/pinstate.cpp +++ b/src/libsync/common/pinstate.cpp @@ -19,26 +19,6 @@ using namespace OCC; - -template <> -QString Utility::enumToDisplayName(PinState pState) -{ - switch (pState) { - 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) { From f25acfd2f4c2bbe937af18bdaad207b534c30fd9 Mon Sep 17 00:00:00 2001 From: Klaas Freitag Date: Thu, 22 Jan 2026 14:49:54 +0100 Subject: [PATCH 8/9] Various review feedback from @theonering --- src/libsync/vfs/vfs.h | 2 +- src/plugins/vfs/xattr/vfs_xattr.cpp | 177 ++++++++++++---------------- src/plugins/vfs/xattr/vfs_xattr.h | 6 +- 3 files changed, 79 insertions(+), 106 deletions(-) diff --git a/src/libsync/vfs/vfs.h b/src/libsync/vfs/vfs.h index 7bc590d45d..8009577c05 100644 --- a/src/libsync/vfs/vfs.h +++ b/src/libsync/vfs/vfs.h @@ -253,9 +253,9 @@ public Q_SLOTS: */ virtual void startImpl(const VfsSetupParams ¶ms) = 0; +private: // the parameters passed to start() std::unique_ptr _setupParams; -private: friend class OwncloudPropagator; }; diff --git a/src/plugins/vfs/xattr/vfs_xattr.cpp b/src/plugins/vfs/xattr/vfs_xattr.cpp index 491f6161c5..135634ba20 100644 --- a/src/plugins/vfs/xattr/vfs_xattr.cpp +++ b/src/plugins/vfs/xattr/vfs_xattr.cpp @@ -12,25 +12,38 @@ #include "libsync/xattr.h" #include "syncfileitem.h" #include "vfs/hydrationjob.h" +#include "theme.h" + +#include #include #include #include #include #include +#include + +using namespace Qt::StringLiterals; +using namespace xattr; Q_LOGGING_CATEGORY(lcVfsXAttr, "sync.vfs.xattr", QtInfoMsg) namespace { -const QString ownerXAttrName = QStringLiteral("user.openvfs.owner"); -const QString etagXAttrName = QStringLiteral("user.openvfs.etag"); -const QString fileidXAttrName = QStringLiteral("user.openvfs.fileid"); -const QString modtimeXAttrName = QStringLiteral("user.openvfs.modtime"); -const QString fileSizeXAttrName = QStringLiteral("user.openvfs.fsize"); -const QString actionXAttrName = QStringLiteral("user.openvfs.action"); -const QString stateXAttrName = QStringLiteral("user.openvfs.state"); -const QString pinstateXAttrName = QStringLiteral("user.openvfs.pinstate"); +const QString ownerXAttrName = u"user.openvfs.owner"_s; +const QString etagXAttrName = u"user.openvfs.etag"_s; +const QString fileidXAttrName = u"user.openvfs.fileid"_s; +const QString modtimeXAttrName = u"user.openvfs.modtime"_s; +const QString fileSizeXAttrName = u"user.openvfs.fsize"_s; +const QString actionXAttrName = u"user.openvfs.action"_s; +const QString stateXAttrName = u"user.openvfs.state"_s; +const QString pinstateXAttrName = u"user.openvfs.pinstate"_s; + +const QString fileStateVirtual = u"virtual"_s; +const QString fileStateHydrate = u"hydrate"_s; +const QString fileStateDehydrate = u"dehydrate"_s; +const QString fileStateHydrated = u"hydrated"_s; + } namespace OCC { @@ -49,10 +62,7 @@ Vfs::Mode VfsXAttr::mode() const QString VfsXAttr::xattrOwnerString() const { - auto s = QByteArray(APPLICATION_EXECUTABLE); - s.append(":"); - s.append(_setupParams->account->uuid().toByteArray(QUuid::WithoutBraces)); - return QString::fromUtf8(s); + return u"%1:%2"_s.arg(Theme::instance()->appName(), params().account->uuid().toString(QUuid::WithoutBraces)); } void VfsXAttr::startImpl(const VfsSetupParams ¶ms) @@ -68,23 +78,18 @@ void VfsXAttr::startImpl(const VfsSetupParams ¶ms) if (!owner) { // set the owner to opencloud to claim it if (!FileSystem::Xattr::setxattr(path, ownerXAttrName, xattrOwnerString() )) { - err = Vfs::tr("Unable to claim the sync root for files on demand"); - return; + err = tr("Unable to claim the sync root for files on demand"); } - } else { + } else if (owner.value() != xattrOwnerString()) { // owner is set. See if it is us - if (owner.value() == xattrOwnerString()) { - // all good - } else { - qCDebug(lcVfsXAttr) << "Root-FS has a different owner" << owner.value() << "Not our vfs!"; - err = Vfs::tr("The sync path is claimed by a different cloud, please check your setup"); - return; - } + qCDebug(lcVfsXAttr) << "Root-FS has a different owner" << owner.value() << "Not our vfs!"; + err = tr("The sync path is claimed by a different cloud, please check your setup"); } - if (err.isEmpty()) + if (err.isEmpty()) { Q_EMIT started(); - else + } else { Q_EMIT error(err); + } } void VfsXAttr::stop() @@ -100,13 +105,11 @@ bool VfsXAttr::socketApiPinStateActionsShown() const return true; } -PlaceHolderAttribs VfsXAttr::placeHolderAttributes(const QString& path) +xattr::PlaceHolderAttribs VfsXAttr::placeHolderAttributes(const QString& path) { PlaceHolderAttribs attribs; const auto fPath = FileSystem::toFilesystemPath(path); - // lambda to handle the Optional return val of xattrGet - attribs._etag = FileSystem::Xattr::getxattr(fPath, etagXAttrName).value_or(QString()); attribs._fileId = FileSystem::Xattr::getxattr(fPath, fileidXAttrName).value_or(QString()); @@ -127,7 +130,7 @@ OCC::Result VfsXAttr::addPlaceholderAttribute(const QString &path auto success = FileSystem::Xattr::setxattr(FileSystem::toFilesystemPath(path), name, value); // Q_ASSERT(success); if (!success) { - return Vfs::tr("Failed to set the extended file attribute"); + return tr("Failed to set the extended file attribute"); } } @@ -150,17 +153,17 @@ OCC::Result VfsXAttr::updateMetad if (!r) { res = OCC::Vfs::ConvertToPlaceholderResult::Locked; } - addPlaceholderAttribute(filePath, actionXAttrName, QStringLiteral("dehydrate")); - addPlaceholderAttribute(filePath, stateXAttrName, QStringLiteral("virtual")); + addPlaceholderAttribute(filePath, actionXAttrName, fileStateDehydrate); + addPlaceholderAttribute(filePath, stateXAttrName, fileStateVirtual); addPlaceholderAttribute(filePath, fileSizeXAttrName, QString::number(syncItem._size)); } else if (syncItem._type == ItemTypeVirtualFileDownload) { - addPlaceholderAttribute(filePath, actionXAttrName, QStringLiteral("hydrate")); + addPlaceholderAttribute(filePath, actionXAttrName, fileStateHydrate); // file gets downloaded and becomes a normal file, the xattr gets removed FileSystem::Xattr::removexattr(localPath, stateXAttrName); FileSystem::Xattr::removexattr(localPath, fileSizeXAttrName); } else if (syncItem._type == ItemTypeVirtualFile) { qCDebug(lcVfsXAttr) << "updateMetadata for virtual file " << syncItem._type; - addPlaceholderAttribute(filePath, stateXAttrName, QStringLiteral("virtual")); + addPlaceholderAttribute(filePath, stateXAttrName, fileStateVirtual); addPlaceholderAttribute(filePath, fileSizeXAttrName, QString::number(syncItem._size)); } else if (syncItem._type == ItemTypeFile) { qCDebug(lcVfsXAttr) << "updateMetadata for normal file " << syncItem._type; @@ -205,36 +208,26 @@ void VfsXAttr::slotHydrateJobFinished() // set the xattrs // the file is not virtual any more, remove the xattrs. No state xattr means local available data - bool ok{true}; - ok = ok && FileSystem::Xattr::removexattr(targetPath, stateXAttrName); - if (!ok) { - qCInfo(lcVfsXAttr) << u"Removing extended file attribute state failed for" << targetPath; - } - ok = ok && FileSystem::Xattr::removexattr(targetPath, actionXAttrName); - ok = ok && FileSystem::Xattr::removexattr(targetPath, fileSizeXAttrName); - if (!ok) { + if (!(FileSystem::Xattr::removexattr(targetPath, stateXAttrName) && + FileSystem::Xattr::removexattr(targetPath, actionXAttrName) && + FileSystem::Xattr::removexattr(targetPath, fileSizeXAttrName) )) { qCInfo(lcVfsXAttr) << u"Removing extended file attribute action failed for" << targetPath; } - if (ok) { - time_t modtime = item->_modtime; - qCInfo(lcVfsXAttr) << u"Setting hydrated file's modtime to" << modtime; + time_t modtime = item->_modtime; + qCInfo(lcVfsXAttr) << u"Setting hydrated file's modtime to" << modtime; - if (!FileSystem::setModTime(targetPath, modtime)) { - qCInfo(lcVfsXAttr) << u"Failed to set the mod time of the hydrated file" << targetPath; - // What can be done in this error condition - ok = false; - } + if (!FileSystem::setModTime(targetPath, modtime)) { + qCInfo(lcVfsXAttr) << u"Failed to set the mod time of the hydrated file" << targetPath; + // What can be done in this error condition } - if (ok) { - // Update the client sync journal database if the file modifications have been successful - const auto result = this->params().journal->setFileRecord(SyncJournalFileRecord::fromSyncFileItem(*item)); - if (!result) { - qCWarning(lcVfsXAttr) << u"Error when setting the file record to the database" << result.error(); - } else { - qCInfo(lcVfsXAttr) << u"Hydration succeeded" << targetPath; - } + // Update the client sync journal database if the file modifications have been successful + const auto result = this->params().journal->setFileRecord(SyncJournalFileRecord::fromSyncFileItem(*item)); + if (!result) { + qCWarning(lcVfsXAttr) << u"Error when setting the file record to the database" << result.error(); + } else { + qCInfo(lcVfsXAttr) << u"Hydration succeeded" << targetPath; } } else { qCWarning(lcVfsXAttr) << u"Hydration succeeded but the file appears to be moved" << targetPath; @@ -254,7 +247,7 @@ Result VfsXAttr::createPlaceholder(const SyncFileItem &item) if (file.exists() && FileSystem::fileChanged(FileSystem::toFilesystemPath(path), FileSystem::FileChangedInfo::fromSyncFileItem(&item))) { - return Vfs::tr("Cannot create a placeholder because a file with the placeholder name already exist"); + return tr("Cannot create a placeholder because a file with the placeholder name already exist"); } if (!file.open(QFile::ReadWrite | QFile::Truncate)) { @@ -267,7 +260,7 @@ Result VfsXAttr::createPlaceholder(const SyncFileItem &item) // FIXME only write attribs if they're different, and/or all together addPlaceholderAttribute(path, fileSizeXAttrName, QString::number(item._size)); - addPlaceholderAttribute(path, stateXAttrName, QStringLiteral("virtual")); + addPlaceholderAttribute(path, stateXAttrName, fileStateVirtual); addPlaceholderAttribute(path, fileidXAttrName, QString::fromUtf8(item._fileId)); addPlaceholderAttribute(path, etagXAttrName, item._etag); FileSystem::setModTime(path, item._modtime); @@ -294,7 +287,7 @@ HydrationJob* VfsXAttr::hydrateFile(const QByteArray &fileId, const QString &tar _hydrationJobs.insert(fileId, hydration); // set an action attrib - addPlaceholderAttribute(targetPath, actionXAttrName, QStringLiteral("hydrate")); + addPlaceholderAttribute(targetPath, actionXAttrName, fileStateHydrate); connect(hydration, &HydrationJob::finished, this, &VfsXAttr::slotHydrateJobFinished); @@ -309,28 +302,19 @@ HydrationJob* VfsXAttr::hydrateFile(const QByteArray &fileId, const QString &tar bool VfsXAttr::needsMetadataUpdate(const SyncFileItem &item) { - // return true if file exists - const auto path = item.localName(); - QFileInfo fi{path}; - - // FIXME: Unsure about this implementation - bool re{false}; - if (fi.exists()) { - re = true; - } - qCDebug(lcVfsXAttr) << "returning" << re; - return re; + const QString path = params().filesystemPath + item.localName(); + + return QFileInfo::exists(path); } bool VfsXAttr::isDehydratedPlaceholder(const QString &filePath) { - const auto fi = QFileInfo(filePath); - bool re{false}; - if (fi.exists()) { + + if (QFileInfo::exists(filePath)) { const auto attribs = placeHolderAttributes(filePath); - re = (attribs.state() == QStringLiteral("virtual")); + return (attribs.state() == fileStateVirtual); } - return re; + return false; } LocalInfo VfsXAttr::statTypeVirtualFile(const std::filesystem::directory_entry &path, ItemType type) @@ -339,7 +323,7 @@ LocalInfo VfsXAttr::statTypeVirtualFile(const std::filesystem::directory_entry & if (type == ItemTypeFile) { auto attribs = placeHolderAttributes(p); - if (attribs.state() == QStringLiteral("virtual")) { + if (attribs.state() == fileStateVirtual) { type = ItemTypeVirtualFile; if (attribs.pinState() == pinStateToString(PinState::AlwaysLocal)) { type = ItemTypeVirtualFileDownload; @@ -435,42 +419,35 @@ void VfsXAttr::fileStatusChanged(const QString& systemFileName, SyncFileStatus f QString VfsXAttr::pinStateToString(PinState pState) const { - QString re; switch (pState) { case OCC::PinState::AlwaysLocal: - re = QStringLiteral("alwayslocal"); - break; + return u"alwayslocal"_s; case OCC::PinState::Inherited: - re = QStringLiteral("interited"); - break; + return u"interited"_s; case OCC::PinState::OnlineOnly: - re = QStringLiteral("onlineonly"); - break; + return u"onlineonly"_s; case OCC::PinState::Unspecified: - re = QStringLiteral("unspecified"); - break; + return u"unspecified"_s; case OCC::PinState::Excluded: - re = QStringLiteral("excluded"); - break; + return u"excluded"_s; }; - return re; + return u"unspecified"_s; } PinState VfsXAttr::stringToPinState(const QString& str) const { - PinState p{PinState::Unspecified}; - if (str.isEmpty() || str == QStringLiteral("unspecified")) { - p = PinState::Unspecified; - } else if( str == QStringLiteral("alwayslocal")) { - p = PinState::AlwaysLocal; - } else if( str == QStringLiteral("inherited")) { - p = PinState::Inherited; - } else if( str == QStringLiteral("unspecified")) { - p = PinState::Unspecified; - } else if( str == QStringLiteral("excluded")) { - p = PinState::Excluded; + if (str.isEmpty() || str == u"unspecified"_s) { + return PinState::Unspecified; + } else if( str == u"alwayslocal"_s) { + return PinState::AlwaysLocal; + } else if( str == u"inherited"_s) { + return PinState::Inherited; + } else if( str == u"unspecified"_s) { + return PinState::Unspecified; + } else if( str == u"excluded"_s) { + return PinState::Excluded; } - return p; + return PinState::Unspecified; } } // namespace OCC diff --git a/src/plugins/vfs/xattr/vfs_xattr.h b/src/plugins/vfs/xattr/vfs_xattr.h index 19d8b5e21f..6ea1132034 100644 --- a/src/plugins/vfs/xattr/vfs_xattr.h +++ b/src/plugins/vfs/xattr/vfs_xattr.h @@ -12,8 +12,6 @@ #include "common/plugin.h" #include "common/result.h" -#include "config.h" - namespace xattr { struct PlaceHolderAttribs { @@ -40,8 +38,6 @@ struct PlaceHolderAttribs { namespace OCC { class HydrationJob; -using namespace xattr; - class VfsXAttr : public Vfs { Q_OBJECT @@ -88,7 +84,7 @@ public Q_SLOTS: private: QString xattrOwnerString() const; - PlaceHolderAttribs placeHolderAttributes(const QString& path); + xattr::PlaceHolderAttribs placeHolderAttributes(const QString& path); OCC::Result addPlaceholderAttribute(const QString &path, const QString &name = {}, const QString &val = {}); OCC::Result removePlaceHolderAttributes(const QString& path); From 6e7e20bcdcb31455fbee2ac8bd30bfd06db3f8b1 Mon Sep 17 00:00:00 2001 From: Klaas Freitag Date: Thu, 22 Jan 2026 15:00:33 +0100 Subject: [PATCH 9/9] Remove left over declaration in header --- src/libsync/common/pinstate.h | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/libsync/common/pinstate.h b/src/libsync/common/pinstate.h index ed5f4d866a..48a7ebbb05 100644 --- a/src/libsync/common/pinstate.h +++ b/src/libsync/common/pinstate.h @@ -137,8 +137,6 @@ using namespace PinStateEnums; template <> OPENCLOUD_SYNC_EXPORT QString Utility::enumToDisplayName(VfsItemAvailability availability); -template <> -OPENCLOUD_SYNC_EXPORT QString Utility::enumToDisplayName(PinState pState); } #endif