From d50d7e5e85d777e32194b756145a82f11e0aa398 Mon Sep 17 00:00:00 2001 From: Hannah von Reth Date: Thu, 19 Feb 2026 10:13:19 +0100 Subject: [PATCH 1/7] Pass private to linked libraries of plugins --- cmake/modules/OCAddVfsPlugin.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake/modules/OCAddVfsPlugin.cmake b/cmake/modules/OCAddVfsPlugin.cmake index 64a5bf282..922eca295 100644 --- a/cmake/modules/OCAddVfsPlugin.cmake +++ b/cmake/modules/OCAddVfsPlugin.cmake @@ -14,7 +14,7 @@ function(add_vfs_plugin) set_target_properties(vfs_${__PLUGIN_NAME} PROPERTIES OUTPUT_NAME "OpenCloud_vfs_${__PLUGIN_NAME}") - target_link_libraries(vfs_${__PLUGIN_NAME} + target_link_libraries(vfs_${__PLUGIN_NAME} PRIVATE libsync ${__PLUGIN_LIBS} ) From 01ed52428cfb5c86b4e74f7997f329ea9894ef6d Mon Sep 17 00:00:00 2001 From: Hannah von Reth Date: Thu, 19 Feb 2026 10:13:30 +0100 Subject: [PATCH 2/7] Use byte array for xattr again, the content can be anything --- src/libsync/filesystem.cpp | 6 ++++-- src/libsync/xattr.cpp | 7 +++---- src/libsync/xattr.h | 5 ++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/libsync/filesystem.cpp b/src/libsync/filesystem.cpp index 793857dec..801e00849 100644 --- a/src/libsync/filesystem.cpp +++ b/src/libsync/filesystem.cpp @@ -294,7 +294,9 @@ std::optional FileSystem::Tags::get(const QString &path, const QString if (Utility::isLinux()) { platformKey = QStringLiteral("user.") + platformKey; } - return Xattr::getxattr(toFilesystemPath(path), platformKey); + if (const auto d = Xattr::getxattr(toFilesystemPath(path), platformKey)) { + return QString::fromUtf8(*d); + } #elif defined(Q_OS_WIN) QFile file(QStringLiteral("%1:%2").arg(path, key)); if (file.open(QIODevice::ReadOnly)) { @@ -314,7 +316,7 @@ OCC::Result FileSystem::Tags::set(const QString &path, const QStr if (Utility::isLinux()) { platformKey = QStringLiteral("user.") + platformKey; } - return Xattr::setxattr(toFilesystemPath(path), platformKey, value); + return Xattr::setxattr(toFilesystemPath(path), platformKey, value.toUtf8()); #elif defined(Q_OS_WIN) QFile file(QStringLiteral("%1:%2").arg(path, key)); if (!file.open(QIODevice::WriteOnly)) { diff --git a/src/libsync/xattr.cpp b/src/libsync/xattr.cpp index 350544a83..50d84b18a 100644 --- a/src/libsync/xattr.cpp +++ b/src/libsync/xattr.cpp @@ -18,7 +18,7 @@ namespace FileSystem { #endif } - std::optional Xattr::getxattr(const std::filesystem::path &path, const QString &name) + std::optional Xattr::getxattr(const std::filesystem::path &path, const QString &name) { QByteArray value; ssize_t res = 0; @@ -32,15 +32,14 @@ namespace FileSystem { } while (res == -1 && errno == ERANGE); if (res > 0) { value.resize(res); - return QString::fromUtf8(value); + return value; } else { return {}; } } - Result Xattr::setxattr(const std::filesystem::path &path, const QString &name, const QString &value) + Result Xattr::setxattr(const std::filesystem::path &path, const QString &name, const QByteArray &data) { - 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 diff --git a/src/libsync/xattr.h b/src/libsync/xattr.h index 9fa417f19..809e6c98b 100644 --- a/src/libsync/xattr.h +++ b/src/libsync/xattr.h @@ -15,9 +15,8 @@ namespace OCC { namespace FileSystem { namespace Xattr { OPENCLOUD_SYNC_EXPORT bool supportsxattr(const std::filesystem::path &path); - 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 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 QByteArray &value); OPENCLOUD_SYNC_EXPORT Result removexattr(const std::filesystem::path &path, const QString &name); } } From 5a444a9334d36ed9cdfc6afb587630df5f3be463 Mon Sep 17 00:00:00 2001 From: Hannah von Reth Date: Thu, 19 Feb 2026 10:13:35 +0100 Subject: [PATCH 3/7] Use new openvfs api --- src/plugins/vfs/xattr/vfs_xattr.cpp | 432 +++++++++++++--------------- src/plugins/vfs/xattr/vfs_xattr.h | 29 -- 2 files changed, 195 insertions(+), 266 deletions(-) diff --git a/src/plugins/vfs/xattr/vfs_xattr.cpp b/src/plugins/vfs/xattr/vfs_xattr.cpp index 079d1b681..68dc09b8d 100644 --- a/src/plugins/vfs/xattr/vfs_xattr.cpp +++ b/src/plugins/vfs/xattr/vfs_xattr.cpp @@ -15,6 +15,8 @@ #include "syncfileitem.h" #include "vfs/hydrationjob.h" +#include + #include #include #include @@ -24,22 +26,20 @@ using namespace std::chrono_literals; using namespace Qt::StringLiterals; -using namespace xattr; Q_LOGGING_CATEGORY(lcVfsXAttr, "sync.vfs.xattr", QtInfoMsg) namespace { - QString openVFSExePath() { return QStringLiteral(OPENVFS_EXE); } -QString xattrOwnerString(const QUuid &accountUuid) +QByteArray xattrOwnerString(const QUuid &accountUuid) { - return u"%1:%2"_s.arg(OCC::Theme::instance()->appName(), accountUuid.toString(QUuid::WithoutBraces)); + return OCC::Theme::instance()->appName().toUtf8() + ":" + accountUuid.toByteArray(QUuid::WithoutBraces); } QString openVFSConfigFilePath() @@ -47,21 +47,69 @@ QString openVFSConfigFilePath() return QStandardPaths::locate(QStandardPaths::ConfigLocation, u"openvfs/config.json"_s); } +OpenVfsAttributes::PlaceHolderAttributes placeHolderAttributes(const std::filesystem::path &path) +{ + const auto data = OCC::FileSystem::Xattr::getxattr(path, QString::fromUtf8(OpenVfsConstants::XAttributeNames::Data)); + if (!data) { + qCWarning(lcVfsXAttr) << "No OpenVFS xattr found for" << path; + } + return OpenVfsAttributes::PlaceHolderAttributes::fromData(path, data ? std::vector{data->cbegin(), data->cend()} : std::vector{}); +} -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; +OpenVfsAttributes::PlaceHolderAttributes placeHolderAttributes(const QString &path) +{ + return placeHolderAttributes(OCC::FileSystem::toFilesystemPath(path)); +} -const QString fileStateVirtual = u"virtual"_s; -const QString fileStateHydrate = u"hydrate"_s; -const QString fileStateDehydrate = u"dehydrate"_s; -const QString fileStateHydrated = u"hydrated"_s; +OCC::Result setPlaceholderAttributes(const OpenVfsAttributes::PlaceHolderAttributes &attributes) +{ + const auto data = attributes.toData(); + return OCC::FileSystem::Xattr::setxattr(attributes.absolutePath, QString::fromUtf8(OpenVfsConstants::XAttributeNames::Data), + {reinterpret_cast(data.data()), static_cast(data.size())}); +} +OCC::Result setPlaceholderAttributes(const OpenVfsAttributes::PlaceHolderAttributes &attributes, time_t modtime) +{ + if (const auto result = setPlaceholderAttributes(attributes); !result) { + return result; + } + OCC::FileSystem::setModTime(attributes.absolutePath, modtime); + return {}; +} + +OpenVfsConstants::PinStates convertPinState(OCC::PinState pState) +{ + switch (pState) { + case OCC::PinState::AlwaysLocal: + return OpenVfsConstants::PinStates::AlwaysLocal; + case OCC::PinState::Inherited: + return OpenVfsConstants::PinStates::Inherited; + case OCC::PinState::OnlineOnly: + return OpenVfsConstants::PinStates::OnlineOnly; + case OCC::PinState::Excluded: + return OpenVfsConstants::PinStates::OnlineOnly; + case OCC::PinState::Unspecified: + return OpenVfsConstants::PinStates::Unspecified; + }; + Q_UNREACHABLE(); +} + +OCC::PinState convertPinState(OpenVfsConstants::PinStates pState) +{ + switch (pState) { + case OpenVfsConstants::PinStates::AlwaysLocal: + return OCC::PinState::AlwaysLocal; + case OpenVfsConstants::PinStates::Inherited: + return OCC::PinState::Inherited; + case OpenVfsConstants::PinStates::OnlineOnly: + return OCC::PinState::OnlineOnly; + case OpenVfsConstants::PinStates::Unspecified: + return OCC::PinState::Unspecified; + case OpenVfsConstants::PinStates::Excluded: + return OCC::PinState::Excluded; + } + Q_UNREACHABLE(); +} #ifdef Q_OS_LINUX @@ -129,7 +177,7 @@ void VfsXAttr::startImpl(const VfsSetupParams ¶ms) // Lets claim the sync root directory for us const auto path = FileSystem::toFilesystemPath(params.filesystemPath); // set the owner to opencloud to claim it - if (!FileSystem::Xattr::setxattr(path, ownerXAttrName, xattrOwnerString(params.account->uuid()))) { + if (!FileSystem::Xattr::setxattr(path, QString::fromUtf8(OpenVfsConstants::XAttributeNames::Owner), xattrOwnerString(params.account->uuid()))) { Q_EMIT error(tr("Unable to claim the sync root for files on demand")); return; } @@ -147,7 +195,6 @@ void VfsXAttr::startImpl(const VfsSetupParams ¶ms) Q_EMIT started(); }); connect(vfsProcess, &QProcess::errorOccurred, this, [logPrefix, vfsProcess, this] { qCWarning(lcVfsXAttr) << logPrefix() << vfsProcess->errorString(); }); - vfsProcess->start(openVFSExePath(), {u"-d"_s, u"-i"_s, openVFSConfigFilePath(), params.filesystemPath}, QIODevice::ReadOnly); } @@ -164,37 +211,6 @@ bool VfsXAttr::socketApiPinStateActionsShown() const return true; } -xattr::PlaceHolderAttribs VfsXAttr::placeHolderAttributes(const QString& path) -{ - PlaceHolderAttribs attribs; - const auto fPath = FileSystem::toFilesystemPath(path); - - attribs._etag = FileSystem::Xattr::getxattr(fPath, etagXAttrName).value_or(QString()); - attribs._fileId = FileSystem::Xattr::getxattr(fPath, fileidXAttrName).value_or(QString()); - - const QString tt = FileSystem::Xattr::getxattr(fPath, modtimeXAttrName).value_or(QString()); - attribs._modtime = tt.toLongLong(); - - 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; -} - -OCC::Result VfsXAttr::addPlaceholderAttribute(const QString &path, const QString& name, const QString& value) -{ - if (!name.isEmpty()) { - auto success = FileSystem::Xattr::setxattr(FileSystem::toFilesystemPath(path), name, value); - // Q_ASSERT(success); - if (!success) { - return tr("Failed to set the extended file attribute"); - } - } - - return {}; -} Result XattrVfsPluginFactory::prepare(const QString &path, const QUuid &accountUuid) const { @@ -238,14 +254,15 @@ Result XattrVfsPluginFactory::prepare(const QString &path, const qCDebug(lcVfsXAttr) << path << "does not support xattributes"; return tr("The filesystem for %1 does not support xattributes.").arg(path); } - const auto owner = FileSystem::Xattr::getxattr(fsPath, ownerXAttrName); - if (accountUuid.isNull() && owner.has_value()) { - qCDebug(lcVfsXAttr) << path << "has an owner set" << owner.value() << "Not our vfs!"; - return tr("The sync path is already claimed by a different account, please check your setup"); - } else if (owner.value_or(QString()) != xattrOwnerString(accountUuid)) { - // owner is set. See if it is us - qCDebug(lcVfsXAttr) << path << "is claimed by a different account" << owner.value() << "Not our vfs!"; - return tr("The sync path is claimed by a different cloud, please check your setup"); + if (const auto owner = FileSystem::Xattr::getxattr(fsPath, QString::fromUtf8(OpenVfsConstants::XAttributeNames::Owner))) { + if (accountUuid.isNull()) { + qCDebug(lcVfsXAttr) << path << "has an owner set" << owner << "Not our vfs!"; + return tr("The sync path is already claimed by a different account, please check your setup"); + } else if (owner != xattrOwnerString(accountUuid)) { + // owner is set. See if it is us + qCDebug(lcVfsXAttr) << path << "is claimed by a different account" << owner << "Not our vfs!"; + return tr("The sync path is claimed by a different cloud, please check your setup"); + } } if (!QFileInfo::exists(openVFSExePath())) { qCDebug(lcVfsXAttr) << "OpenVFS executable not found at" << openVFSExePath(); @@ -262,51 +279,68 @@ Result XattrVfsPluginFactory::prepare(const QString &path, const OCC::Result VfsXAttr::updateMetadata(const SyncFileItem &syncItem, const QString &filePath, const QString &replacesFile) { - Q_UNUSED(replacesFile); - const auto localPath = FileSystem::toFilesystemPath(filePath); - - qCDebug(lcVfsXAttr) << localPath << syncItem._type; - - // PlaceHolderAttribs attribs = placeHolderAttributes(localPath); - OCC::Vfs::ConvertToPlaceholderResult res{OCC::Vfs::ConvertToPlaceholderResult::Ok}; - - if (syncItem._type == ItemTypeVirtualFileDehydration) { // - // FIXME: Error handling - auto r = createPlaceholder(syncItem); - if (!r) { - res = OCC::Vfs::ConvertToPlaceholderResult::Locked; + if (syncItem._type == ItemTypeVirtualFileDehydration) { + // replace the file with a placeholder + if (const auto result = createPlaceholder(syncItem); !result) { + qCCritical(lcVfsXAttr) << "Failed to create placeholder for" << filePath << result.error(); + return result.error(); } - addPlaceholderAttribute(filePath, actionXAttrName, fileStateDehydrate); - addPlaceholderAttribute(filePath, stateXAttrName, fileStateVirtual); - addPlaceholderAttribute(filePath, fileSizeXAttrName, QString::number(syncItem._size)); - } else if (syncItem._type == ItemTypeVirtualFileDownload) { - 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, fileStateVirtual); - 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; + return ConvertToPlaceholderResult::Ok; } else { - qCDebug(lcVfsXAttr) << "Unexpected syncItem Type" << syncItem._type; - Q_UNREACHABLE(); - } - - FileSystem::setModTime(localPath, syncItem._modtime); - - addPlaceholderAttribute(filePath, fileidXAttrName, QString::fromUtf8(syncItem._fileId)); - addPlaceholderAttribute(filePath, etagXAttrName, syncItem._etag); + OpenVfsAttributes::PlaceHolderAttributes attributes = [&] { + // load the previous attributes + if (!replacesFile.isEmpty()) { + if (const auto attr = placeHolderAttributes(replacesFile)) { + return attr; + } + } + if (const auto attr = placeHolderAttributes(filePath)) { + return attr; + } + Q_ASSERT(QFileInfo::exists(filePath)); + // generate new meta data for an existing file + auto attr = OpenVfsAttributes::PlaceHolderAttributes::create( + FileSystem::toFilesystemPath(filePath), syncItem._etag.toStdString(), syncItem._fileId.toStdString(), syncItem._size); + attr.state = OpenVfsConstants::States::Hydrated; + return attr; + }(); + Q_ASSERT(attributes); + + attributes.size = syncItem._size; + attributes.fileId = syncItem._fileId.toStdString(); + attributes.etag = syncItem._etag.toStdString(); + + qCDebug(lcVfsXAttr) << attributes.absolutePath << syncItem._type; + + switch (syncItem._type) { + case ItemTypeVirtualFileDownload: + attributes.state = OpenVfsConstants::States::Hydrating; + break; + case ItemTypeVirtualFile: + [[fallthrough]]; + case ItemTypeVirtualFileDehydration: + qCDebug(lcVfsXAttr) << "updateMetadata for virtual file " << syncItem._type; + attributes.state = OpenVfsConstants::States::DeHydrated; + break; + case ItemTypeFile: + [[fallthrough]]; + case ItemTypeDirectory: + qCDebug(lcVfsXAttr) << "updateMetadata for" << syncItem._type; + attributes.state = OpenVfsConstants::States::Hydrated; + break; + case ItemTypeSymLink: + [[fallthrough]]; + case ItemTypeUnsupported: + Q_UNREACHABLE(); + } - // remove the action marker again - FileSystem::Xattr::removexattr(localPath, actionXAttrName); + if (const auto result = setPlaceholderAttributes(attributes, syncItem._modtime); !result) { + qCCritical(lcVfsXAttr) << "Failed to update placeholder for" << filePath << result.error(); + return result.error(); + } - return res; + return ConvertToPlaceholderResult::Ok; + } } void VfsXAttr::slotHydrateJobFinished() @@ -328,23 +362,6 @@ void VfsXAttr::slotHydrateJobFinished() } 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 - 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; - } - - 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 - } - // 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) { @@ -362,39 +379,22 @@ void VfsXAttr::slotHydrateJobFinished() Result VfsXAttr::createPlaceholder(const SyncFileItem &item) { - const auto path = QDir::toNativeSeparators(params().filesystemPath + item.localName()); - - qCDebug(lcVfsXAttr) << path; - - QFile file(path); - if (file.exists() - && FileSystem::fileChanged(FileSystem::toFilesystemPath(path), - FileSystem::FileChangedInfo::fromSyncFileItem(&item))) { - return tr("Cannot create a placeholder because a file with the placeholder name already exist"); + const auto path = FileSystem::toFilesystemPath(params().filesystemPath + item.localName()); + if (std::filesystem::exists(path)) { + if (item._type == ItemTypeVirtualFileDehydration && FileSystem::fileChanged(path, FileSystem::FileChangedInfo::fromSyncFileItem(&item))) { + return tr("Cannot dehydrate a placeholder because the file changed"); + } + Q_ASSERT(item._type == ItemTypeVirtualFile); } - + QFile file(path); if (!file.open(QFile::ReadWrite | QFile::Truncate)) { return file.errorString(); } file.write(""); file.close(); - 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)); - addPlaceholderAttribute(path, stateXAttrName, fileStateVirtual); - 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(path); - if (pin && *pin == PinState::AlwaysLocal) { - setPinState(item._renameTarget, PinState::Unspecified); - } - - return {}; + const auto attributes = OpenVfsAttributes::PlaceHolderAttributes::create(path, item._etag.toStdString(), item._fileId.toStdString(), item._size); + return setPlaceholderAttributes(attributes, item._modtime); } HydrationJob* VfsXAttr::hydrateFile(const QByteArray &fileId, const QString &targetPath) @@ -405,15 +405,21 @@ HydrationJob* VfsXAttr::hydrateFile(const QByteArray &fileId, const QString &tar return {}; } + if (auto attr = placeHolderAttributes(targetPath)) { + attr.state = OpenVfsConstants::States::Hydrating; + if (auto res = setPlaceholderAttributes(attr); !res) { + qCWarning(lcVfsXAttr) << u"Failed to set attributes for" << targetPath << res.error(); + return nullptr; + } + } else { + qCWarning(lcVfsXAttr) << u"Failed to get attributes for" << targetPath; + return nullptr; + } 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, fileStateHydrate); - 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()); @@ -426,151 +432,103 @@ HydrationJob* VfsXAttr::hydrateFile(const QByteArray &fileId, const QString &tar bool VfsXAttr::needsMetadataUpdate(const SyncFileItem &item) { const QString path = params().filesystemPath + item.localName(); - - return QFileInfo::exists(path); + // if the attributes do not exist we need to add them + return QFileInfo::exists(path) && !placeHolderAttributes(path); } bool VfsXAttr::isDehydratedPlaceholder(const QString &filePath) { - if (QFileInfo::exists(filePath)) { - const auto attribs = placeHolderAttributes(filePath); - return (attribs.state() == fileStateVirtual); + return placeHolderAttributes(filePath).state == OpenVfsConstants::States::DeHydrated; } return false; } LocalInfo VfsXAttr::statTypeVirtualFile(const std::filesystem::directory_entry &path, ItemType type) { - const QString p = FileSystem::fromFilesystemPath(path.path()); if (type == ItemTypeFile) { - - auto attribs = placeHolderAttributes(p); - if (attribs.state() == fileStateVirtual) { + const auto attribs = placeHolderAttributes(path.path()); + if (attribs.state == OpenVfsConstants::States::DeHydrated) { type = ItemTypeVirtualFile; - if (attribs.pinState() == pinStateToString(PinState::AlwaysLocal)) { + if (attribs.pinState == convertPinState(PinState::AlwaysLocal)) { type = ItemTypeVirtualFileDownload; } } else { - if (attribs.pinState() == pinStateToString(PinState::OnlineOnly)) { + if (attribs.pinState == convertPinState(PinState::OnlineOnly)) { type = ItemTypeVirtualFileDehydration; } } } - qCDebug(lcVfsXAttr) << p << Utility::enumToString(type); - + qCDebug(lcVfsXAttr) << path.path().native() << Utility::enumToString(type); return LocalInfo(path, type); } -// expects a relative path bool VfsXAttr::setPinState(const QString &folderPath, PinState state) { - const auto localPath = QDir::toNativeSeparators(params().filesystemPath + folderPath); + const QString localPath = params().filesystemPath + folderPath; qCDebug(lcVfsXAttr) << localPath << state; - - if (state == PinState::AlwaysLocal || state == PinState::OnlineOnly || state == PinState::Excluded) { - auto stateStr = pinStateToString(state); - addPlaceholderAttribute(localPath, pinstateXAttrName, stateStr); - } else { - qCDebug(lcVfsXAttr) << "Do not set Pinstate" << pinStateToString(state) << ", remove pinstate xattr"; - FileSystem::Xattr::removexattr(FileSystem::toFilesystemPath(localPath), pinstateXAttrName); + auto attribs = placeHolderAttributes(localPath); + if (!attribs) { + // the file is not yet converted + return false; + } + attribs.pinState = convertPinState(state); + if (!setPlaceholderAttributes(attribs)) { + return false; } return true; } Optional VfsXAttr::pinState(const QString &folderPath) { - - PlaceHolderAttribs attribs = placeHolderAttributes(folderPath); - - 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; + const auto attribs = placeHolderAttributes(params().filesystemPath + folderPath); + if (!attribs) { + qCDebug(lcVfsXAttr) << u"Couldn't find pin state for regular non-placeholder file" << folderPath; + return {}; } - qCDebug(lcVfsXAttr) << folderPath << pState; - - return pState; + return convertPinState(attribs.pinState); } Vfs::AvailabilityResult VfsXAttr::availability(const QString &folderPath) { - - const auto basePinState = pinState(folderPath); - Vfs::AvailabilityResult res {VfsItemAvailability::Mixed}; - - if (basePinState) { - switch (*basePinState) { + const auto attribs = placeHolderAttributes(params().filesystemPath + folderPath); + if (attribs) { + switch (convertPinState(attribs.pinState)) { case OCC::PinState::AlwaysLocal: - res = VfsItemAvailability::AlwaysLocal; - break; - case OCC::PinState::Inherited: - break; + return VfsItemAvailability::AlwaysLocal; case OCC::PinState::OnlineOnly: - res = VfsItemAvailability::OnlineOnly; - break; + return VfsItemAvailability::OnlineOnly; + case OCC::PinState::Inherited: { + switch (attribs.state) { + case OpenVfsConstants::States::Hydrated: + return VfsItemAvailability::AllHydrated; + case OpenVfsConstants::States::DeHydrated: + return VfsItemAvailability::AllDehydrated; + case OpenVfsConstants::States::Hydrating: + return VfsItemAvailability::Mixed; + } + } + Q_UNREACHABLE(); case OCC::PinState::Unspecified: - break; + [[fallthrough]]; case OCC::PinState::Excluded: - break; + return VfsItemAvailability::Mixed; }; - res = VfsItemAvailability::Mixed; } else { - res = AvailabilityError::NoSuchItem; + return AvailabilityError::NoSuchItem; } - qCDebug(lcVfsXAttr) << folderPath << res.get(); - - return res; + return VfsItemAvailability::Mixed; } void VfsXAttr::fileStatusChanged(const QString& systemFileName, SyncFileStatus fileStatus) { if (fileStatus.tag() == SyncFileStatus::StatusExcluded) { - setPinState(systemFileName, PinState::Excluded); + const QString rel = systemFileName.mid(params().filesystemPath.length()); + setPinState(rel, PinState::Excluded); return; } - qCDebug(lcVfsXAttr) << systemFileName << fileStatus; } -QString VfsXAttr::pinStateToString(PinState pState) const -{ - switch (pState) { - case OCC::PinState::AlwaysLocal: - return u"alwayslocal"_s; - case OCC::PinState::Inherited: - return u"interited"_s; - case OCC::PinState::OnlineOnly: - return u"onlineonly"_s; - case OCC::PinState::Unspecified: - return u"unspecified"_s; - case OCC::PinState::Excluded: - return u"excluded"_s; - }; - return u"unspecified"_s; -} - -PinState VfsXAttr::stringToPinState(const QString& str) const -{ - 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 PinState::Unspecified; -} } // namespace OCC diff --git a/src/plugins/vfs/xattr/vfs_xattr.h b/src/plugins/vfs/xattr/vfs_xattr.h index ef2b74d8a..9b543c668 100644 --- a/src/plugins/vfs/xattr/vfs_xattr.h +++ b/src/plugins/vfs/xattr/vfs_xattr.h @@ -15,28 +15,6 @@ #include "common/plugin.h" #include "common/result.h" -namespace xattr { - -struct PlaceHolderAttribs { -public: - qint64 size() const { return _size; } - QString fileId() const { return _fileId; } - time_t modTime() const {return _modtime; } - QString eTag() const { return _etag; } - QString pinState() const { return _pinState; } - QString action() const { return _action; } - QString state() const { return _state; } - - qint64 _size; - QString _fileId; - time_t _modtime; - QString _etag; - QString _pinState; - QString _action; - QString _state; - -}; -} namespace OCC { class HydrationJob; @@ -71,9 +49,6 @@ class VfsXAttr : public Vfs HydrationJob* hydrateFile(const QByteArray &fileId, const QString& targetPath) override; - QString pinStateToString(PinState) const; - PinState stringToPinState(const QString&) const; - Q_SIGNALS: void finished(Result); @@ -86,10 +61,6 @@ public Q_SLOTS: void startImpl(const VfsSetupParams ¶ms) override; private: - xattr::PlaceHolderAttribs placeHolderAttributes(const QString& path); - OCC::Result addPlaceholderAttribute(const QString &path, const QString &name = {}, const QString &val = {}); - OCC::Result removePlaceHolderAttributes(const QString& path); - QMap _hydrationJobs; }; From 8a1878113029d6a8f21b24928ae3b920f11b75a5 Mon Sep 17 00:00:00 2001 From: Hannah von Reth Date: Thu, 19 Feb 2026 10:13:40 +0100 Subject: [PATCH 4/7] Enable dep to openvfs --- .github/workflows/craft_override.ini | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/craft_override.ini b/.github/workflows/craft_override.ini index 985bad65f..a30906f28 100644 --- a/.github/workflows/craft_override.ini +++ b/.github/workflows/craft_override.ini @@ -6,10 +6,6 @@ Packager/CreateCache = False Paths/CCACHE_DIR = ${Env:HOME}/ccache Compile/UseCCache = True -[BlueprintSettings] -# ignore for now -opencloud/openvfs.ignored=1 - [linux-gcc-x86_64] Environment/SourceCommand = export PKG_CONFIG_PATH=(/usr/bin/pkg-config --variable pc_path pkg-config) && source /opt/rh/gcc-toolset-14/enable From 3d0ae34c8679568abd4b120dc85048224a7ca069 Mon Sep 17 00:00:00 2001 From: Hannah von Reth Date: Thu, 19 Feb 2026 10:06:54 +0100 Subject: [PATCH 5/7] Introduce Path object to siplify use of std::filesystem::path with QString --- src/gui/folder.cpp | 1 - src/libsync/CMakeLists.txt | 2 + src/libsync/common/filesystembase.cpp | 11 +++-- src/libsync/common/filesystembase.h | 4 +- src/libsync/path.cpp | 31 ++++++++++++ src/libsync/path.h | 68 ++++++++++++++++++++++++++ src/libsync/vfs/vfs.cpp | 15 +++++- src/libsync/vfs/vfs.h | 22 ++++----- src/plugins/vfs/cfapi/cfapiwrapper.cpp | 10 ++-- src/plugins/vfs/cfapi/vfs_cfapi.cpp | 15 +++--- test/testutils/syncenginetestutils.cpp | 1 - 11 files changed, 146 insertions(+), 34 deletions(-) create mode 100644 src/libsync/path.cpp create mode 100644 src/libsync/path.h diff --git a/src/gui/folder.cpp b/src/gui/folder.cpp index fb69337f6..a7d4ed84b 100644 --- a/src/gui/folder.cpp +++ b/src/gui/folder.cpp @@ -520,7 +520,6 @@ void Folder::startVfs() OC_ENFORCE(_vfs->mode() == _definition.virtualFilesMode); VfsSetupParams vfsParams(_accountState->account(), webDavUrl(), _definition.spaceId(), displayName(), _engine.get()); - vfsParams.filesystemPath = path(); vfsParams.journal = &_journal; vfsParams.providerDisplayName = Theme::instance()->appNameGUI(); vfsParams.providerName = Theme::instance()->appName(); diff --git a/src/libsync/CMakeLists.txt b/src/libsync/CMakeLists.txt index 4d3c6b02e..4c4dbe402 100644 --- a/src/libsync/CMakeLists.txt +++ b/src/libsync/CMakeLists.txt @@ -64,6 +64,8 @@ add_library(libsync SHARED abstractcorejob.cpp appprovider.cpp + + path.cpp ) if(WIN32) diff --git a/src/libsync/common/filesystembase.cpp b/src/libsync/common/filesystembase.cpp index 3e6a78da8..2e21b9c0c 100644 --- a/src/libsync/common/filesystembase.cpp +++ b/src/libsync/common/filesystembase.cpp @@ -51,12 +51,13 @@ namespace OCC { Q_LOGGING_CATEGORY(lcFileSystem, "sync.filesystem", QtInfoMsg) -std::filesystem::path FileSystem::toFilesystemPath(QString path) +std::filesystem::path FileSystem::toFilesystemPath(const QString &path) { #ifdef Q_OS_WIN - path = FileSystem::longWinPath(path); + return QtPrivate::toFilesystemPath(FileSystem::longWinPath(path)); +#else + return QtPrivate::toFilesystemPath(path); #endif - return std::filesystem::path(reinterpret_cast(path.cbegin()), reinterpret_cast(path.cend())); } QString FileSystem::fromFilesystemPath(const std::filesystem::path &path) @@ -71,9 +72,9 @@ QString FileSystem::fromFilesystemPath(const std::filesystem::path &path) return QDir::fromNativeSeparators(QString::fromWCharArray(view.data(), view.length())); #elif defined(Q_OS_MACOS) // based on QFile::decodeName - return QString::fromStdString(path.native()).normalized(QString::NormalizationForm_C); + return QtPrivate::fromFilesystemPath(path).normalized(QString::NormalizationForm_C); #else - return QString::fromStdString(path.native()); + return QtPrivate::fromFilesystemPath(path); #endif } diff --git a/src/libsync/common/filesystembase.h b/src/libsync/common/filesystembase.h index 5c48bae8c..bb61a220c 100644 --- a/src/libsync/common/filesystembase.h +++ b/src/libsync/common/filesystembase.h @@ -27,7 +27,6 @@ #include #include -#include class QFile; @@ -48,9 +47,10 @@ OPENCLOUD_SYNC_EXPORT Q_DECLARE_LOGGING_CATEGORY(lcFileSystem) { OPENCLOUD_SYNC_EXPORT Q_NAMESPACE; - OPENCLOUD_SYNC_EXPORT std::filesystem::path toFilesystemPath(QString path); + OPENCLOUD_SYNC_EXPORT std::filesystem::path toFilesystemPath(const QString &path); OPENCLOUD_SYNC_EXPORT QString fromFilesystemPath(const std::filesystem::path &path); + /** * List of characters not allowd in filenames on Windows */ diff --git a/src/libsync/path.cpp b/src/libsync/path.cpp new file mode 100644 index 000000000..1b6741e2f --- /dev/null +++ b/src/libsync/path.cpp @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2025 Hannah von Reth + +#include "path.h" + +#include "libsync/common/filesystembase.h" + +OCC::FileSystem::Path::Path(QAnyStringView path) + : _path(toFilesystemPath(path.toString())) +{ +} + +OCC::FileSystem::Path::Path(const std::filesystem::path &path) + : _path(path) +{ +} + +OCC::FileSystem::Path::Path(std::filesystem::path &&path) + : _path(std::move(path)) +{ +} + +OCC::FileSystem::Path OCC::FileSystem::Path::relative(QAnyStringView path) +{ + return QtPrivate::toFilesystemPath(path.toString()); +} + +QString OCC::FileSystem::Path::toString() const +{ + return fromFilesystemPath(_path); +} diff --git a/src/libsync/path.h b/src/libsync/path.h new file mode 100644 index 000000000..f072e5510 --- /dev/null +++ b/src/libsync/path.h @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2025 Hannah von Reth + +#pragma once + +#include "libsync/opencloudsynclib.h" + +#include + +#include + +namespace OCC { +namespace FileSystem { + class OPENCLOUD_SYNC_EXPORT Path + { + public: + /** + * Will create an absolute path from the given QString. + * On Windows it will create a UNC path starting with "\\?\". + * For path segments use Path::relative. + * @param path + */ + explicit Path(QAnyStringView path); + + Path(const std::filesystem::path &path); + + Path(std::filesystem::path &&path); + + /** + * Creates a relative path segment + * @param path + * @return A relative path + */ + static Path relative(QAnyStringView path); + + const std::filesystem::path &get() const { return _path; } + + [[nodiscard]] operator std::filesystem::path() const { return _path; } + + explicit operator QString() const { return toString(); } + + Path operator/(const Path &other) const { return _path / other._path; } + Path operator/(const std::filesystem::path &other) const { return _path / other; } + Path operator/(QAnyStringView other) const { return _path / relative(other); } + + Path &operator/=(const Path &other) + { + _path /= other._path; + return *this; + } + Path &operator/=(const std::filesystem::path &other) + { + _path /= other; + return *this; + } + Path &operator/=(QAnyStringView other) + { + _path /= relative(other); + return *this; + } + + QString toString() const; + + private: + std::filesystem::path _path; + }; +} +} diff --git a/src/libsync/vfs/vfs.cpp b/src/libsync/vfs/vfs.cpp index 5da1b8992..885f5a0de 100644 --- a/src/libsync/vfs/vfs.cpp +++ b/src/libsync/vfs/vfs.cpp @@ -23,6 +23,7 @@ #include "libsync/common/syncjournaldb.h" #include "libsync/common/version.h" #include "libsync/filesystem.h" +#include "libsync/syncengine.h" #include #include @@ -100,7 +101,7 @@ void Vfs::wipeDehydratedVirtualFiles() // If the local file is a dehydrated placeholder, wipe it too. // Otherwise leave it to allow the next sync to have a new-new conflict. - const QString absolutePath = _setupParams->filesystemPath + relativePath; + const auto absolutePath = QString(_setupParams->root() / relativePath); if (QFile::exists(absolutePath)) { // according to our db this is a dehydrated file, check it to be sure if (isDehydratedPlaceholder(absolutePath)) { @@ -261,7 +262,9 @@ VfsSetupParams::VfsSetupParams(const AccountPtr &account, const QUrl &baseUrl, c , _syncEngine(syncEngine) , _spaceId(spaceId) , _folderDisplayName(folderDisplayName) + , _root(syncEngine->localPath()) { + Q_ASSERT(filesystemPath().endsWith('/'_L1)); } QString VfsSetupParams::folderDisplayName() const @@ -273,3 +276,13 @@ SyncEngine *VfsSetupParams::syncEngine() const { return _syncEngine; } + +QString VfsSetupParams::filesystemPath() const +{ + return _root.toString(); +} + +const FileSystem::Path &VfsSetupParams::root() const +{ + return _root; +} diff --git a/src/libsync/vfs/vfs.h b/src/libsync/vfs/vfs.h index 851475d62..4bb40247e 100644 --- a/src/libsync/vfs/vfs.h +++ b/src/libsync/vfs/vfs.h @@ -13,21 +13,21 @@ */ #pragma once -#include "../common/pinstate.h" -#include "../common/result.h" -#include "../common/syncfilestatus.h" -#include "../common/utility.h" -#include "assert.h" +#include "filesystem.h" #include "libsync/accountfwd.h" +#include "libsync/common/pinstate.h" +#include "libsync/common/result.h" +#include "libsync/common/syncfilestatus.h" +#include "libsync/common/utility.h" #include "libsync/discoveryinfo.h" #include "libsync/opencloudsynclib.h" +#include "libsync/path.h" #include #include #include #include -#include #include #include @@ -43,11 +43,7 @@ class HydrationJob; struct OPENCLOUD_SYNC_EXPORT VfsSetupParams { explicit VfsSetupParams(const AccountPtr &account, const QUrl &baseUrl, const QString &spaceId, const QString &folderDisplayName, SyncEngine *syncEngine); - /** The full path to the folder on the local filesystem - * - * Always ends with /. - */ - QString filesystemPath; + QString folderDisplayName() const; /// Account url, credentials etc for network calls @@ -69,11 +65,15 @@ struct OPENCLOUD_SYNC_EXPORT VfsSetupParams SyncEngine *syncEngine() const; + QString filesystemPath() const; + const FileSystem::Path &root() const; + private: QUrl _baseUrl; SyncEngine *_syncEngine; QString _spaceId; QString _folderDisplayName; + FileSystem::Path _root; }; /** Interface describing how to deal with virtual/placeholder files. diff --git a/src/plugins/vfs/cfapi/cfapiwrapper.cpp b/src/plugins/vfs/cfapi/cfapiwrapper.cpp index 9cfba493f..5a89ad0c9 100644 --- a/src/plugins/vfs/cfapi/cfapiwrapper.cpp +++ b/src/plugins/vfs/cfapi/cfapiwrapper.cpp @@ -223,7 +223,7 @@ void CALLBACK cfApiRename(const CF_CALLBACK_INFO *callbackInfo, const CF_CALLBAC if (callbackParameters->Rename.Flags & (CF_CALLBACK_RENAME_FLAG_TARGET_IN_SCOPE | CF_CALLBACK_RENAME_FLAG_SOURCE_IN_SCOPE) && // CF_CALLBACK_RENAME_FLAG_TARGET_IN_SCOPE is also set for any other sync root we manage // but in that case windows will hydrate the file before its moved - OCC::FileSystem::isChildPathOf(target, context.vfs->params().filesystemPath) && context.vfs->isDehydratedPlaceholder(context.path) + OCC::FileSystem::isChildPathOf(target, context.vfs->params().filesystemPath()) && context.vfs->isDehydratedPlaceholder(context.path) && context.vfs->params().syncEngine()->isExcluded(qtPath)) { reject = true; } @@ -356,7 +356,7 @@ QString createSyncRootID(const QString &providerName, const QUuid &accountUUID, void OCC::CfApiWrapper::registerSyncRoot(const VfsSetupParams ¶ms, const std::function &callback) { - const auto nativePath = QDir::toNativeSeparators(params.filesystemPath); + const auto nativePath = QDir::toNativeSeparators(params.filesystemPath()); winrt::StorageFolder::GetFolderFromPathAsync(reinterpret_cast(nativePath.utf16())) .Completed([params, callback](const winrt::IAsyncOperation &result, winrt::AsyncStatus status) { if (status != winrt::AsyncStatus::Completed) { @@ -365,7 +365,7 @@ void OCC::CfApiWrapper::registerSyncRoot(const VfsSetupParams ¶ms, const std } try { const auto iconPath = QCoreApplication::applicationFilePath(); - const auto id = createSyncRootID(params.providerName, params.account->uuid(), params.filesystemPath); + const auto id = createSyncRootID(params.providerName, params.account->uuid(), params.filesystemPath()); const auto version = params.providerVersion.toString(); winrt::StorageProviderSyncRootInfo info; @@ -400,7 +400,7 @@ void OCC::CfApiWrapper::registerSyncRoot(const VfsSetupParams ¶ms, const std } callback({}); } catch (const winrt::hresult_error &ex) { - callback(u"Failed to register sync root %1: %2"_s.arg(params.filesystemPath, Utility::formatWinError(ex.code()))); + callback(u"Failed to register sync root %1: %2"_s.arg(params.filesystemPath(), Utility::formatWinError(ex.code()))); } }); } @@ -431,7 +431,7 @@ OCC::Result OCC::CfApiWrapper::unregisterSyncRoot(const VfsSetupP try { std::lock_guard lock(sRegister_mutex); winrt::StorageProviderSyncRootManager::Unregister( - reinterpret_cast(createSyncRootID(params.providerName, params.account->uuid(), params.filesystemPath).utf16())); + reinterpret_cast(createSyncRootID(params.providerName, params.account->uuid(), params.filesystemPath()).utf16())); } catch (winrt::hresult_error const &ex) { return u"unregisterSyncRoot failed: %1"_s.arg(Utility::formatWinError(ex.code())); } diff --git a/src/plugins/vfs/cfapi/vfs_cfapi.cpp b/src/plugins/vfs/cfapi/vfs_cfapi.cpp index 15b1384f4..666fc410e 100644 --- a/src/plugins/vfs/cfapi/vfs_cfapi.cpp +++ b/src/plugins/vfs/cfapi/vfs_cfapi.cpp @@ -118,7 +118,7 @@ void VfsCfApi::startImpl(const VfsSetupParams ¶ms) cfapi::registerSyncRoot(params, [this](const QString &errorMessage) { if (errorMessage.isEmpty()) { - auto connectResult = cfapi::connectSyncRoot(this->params().filesystemPath, this); + auto connectResult = cfapi::connectSyncRoot(this->params().filesystemPath(), this); if (!connectResult) { qCCritical(lcCfApi) << u"Initialization failed, couldn't connect sync root:" << connectResult.error(); return; @@ -155,7 +155,7 @@ void VfsCfApi::stop() if (_connectionKey.Internal != 0) { const auto result = cfapi::disconnectSyncRoot(std::move(_connectionKey)); if (!result) { - qCCritical(lcCfApi) << u"Disconnect failed for" << params().filesystemPath << u":" << result.error(); + qCCritical(lcCfApi) << u"Disconnect failed for" << params().filesystemPath() << u":" << result.error(); } } } @@ -164,7 +164,7 @@ void VfsCfApi::unregisterFolder() { const auto result = cfapi::unregisterSyncRoot(params()); if (!result) { - qCCritical(lcCfApi) << u"Unregistration failed for" << params().filesystemPath << u":" << result.error(); + qCCritical(lcCfApi) << u"Unregistration failed for" << params().filesystemPath() << u":" << result.error(); } #if 0 @@ -197,15 +197,14 @@ Result VfsCfApi::updateMetadata(const Result VfsCfApi::createPlaceholder(const SyncFileItem &item) { - Q_ASSERT(params().filesystemPath.endsWith('/'_L1)); - const auto localPath = QDir::toNativeSeparators(params().filesystemPath + item.localName()); + const auto localPath = QDir::toNativeSeparators(params().filesystemPath() + item.localName()); const auto result = cfapi::createPlaceholderInfo(localPath, item._modtime, item._size, item._fileId); return result; } bool VfsCfApi::needsMetadataUpdate(const SyncFileItem &item) { - const QString path = params().filesystemPath + item.localName(); + const QString path = params().filesystemPath() + item.localName(); if (!QFileInfo::exists(path)) { return false; } @@ -258,13 +257,13 @@ bool VfsCfApi::setPinState(const QString &folderPath, PinState state) { qCDebug(lcCfApi) << u"setPinState" << folderPath << state; - const auto localPath = QDir::toNativeSeparators(params().filesystemPath + folderPath); + const auto localPath = QDir::toNativeSeparators(params().filesystemPath() + folderPath); return static_cast(cfapi::setPinState(localPath, state, cfapi::Recurse)); } Optional VfsCfApi::pinState(const QString &folderPath) { - const auto localPath = QDir::toNativeSeparators(params().filesystemPath + folderPath); + const auto localPath = QDir::toNativeSeparators(params().filesystemPath() + folderPath); const auto info = cfapi::findPlaceholderInfo(localPath); if (!info) { qCDebug(lcCfApi) << u"Couldn't find pin state for regular non-placeholder file" << localPath; diff --git a/test/testutils/syncenginetestutils.cpp b/test/testutils/syncenginetestutils.cpp index 90a7b24ee..751ce24db 100644 --- a/test/testutils/syncenginetestutils.cpp +++ b/test/testutils/syncenginetestutils.cpp @@ -892,7 +892,6 @@ void FakeFolder::switchToVfs(QSharedPointer vfs) _syncEngine->setSyncOptions(opts); OCC::VfsSetupParams vfsParams(account(), OCC::TestUtils::dummyDavUrl(), QString(), u"DisplayName"_s, &syncEngine()); - vfsParams.filesystemPath = localPath(); vfsParams.journal = _journalDb.get(); vfsParams.providerName = QStringLiteral("OC-TEST"); vfsParams.providerDisplayName = QStringLiteral("OC-TEST"); From 8d52cc9dca60be3aaa0a414a1ad3868ead7232c0 Mon Sep 17 00:00:00 2001 From: Hannah von Reth Date: Thu, 19 Feb 2026 11:35:26 +0100 Subject: [PATCH 6/7] Implement inherited pin state openvfs --- src/libsync/discoveryphase.h | 3 +- src/plugins/vfs/xattr/vfs_xattr.cpp | 64 +++++++++++++++++------------ src/plugins/vfs/xattr/vfs_xattr.h | 3 ++ 3 files changed, 42 insertions(+), 28 deletions(-) diff --git a/src/libsync/discoveryphase.h b/src/libsync/discoveryphase.h index 600491e23..81188ef68 100644 --- a/src/libsync/discoveryphase.h +++ b/src/libsync/discoveryphase.h @@ -63,8 +63,7 @@ class DiscoverySingleLocalDirectoryJob : public QObject, public QRunnable private: QString _localPath; AccountPtr _account; - OCC::Vfs* _vfs; -public: + OCC::Vfs *_vfs; }; diff --git a/src/plugins/vfs/xattr/vfs_xattr.cpp b/src/plugins/vfs/xattr/vfs_xattr.cpp index 68dc09b8d..724aadb70 100644 --- a/src/plugins/vfs/xattr/vfs_xattr.cpp +++ b/src/plugins/vfs/xattr/vfs_xattr.cpp @@ -175,31 +175,38 @@ void VfsXAttr::startImpl(const VfsSetupParams ¶ms) qCDebug(lcVfsXAttr, "Start XAttr VFS"); // Lets claim the sync root directory for us - const auto path = FileSystem::toFilesystemPath(params.filesystemPath); // set the owner to opencloud to claim it - if (!FileSystem::Xattr::setxattr(path, QString::fromUtf8(OpenVfsConstants::XAttributeNames::Owner), xattrOwnerString(params.account->uuid()))) { + if (!FileSystem::Xattr::setxattr(params.root(), QString::fromUtf8(OpenVfsConstants::XAttributeNames::Owner), xattrOwnerString(params.account->uuid()))) { Q_EMIT error(tr("Unable to claim the sync root for files on demand")); return; } - auto vfsProcess = new QProcess(this); + qCDebug(lcVfsXAttr) << "Mounting" << openVFSExePath() << params.root().toString(); + _openVfsProcess = new QProcess(this); // merging the channels and piping the output to our log lead to deadlocks - vfsProcess->setProcessChannelMode(QProcess::ForwardedChannels); - const auto logPrefix = [vfsProcess, path = params.filesystemPath] { return u"[%1 %2] "_s.arg(QString::number(vfsProcess->processId()), path); }; - connect(vfsProcess, &QProcess::finished, vfsProcess, [logPrefix, vfsProcess] { - qCInfo(lcVfsXAttr) << logPrefix() << "finished" << vfsProcess->exitCode(); - vfsProcess->deleteLater(); + _openVfsProcess->setProcessChannelMode(QProcess::ForwardedChannels); + const auto logPrefix = [path = params.filesystemPath(), this] { return u"[%1 %2] "_s.arg(QString::number(_openVfsProcess->processId()), path); }; + connect(_openVfsProcess, &QProcess::finished, _openVfsProcess, [logPrefix, this] { + qCInfo(lcVfsXAttr) << logPrefix() << "finished" << _openVfsProcess->exitCode(); + _openVfsProcess->deleteLater(); }); - connect(vfsProcess, &QProcess::started, this, [logPrefix, this] { + connect(_openVfsProcess, &QProcess::started, this, [logPrefix, this] { qCInfo(lcVfsXAttr) << logPrefix() << u"started"; - Q_EMIT started(); + // TODO: + // give it time to mount + QTimer::singleShot(1s, this, &Vfs::started); }); - connect(vfsProcess, &QProcess::errorOccurred, this, [logPrefix, vfsProcess, this] { qCWarning(lcVfsXAttr) << logPrefix() << vfsProcess->errorString(); }); - vfsProcess->start(openVFSExePath(), {u"-d"_s, u"-i"_s, openVFSConfigFilePath(), params.filesystemPath}, QIODevice::ReadOnly); + connect(_openVfsProcess, &QProcess::errorOccurred, this, [logPrefix, this] { qCWarning(lcVfsXAttr) << logPrefix() << _openVfsProcess->errorString(); }); + _openVfsProcess->start(openVFSExePath(), {u"-d"_s, u"-i"_s, openVFSConfigFilePath(), params.filesystemPath()}, QIODevice::ReadOnly); } void VfsXAttr::stop() { + if (_openVfsProcess) { + _openVfsProcess->terminate(); + _openVfsProcess->waitForFinished(); + _openVfsProcess->deleteLater(); + } } void VfsXAttr::unregisterFolder() @@ -379,14 +386,14 @@ void VfsXAttr::slotHydrateJobFinished() Result VfsXAttr::createPlaceholder(const SyncFileItem &item) { - const auto path = FileSystem::toFilesystemPath(params().filesystemPath + item.localName()); + const auto path = params().root() / item.localName(); if (std::filesystem::exists(path)) { if (item._type == ItemTypeVirtualFileDehydration && FileSystem::fileChanged(path, FileSystem::FileChangedInfo::fromSyncFileItem(&item))) { return tr("Cannot dehydrate a placeholder because the file changed"); } Q_ASSERT(item._type == ItemTypeVirtualFile); } - QFile file(path); + QFile file(path.get()); if (!file.open(QFile::ReadWrite | QFile::Truncate)) { return file.errorString(); } @@ -431,9 +438,9 @@ HydrationJob* VfsXAttr::hydrateFile(const QByteArray &fileId, const QString &tar bool VfsXAttr::needsMetadataUpdate(const SyncFileItem &item) { - const QString path = params().filesystemPath + item.localName(); + const auto path = params().root() / item.localName(); // if the attributes do not exist we need to add them - return QFileInfo::exists(path) && !placeHolderAttributes(path); + return QFileInfo::exists(path.toString()) && !placeHolderAttributes(path); } bool VfsXAttr::isDehydratedPlaceholder(const QString &filePath) @@ -465,8 +472,8 @@ LocalInfo VfsXAttr::statTypeVirtualFile(const std::filesystem::directory_entry & bool VfsXAttr::setPinState(const QString &folderPath, PinState state) { - const QString localPath = params().filesystemPath + folderPath; - qCDebug(lcVfsXAttr) << localPath << state; + const auto localPath = params().root() / folderPath; + qCDebug(lcVfsXAttr) << localPath.toString() << state; auto attribs = placeHolderAttributes(localPath); if (!attribs) { // the file is not yet converted @@ -481,17 +488,22 @@ bool VfsXAttr::setPinState(const QString &folderPath, PinState state) Optional VfsXAttr::pinState(const QString &folderPath) { - const auto attribs = placeHolderAttributes(params().filesystemPath + folderPath); - if (!attribs) { - qCDebug(lcVfsXAttr) << u"Couldn't find pin state for regular non-placeholder file" << folderPath; - return {}; + for (auto relativePath = FileSystem::Path::relative(folderPath).get();; relativePath = relativePath.parent_path()) { + const auto attributes = placeHolderAttributes(params().root() / relativePath); + if (!attributes) { + qCDebug(lcVfsXAttr) << "Couldn't find pin state for placeholder file" << folderPath; + return {}; + } + // if the state is inherited and we still have a parent path, retreive that instead. + if (attributes.pinState != OpenVfsConstants::PinStates::Inherited || !relativePath.has_relative_path()) { + return convertPinState(attributes.pinState); + } } - return convertPinState(attribs.pinState); } Vfs::AvailabilityResult VfsXAttr::availability(const QString &folderPath) { - const auto attribs = placeHolderAttributes(params().filesystemPath + folderPath); + const auto attribs = placeHolderAttributes(params().root() / folderPath); if (attribs) { switch (convertPinState(attribs.pinState)) { case OCC::PinState::AlwaysLocal: @@ -523,8 +535,8 @@ Vfs::AvailabilityResult VfsXAttr::availability(const QString &folderPath) void VfsXAttr::fileStatusChanged(const QString& systemFileName, SyncFileStatus fileStatus) { if (fileStatus.tag() == SyncFileStatus::StatusExcluded) { - const QString rel = systemFileName.mid(params().filesystemPath.length()); - setPinState(rel, PinState::Excluded); + const FileSystem::Path rel = std::filesystem::relative(FileSystem::Path(systemFileName), params().root()); + setPinState(rel.toString(), PinState::Excluded); return; } qCDebug(lcVfsXAttr) << systemFileName << fileStatus; diff --git a/src/plugins/vfs/xattr/vfs_xattr.h b/src/plugins/vfs/xattr/vfs_xattr.h index 9b543c668..7e76eb254 100644 --- a/src/plugins/vfs/xattr/vfs_xattr.h +++ b/src/plugins/vfs/xattr/vfs_xattr.h @@ -15,6 +15,8 @@ #include "common/plugin.h" #include "common/result.h" +#include + namespace OCC { class HydrationJob; @@ -62,6 +64,7 @@ public Q_SLOTS: private: QMap _hydrationJobs; + QPointer _openVfsProcess; }; class XattrVfsPluginFactory : public QObject, public DefaultPluginFactory From 390c17dbf44e8282b4cd9241032da9f69fc58088 Mon Sep 17 00:00:00 2001 From: Hannah von Reth Date: Thu, 19 Feb 2026 11:35:35 +0100 Subject: [PATCH 7/7] Activate first unit tests for openvfs --- test/testblacklist.cpp | 7 ++++++- test/testdatabaseerror.cpp | 13 ++++++++----- test/testdownload.cpp | 5 +++++ test/testutils/syncenginetestutils.cpp | 11 ++++++----- 4 files changed, 25 insertions(+), 11 deletions(-) diff --git a/test/testblacklist.cpp b/test/testblacklist.cpp index ff41b751b..a6de4afe3 100644 --- a/test/testblacklist.cpp +++ b/test/testblacklist.cpp @@ -28,6 +28,11 @@ private Q_SLOTS: QTest::newRow("Vfs::Mode::Off") << Vfs::Mode::Off << false; + if (VfsPluginManager::instance().isVfsPluginAvailable(Vfs::Mode::XAttr)) { + QTest::newRow("Vfs::Mode::Xattr dehydrdeated") << Vfs::Mode::XAttr << false; + QTest::newRow("Vfs::Mode::Xattr hydrated") << Vfs::Mode::XAttr << true; + } + if (VfsPluginManager::instance().isVfsPluginAvailable(Vfs::Mode::WindowsCfApi)) { QTest::newRow("Vfs::Mode::WindowsCfApi dehydrated") << Vfs::Mode::WindowsCfApi << true; @@ -82,7 +87,7 @@ private Q_SLOTS: modifier.insert(testFileName); fakeFolder.serverErrorPaths().append(testFileName, 500); // will be blacklisted const bool syncResult = fakeFolder.applyLocalModificationsAndSync(); - if (vfsMode == Vfs::Mode::WindowsCfApi && filesAreDehydrated && remote) { + if (filesAreDehydrated && remote) { // With dehydrated files, only a PROPFIND is done, but not a GET request. // And it is the GET request that fails, and causes a blacklist entry, all "syncs" will succeed. QVERIFY(syncResult); diff --git a/test/testdatabaseerror.cpp b/test/testdatabaseerror.cpp index b160eb24a..7a6c98237 100644 --- a/test/testdatabaseerror.cpp +++ b/test/testdatabaseerror.cpp @@ -24,11 +24,14 @@ private Q_SLOTS: QTest::newRow("Vfs::Mode::Off") << Vfs::Mode::Off << false; + if (VfsPluginManager::instance().isVfsPluginAvailable(Vfs::Mode::XAttr)) { + QTest::newRow("Vfs::Mode::Xattr dehydrdeated") << Vfs::Mode::XAttr << false; + QTest::newRow("Vfs::Mode::Xattr hydrated") << Vfs::Mode::XAttr << true; + } + if (VfsPluginManager::instance().isVfsPluginAvailable(Vfs::Mode::WindowsCfApi)) { QTest::newRow("Vfs::Mode::WindowsCfApi dehydrated") << Vfs::Mode::WindowsCfApi << true; - - // TODO: the hydrated version will fail due to an issue in the winvfs plugin, so leave it disabled for now. - // QTest::newRow("Vfs::Mode::WindowsCfApi hydrated") << Vfs::Mode::WindowsCfApi << false; + QTest::newRow("Vfs::Mode::WindowsCfApi hydrated") << Vfs::Mode::WindowsCfApi << false; } else if (Utility::isWindows()) { qWarning("Skipping Vfs::Mode::WindowsCfApi"); } @@ -43,8 +46,8 @@ private Q_SLOTS: QFETCH_GLOBAL(Vfs::Mode, vfsMode); QFETCH_GLOBAL(bool, filesAreDehydrated); - if (vfsMode == Vfs::Mode::WindowsCfApi) { - QSKIP("Known to be broken, see https://github.com/owncloud/client-desktop-vfs-win/issues/22"); + if (filesAreDehydrated) { + QSKIP("Appending to a virtual file the client doesn't know about can never work"); } FileInfo finalState; diff --git a/test/testdownload.cpp b/test/testdownload.cpp index f1befd3f5..84e73703f 100644 --- a/test/testdownload.cpp +++ b/test/testdownload.cpp @@ -73,6 +73,11 @@ private Q_SLOTS: QTest::newRow("Vfs::Mode::Off") << Vfs::Mode::Off << false; + if (VfsPluginManager::instance().isVfsPluginAvailable(Vfs::Mode::XAttr)) { + QTest::newRow("Vfs::Mode::Xattr dehydrdeated") << Vfs::Mode::XAttr << false; + QTest::newRow("Vfs::Mode::Xattr hydrated") << Vfs::Mode::XAttr << true; + } + if (VfsPluginManager::instance().isVfsPluginAvailable(Vfs::Mode::WindowsCfApi)) { QTest::newRow("Vfs::Mode::WindowsCfApi dehydrated") << Vfs::Mode::WindowsCfApi << true; diff --git a/test/testutils/syncenginetestutils.cpp b/test/testutils/syncenginetestutils.cpp index 751ce24db..2c1a28c08 100644 --- a/test/testutils/syncenginetestutils.cpp +++ b/test/testutils/syncenginetestutils.cpp @@ -865,13 +865,14 @@ FakeFolder::FakeFolder(const FileInfo &fileTemplate, OCC::Vfs::Mode vfsMode, boo Q_ASSERT(vfs); } - // Ensure we have a valid Vfs instance "running" - switchToVfs(vfs); - if (vfsMode != OCC::Vfs::Mode::Off) { - const auto pinState = filesAreDehydrated ? OCC::PinState::OnlineOnly : OCC::PinState::AlwaysLocal; - OC_ENFORCE(vfs->setPinState(QString(), pinState)); + connect(vfs.data(), &OCC::Vfs::started, this, [vfs, filesAreDehydrated] { + const auto pinState = filesAreDehydrated ? OCC::PinState::OnlineOnly : OCC::PinState::AlwaysLocal; + OC_ENFORCE(vfs->setPinState(QString(), pinState)); + }); } + // Ensure we have a valid Vfs instance "running" + switchToVfs(vfs); // A new folder will update the local file state database on first sync. // To have a state matching what users will encounter, we have to a sync