diff --git a/admin/linux/flatpak/com.nextcloud.desktopclient.nextcloud.metainfo.xml b/admin/linux/flatpak/com.nextcloud.desktopclient.nextcloud.metainfo.xml new file mode 100644 index 0000000000000..6a93eb5c47a45 --- /dev/null +++ b/admin/linux/flatpak/com.nextcloud.desktopclient.nextcloud.metainfo.xml @@ -0,0 +1,205 @@ + + + com.nextcloud.desktopclient.nextcloud + Nextcloud Desktop Client + GPL-2.0+ + Sync and collaborate on your desktop or laptop + CC0-1.0 + + Nextcloud GmbH + + +

The Nextcloud desktop client keeps photos and documents always up to date, enabling you to work like you always did. +Any file you add, modify or delete in the synced folders on your desktop or laptop will show up, change or disappear on the server and all other connected devices.

+
+ com.nextcloud.desktopclient.nextcloud.desktop + + + The options dialog + https://nextcloud.com/wp-content/uploads/2022/04/linux.png + + + + #0082c9 + #0082c9 + + https://nextcloud.com + https://github.com/nextcloud/desktop/issues + https://docs.nextcloud.com/server/latest/user_manual/en/desktop/index.html + https://github.com/nextcloud/desktop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/admin/linux/flatpak/com.nextcloud.desktopclient.nextcloud.yml b/admin/linux/flatpak/com.nextcloud.desktopclient.nextcloud.yml new file mode 100644 index 0000000000000..c9203e27118d8 --- /dev/null +++ b/admin/linux/flatpak/com.nextcloud.desktopclient.nextcloud.yml @@ -0,0 +1,166 @@ +app-id: com.nextcloud.desktopclient.nextcloud + +runtime: org.kde.Platform +runtime-version: '6.10' +sdk: org.kde.Sdk + +command: nextcloud + +finish-args: + - --device=all + - --socket=wayland + - --socket=fallback-x11 + - --share=network + - --share=ipc + - --filesystem=host:rw + - --filesystem=/usr/bin/fusermount3 + - --filesystem=xdg-run/Nextcloud:create + - --talk-name=org.freedesktop.Notifications + - --talk-name=org.freedesktop.secrets + - --talk-name=org.kde.StatusNotifierWatcher + - --own-name=com.nextcloudgmbh.Nextcloud + - --talk-name=org.kde.kwalletd5 + - --talk-name=org.kde.kwalletd6 + - --env=TMPDIR=/var/tmp + +cleanup: + - /lib/*.a + - /lib/*.la + - /lib/x86_64-linux-gnu/pkgconfig + - /share/doc + - /share/icons/hicolor/1024x1024 + +modules: + + - name: qtkeychain + buildsystem: cmake-ninja + config-opts: + - -DCMAKE_INSTALL_LIBDIR=lib + - -DBUILD_TRANSLATIONS=NO + - -DBUILD_WITH_QT6=1 + cleanup: + - /include + - /mkspecs + - /lib/cmake + sources: + - type: archive + url: https://github.com/frankosterfeld/qtkeychain/archive/0.15.0.tar.gz + sha256: f4254dc8f0933b06d90672d683eab08ef770acd8336e44dfa030ce041dc2ca22 + x-checker-data: + type: anitya + url-template: https://github.com/frankosterfeld/qtkeychain/archive/$version.tar.gz + stable-only: true + project-id: 4138 + + - name: rsvg-convert-shim + buildsystem: simple + build-commands: + - mkdir -p /app/bin && ln -s /usr/bin/rsvg-convert /app/bin/inkscape + cleanup: + - '*' + + - name: libcloudproviders + buildsystem: meson + config-opts: + - -Dvapigen=false + - -Dintrospection=false + cleanup: + - /include + sources: + - type: git + url: https://gitlab.gnome.org/World/libcloudproviders.git + tag: 0.4.0 + commit: 510f0eee666e4f99e8b11eb55945347db371b47c + x-checker-data: + type: anitya + tag-template: $version + stable-only: true + project-id: 21207 + + - name: libp11 + config-opts: + - --with-enginesdir=${FLATPAK_DEST}/lib/engines-3/ + - --with-modulesdir=${FLATPAK_DEST}/lib/ossl-modules/ + cleanup: + - /include + sources: + - type: archive + url: https://github.com/OpenSC/libp11/releases/download/libp11-0.4.20/libp11-0.4.20.tar.gz + sha256: a125e0310ff10c189fc1b32a9652101486ea94a6b07c677a30e90e3638d2db48 + x-checker-data: + type: anitya + url-template: https://github.com/OpenSC/libp11/releases/download/libp11-$version/libp11-$version.tar.gz + stable-only: true + project-id: 1699 + + - name: nlohmann_json + buildsystem: cmake-ninja + config-opts: + - -DJSON_BuildTests=OFF + sources: + - type: archive + url: https://github.com/nlohmann/json/releases/download/v3.12.0/json.tar.xz + sha256: 42f6e95cad6ec532fd372391373363b62a14af6d771056dbfc86160e6dfff7aa + x-checker-data: + type: anitya + url-template: https://github.com/nlohmann/json/releases/download/v$version/json.tar.xz + stable-only: true + + - name: libfuse + buildsystem: meson + config-opts: + - -Dinitscriptdir="" + - -Dudevrulesdir="" + - -Dutils=false + sources: + - type: archive + url: https://github.com/libfuse/libfuse/releases/download/fuse-3.18.2/fuse-3.18.2.tar.gz + sha256: f01de85717e20adf5f98aff324acd85dd73d61a5ca3834d573dcf0bd6e54a298 + x-checker-data: + type: anitya + url-template: https://github.com/libfuse/libfuse/releases/download/fuse-$version/fuse-$version.tar.gz + stable-only: true + + - name: openvfs + buildsystem: cmake-ninja + builddir: true + config-opts: + - -DCMAKE_BUILD_TYPE=Release + sources: + - type: git + url: https://github.com/opencloud-eu/openvfs.git + commit: 3d72711ebb42ac78ae40ee5fcbd1783abb419e7e + + - name: KDSingleApplication + buildsystem: cmake-ninja + builddir: true + config-opts: + - -DCMAKE_BUILD_TYPE=Release + - -DKDSingleApplication_EXAMPLES=OFF + sources: + - type: archive + url: https://github.com/KDAB/KDSingleApplication/releases/download/v1.2.1/kdsingleapplication-1.2.1.tar.gz + sha256: e3254ce9dc5ecf6d61ef83264bc61d486a307f0e3c9ed1bb2176f068cdbcbe09 + x-checker-data: + type: anitya + project-id: 370439 + url-template: https://github.com/KDAB/KDSingleApplication/releases/download/v$version/kdsingleapplication-$version.tar.gz + + - name: nextcloud-client + buildsystem: cmake-ninja + config-opts: + - -DCMAKE_BUILD_TYPE=Release + - -DCMAKE_INSTALL_LIBDIR=lib + - -DBUILD_SHELL_INTEGRATION_DOLPHIN=0 + - -DBUILD_SHELL_INTEGRATION_NAUTILUS=0 + - -DBUILD_UPDATER=OFF + - -DPLUGIN_INSTALL_DIR=/app/lib/plugins + - -DPLUGINDIR=/app/lib/plugins + - -DQT_MAJOR_VERSION=6 + cleanup: + - /include + post-install: + - install -Dm644 -t /app/share/metainfo admin/linux/flatpak/com.nextcloud.desktopclient.nextcloud.metainfo.xml + sources: + - type: dir + path: "../../.." diff --git a/src/common/common.cmake b/src/common/common.cmake index 564b6c69b5776..b618bdbead96b 100644 --- a/src/common/common.cmake +++ b/src/common/common.cmake @@ -15,7 +15,10 @@ set(common_SOURCES ${CMAKE_CURRENT_LIST_DIR}/syncjournalfilerecord.cpp ${CMAKE_CURRENT_LIST_DIR}/utility.cpp ${CMAKE_CURRENT_LIST_DIR}/remotepermissions.cpp + ${CMAKE_CURRENT_LIST_DIR}/vfs.h ${CMAKE_CURRENT_LIST_DIR}/vfs.cpp + ${CMAKE_CURRENT_LIST_DIR}/path.h + ${CMAKE_CURRENT_LIST_DIR}/path.cpp ${CMAKE_CURRENT_LIST_DIR}/pinstate.cpp ${CMAKE_CURRENT_LIST_DIR}/plugin.cpp ${CMAKE_CURRENT_LIST_DIR}/syncfilestatus.cpp diff --git a/src/common/filesystembase.cpp b/src/common/filesystembase.cpp index d29bd6b49a017..85b42c7b060d9 100644 --- a/src/common/filesystembase.cpp +++ b/src/common/filesystembase.cpp @@ -32,6 +32,33 @@ namespace OCC { Q_LOGGING_CATEGORY(lcFileSystem, "nextcloud.sync.filesystem", QtInfoMsg) +std::filesystem::path FileSystem::toFilesystemPath(const QString &path) +{ +#ifdef Q_OS_WIN + return QtPrivate::toFilesystemPath(FileSystem::longWinPath(path)); +#else + return QtPrivate::toFilesystemPath(path); +#endif +} + +QString FileSystem::fromFilesystemPath(const std::filesystem::path &path) +{ +#ifdef Q_OS_WIN + constexpr std::wstring_view prefix = LR"(\\?\)"; + std::wstring nativePath = path.native(); + auto view = std::wstring_view(nativePath); + if (nativePath.starts_with(prefix)) { + view = view.substr(prefix.size()); + } + return QDir::fromNativeSeparators(QString::fromWCharArray(view.data(), view.length())); +#elif defined(Q_OS_MACOS) + // based on QFile::decodeName + return QtPrivate::fromFilesystemPath(path).normalized(QString::NormalizationForm_C); +#else + return QtPrivate::fromFilesystemPath(path); +#endif +} + QString FileSystem::longWinPath(const QString &inpath) { #ifdef Q_OS_WIN @@ -800,6 +827,45 @@ bool FileSystem::isJunction(const QString &filename) #endif } + +FileSystem::ChildResults FileSystem::isChildPathOf2(QStringView child, QStringView parent) +{ + // if it is a relative path assume a local file, resolve it based on root + const auto sensitivity = Utility::fsCaseSensitivity(); + + // Fast-path the 3 common cases in this if-else statement: + if (parent.isEmpty()) { + // The empty parent is often used as the sync root, as (child) items do not start with a `/` + return ChildResult::IsChild | ChildResult::IsParentEmpty; + } + if (child.compare(parent, sensitivity) == 0) { + return ChildResult::IsChild | ChildResult::IsEqual; + } + const auto isSeparator = [](QChar c) { return c == QLatin1Char('/') || (Utility::isWindows() && c == QLatin1Char('\\')); }; + if (isSeparator(parent.back())) { + // Here we can do a normal prefix check, because the parent is "terminated" with a slash, + // and we can't walk into the case in the else below. + if (child.startsWith(parent, sensitivity)) { + return ChildResult::IsChild; + } + // else: do the `cleanPath` version below + } + + // Slow path (`QDir::cleanPath` does lots of string operations): + const QString cleanParent = QDir::cleanPath(parent.toString()); + const QString cleanChild = QDir::cleanPath(child.toString()); + // both paths are the same /root/foo == /root/foo + if (cleanChild.compare(cleanParent, sensitivity) == 0) { + return ChildResult::IsChild | ChildResult::IsEqual; + } + // cleanPath removes trailing slashes, add one to parent to be sure we handle a child path + // /root/foo/bar is not a child of /root/fo, therefore, the trailing slash is important + if (cleanChild.startsWith(cleanParent + QLatin1Char('/'), sensitivity)) { + return ChildResult::IsChild; + } + return ChildResult::IsNoChild; +} + #ifdef Q_OS_WIN QString FileSystem::pathtoUNC(const QString &_str) { diff --git a/src/common/filesystembase.h b/src/common/filesystembase.h index eab41703e5b82..ac1539f5701a1 100644 --- a/src/common/filesystembase.h +++ b/src/common/filesystembase.h @@ -37,6 +37,9 @@ OCSYNC_EXPORT Q_DECLARE_LOGGING_CATEGORY(lcFileSystem) namespace FileSystem { OCSYNC_EXPORT Q_NAMESPACE; + OCSYNC_EXPORT std::filesystem::path toFilesystemPath(const QString &path); + OCSYNC_EXPORT QString fromFilesystemPath(const std::filesystem::path &path); + enum class FolderPermissions { ReadOnly, ReadWrite, @@ -206,6 +209,20 @@ namespace FileSystem { * Returns whether the file is a junction (windows only) */ bool OCSYNC_EXPORT isJunction(const QString &filename); + + /** + * Returns whether a Path is a child of another + */ + enum class ChildResult : uint8_t { IsNoChild = 0, IsChild = 0x1 << 0, IsEqual = 0x1 << 1, IsParentEmpty = 0x1 << 2 }; + Q_FLAG_NS(ChildResult); + Q_DECLARE_FLAGS(ChildResults, ChildResult) + Q_DECLARE_OPERATORS_FOR_FLAGS(ChildResults) + + ChildResults OCSYNC_EXPORT isChildPathOf2(QStringView child, QStringView parent); + inline bool isChildPathOf(QStringView child, QStringView parent) + { + return isChildPathOf2(child, parent) & ChildResult::IsChild; + } } /** @} */ diff --git a/src/common/path.cpp b/src/common/path.cpp new file mode 100644 index 0000000000000..566713b11e1f2 --- /dev/null +++ b/src/common/path.cpp @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2025 Hannah von Reth + +#include "path.h" + +#include "common/filesystembase.h" + +OCC::FileSystem::Path::Path(QStringView 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 +{ + if (_path.empty()) { + return {}; + } + return fromFilesystemPath(_path); +} + +bool OCC::FileSystem::Path::exists() const +{ + if (_path.empty()) { + return false; + } + std::error_code ec; + const bool exists = std::filesystem::exists(_path, ec); + if (ec) { + qCCritical(lcFileSystem) << u"Error checking existence of" << _path.native() << ec.value() << ec.message(); + return false; + } + return exists; +} + +QDebug operator<<(QDebug debug, const OCC::FileSystem::Path &path) +{ + return debug << u"Path(" << path.toString() << u")"; +} diff --git a/src/common/path.h b/src/common/path.h new file mode 100644 index 0000000000000..42e335ddf5bce --- /dev/null +++ b/src/common/path.h @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2025 Hannah von Reth + +#pragma once + +#include "ocsynclib.h" + +#include + +#include + +namespace OCC { +namespace FileSystem { + class OCSYNC_EXPORT Path + { + public: + Path() = default; + /** + * 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(QStringView 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; } + [[nodiscard]] operator const std::filesystem::path &() const { return _path; } + const std::filesystem::path *operator->() 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/(QStringView 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/=(QStringView other) + { + _path /= relative(other); + return *this; + } + + [[nodiscard]] QString toString() const; + + [[nodiscard]] bool exists() const; + + private: + std::filesystem::path _path; + }; +} +} + + +OCSYNC_EXPORT QDebug operator<<(QDebug debug, const OCC::FileSystem::Path &path); diff --git a/src/common/plugin.cpp b/src/common/plugin.cpp index 905dfe67e7e3a..9354d8d6fca02 100644 --- a/src/common/plugin.cpp +++ b/src/common/plugin.cpp @@ -12,6 +12,11 @@ namespace OCC { PluginFactory::~PluginFactory() = default; +bool PluginFactory::checkAvailability() const +{ + return true; +} + QString pluginFileName(const QString &type, const QString &name) { return QStringLiteral("%1sync_%2_%3") diff --git a/src/common/plugin.h b/src/common/plugin.h index 8fa354c4a5130..2bdaa3c459d1a 100644 --- a/src/common/plugin.h +++ b/src/common/plugin.h @@ -7,6 +7,8 @@ #pragma once #include "ocsynclib.h" +#include "result.h" + #include namespace OCC { @@ -16,6 +18,15 @@ class OCSYNC_EXPORT PluginFactory public: virtual ~PluginFactory(); virtual QObject* create(QObject* parent) = 0; + + [[nodiscard]] virtual bool checkAvailability() const; + + /** + * @param path The path for which the plugin should be prepared + * @param accountUuid The UUID of the account for which the plugin should be prepared (might be null during account setup) + * @return Nothing or an error string + */ + [[nodiscard]] virtual Result prepare(const QString &path, const QUuid &accountUuid) const = 0; }; template diff --git a/src/common/syncfilestatus.cpp b/src/common/syncfilestatus.cpp index 05dc576c89867..81afb3b5601a4 100644 --- a/src/common/syncfilestatus.cpp +++ b/src/common/syncfilestatus.cpp @@ -5,6 +5,8 @@ #include "syncfilestatus.h" +#include + namespace OCC { SyncFileStatus::SyncFileStatus() = default; @@ -68,3 +70,11 @@ QString SyncFileStatus::toSocketAPIString() const return statusString; } } + +QDebug &operator<<(QDebug &debug, const OCC::SyncFileStatus &item) +{ + QDebugStateSaver saver(debug); + debug.setAutoInsertSpaces(false); + debug << u"OCC::SyncFileStatus(shared=" << item.shared() << u", tag=" << item.tag() << u")"; + return debug; +} diff --git a/src/common/syncfilestatus.h b/src/common/syncfilestatus.h index ce16c26c4edb7..6ca7903e5818b 100644 --- a/src/common/syncfilestatus.h +++ b/src/common/syncfilestatus.h @@ -57,6 +57,7 @@ inline bool operator!=(const SyncFileStatus &a, const SyncFileStatus &b) return !(a == b); } } +OCSYNC_EXPORT QDebug &operator<<(QDebug &debug, const OCC::SyncFileStatus &item); Q_DECLARE_METATYPE(OCC::SyncFileStatus) diff --git a/src/common/syncjournalfilerecord.h b/src/common/syncjournalfilerecord.h index 5258785fbefbe..19d38be018a68 100644 --- a/src/common/syncjournalfilerecord.h +++ b/src/common/syncjournalfilerecord.h @@ -53,6 +53,8 @@ class OCSYNC_EXPORT SyncJournalFileRecord [[nodiscard]] QByteArray numericFileId() const; [[nodiscard]] QDateTime modDateTime() const { return Utility::qDateTimeFromTime_t(_modtime); } + SyncJournalFileRecord fromSyncFileItem(const SyncFileItem &syncFile); + [[nodiscard]] bool isDirectory() const { return _type == ItemTypeVirtualDirectory || _type == ItemTypeDirectory; } [[nodiscard]] bool isFile() const { return _type == ItemTypeFile || _type == ItemTypeVirtualFileDehydration; } [[nodiscard]] bool isVirtualFile() const { return _type == ItemTypeVirtualFile || _type == ItemTypeVirtualFileDownload; } diff --git a/src/common/utility.h b/src/common/utility.h index f6dd48298acde..5a1dfedc043c7 100644 --- a/src/common/utility.h +++ b/src/common/utility.h @@ -227,6 +227,11 @@ namespace Utility { // if false, the two cases are two different files. OCSYNC_EXPORT bool fsCasePreserving(); + inline auto fsCaseSensitivity() + { + return fsCasePreserving() ? Qt::CaseInsensitive : Qt::CaseSensitive; + } + // Check if two paths that MUST exist are equal. This function // uses QDir::canonicalPath() to judge and cares for the systems // case sensitivity. diff --git a/src/common/vfs.cpp b/src/common/vfs.cpp index fab934f336b39..dad63f0ea6193 100644 --- a/src/common/vfs.cpp +++ b/src/common/vfs.cpp @@ -37,6 +37,8 @@ QString Vfs::modeToString(Mode mode) return QStringLiteral("wincfapi"); case XAttr: return QStringLiteral("xattr"); + case OpenVFS: + return QStringLiteral("openvfs"); } return QStringLiteral("off"); } @@ -50,6 +52,10 @@ Optional Vfs::modeFromString(const QString &str) return WithSuffix; } else if (str == QLatin1String("wincfapi")) { return WindowsCfApi; + } else if (str == QLatin1String("xattr")) { + return XAttr; + } else if (str == QLatin1String("openvfs")) { + return OpenVFS; } return {}; } @@ -137,18 +143,27 @@ VfsOff::VfsOff(QObject *parent) VfsOff::~VfsOff() = default; +HydrationJob *VfsOff::hydrateFile([[maybe_unused]] const QByteArray &fileId, [[maybe_unused]] const QString &targetPath) +{ + return nullptr; +} + static QString modeToPluginName(Vfs::Mode mode) { - if (mode == Vfs::WithSuffix) { + switch (mode) { + case Vfs::Off: + return {}; + case Vfs::WithSuffix: return QStringLiteral("suffix"); - } - if (mode == Vfs::WindowsCfApi) { + case Vfs::WindowsCfApi: return QStringLiteral("cfapi"); - } - if (mode == Vfs::XAttr) { + case Vfs::XAttr: return QStringLiteral("xattr"); + case Vfs::OpenVFS: + return QStringLiteral("openvfs"); } - return QString(); + + return {}; } Q_LOGGING_CATEGORY(lcPlugin, "plugins", QtInfoMsg) @@ -203,6 +218,10 @@ Vfs::Mode OCC::bestAvailableVfsMode() return Vfs::WindowsCfApi; } + if (isVfsPluginAvailable(Vfs::OpenVFS)) { + return Vfs::OpenVFS; + } + if (isVfsPluginAvailable(Vfs::WithSuffix)) { return Vfs::WithSuffix; } @@ -260,6 +279,10 @@ std::unique_ptr OCC::createVfsFromPlugin(Vfs::Mode mode) qCCritical(lcPlugin) << "Plugin" << loader.fileName() << "does not implement PluginFactory"; return nullptr; } + if (!factory->checkAvailability()) { + qCCritical(lcPlugin) << "Plugin" << loader.fileName() << "does not implement PluginFactory"; + return nullptr; + } auto vfs = std::unique_ptr(qobject_cast(factory->create(nullptr))); if (!vfs) { @@ -270,3 +293,8 @@ std::unique_ptr OCC::createVfsFromPlugin(Vfs::Mode mode) qCInfo(lcPlugin) << "Created VFS instance from plugin" << pluginPath; return vfs; } + +const FileSystem::Path &VfsSetupParams::root() const +{ + return rootPath; +} diff --git a/src/common/vfs.h b/src/common/vfs.h index b9f6a2b6cb72e..d92ed1c4c367d 100644 --- a/src/common/vfs.h +++ b/src/common/vfs.h @@ -6,9 +6,11 @@ #pragma once #include "ocsynclib.h" + #include "result.h" #include "syncfilestatus.h" #include "pinstate.h" +#include "path.h" #include "common/remoteinfo.h" @@ -31,6 +33,7 @@ class SyncJournalDb; class VfsPrivate; class SyncFileItem; using SyncFileItemPtr = QSharedPointer; +class HydrationJob; enum class VirtualItemCreationStatus { Unknown, @@ -54,6 +57,7 @@ struct OCSYNC_EXPORT VfsSetupParams * Always ends with /. */ QString filesystemPath; + FileSystem::Path rootPath; // Folder display name in Windows Explorer QString displayName; @@ -82,11 +86,14 @@ struct OCSYNC_EXPORT VfsSetupParams /// Strings potentially passed on to the platform QString providerName; QString providerVersion; + QString socketPath; /** when registering with the system we might use * a different presentaton to identify the accounts */ bool multipleAccountsRegistered = false; + + const FileSystem::Path &root() const; }; /** Interface describing how to deal with virtual/placeholder files. @@ -116,6 +123,7 @@ class OCSYNC_EXPORT Vfs : public QObject WithSuffix, WindowsCfApi, XAttr, + OpenVFS, }; Q_ENUM(Mode) enum class ConvertToPlaceholderResult { @@ -286,6 +294,15 @@ class OCSYNC_EXPORT Vfs : public QObject */ [[nodiscard]] virtual AvailabilityResult availability(const QString &folderPath, const AvailabilityRecursivity recursiveCheck) = 0; + /** Start a hydration (download of remote contents) of a file. + * + * The fileId is the SyncFileItem::id() value of the file to hydrate. + * The targetPath is the absolute path to the local file to hydrate. + * + * Returns a QFuture void if successful and QFuture QString if an error occurs. + */ + [[nodiscard]] virtual HydrationJob *hydrateFile(const QByteArray &fileId, const QString &targetPath) = 0; + public Q_SLOTS: /** Update in-sync state based on SyncFileStatusTracker signal. * @@ -303,6 +320,15 @@ public Q_SLOTS: // Emitted when hydration fails void failureHydrating(int errorCode, int statusCode, const QString &errorString, const QString &fileName); + /// we encountered an error + void error(const QString &error); + + /// start complete + void started(); + + /// The vfs plugin detected that the meta data are out of sync and requests a sync with the server + void needSync(); + protected: /** Setup the plugin for the folder. * @@ -361,6 +387,8 @@ class OCSYNC_EXPORT VfsOff : public Vfs Optional pinState(const QString &) override { return PinState::AlwaysLocal; } AvailabilityResult availability(const QString &, const AvailabilityRecursivity) override { return VfsItemAvailability::AlwaysLocal; } + [[nodiscard]] HydrationJob *hydrateFile(const QByteArray &fileId, const QString &targetPath) override; + public Q_SLOTS: void fileStatusChanged(const QString &, OCC::SyncFileStatus) override {} diff --git a/src/gui/accountmanager.cpp b/src/gui/accountmanager.cpp index 38b6923294ad3..01bef8c8caa97 100644 --- a/src/gui/accountmanager.cpp +++ b/src/gui/accountmanager.cpp @@ -36,6 +36,7 @@ constexpr auto authTypeC = "authType"; constexpr auto userC = "user"; constexpr auto displayNameC = "displayName"; constexpr auto httpUserC = "http_user"; +constexpr auto accountUuidC = "account_uuid"; constexpr auto davUserC = "dav_user"; constexpr auto webflowUserC = "webflow_user"; constexpr auto shibbolethUserC = "shibboleth_shib_user"; @@ -426,6 +427,7 @@ void AccountManager::saveAccountHelper(const AccountPtr &account, QSettings &set } else { settings.setValue(QLatin1String(urlC), account->_url.toString()); } + settings.setValue(QLatin1String(accountUuidC), account->_uuid); settings.setValue(QLatin1String(davUserC), account->_davUser); settings.setValue(QLatin1String(displayNameC), account->davDisplayName()); settings.setValue(QLatin1String(serverVersionC), account->_serverVersion); @@ -652,6 +654,7 @@ AccountPtr AccountManager::loadAccountHelper(QSettings &settings) settings.value(QLatin1String(serverDesktopEnterpriseUpdateChannelC), QVariant::fromValue(UpdateChannel::Invalid.toString())).toString()); acc->_skipE2eeMetadataChecksumValidation = settings.value(QLatin1String(skipE2eeMetadataChecksumValidationC), {}).toBool(); acc->_davUser = settings.value(QLatin1String(davUserC)).toString(); + acc->_uuid = QUuid{settings.value(QLatin1String(accountUuidC)).toString()}; #ifdef BUILD_FILE_PROVIDER_MODULE acc->setFileProviderDomainIdentifier(settings.value(QLatin1String(fileProviderDomainIdentifierC)).toString()); #endif diff --git a/src/gui/application.cpp b/src/gui/application.cpp index dcbaeaacb917d..88b6d41830b77 100644 --- a/src/gui/application.cpp +++ b/src/gui/application.cpp @@ -416,6 +416,10 @@ Application::Application(int &argc, char **argv) qCInfo(lcApplication) << "VFS suffix plugin is available"; } + if (isVfsPluginAvailable(Vfs::OpenVFS)) { + qCInfo(lcApplication) << "VFS openvfs linux plugin is available"; + } + _theme->setSystrayUseMonoIcons(ConfigFile().monoIcons()); connect(this, &Application::systemPaletteChanged, _theme, &Theme::systemPaletteHasChanged); diff --git a/src/gui/folder.cpp b/src/gui/folder.cpp index b0eac7d97045f..527c00527a5cf 100644 --- a/src/gui/folder.cpp +++ b/src/gui/folder.cpp @@ -584,6 +584,7 @@ void Folder::startVfs() qCDebug(lcFolder) << "Display name for VFS folder will be:" << displayName; VfsSetupParams vfsParams; vfsParams.filesystemPath = path(); + vfsParams.rootPath = FileSystem::Path{path()}; vfsParams.displayName = displayName; vfsParams.alias = alias(); vfsParams.navigationPaneClsid = navigationPaneClsid().toString(); @@ -592,6 +593,7 @@ void Folder::startVfs() vfsParams.journal = &_journal; vfsParams.providerName = Theme::instance()->appNameGUI(); vfsParams.providerVersion = Theme::instance()->version(); + vfsParams.socketPath = FolderMan::instance()->socketApi()->socketPath(); vfsParams.multipleAccountsRegistered = AccountManager::instance()->accounts().size() > 1; connect(_vfs.data(), &Vfs::beginHydrating, this, &Folder::slotHydrationStarts); @@ -601,6 +603,14 @@ void Folder::startVfs() connect(&_engine->syncFileStatusTracker(), &SyncFileStatusTracker::fileStatusChanged, _vfs.data(), &Vfs::fileStatusChanged); + connect(_vfs.get(), &Vfs::needSync, this, [this] { + if (canSync()) { + // the vfs plugin detected that its metadata is out of sync and requests a new sync + // the request has a hight priority as it is probably issued after a user request + FolderMan::instance()->scheduleFolder(this); + } + }); + _vfs->start(vfsParams); // Immediately mark the sqlite temporaries as excluded. They get recreated diff --git a/src/gui/socketapi/CMakeLists.txt b/src/gui/socketapi/CMakeLists.txt index abb6bcea3fcb4..b93ab30704321 100644 --- a/src/gui/socketapi/CMakeLists.txt +++ b/src/gui/socketapi/CMakeLists.txt @@ -3,6 +3,7 @@ # SPDX-License-Identifier: GPL-2.0-or-later target_sources(nextcloudCore PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/socketapi.h + ${CMAKE_CURRENT_SOURCE_DIR}/socketapi_p.h ${CMAKE_CURRENT_SOURCE_DIR}/socketapi.cpp ${CMAKE_CURRENT_SOURCE_DIR}/socketapi_p.h ) diff --git a/src/gui/socketapi/socketapi.cpp b/src/gui/socketapi/socketapi.cpp index e1bebea394d95..5f9d1588fc275 100644 --- a/src/gui/socketapi/socketapi.cpp +++ b/src/gui/socketapi/socketapi.cpp @@ -35,6 +35,7 @@ #ifndef OWNCLOUD_TEST #include "sharemanager.h" #endif +#include "hydrationjob.h" #include #include @@ -247,6 +248,7 @@ SocketApi::SocketApi(QObject *parent) { qRegisterMetaType("SocketListener*"); qRegisterMetaType>("QSharedPointer"); + qRegisterMetaType>("QSharedPointer"); #if defined(Q_OS_MACOS) // On macOS, shell extensions communicate via XPC (FinderSyncXPC / FinderSyncService). @@ -277,6 +279,9 @@ SocketApi::SocketApi(QObject *parent) qCWarning(lcSocketApi) << "An unexpected system detected, this probably won't work."; } + m_socketPath = socketPath; + Q_EMIT socketPathChanged(); + QLocalServer::removeServer(socketPath); // Create the socket path: if (!Utility::isMac()) { @@ -387,13 +392,15 @@ void SocketApi::slotReadSocket() const QByteArray command = line.mid(0, argPos).toUtf8().toUpper(); const int indexOfMethod = [&] { QByteArray functionWithArguments = QByteArrayLiteral("command_"); - if (command.startsWith("ASYNC_")) { + if (command.startsWith(QLatin1String("ASYNC_"))) { functionWithArguments += command + QByteArrayLiteral("(QSharedPointer)"); + } else if (command.startsWith(QLatin1String("V2/"))) { + functionWithArguments += QByteArrayLiteral("V2_") + command.mid(3) + QByteArrayLiteral("(QSharedPointer)"); } else { functionWithArguments += command + QByteArrayLiteral("(QString,SocketListener*)"); } - Q_ASSERT(staticMetaObject.normalizedSignature(functionWithArguments) == functionWithArguments); - const auto out = staticMetaObject.indexOfMethod(functionWithArguments); + Q_ASSERT(staticMetaObject.normalizedSignature(functionWithArguments.constData()) == functionWithArguments); + const auto out = staticMetaObject.indexOfMethod(functionWithArguments.constData()); if (out == -1) { listener->sendError(QStringLiteral("Function %1 not found").arg(QString::fromUtf8(functionWithArguments))); } @@ -430,6 +437,21 @@ void SocketApi::slotReadSocket() .invoke(this, Qt::QueuedConnection, Q_ARG(QString, argument), Q_ARG(SocketListener *, listener.data())); } + } else if (command.startsWith(QLatin1String("V2/"))) { + QJsonParseError error; + const auto json = QJsonDocument::fromJson(argument.toUtf8(), &error).object(); + if (error.error != QJsonParseError::NoError) { + qCWarning(lcSocketApi()) << u"Invalid json" << argument << error.errorString(); + listener->sendError(error.errorString()); + return; + } + auto socketApiJob = QSharedPointer::create(listener, command, json); + if (indexOfMethod != -1) { + staticMetaObject.method(indexOfMethod).invoke(this, Qt::QueuedConnection, Q_ARG(QSharedPointer, socketApiJob)); + } else { + qCWarning(lcSocketApi) << u"The command is not supported by this version of the client:" << command << u"with argument:" << argument; + socketApiJob->failure(QStringLiteral("command not found")); + } } else { if (indexOfMethod != -1) { // to ensure that listener is still valid we need to call it with Qt::DirectConnection @@ -1137,6 +1159,36 @@ void SocketApi::command_UNLOCK_FILE(const QString &localFile, SocketListener *li setFileLock(localFile, SyncFileItem::LockStatus::UnlockedItem); } +void SocketApi::command_V2_HYDRATE_FILE(const QSharedPointer &job) const +{ + const auto &arguments = job->arguments(); + + const QByteArray fileId = arguments[QStringLiteral("fileId")].toString().toUtf8(); + const QString targetPath = arguments[QStringLiteral("file")].toString(); + + auto fileData = FileData::get(targetPath); + + if (fileData.folder) { + HydrationJob *hydJob = fileData.folder->vfs().hydrateFile(fileId, targetPath); + + if (hydJob) { + connect(hydJob, &HydrationJob::finished, this, [job, hydJob] { + job->success({{QStringLiteral("status"), QStringLiteral("OK")}}); + 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")); + } +} + void SocketApi::setFileLock(const QString &localFile, const SyncFileItem::LockStatus lockState) const { const auto fileData = FileData::get(localFile); @@ -1783,6 +1835,50 @@ void SocketApiJob::reject(const QString &response) _socketListener->sendMessage(QStringLiteral("REJECT|") + _jobId + QLatin1Char('|') + response); } +QString SocketApi::socketPath() const +{ + return m_socketPath; +} + +SocketApiJobV2::SocketApiJobV2(const QSharedPointer &socketListener, const QString &command, const QJsonObject &arguments) + : _socketListener(socketListener) + , _command(command) + , _jobId(arguments[QStringLiteral("id")].toString()) + , _arguments(arguments[QStringLiteral("arguments")].toObject()) +{ + Q_ASSERT(!_jobId.isEmpty()); +} + +void SocketApiJobV2::success(const QJsonObject &response) const +{ + doFinish(response); +} + +void SocketApiJobV2::failure(const QString &error) const +{ + doFinish({{QStringLiteral("error"), error}}); +} + +void SocketApiJobV2::doFinish(const QJsonObject &obj) const +{ + QJsonObject data{{QStringLiteral("id"), _jobId}, {QStringLiteral("arguments"), obj}}; + if (!_warning.isEmpty()) { + data[QStringLiteral("warning")] = _warning; + } + _socketListener->sendMessage(_command + QStringLiteral("_RESULT:") + QString::fromUtf8(QJsonDocument(data).toJson(QJsonDocument::Compact))); + Q_EMIT finished(); +} + +QString SocketApiJobV2::warning() const +{ + return _warning; +} + +void SocketApiJobV2::setWarning(const QString &warning) +{ + _warning = warning; +} + } // namespace OCC #include "socketapi.moc" diff --git a/src/gui/socketapi/socketapi.h b/src/gui/socketapi/socketapi.h index 096049dbce365..4d3c006eaa2a0 100644 --- a/src/gui/socketapi/socketapi.h +++ b/src/gui/socketapi/socketapi.h @@ -27,6 +27,7 @@ class Folder; class SocketListener; class DirectEditor; class SocketApiJob; +class SocketApiJobV2; namespace Mac { class FinderSyncService; @@ -46,6 +47,8 @@ class SocketApi : public QObject { Q_OBJECT + Q_PROPERTY(QString socketPath READ socketPath NOTIFY socketPathChanged FINAL) + enum SharingContextItemEncryptedFlag { EncryptedItem, NotEncryptedItem @@ -63,6 +66,8 @@ class SocketApi : public QObject explicit SocketApi(QObject *parent = nullptr); ~SocketApi() override; + [[nodiscard]] QString socketPath() const; + public Q_SLOTS: void slotUpdateFolderView(OCC::Folder *f); void slotUnregisterPath(const QString &alias); @@ -77,6 +82,8 @@ public Q_SLOTS: void resolveConflictCommandReceived(const QString &conflictedPath, const QString &basePath, const QString &baseName, const QString &folderAlias); void moveItemCommandReceived(const QString &localPath, const QString &defaultTarget); + void socketPathChanged(); + private Q_SLOTS: void slotNewConnection(); void onLostConnection(); @@ -145,6 +152,7 @@ private Q_SLOTS: Q_INVOKABLE void command_UNLOCK_FILE(const QString &localFile, OCC::SocketListener *listener); Q_INVOKABLE void command_FILE_ACTIONS(const QString &localFile, OCC::SocketListener *listener); Q_INVOKABLE void command_FILES_GOVERNANCE_LABELS(const QString &localFile, OCC::SocketListener *listener); + Q_INVOKABLE void command_V2_HYDRATE_FILE(const QSharedPointer &job) const; void setFileLock(const QString &localFile, const SyncFileItem::LockStatus lockState) const; @@ -203,6 +211,7 @@ private Q_SLOTS: QSet _registeredAliases; QMap> _listeners; QLocalServer _localServer; + QString m_socketPath; }; } diff --git a/src/gui/socketapi/socketapi_p.h b/src/gui/socketapi/socketapi_p.h index ba68a895ba768..8e483ae6a90e5 100644 --- a/src/gui/socketapi/socketapi_p.h +++ b/src/gui/socketapi/socketapi_p.h @@ -132,6 +132,39 @@ class SocketApiJob : public QObject QJsonObject _arguments; }; +class SocketApiJobV2 : public QObject +{ + Q_OBJECT +public: + explicit SocketApiJobV2(const QSharedPointer &socketListener, const QString &command, const QJsonObject &arguments); + + void success(const QJsonObject &response) const; + void failure(const QString &error) const; + + const QJsonObject &arguments() const + { + return _arguments; + } + QString command() const + { + return _command; + } + + QString warning() const; + void setWarning(const QString &warning); + +Q_SIGNALS: + void finished() const; + +private: + void doFinish(const QJsonObject &obj) const; + + QSharedPointer _socketListener; + const QString _command; + QString _jobId; + QJsonObject _arguments; + QString _warning; +}; } Q_DECLARE_METATYPE(OCC::SocketListener *) diff --git a/src/gui/wizard/accountwizardcontroller.cpp b/src/gui/wizard/accountwizardcontroller.cpp index 901e61354fea8..54a913ac4cecf 100644 --- a/src/gui/wizard/accountwizardcontroller.cpp +++ b/src/gui/wizard/accountwizardcontroller.cpp @@ -103,9 +103,11 @@ AccountWizardController::AccountWizardController(QObject *parent) _largeFolderThresholdMb = static_cast(largeFolderLimit.second); _askBeforeExternalStorage = cfg.confirmExternalStorage(); +#ifndef Q_OS_LINUX if (canUseVirtualFiles()) { _syncMode = VirtualFiles; } +#endif } AccountWizardController::~AccountWizardController() = default; diff --git a/src/gui/wizard/qml/OptionRow.qml b/src/gui/wizard/qml/OptionRow.qml index fa1f127e9a6a5..bed0dfe2a871a 100644 --- a/src/gui/wizard/qml/OptionRow.qml +++ b/src/gui/wizard/qml/OptionRow.qml @@ -21,7 +21,9 @@ Control { signal clicked() - implicitHeight: descriptionLabel.text === "" ? 42 : 56 + implicitHeight: descriptionLabel.text === "" + ? 42 + : Math.max(56, 2 * root.padding + titleLabel.implicitHeight + descriptionLabel.implicitHeight) padding: 10 background: Rectangle { @@ -76,7 +78,7 @@ Control { color: root.hintTextColor font.pixelSize: Style.pixelSize wrapMode: Text.WordWrap - maximumLineCount: 2 + maximumLineCount: 4 } } } diff --git a/src/gui/wizard/qml/SyncOptionsPage.qml b/src/gui/wizard/qml/SyncOptionsPage.qml index b06206a378693..4f2fb67a54d18 100644 --- a/src/gui/wizard/qml/SyncOptionsPage.qml +++ b/src/gui/wizard/qml/SyncOptionsPage.qml @@ -87,8 +87,10 @@ Item { OptionRow { Layout.fillWidth: true visible: root.controller.canUseVirtualFiles - title: root.controller.isUsingFileProvider ? qsTr("File Provider") : qsTr("Virtual files") - description: qsTr("Download files on-demand") + title: root.controller.isUsingFileProvider ? qsTr("File Provider") : (Qt.platform.os === "linux" ? qsTr("Virtual files (beta)") : qsTr("Virtual files")) + description: Qt.platform.os === "linux" + ? qsTr("Download files on-demand. This feature is in an early preview. Recommended only for testing.") + : qsTr("Download files on-demand") selected: root.controller.syncMode === AccountWizardController.VirtualFiles onClicked: root.controller.setSyncMode(AccountWizardController.VirtualFiles) } diff --git a/src/libsync/CMakeLists.txt b/src/libsync/CMakeLists.txt index 85b3363e03fed..96328a1b79e57 100644 --- a/src/libsync/CMakeLists.txt +++ b/src/libsync/CMakeLists.txt @@ -166,6 +166,10 @@ set(libsync_SRCS creds/keychainchunk.cpp caseclashconflictsolver.h caseclashconflictsolver.cpp + chronoelapsedtimer.h + chronoelapsedtimer.cpp + hydrationjob.h + hydrationjob.cpp ) if (WIN32) diff --git a/src/libsync/account.cpp b/src/libsync/account.cpp index 983d984318fa3..261cf35653cf1 100644 --- a/src/libsync/account.cpp +++ b/src/libsync/account.cpp @@ -82,6 +82,7 @@ Account::Account(QObject *parent) , _capabilities(QVariantMap()) , _serverColor(Theme::defaultColor()) , _e2e{} + , _uuid{QUuid::createUuid()} { qRegisterMetaType("AccountPtr"); qRegisterMetaType("Account*"); @@ -1584,6 +1585,11 @@ bool Account::serverHasIntegration() const return _serverHasIntegration; } +QUuid Account::uuid() const +{ + return _uuid; +} + void Account::updateServerHasIntegration() { _serverHasIntegration = capabilities().serverHasClientIntegration(); diff --git a/src/libsync/account.h b/src/libsync/account.h index a0d376d3f2434..0452ab5940ea1 100644 --- a/src/libsync/account.h +++ b/src/libsync/account.h @@ -29,6 +29,7 @@ #include #include #include +#include #ifndef TOKEN_AUTH_ONLY #include @@ -78,6 +79,7 @@ class OWNCLOUDSYNC_EXPORT Account : public QObject { Q_OBJECT Q_PROPERTY(QString id MEMBER _id) + Q_PROPERTY(QUuid uuid MEMBER _uuid) Q_PROPERTY(QString davUser MEMBER _davUser) Q_PROPERTY(QString davDisplayName MEMBER _davDisplayName) Q_PROPERTY(QString prettyName READ prettyName NOTIFY prettyNameChanged) @@ -440,6 +442,8 @@ class OWNCLOUDSYNC_EXPORT Account : public QObject [[nodiscard]] bool serverHasIntegration() const; + [[nodiscard]] QUuid uuid() const; + public Q_SLOTS: /// Used when forgetting credentials void clearQNAMCache(); @@ -609,6 +613,7 @@ private Q_SLOTS: void updateServerHasIntegration(); bool _serverHasIntegration; + QUuid _uuid; /* IMPORTANT - remove later - FIXME MS@2019-12-07 --> * TODO: For "Log out" & "Remove account": Remove client CA certs and KEY! diff --git a/src/libsync/chronoelapsedtimer.cpp b/src/libsync/chronoelapsedtimer.cpp new file mode 100644 index 0000000000000..c910d663ee0ad --- /dev/null +++ b/src/libsync/chronoelapsedtimer.cpp @@ -0,0 +1,60 @@ +/* + * SPDX-FileCopyrightText: 2025 ownCloud GmbH + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#include "chronoelapsedtimer.h" + +#include + +using namespace OCC::Utility; +using namespace std::chrono; + +ChronoElapsedTimer::ChronoElapsedTimer(bool start) + : _start(steady_clock::now()) + , _started(start) +{ +} + +bool ChronoElapsedTimer::isStarted() const +{ + return _started; +} + +void ChronoElapsedTimer::reset() +{ + _start = steady_clock::now(); + _end = {}; + _started = true; +} + +void ChronoElapsedTimer::stop() +{ + Q_ASSERT(_end == steady_clock::time_point{}); + Q_ASSERT(_started); + _end = steady_clock::now(); + // don't set _started to false, so that we can still call duration() later +} + +nanoseconds ChronoElapsedTimer::duration() const +{ + if (!_started) { + return nanoseconds::max(); + } + if (_end != steady_clock::time_point{}) { + return _end - _start; + } + return steady_clock::now() - _start; +} + +QDebug operator<<(QDebug debug, const ChronoElapsedTimer &timer) +{ + QDebugStateSaver save(debug); + debug.nospace(); + auto duration = timer.duration(); + const auto h = duration_cast(duration); + const auto min = duration_cast(duration -= h); + const auto s = duration_cast(duration -= min); + const auto ms = duration_cast(duration -= s); + return debug << u"duration(" << h.count() << u"h, " << min.count() << u"min, " << s.count() << u"s, " << ms.count() << u"ms)"; +} diff --git a/src/libsync/chronoelapsedtimer.h b/src/libsync/chronoelapsedtimer.h new file mode 100644 index 0000000000000..7ce56f131a286 --- /dev/null +++ b/src/libsync/chronoelapsedtimer.h @@ -0,0 +1,47 @@ +/* + * SPDX-FileCopyrightText: 2025 ownCloud GmbH + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#pragma once + +#include "owncloudlib.h" + +#include + +#include + +namespace OCC::Utility { + +/** + * Meassure time using std::chrono::steady_clock + */ +class OWNCLOUDSYNC_EXPORT ChronoElapsedTimer +{ +public: + ChronoElapsedTimer(bool start = true); + + bool isStarted() const; + /** + * Resets and start the timer + */ + void reset(); + /** + * Stops the timer + */ + void stop(); + /** + * Returns the elapsed time. + * If the timer is stopped it is the time between start and stop of the timer. + */ + std::chrono::nanoseconds duration() const; + +private: + std::chrono::steady_clock::time_point _start = {}; + std::chrono::steady_clock::time_point _end = {}; + bool _started = false; +}; + +} + +OWNCLOUDSYNC_EXPORT QDebug operator<<(QDebug debug, const OCC::Utility::ChronoElapsedTimer &in); diff --git a/src/libsync/filesystem.cpp b/src/libsync/filesystem.cpp index 67ca2ae42cfef..f3f1afecca4fd 100644 --- a/src/libsync/filesystem.cpp +++ b/src/libsync/filesystem.cpp @@ -879,4 +879,40 @@ bool FileSystem::uncheckedRenameReplace(const QString &originFileName, const QSt return true; } +time_t FileSystem::fileTimeToTime_t(std::filesystem::file_time_type fileTime) +{ +#ifdef HAS_CLOCK_CAST + return std::chrono::system_clock::to_time_t(std::chrono::clock_cast(fileTime)); +#else + const auto systemTime = std::chrono::time_point_cast(std::chrono::file_clock::to_sys(fileTime)); + return std::chrono::system_clock::to_time_t(systemTime); +#endif +} + +std::filesystem::file_time_type FileSystem::time_tToFileTime(time_t fileTime) +{ +#ifdef HAS_CLOCK_CAST + return std::chrono::clock_cast(std::chrono::system_clock::from_time_t(fileTime)); +#else + return std::chrono::file_clock::from_sys(std::chrono::system_clock::from_time_t(fileTime)); +#endif +} + +bool FileSystem::setModTime(const std::filesystem::path &filename, time_t modTime) +{ +#ifdef Q_OS_WINDOWS + // the access denied ACEs also prevents us from changing the modtime -> make it writable if needed + FilePermissionsRestore restore(filename, FileSystem::FolderPermissions::ReadWrite); +#endif + + std::error_code rc; + std::filesystem::last_write_time(filename, time_tToFileTime(modTime), rc); + if (rc) { + qCWarning(lcFileSystem) << u"Error setting mtime for" << filename << u"failed: rc" << rc.value() << u", error message:" << rc.message(); + Q_ASSERT(!rc); + return false; + } + return true; +} + } // namespace OCC diff --git a/src/libsync/filesystem.h b/src/libsync/filesystem.h index a3740db43ef50..f7b9a166453a8 100644 --- a/src/libsync/filesystem.h +++ b/src/libsync/filesystem.h @@ -82,6 +82,11 @@ namespace FileSystem { bool OWNCLOUDSYNC_EXPORT setModTime(const QString &filename, time_t modTime); + OWNCLOUDSYNC_EXPORT time_t fileTimeToTime_t(std::filesystem::file_time_type fileTime); + OWNCLOUDSYNC_EXPORT std::filesystem::file_time_type time_tToFileTime(time_t fileTime); + + bool OWNCLOUDSYNC_EXPORT setModTime(const std::filesystem::path &filename, time_t modTime); + /** * @brief Get the size for a file * diff --git a/src/libsync/hydrationjob.cpp b/src/libsync/hydrationjob.cpp new file mode 100644 index 0000000000000..9fd444878d27a --- /dev/null +++ b/src/libsync/hydrationjob.cpp @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2025 Hannah von Reth + +#include "hydrationjob.h" + +#include "common/syncjournaldb.h" +#include "common/vfs.h" +#include "propagatedownload.h" + +using namespace OCC; + +HydrationJob::HydrationJob(Vfs *vfs, const QByteArray &fileId, std::unique_ptr &&device, QObject *parent) + : QObject(parent) + , _vfs(vfs) + , _fileId(fileId) + , _device(std::move(device)) +{ +} + +void HydrationJob::setTargetFile(const QString &fileName) +{ + _fileName = fileName; +} + +QString HydrationJob::targetFileName() const +{ + return _fileName; +} + +void HydrationJob::start() +{ + const auto dbQueryResult = _vfs->params().journal->getFileRecordsByFileId(_fileId, [this](const SyncJournalFileRecord &record) { + Q_ASSERT(!_record.isValid()); + _record = record; + }); + if (!dbQueryResult || !_record.isValid()) { + Q_EMIT error(tr("Failed to find fileId: %1 in db").arg(QString::fromUtf8(_fileId))); + Q_EMIT _vfs->needSync(); + return; + } + if (!_device->open(QIODevice::WriteOnly)) { + Q_EMIT error(_device->errorString()); + return; + } + + Q_ASSERT(!_job); + // propagator()->account(), + // propagator()->fullRemotePath(isEncrypted() ? _item->_encryptedFileName : _item->_file), + // &_tmpFile, headers, expectedEtagForResume, _resumeStart, this + _job = new GETFileJob(_vfs->params().account, _vfs->params().remotePath + _record.path(), _device.get(), {}, {}, 0, this); + _job->setExpectedContentLength(_record._fileSize); + //_job->setPriority(QNetworkRequest::HighPriority); + connect(_job, &GETFileJob::finishedSignal, this, [this] { + QString errorMsg; + if (_job->reply()->error() != 0 /*|| (_job->httpStatusCode() != 200 && _job->httpStatusCode() != 204)*/) { + errorMsg = _job->reply()->errorString(); + } + + if (_job->contentLength() != -1) { + const auto size = _job->resumeStart() + _job->contentLength(); + if (size != _record._fileSize) { + errorMsg = tr("Unexpected file size transferred. Expected %1 received %2").arg(QString::number(_record._fileSize), QString::number(size)); + // assume that the local and the remote metadata are out of sync + Q_EMIT _vfs->needSync(); + } + } + // if (_job->aborted()) { + // errorMsg = tr("Aborted."); + // } + + if (!errorMsg.isEmpty()) { + Q_EMIT error(errorMsg); + return; + } + + Q_EMIT finished(); + }); + _job->start(); +} + +void HydrationJob::abort() +{ + // if (_job) { + // _job->abort(); + // } +} + +Vfs *HydrationJob::vfs() const +{ + return _vfs; +} + +SyncJournalFileRecord HydrationJob::record() const +{ + return _record; +} diff --git a/src/libsync/hydrationjob.h b/src/libsync/hydrationjob.h new file mode 100644 index 0000000000000..3e89a850fec7e --- /dev/null +++ b/src/libsync/hydrationjob.h @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2025 Hannah von Reth + +#pragma once + +#include "owncloudlib.h" + +#include "common/syncjournalfilerecord.h" + +#include +#include + +namespace OCC +{ +class Vfs; +class GETFileJob; + +class OWNCLOUDSYNC_EXPORT HydrationJob : public QObject +{ + Q_OBJECT +public: + explicit HydrationJob(Vfs *vfs, const QByteArray &fileId, std::unique_ptr &&device, QObject *parent); + + 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); + [[nodiscard]] QString targetFileName() const; + + [[nodiscard]] Vfs *vfs() const; + + [[nodiscard]] SyncJournalFileRecord record() const; + + [[nodiscard]] QByteArray fileId() const + { + return _fileId; + } + +Q_SIGNALS: + void finished(); + void error(const QString &error); + +private: + Vfs *_vfs = nullptr; + QByteArray _fileId; + std::unique_ptr _device; + QString _fileName; + SyncJournalFileRecord _record; + GETFileJob *_job = nullptr; +}; +} diff --git a/src/libsync/logger.cpp b/src/libsync/logger.cpp index cc39dd27935e7..f8ce92e96c630 100644 --- a/src/libsync/logger.cpp +++ b/src/libsync/logger.cpp @@ -25,6 +25,8 @@ #include // for stdout #endif +using namespace Qt::StringLiterals; + namespace { constexpr int CrashLogSize = 20; @@ -220,7 +222,7 @@ void Logger::setLogFlush(bool flush) void Logger::setLogDebug(bool debug) { - const QSet rules = {debug ? QStringLiteral("nextcloud.*.debug=true") : QString()}; + const QSet rules = {debug ? u"nextcloud.*.debug=true"_s : QString{}, debug ? u"sync.vfs.openvfs=true"_s : QString{}}; if (debug) { addLogRule(rules); } else { diff --git a/src/libsync/syncfileitem.cpp b/src/libsync/syncfileitem.cpp index 00b7203655580..2b3f3495dd3ae 100644 --- a/src/libsync/syncfileitem.cpp +++ b/src/libsync/syncfileitem.cpp @@ -271,4 +271,39 @@ void SyncFileItem::updateLockStateFromDbRecord(const SyncJournalFileRecord &dbRe _lockToken = dbRecord._lockstate._lockToken; } +SyncJournalFileRecord SyncFileItem::fromSyncFileItem(const SyncFileItem &syncFile) +{ + SyncJournalFileRecord rec(syncFile.destination().toUtf8(), syncFile._type); + rec._modtime = syncFile._modtime; + rec._type = syncFile._type; + + // Some types should never be written to the database when propagation completes + switch (syncFile._type) { + case CSyncEnums::ItemTypeVirtualFileDownload: + rec._type = ItemTypeFile; + break; + case CSyncEnums::ItemTypeVirtualFileDehydration: + rec._type = ItemTypeVirtualFile; + break; + case CSyncEnums::ItemTypeFile: + case CSyncEnums::ItemTypeSoftLink: + case CSyncEnums::ItemTypeDirectory: + case CSyncEnums::ItemTypeSkip: + case CSyncEnums::ItemTypeVirtualFile: + case CSyncEnums::ItemTypeVirtualDirectory: + break; + } + + rec._etag = syncFile._etag; + rec._fileId = syncFile._fileId; + rec._fileSize = syncFile._size; + rec._inode = syncFile._inode; + rec._remotePerm = syncFile._remotePerm; + rec._serverHasIgnoredFiles = syncFile._serverHasIgnoredFiles; + rec._checksumHeader = syncFile._checksumHeader; + //Q_ASSERT(rec.validateRecord()); + + return rec; +} + } diff --git a/src/libsync/syncfileitem.h b/src/libsync/syncfileitem.h index d728044017aee..84a8903114904 100644 --- a/src/libsync/syncfileitem.h +++ b/src/libsync/syncfileitem.h @@ -231,6 +231,8 @@ class OWNCLOUDSYNC_EXPORT SyncFileItem void updateLockStateFromDbRecord(const SyncJournalFileRecord &dbRecord); + [[nodiscard]] static SyncJournalFileRecord fromSyncFileItem(const SyncFileItem &syncFile); + // Variables useful for everybody /** The syncfolder-relative filesystem path that the operation is about @@ -349,6 +351,11 @@ class OWNCLOUDSYNC_EXPORT SyncFileItem static constexpr char usedBytesC[] = "quota-used-bytes"; }; FolderQuota _folderQuota; + + QString localName() const + { + return _file; + } }; inline bool operator<(const SyncFileItemPtr &item1, const SyncFileItemPtr &item2) diff --git a/src/libsync/theme.cpp b/src/libsync/theme.cpp index 2aee36b23d6f7..d51788552ac21 100644 --- a/src/libsync/theme.cpp +++ b/src/libsync/theme.cpp @@ -1022,7 +1022,7 @@ QPixmap Theme::createColorAwarePixmap(const QString &name) bool Theme::showVirtualFilesOption() const { const auto vfsMode = bestAvailableVfsMode(); - return ConfigFile().showExperimentalOptions() || vfsMode == Vfs::WindowsCfApi; + return ConfigFile().showExperimentalOptions() || vfsMode == Vfs::WindowsCfApi || vfsMode == Vfs::OpenVFS; } bool Theme::enforceVirtualFilesSyncFolder() const diff --git a/src/libsync/vfs/CMakeLists.txt b/src/libsync/vfs/CMakeLists.txt index de48e10ee9bd6..40bfa8eadb76b 100644 --- a/src/libsync/vfs/CMakeLists.txt +++ b/src/libsync/vfs/CMakeLists.txt @@ -5,7 +5,8 @@ # that create directories for the build. #file(GLOB VIRTUAL_FILE_SYSTEM_PLUGINS RELATIVE ${CMAKE_CURRENT_LIST_DIR} "*") -list(APPEND VIRTUAL_FILE_SYSTEM_PLUGINS "suffix" "cfapi" "xattr") +set(VIRTUAL_FILE_SYSTEM_PLUGINS "") +list(APPEND VIRTUAL_FILE_SYSTEM_PLUGINS "suffix" "cfapi" "xattr" "openvfs") message("list of plugins ${VIRTUAL_FILE_SYSTEM_PLUGINS}") diff --git a/src/libsync/vfs/cfapi/vfs_cfapi.cpp b/src/libsync/vfs/cfapi/vfs_cfapi.cpp index 3872fb3ce49d5..7524bd15e595f 100644 --- a/src/libsync/vfs/cfapi/vfs_cfapi.cpp +++ b/src/libsync/vfs/cfapi/vfs_cfapi.cpp @@ -657,4 +657,9 @@ VfsCfApi::HydratationAndPinStates VfsCfApi::computeRecursiveHydrationAndPinState } } +Result CfApiVfsPluginFactory::prepare([[maybe_unused]] const QString &path, [[maybe_unused]] const QUuid &accountUuid) const +{ + return {}; +} + } // namespace OCC diff --git a/src/libsync/vfs/openvfs/CMakeLists.txt b/src/libsync/vfs/openvfs/CMakeLists.txt new file mode 100644 index 0000000000000..8b94a211ce01c --- /dev/null +++ b/src/libsync/vfs/openvfs/CMakeLists.txt @@ -0,0 +1,78 @@ +# SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors +# SPDX-FileCopyrightText: 2025 OpenCloud GmbH and OpenCloud contributors +# SPDX-License-Identifier: GPL-2.0-or-later + +if (NOT WIN32) + find_package(OpenVFS 0.1.0 QUIET) + set_package_properties(OpenVFS PROPERTIES TYPE OPTIONAL + DESCRIPTION "A Virtual Filesystem Layer for cloud storages for the free desktop" + URL "https://github.com/opencloud-eu/openvfs/") + + if(TARGET OpenVFS::OpenVFS) + get_target_property (OPEN_VFS_CONFIG OpenVFS::OpenVFS IMPORTED_CONFIGURATIONS) + get_target_property (OPEN_VFS_PATH OpenVFS::OpenVFS IMPORTED_LOCATION_${OPEN_VFS_CONFIG}) + message(STATUS "Using OpenVFS ${OPEN_VFS_CONFIG} (${OPEN_VFS_PATH})") + + set(vfs_openvfs_SRCS + vfs_openvfs.h + vfs_openvfs.cpp + xattr.h + xattr.cpp + ) + + add_library(nextcloudsync_vfs_openvfs SHARED + ${vfs_openvfs_SRCS} + ) + + target_link_libraries(nextcloudsync_vfs_openvfs PRIVATE Nextcloud::sync OpenVFS::LibOpenVFS) + + target_compile_definitions(nextcloudsync_vfs_openvfs PRIVATE OPENVFS_EXE=\"${OPEN_VFS_PATH}\") + + set_target_properties(nextcloudsync_vfs_openvfs + PROPERTIES + LIBRARY_OUTPUT_DIRECTORY + ${BIN_OUTPUT_DIRECTORY} + RUNTIME_OUTPUT_DIRECTORY + ${BIN_OUTPUT_DIRECTORY} + PREFIX + "" + AUTOMOC + TRUE + LIBRARY_OUTPUT_NAME + ${APPLICATION_EXECUTABLE}sync_vfs_openvfs + RUNTIME_OUTPUT_NAME + ${APPLICATION_EXECUTABLE}sync_vfs_openvfs + ) + + target_include_directories(nextcloudsync_vfs_openvfs BEFORE PUBLIC ${CMAKE_CURRENT_BINARY_DIR} INTERFACE ${CMAKE_CURRENT_BINARY_DIR}) + + if(APPLE) + # for being loadable when client run from build dir + set(vfs_buildoutputdir "${BIN_OUTPUT_DIRECTORY}/${OWNCLOUD_OSX_BUNDLE}/Contents/PlugIns/") + set_target_properties(nextcloudsync_vfs_openvfs + PROPERTIES + LIBRARY_OUTPUT_DIRECTORY ${vfs_buildoutputdir} + RUNTIME_OUTPUT_DIRECTORY ${vfs_buildoutputdir} + ) + # For being loadable when client run from install dir (after make macdeployqt) + set(vfs_installdir "${LIB_INSTALL_DIR}/../PlugIns") + else() + set(vfs_installdir "${PLUGINDIR}") + endif() + + generate_export_header(nextcloudsync_vfs_openvfs + BASE_NAME nextcloudsync_vfs_openvfs + EXPORT_MACRO_NAME NEXTCLOUD_OPENVFS_EXPORT + EXPORT_FILE_NAME openvfsexport.h + ) + + install(TARGETS nextcloudsync_vfs_openvfs + LIBRARY DESTINATION "${vfs_installdir}" + RUNTIME DESTINATION "${vfs_installdir}" + ) + + if (WIN32) + install(FILES $ DESTINATION bin OPTIONAL) + endif() + endif() +endif() diff --git a/src/libsync/vfs/openvfs/vfs_openvfs.cpp b/src/libsync/vfs/openvfs/vfs_openvfs.cpp new file mode 100644 index 0000000000000..3472138a64537 --- /dev/null +++ b/src/libsync/vfs/openvfs/vfs_openvfs.cpp @@ -0,0 +1,700 @@ +/* + * 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_openvfs.h" + +#include "account.h" +#include "common/path.h" +#include "common/syncjournaldb.h" +#include "filesystem.h" +#include "hydrationjob.h" +#include "syncfileitem.h" +#include "theme.h" +#include "xattr.h" + +#include + +#include +#include +#include +#include +#include +#include + +#include + +using namespace std::chrono_literals; +using namespace Qt::StringLiterals; + +Q_LOGGING_CATEGORY(lcOpenVFS, "sync.vfs.openvfs", QtInfoMsg) + + +namespace { +OCC::FileSystem::Path openVFSExePath() +{ + auto openVFS = OCC::FileSystem::Path(std::filesystem::path(OPENVFS_EXE)); + auto binary = OCC::FileSystem::Path(qApp->applicationDirPath()) / openVFS.get().filename(); + if (binary.exists()) { + return binary; + } + return openVFS; +} + + +QString xattrOwnerString(const QUuid &accountUuid) +{ + return u"%1:%2"_s.arg(OCC::Theme::instance()->appName(), accountUuid.toString(QUuid::WithoutBraces)); +} + +OCC::FileSystem::Path openVFSConfigFilePath() +{ + auto systemPath = OCC::FileSystem::Path(QStandardPaths::locate(QStandardPaths::ConfigLocation, u"openvfs/config.json"_s)); + if (!systemPath.get().empty()) { + return systemPath; + } + // if (OCC::Utility::runningInAppImage()) { + // auto appimagePath = OCC::FileSystem::Path(qApp->applicationDirPath()) / "../etc/xdg/openvfs/config.json"; + // if (appimagePath.exists()) { + // return appimagePath; + // } + // } + return {}; +} + +OpenVFS::PlaceHolderAttributes placeHolderAttributes(const std::filesystem::path &path) +{ + const auto data = OCC::FileSystem::Xattr::getxattr(path, QString::fromUtf8(OpenVFS::Constants::XAttributeNames::Data)); + if (!data) { + qCWarning(lcOpenVFS) << u"No OpenVFS xattr found for" << path.native(); + } + return OpenVFS::PlaceHolderAttributes::fromData(path, data ? std::vector{data->cbegin(), data->cend()} : std::vector{}); +} + +OpenVFS::PlaceHolderAttributes placeHolderAttributes(const QString &path) +{ + return placeHolderAttributes(OCC::FileSystem::toFilesystemPath(path)); +} + +OCC::Result setPlaceholderAttributes(const OpenVFS::PlaceHolderAttributes &attributes) +{ + Q_ASSERT(attributes.validate()); + const auto data = attributes.toData(); + return OCC::FileSystem::Xattr::setxattr(attributes.absolutePath, QString::fromUtf8(OpenVFS::Constants::XAttributeNames::Data), + {reinterpret_cast(data.data()), static_cast(data.size())}); +} + +OCC::Result setPlaceholderAttributes(const OpenVFS::PlaceHolderAttributes &attributes, time_t modtime) +{ + if (const auto result = setPlaceholderAttributes(attributes); !result) { + return result; + } + OCC::FileSystem::setModTime(attributes.absolutePath, modtime); + return {}; +} + +OpenVFS::Constants::PinStates convertPinState(OCC::PinState pState) +{ + switch (pState) { + case OCC::PinState::AlwaysLocal: + return OpenVFS::Constants::PinStates::AlwaysLocal; + case OCC::PinState::Inherited: + return OpenVFS::Constants::PinStates::Inherited; + case OCC::PinState::OnlineOnly: + return OpenVFS::Constants::PinStates::OnlineOnly; + case OCC::PinState::Excluded: + return OpenVFS::Constants::PinStates::Excluded; + case OCC::PinState::Unspecified: + return OpenVFS::Constants::PinStates::Unspecified; + }; + Q_UNREACHABLE(); +} + +OCC::PinState convertPinState(OpenVFS::Constants::PinStates pState) +{ + switch (pState) { + case OpenVFS::Constants::PinStates::AlwaysLocal: + return OCC::PinState::AlwaysLocal; + case OpenVFS::Constants::PinStates::Inherited: + return OCC::PinState::Inherited; + case OpenVFS::Constants::PinStates::OnlineOnly: + return OCC::PinState::OnlineOnly; + case OpenVFS::Constants::PinStates::Unspecified: + return OCC::PinState::Unspecified; + case OpenVFS::Constants::PinStates::Excluded: + return OCC::PinState::Excluded; + } + Q_UNREACHABLE(); +} + +#ifdef Q_OS_LINUX + +// Helper function to parse paths that the kernel inserts escape sequences +// for. +// https://github.com/qt/qtbase/blob/f47d9bcb45c77183c23e406df415ec2d9f4acbc4/src/corelib/io/qstorageinfo_linux.cpp#L72 +QByteArray parseMangledPath(QByteArrayView path) +{ + // The kernel escapes with octal the following characters: + // space ' ', tab '\t', backslash '\\', and newline '\n' + // See: + // https://codebrowser.dev/linux/linux/fs/proc_namespace.c.html#show_mountinfo + // https://codebrowser.dev/linux/linux/fs/seq_file.c.html#mangle_path + + QByteArray ret(path.size(), '\0'); + char *dst = ret.data(); + const char *src = path.data(); + const char *srcEnd = path.data() + path.size(); + while (src != srcEnd) { + switch (*src) { + case ' ': // Shouldn't happen + return {}; + + case '\\': { + // It always uses exactly three octal characters. + ++src; + char c = (*src++ - '0') << 6; + c |= (*src++ - '0') << 3; + c |= (*src++ - '0'); + *dst++ = c; + break; + } + + default: + *dst++ = *src++; + break; + } + } + // If "path" contains any of the characters this method is demangling, + // "ret" would be oversized with extra '\0' characters at the end. + ret.resize(dst - ret.data()); + return ret; +} +#endif +} + +namespace OCC { + +OpenVFS::OpenVFS(QObject *parent) + : Vfs(parent) +{ +} + +OpenVFS::~OpenVFS() = default; + +Vfs::Mode OpenVFS::mode() const +{ + return Mode::OpenVFS; +} + +void OpenVFS::startImpl(const VfsSetupParams ¶ms) +{ + qCDebug(lcOpenVFS, "Start OpenVFS VFS"); + + // Lets claim the sync root directory for us + // set the owner to opencloud to claim it + const auto owner = xattrOwnerString(params.account->uuid()).toStdString(); + if (const auto info = ::OpenVFS::Registration::registerFilesystem(std::filesystem::path{params.filesystemPath.toStdWString()}, owner); !info) { + if (info.owner() != owner) { + Q_EMIT error(tr("Unable to claim the sync root for files on demand, the folder is already claimed by %1").arg(info.owner())); + return; + } + Q_EMIT error(tr("Unable to retrieve registration info. Error: %1").arg(info.error())); + return; + } + + qCDebug(lcOpenVFS) << "Mounting" << openVFSExePath().toString() << params.filesystemPath; + _openVfsProcess = new QProcess(this); + // merging the channels and piping the output to our log lead to deadlocks + _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, this, [logPrefix, this] { qCFatal(lcOpenVFS) << logPrefix() << "finished" << _openVfsProcess->exitCode(); }); + connect(_openVfsProcess, &QProcess::started, this, [logPrefix, this] { + qCInfo(lcOpenVFS) << logPrefix() << u"started"; + // TODO: + // give it time to mount + QTimer::singleShot(1s, this, &Vfs::started); + }); + connect(_openVfsProcess, &QProcess::errorOccurred, this, [logPrefix, this] { qCWarning(lcOpenVFS) << logPrefix() << _openVfsProcess->errorString(); }); + + QStringList pparams{u"-f"_s, u"-i"_s, openVFSConfigFilePath().toString(), u"-o"_s, xattrOwnerString(params.account->uuid()), u"-s"_s, params.socketPath, + params.filesystemPath}; + + // Enable _lots_ of logging for fuse layer + if (qEnvironmentVariableIntValue("OPENCLOUD_OPENVFS_LOG_FUSE") == 1) { + pparams.prepend(u"-d"_s); + } + + qCDebug(lcOpenVFS) << "Starting openvfs" << openVFSExePath().toString() << pparams; + _openVfsProcess->start(openVFSExePath().toString(), pparams, QIODevice::ReadOnly); +} + +void OpenVFS::stop() +{ + if (_openVfsProcess) { + // disconnect qFatal on subprocess exit + disconnect(_openVfsProcess, &QProcess::finished, nullptr, nullptr); + _openVfsProcess->terminate(); + _openVfsProcess->waitForFinished(); + _openVfsProcess->deleteLater(); + } +} + +void OpenVFS::unregisterFolder() +{ + ::OpenVFS::Registration::unregisterFilesystem({std::filesystem::path{params().filesystemPath.toStdWString()}, xattrOwnerString(params().account->uuid()).toStdString()}); +} + +bool OpenVFS::socketApiPinStateActionsShown() const +{ + return true; +} + + +bool OpenVfsPluginFactory::checkAvailability() const +{ +#ifdef Q_OS_LINUX + if (!FileSystem::Path("/dev/fuse").exists()) { + qCWarning(lcOpenVFS) << u"Fuse is not installed or available on the system"; + return false; + } + if (!QFile::exists(u"/usr/bin/fusermount3"_s) && QStandardPaths::findExecutable(u"fusermount3"_s).isEmpty()) { + qCWarning(lcOpenVFS) << u"fusermount3 is not installed on the system"; + return false; + } +#endif + return true; +} + +Result OpenVfsPluginFactory::prepare(const QString &path, const QUuid &accountUuid) const +{ +#ifdef Q_OS_LINUX + // we can't use QStorageInfo as it does not list fuse mounts + if (!_cacheTimer.isStarted() || _cacheTimer.duration() > 30s) { + _fuseMountCache.clear(); + QFile file(u"/proc/self/mountinfo"_s); + if (file.open(QIODevice::ReadOnly)) { + const auto lines = file.readAll().split('\n'); + file.close(); + for (auto &line : lines) { + auto fields = line.split(' '); + if (fields.size() >= 9 && fields[8] == "fuse.openvfs") { + _fuseMountCache << QString::fromUtf8(parseMangledPath(fields[4])); + } + } + } else { + qCWarning(lcOpenVFS) << "Failed to read /proc/self/mountinfo" << file.errorString(); + return tr("Failed to read /proc/self/mountinfo"); + } + } + if (std::ranges::find_if(_fuseMountCache, [&](const QString &p) { return FileSystem::isChildPathOf2(path, p).testFlag(FileSystem::ChildResult::IsEqual); }) + != _fuseMountCache.cend()) { + QProcess process; + process.setProcessChannelMode(QProcess::MergedChannels); + process.start(u"fusermount"_s, {u"-zu"_s, path}); + // TODO: don't block? + process.waitForFinished(); + if (process.exitCode() != 0) { + const auto output = process.readAll(); + qCWarning(lcOpenVFS) << "Failed to unmount the OpenVFS mount" << path << output; + return tr("Failed to unmount the OpenVFS mount %1 Error:%2").arg(path, output); + } else { + qCDebug(lcOpenVFS) << "Unmounted OpenVFS mount" << path; + } + } +#endif + const auto fsPath = FileSystem::toFilesystemPath(path); + if (!FileSystem::Xattr::supportsxattr(fsPath)) { + qCDebug(lcOpenVFS) << path << "does not support xattributes"; + return tr("The filesystem for %1 does not support xattributes.").arg(path); + } + if (const auto info = ::OpenVFS::Registration::fromAttributes(fsPath); info && info.owner() != xattrOwnerString(accountUuid).toStdString()) { + return tr("The sync path is already claimed by %1").arg(info.owner()); + } + if (!openVFSExePath().exists()) { + qCDebug(lcOpenVFS) << "OpenVFS executable not found at" << openVFSExePath().toString(); + return tr("OpenVFS executable not found, please install it"); + } + const auto vfsConfig = openVFSConfigFilePath(); + if (!vfsConfig.get().empty()) { + qCDebug(lcOpenVFS) << "Using config file" << vfsConfig.toString(); + } else { + return tr("Failed to find the OpenVFS config file, please check your installation."); + } + return {}; +} + +OCC::Result OpenVFS::updateMetadata( + const SyncFileItem &syncItem, const QString &filePath, const QString &replacesFile) +{ + if (syncItem._type == ItemTypeVirtualFileDehydration) { + // replace the file with a placeholder + if (const auto result = createPlaceholder(syncItem); !result) { + qCCritical(lcOpenVFS) << "Failed to create placeholder for" << filePath << result.error(); + return result.error(); + } + return ConvertToPlaceholderResult::Ok; + } else { + ::OpenVFS::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 = ::OpenVFS::PlaceHolderAttributes::create( + FileSystem::toFilesystemPath(filePath), syncItem._etag.toStdString(), syncItem._fileId.toStdString(), syncItem._size); + attr.state = ::OpenVFS::Constants::States::Hydrated; + return attr; + }(); + Q_ASSERT(attributes); + + attributes.size = syncItem._size; + attributes.fileId = syncItem._fileId.toStdString(); + attributes.etag = syncItem._etag.toStdString(); + + qCDebug(lcOpenVFS) << attributes.absolutePath.native() << syncItem._type; + + switch (syncItem._type) { + case ItemTypeVirtualFileDownload: + attributes.state = ::OpenVFS::Constants::States::Hydrating; + break; + case ItemTypeVirtualFile: + [[fallthrough]]; + case ItemTypeVirtualFileDehydration: + qCDebug(lcOpenVFS) << "updateMetadata for virtual file " << syncItem._type; + attributes.state = ::OpenVFS::Constants::States::DeHydrated; + break; + case ItemTypeFile: + // hydrated files must not have a size attribute != 0 + attributes.size = 0; + [[fallthrough]]; + case ItemTypeDirectory: + case CSyncEnums::ItemTypeVirtualDirectory: + qCDebug(lcOpenVFS) << "updateMetadata for" << syncItem._type; + attributes.state = ::OpenVFS::Constants::States::Hydrated; + break; + case ItemTypeSoftLink: + Q_UNREACHABLE(); + break; + case CSyncEnums::ItemTypeSkip: + Q_UNREACHABLE(); + break; + } + + if (const auto result = setPlaceholderAttributes(attributes, syncItem._modtime); !result) { + qCCritical(lcOpenVFS) << "Failed to update placeholder for" << filePath << result.error(); + return result.error(); + } + return ConvertToPlaceholderResult::Ok; + } +} + +void OpenVFS::slotHydrateJobFinished() +{ + HydrationJob *hydration = qobject_cast(sender()); + + const auto targetPath = FileSystem::toFilesystemPath(hydration->targetFileName()); + Q_ASSERT(!targetPath.empty()); + + qCInfo(lcOpenVFS) << u"Hydration Job finished for" << targetPath.native(); + + if (std::filesystem::exists(targetPath)) { + auto item = OCC::SyncFileItem::fromSyncJournalFileRecord(hydration->record()); + // the file is now downloaded + item->_type = ItemTypeFile; + + if (auto inode = 0ull;FileSystem::getInode(QString::fromStdWString(targetPath.wstring()), &inode)) { + item->_inode = inode; + } else { + qCWarning(lcOpenVFS) << u"Failed to get inode for" << targetPath.native(); + } + // Update the client sync journal database if the file modifications have been successful + const auto result = this->params().journal->setFileRecord(SyncFileItem::fromSyncFileItem(*item)); + if (!result) { + qCWarning(lcOpenVFS) << u"Error when setting the file record to the database" << result.error(); + } else { + qCInfo(lcOpenVFS) << u"Hydration succeeded" << targetPath.native(); + + ::OpenVFS::PlaceHolderAttributes attributes = [filePath = hydration->targetFileName(), item] { + if (const auto attr = placeHolderAttributes(filePath)) { + return attr; + } + Q_ASSERT(QFileInfo::exists(filePath)); + // generate new meta data for an existing file + auto attr = ::OpenVFS::PlaceHolderAttributes::create(FileSystem::toFilesystemPath(filePath), + item->_etag.toStdString(), + item->_fileId.toStdString(), + item->_size); + attr.state = ::OpenVFS::Constants::States::Hydrated; + return attr; + }(); + Q_ASSERT(attributes); + + if (attributes.isOk() && attributes.validate()) { + attributes.size = 0; + attributes.state = ::OpenVFS::Constants::States::Hydrated; + + if (const auto result = setPlaceholderAttributes(attributes, item->_modtime); !result) { + qCCritical(lcOpenVFS) << "Failed to update placeholder for" << hydration->targetFileName() << result.error(); + return; + } + } + } + } else { + qCWarning(lcOpenVFS) << u"Hydration succeeded but the file appears to be moved" << targetPath.native(); + } + + hydration->deleteLater(); + this->_hydrationJobs.remove(hydration->fileId()); +} + +Result OpenVFS::createPlaceholder(const SyncFileItem &item) +{ + const auto path = params().root() / item.localName(); + if (path.exists()) { + Q_ASSERT(item._type == ItemTypeVirtualFileDehydration); + if (item._type == ItemTypeVirtualFileDehydration && FileSystem::fileChanged(path.toString(), item._size, item._modtime)) { + return tr("Cannot dehydrate a placeholder because the file changed"); + } + } + QFile file(path.get()); + if (!file.open(QFile::ReadWrite | QFile::Truncate)) { + return file.errorString(); + } + file.write(""); + file.close(); + + const auto attributes = ::OpenVFS::PlaceHolderAttributes::create(path, item._etag.toStdString(), item._fileId.toStdString(), item._size); + return setPlaceholderAttributes(attributes, item._modtime); +} + +HydrationJob *OpenVFS::hydrateFile(const QByteArray &fileId, const QString &targetPath) +{ + qCInfo(lcOpenVFS) << u"Requesting hydration for" << fileId; + if (_hydrationJobs.contains(fileId)) { + qCWarning(lcOpenVFS) << u"Ignoring hydration request for running hydration for fileId" << fileId; + return {}; + } + + if (auto attr = placeHolderAttributes(targetPath)) { + attr.state = ::OpenVFS::Constants::States::Hydrating; + if (auto res = setPlaceholderAttributes(attr); !res) { + qCWarning(lcOpenVFS) << u"Failed to set attributes for" << targetPath << res.error(); + return nullptr; + } + } else { + qCWarning(lcOpenVFS) << 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); + + connect(hydration, &HydrationJob::finished, this, &OpenVFS::slotHydrateJobFinished); + connect(hydration, &HydrationJob::error, this, [this, hydration](const QString &error) { + qCWarning(lcOpenVFS) << u"Hydration failed" << error; + this->_hydrationJobs.remove(hydration->fileId()); + hydration->deleteLater(); + }); + + return hydration; +} + +QString OpenVFS::fileSuffix() const +{ + return {}; +} + +bool OpenVFS::isHydrating() const +{ + return false; +} + +Result OpenVFS::updatePlaceholderMarkInSync([[maybe_unused]] const QString &filePath, [[maybe_unused]] const SyncFileItem &item) +{ + return {Vfs::ConvertToPlaceholderResult::Error}; +} + +bool OpenVFS::isPlaceHolderInSync([[maybe_unused]] const QString &filePath) const +{ + return false; +} + +Result OpenVFS::createPlaceholders([[maybe_unused]] const QList &items) +{ + auto result = Result{}; + + for (const auto &oneItem : items) { + const auto itemResult = createPlaceholder(*oneItem); + if (!itemResult) { + result = itemResult; + break; + } + } + + return result; +} + +Result OpenVFS::dehydratePlaceholder([[maybe_unused]] const SyncFileItem &item) +{ + const auto path = params().root() / item.localName(); + if (path.exists()) { + FileSystem::remove(path.toString()); + } + return createPlaceholder(item); +} + +Result OpenVFS::convertToPlaceholder(const QString &, const SyncFileItem &, const QString &, UpdateMetadataTypes) +{ + return {Vfs::ConvertToPlaceholderResult::Error}; +} + +bool OpenVFS::needsMetadataUpdate(const SyncFileItem &item) +{ + const auto path = params().root() / item.localName(); + // if the attributes do not exist we need to add them + return QFileInfo::exists(path.toString()) && !placeHolderAttributes(path); +} + +bool OpenVFS::isDehydratedPlaceholder(const QString &filePath) +{ + if (QFileInfo::exists(filePath)) { + return placeHolderAttributes(filePath).state == ::OpenVFS::Constants::States::DeHydrated; + } + return false; +} + +bool OpenVFS::statTypeVirtualFile(csync_file_stat_t *stat, void *statData) +{ + if (stat->type == ItemTypeDirectory) { + return false; + } + + const auto parentPath = static_cast(statData); + Q_ASSERT(!parentPath->endsWith('/')); + Q_ASSERT(!stat->path.startsWith('/')); + + const auto path = QByteArray(*parentPath + '/' + stat->path); + const auto pin = [=, this] { + const auto absolutePath = QString::fromUtf8(path); + Q_ASSERT(absolutePath.startsWith(params().filesystemPath.toUtf8())); + const auto folderPath = absolutePath.mid(params().filesystemPath.length()); + return pinState(folderPath); + }(); + + if (stat->type == ItemTypeFile) { + const auto attribs = placeHolderAttributes(path); + if (attribs.state == ::OpenVFS::Constants::States::DeHydrated) { + stat->type = ItemTypeVirtualFile; + if (attribs.pinState == convertPinState(PinState::AlwaysLocal)) { + stat->type = ItemTypeVirtualFileDownload; + } + } else { + if (attribs.pinState == convertPinState(PinState::OnlineOnly)) { + stat->type = ItemTypeVirtualFileDehydration; + } + } + } + return false; +} + +// LocalInfo OpenVFS::statTypeVirtualFile(const std::filesystem::directory_entry &path, ItemType type) +// { +// if (type == ItemTypeFile) { +// const auto attribs = placeHolderAttributes(path.path()); +// if (attribs.state == ::OpenVFS::Constants::States::DeHydrated) { +// type = ItemTypeVirtualFile; +// if (attribs.pinState == convertPinState(PinState::AlwaysLocal)) { +// type = ItemTypeVirtualFileDownload; +// } +// } else { +// if (attribs.pinState == convertPinState(PinState::OnlineOnly)) { +// type = ItemTypeVirtualFileDehydration; +// } +// } +// } +// qCDebug(lcOpenVFS) << path.path().native() << Utility::enumToString(type); +// return LocalInfo(path, type); +// } + +bool OpenVFS::setPinState(const QString &folderPath, PinState state) +{ + const auto localPath = params().root() / folderPath; + qCDebug(lcOpenVFS) << localPath.toString() << state; + 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 OpenVFS::pinState(const QString &folderPath) +{ + for (auto relativePath = FileSystem::Path::relative(folderPath).get();; relativePath = relativePath.parent_path()) { + const auto attributes = placeHolderAttributes(params().root() / relativePath); + if (!attributes) { + qCDebug(lcOpenVFS) << "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 != ::OpenVFS::Constants::PinStates::Inherited || !relativePath.has_relative_path()) { + return convertPinState(attributes.pinState); + } + } +} + +Vfs::AvailabilityResult OpenVFS::availability(const QString &folderPath, [[maybe_unused]] const AvailabilityRecursivity recursiveCheck) +{ + const auto attribs = placeHolderAttributes(params().root() / folderPath); + if (attribs) { + switch (convertPinState(attribs.pinState)) { + case OCC::PinState::AlwaysLocal: + return VfsItemAvailability::AlwaysLocal; + case OCC::PinState::OnlineOnly: + return VfsItemAvailability::OnlineOnly; + case OCC::PinState::Inherited: { + switch (attribs.state) { + case ::OpenVFS::Constants::States::Hydrated: + return VfsItemAvailability::AllHydrated; + case ::OpenVFS::Constants::States::DeHydrated: + return VfsItemAvailability::AllDehydrated; + case ::OpenVFS::Constants::States::Hydrating: + return VfsItemAvailability::Mixed; + } + } + Q_UNREACHABLE(); + case OCC::PinState::Unspecified: + [[fallthrough]]; + case OCC::PinState::Excluded: + return VfsItemAvailability::Mixed; + }; + } else { + return AvailabilityError::NoSuchItem; + } + return VfsItemAvailability::Mixed; +} + +void OpenVFS::fileStatusChanged(const QString &systemFileName, SyncFileStatus fileStatus) +{ + if (fileStatus.tag() == SyncFileStatus::StatusExcluded) { + const FileSystem::Path rel = FileSystem::Path(systemFileName)->lexically_relative(params().root()); + setPinState(rel.toString(), PinState::Excluded); + return; + } + qCDebug(lcOpenVFS) << systemFileName << fileStatus; +} + + +} // namespace OCC diff --git a/src/libsync/vfs/openvfs/vfs_openvfs.h b/src/libsync/vfs/openvfs/vfs_openvfs.h new file mode 100644 index 0000000000000..1745274073959 --- /dev/null +++ b/src/libsync/vfs/openvfs/vfs_openvfs.h @@ -0,0 +1,100 @@ +/* + * 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 "common/vfs.h" +#include "common/plugin.h" +#include "common/result.h" +#include "discoveryphase.h" +#include "chronoelapsedtimer.h" + +#include +#include +#include +#include + +#include + +namespace OCC { +class HydrationJob; + +class OpenVFS : public Vfs +{ + Q_OBJECT + +public: + explicit OpenVFS(QObject *parent = nullptr); + ~OpenVFS() 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; + + bool needsMetadataUpdate(const SyncFileItem &item) override; + bool isDehydratedPlaceholder(const QString &filePath) override; + [[nodiscard]] bool statTypeVirtualFile(csync_file_stat_t *stat, void *stat_data) 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; + [[nodiscard]] AvailabilityResult availability(const QString &folderPath, const AvailabilityRecursivity recursiveCheck) override; + + HydrationJob *hydrateFile(const QByteArray &fileId, const QString &targetPath); + + [[nodiscard]] QString fileSuffix() const override; + + [[nodiscard]] bool isHydrating() const override; + + [[nodiscard]] Result updatePlaceholderMarkInSync(const QString &filePath, const SyncFileItem &item) override; + + [[nodiscard]] bool isPlaceHolderInSync(const QString &filePath) const override; + + [[nodiscard]] Result createPlaceholders(const QList &items) override; + + [[nodiscard]] Result dehydratePlaceholder(const SyncFileItem &item) override; + + [[nodiscard]] Result convertToPlaceholder(const QString&, const OCC::SyncFileItem&, const QString&, UpdateMetadataTypes) override; + +Q_SIGNALS: + void finished(OCC::Result); + +public Q_SLOTS: + void fileStatusChanged(const QString &systemFileName, OCC::SyncFileStatus fileStatus) override; + + void slotHydrateJobFinished(); + +protected: + void startImpl(const VfsSetupParams ¶ms) override; + +private: + QMap _hydrationJobs; + QPointer _openVfsProcess; +}; + +class OpenVfsPluginFactory : public QObject, public DefaultPluginFactory +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.owncloud.PluginFactory" FILE "vfspluginmetadata.json") + Q_INTERFACES(OCC::PluginFactory) + +public: + [[nodiscard]] bool checkAvailability() const override; + Result prepare(const QString &path, const QUuid &accountUuid) const override; + +private: + mutable Utility::ChronoElapsedTimer _cacheTimer = false; + mutable QStringList _fuseMountCache; +}; + +} // namespace OCC diff --git a/src/libsync/vfs/openvfs/xattr.cpp b/src/libsync/vfs/openvfs/xattr.cpp new file mode 100644 index 0000000000000..50d84b18a896e --- /dev/null +++ b/src/libsync/vfs/openvfs/xattr.cpp @@ -0,0 +1,76 @@ +// 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 { + bool Xattr::supportsxattr(const std::filesystem::path &path) + { +#ifdef Q_OS_MAC + return !(::listxattr(path.c_str(), nullptr, 0, XATTR_NOFOLLOW) == -1 && errno == ENOTSUP); +#else + return !(::listxattr(path.c_str(), nullptr, 0) == -1 && errno == ENOTSUP); +#endif + } + + 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 value; + } else { + return {}; + } + } + + Result Xattr::setxattr(const std::filesystem::path &path, const QString &name, const QByteArray &data) + { +#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/vfs/openvfs/xattr.h b/src/libsync/vfs/openvfs/xattr.h new file mode 100644 index 0000000000000..a76b11c1ed25d --- /dev/null +++ b/src/libsync/vfs/openvfs/xattr.h @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2025 Hannah von Reth + +#pragma once + +#include "owncloudlib.h" + +#include "common/result.h" + +#include +#include + +#include + +namespace OCC { +namespace FileSystem { + namespace Xattr { + OWNCLOUDSYNC_EXPORT bool supportsxattr(const std::filesystem::path &path); + OWNCLOUDSYNC_EXPORT std::optional getxattr(const std::filesystem::path &path, const QString &name); + OWNCLOUDSYNC_EXPORT Result setxattr(const std::filesystem::path &path, const QString &name, const QByteArray &value); + OWNCLOUDSYNC_EXPORT Result removexattr(const std::filesystem::path &path, const QString &name); + } +} +} diff --git a/src/libsync/vfs/suffix/vfs_suffix.cpp b/src/libsync/vfs/suffix/vfs_suffix.cpp index 64466493376c8..4bc43b8ce80fb 100644 --- a/src/libsync/vfs/suffix/vfs_suffix.cpp +++ b/src/libsync/vfs/suffix/vfs_suffix.cpp @@ -182,10 +182,20 @@ bool VfsSuffix::setPinState(const QString &folderPath, PinState state) return setPinStateInDb(folderPath, state); } +HydrationJob *VfsSuffix::hydrateFile([[maybe_unused]] const QByteArray &fileId, [[maybe_unused]] const QString &targetPath) +{ + return nullptr; +} + Vfs::AvailabilityResult VfsSuffix::availability(const QString &folderPath, const AvailabilityRecursivity recursiveCheck) { Q_UNUSED(recursiveCheck) return availabilityInDb(folderPath); } +Result SuffixVfsPluginFactory::prepare([[maybe_unused]] const QString &path, [[maybe_unused]] const QUuid &accountUuid) const +{ + return {}; +} + } // namespace OCC diff --git a/src/libsync/vfs/suffix/vfs_suffix.h b/src/libsync/vfs/suffix/vfs_suffix.h index 811cd7113368f..fa2b486e08466 100644 --- a/src/libsync/vfs/suffix/vfs_suffix.h +++ b/src/libsync/vfs/suffix/vfs_suffix.h @@ -49,6 +49,8 @@ class VfsSuffix : public Vfs { return pinStateInDb(folderPath); } AvailabilityResult availability(const QString &folderPath, const AvailabilityRecursivity recursiveCheck) override; + [[nodiscard]] HydrationJob *hydrateFile(const QByteArray &fileId, const QString &targetPath) override; + public Q_SLOTS: void fileStatusChanged(const QString &, OCC::SyncFileStatus) override {} @@ -61,6 +63,9 @@ class SuffixVfsPluginFactory : public QObject, public DefaultPluginFactory prepare(const QString &path, const QUuid &accountUuid) const override; }; } // namespace OCC diff --git a/src/libsync/vfs/xattr/vfs_xattr.cpp b/src/libsync/vfs/xattr/vfs_xattr.cpp index e5bd5e213c59a..5299db8e9201d 100644 --- a/src/libsync/vfs/xattr/vfs_xattr.cpp +++ b/src/libsync/vfs/xattr/vfs_xattr.cpp @@ -196,6 +196,11 @@ Optional VfsXAttr::pinState(const QString &folderPath) return pinStateInDb(folderPath); } +HydrationJob *VfsXAttr::hydrateFile([[maybe_unused]] const QByteArray &fileId, [[maybe_unused]] const QString &targetPath) +{ + return nullptr; +} + Vfs::AvailabilityResult VfsXAttr::availability(const QString &folderPath, const AvailabilityRecursivity recursiveCheck) { Q_UNUSED(recursiveCheck) @@ -206,4 +211,9 @@ void VfsXAttr::fileStatusChanged(const QString &, SyncFileStatus) { } +Result XattrVfsPluginFactory::prepare([[maybe_unused]] const QString &path, [[maybe_unused]] const QUuid &accountUuid) const +{ + return {}; +} + } // namespace OCC diff --git a/src/libsync/vfs/xattr/vfs_xattr.h b/src/libsync/vfs/xattr/vfs_xattr.h index f60e9b70cae79..e4f205eb72dd3 100644 --- a/src/libsync/vfs/xattr/vfs_xattr.h +++ b/src/libsync/vfs/xattr/vfs_xattr.h @@ -50,6 +50,8 @@ class VfsXAttr : public Vfs Optional pinState(const QString &folderPath) override; AvailabilityResult availability(const QString &folderPath, const AvailabilityRecursivity recursiveCheck) override; + [[nodiscard]] HydrationJob *hydrateFile(const QByteArray &fileId, const QString &targetPath) override; + public Q_SLOTS: void fileStatusChanged(const QString &systemFileName, OCC::SyncFileStatus fileStatus) override; @@ -62,6 +64,9 @@ class XattrVfsPluginFactory : public QObject, public DefaultPluginFactory prepare(const QString &path, const QUuid &accountUuid) const override; }; } // namespace OCC diff --git a/test/syncenginetestutils.cpp b/test/syncenginetestutils.cpp index 524dffdc5fef3..b0f774a948f27 100644 --- a/test/syncenginetestutils.cpp +++ b/test/syncenginetestutils.cpp @@ -1322,6 +1322,7 @@ void FakeFolder::switchToVfs(QSharedPointer vfs) OCC::VfsSetupParams vfsParams; vfsParams.filesystemPath = localPath(); + vfsParams.rootPath = OCC::FileSystem::Path{localPath()}; vfsParams.remotePath = _remotePath + u"/"_s; vfsParams.account = _account; vfsParams.journal = _journalDb.get(); diff --git a/test/testaccountwizardcontroller.cpp b/test/testaccountwizardcontroller.cpp index 6a813a7eb28f0..d7e177312b80a 100644 --- a/test/testaccountwizardcontroller.cpp +++ b/test/testaccountwizardcontroller.cpp @@ -355,6 +355,17 @@ private Q_SLOTS: QVERIFY(controller.startLoginFlowAutomatically()); } +#ifdef Q_OS_LINUX + void defaultSyncModeIsNotVirtualFilesOnLinux() + { + AccountWizardController controller; + + if (controller.canUseVirtualFiles()) { + QCOMPARE(controller.syncMode(), AccountWizardController::SyncEverything); + } + } +#endif + #ifdef BUILD_FILE_PROVIDER_MODULE // Keep this test last: it redirects the configuration directory. void followsAppLevelFileProviderMode()