diff --git a/CMakeLists.txt b/CMakeLists.txt index d645f23555..03cfaf70a0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -101,7 +101,7 @@ add_feature_info(AppImageUpdate WITH_APPIMAGEUPDATER "Built-in libappimageupdate option(WITH_EXTERNAL_BRANDING "A URL to an external branding repo" "") # specify additional vfs plugins -set(VIRTUAL_FILE_SYSTEM_PLUGINS off cfapi CACHE STRING "Name of internal plugin in src/libsync/vfs or the locations of virtual file plugins") +set(VIRTUAL_FILE_SYSTEM_PLUGINS off cfapi xattr CACHE STRING "Name of internal plugin in src/libsync/vfs or the locations of virtual file plugins") if(APPLE) set( SOCKETAPI_TEAM_IDENTIFIER_PREFIX "" CACHE STRING "SocketApi prefix (including a following dot) that must match the codesign key's TeamIdentifier/Organizational Unit" ) diff --git a/src/gui/folderwizard/folderwizard.cpp b/src/gui/folderwizard/folderwizard.cpp index c414f98078..b9356d6976 100644 --- a/src/gui/folderwizard/folderwizard.cpp +++ b/src/gui/folderwizard/folderwizard.cpp @@ -121,7 +121,12 @@ const AccountStatePtr &FolderWizardPrivate::accountState() bool FolderWizardPrivate::useVirtualFiles() const { +#ifdef Q_OS_WIN return VfsPluginManager::instance().bestAvailableVfsMode() == Vfs::WindowsCfApi; +#elif defined(Q_OS_UNIX) && !defined(Q_OS_MAC) + return VfsPluginManager::instance().bestAvailableVfsMode() == Vfs::XAttr; +#endif + return false; } FolderWizard::FolderWizard(const AccountStatePtr &account, QWidget *parent) diff --git a/src/gui/guiutility.cpp b/src/gui/guiutility.cpp index 7215aa3b1a..fc545c6121 100644 --- a/src/gui/guiutility.cpp +++ b/src/gui/guiutility.cpp @@ -100,7 +100,7 @@ void Utility::markDirectoryAsSyncRoot(const QString &path, const QUuid &accountU Q_ASSERT(getDirectorySyncRootMarkings(path).first.isEmpty()); Q_ASSERT(getDirectorySyncRootMarkings(path).second.isNull()); - auto result1 = FileSystem::Tags::set(path, dirTag(), Theme::instance()->orgDomainName().toUtf8()); + auto result1 = FileSystem::Tags::set(path, dirTag(), Theme::instance()->orgDomainName()); if (!result1) { qCWarning(lcGuiUtility) << QStringLiteral("Failed to set tag on »%1«: %2").arg(path, result1.error()) #ifdef Q_OS_WIN @@ -110,7 +110,7 @@ void Utility::markDirectoryAsSyncRoot(const QString &path, const QUuid &accountU return; } - auto result2 = FileSystem::Tags::set(path, uuidTag(), accountUuid.toString().toUtf8()); + auto result2 = FileSystem::Tags::set(path, uuidTag(), accountUuid.toString()); if (!result2) { qCWarning(lcGuiUtility) << QStringLiteral("Failed to set tag on »%1«: %2").arg(path, result2.error()) #ifdef Q_OS_WIN @@ -127,7 +127,7 @@ std::pair Utility::getDirectorySyncRootMarkings(const QString &p auto existingUuidTag = FileSystem::Tags::get(path, uuidTag()); if (existingDirTag.has_value() && existingUuidTag.has_value()) { - return {QString::fromUtf8(existingDirTag.value()), QUuid::fromString(QString::fromUtf8(existingUuidTag.value()))}; + return {existingDirTag.value(), QUuid::fromString(existingUuidTag.value())}; } return {}; diff --git a/src/gui/socketapi/socketapi.cpp b/src/gui/socketapi/socketapi.cpp index b142a1bf9d..c71d8f0672 100644 --- a/src/gui/socketapi/socketapi.cpp +++ b/src/gui/socketapi/socketapi.cpp @@ -32,6 +32,7 @@ #include "syncengine.h" #include "syncfileitem.h" #include "theme.h" +#include "vfs/hydrationjob.h" #include #include @@ -427,6 +428,24 @@ void SocketApi::command_RETRIEVE_FILE_STATUS(const QString &argument, SocketList listener->registerMonitoredDirectory(qHash(directory)); statusString = fileData.syncFileStatus().toSocketAPIString(); + +#ifdef Q_OS_LINUX + // append vfs status in case... + QString vfsStatus; + const auto fileType = fileData.journalRecord().type(); + if (fileType == ItemTypeVirtualFileDownload || fileType == ItemTypeVirtualFile) { + vfsStatus = QStringLiteral("+VIRT"); + } + const auto pState = fileData.folder->vfs().pinState(argument); + if (pState) { + if (*pState == PinState::AlwaysLocal) { + vfsStatus += QStringLiteral("+AL"); + } else if (*pState == PinState::OnlineOnly) { + vfsStatus += QStringLiteral("+OO"); + } + } + statusString.append(vfsStatus); +#endif } const QString message = QStringLiteral("STATUS:") % statusString % QLatin1Char(':') % QDir::toNativeSeparators(argument); @@ -476,7 +495,9 @@ void SocketApi::command_SHARE(const QString &localFile, SocketListener *listener void SocketApi::command_VERSION(const QString &, SocketListener *listener) { - listener->sendMessage(QStringLiteral("VERSION:%1:%2").arg(OCC::Version::versionWithBuildNumber().toString(), QStringLiteral(MIRALL_SOCKET_API_VERSION))); + listener->sendMessage(QStringLiteral("VERSION:%1:%2:%3").arg(OCC::Version::versionWithBuildNumber().toString(), + QStringLiteral(MIRALL_SOCKET_API_VERSION), + QString::number(qApp->applicationPid()))); } void SocketApi::command_SHARE_MENU_TITLE(const QString &, SocketListener *listener) @@ -665,23 +686,27 @@ void SocketApi::command_V2_HYDRATE_FILE(const QSharedPointer &jo { const auto &arguments = job->arguments(); - const QString targetPath = arguments[QStringLiteral("file")].toString(); const QByteArray fileId = arguments[QStringLiteral("fileId")].toString().toUtf8(); + const QString targetPath = arguments[QStringLiteral("file")].toString(); auto fileData = FileData::get(targetPath); if (fileData.folder) { - auto watcher = new QFutureWatcher>(); - connect(watcher, &QFutureWatcher>::finished, this, [job, watcher] { - const auto resut = watcher->result>(); - watcher->deleteLater(); - if (!resut) { - job->success({{QStringLiteral("status"), QStringLiteral("ERROR")}, {QStringLiteral("error"), resut.error()}}); - } else { + HydrationJob *hydJob = fileData.folder->vfs().hydrateFile(fileId, targetPath); + + if (hydJob) { + connect(hydJob, &HydrationJob::finished, this, [job, hydJob] { job->success({{QStringLiteral("status"), QStringLiteral("OK")}}); - } - }); - watcher->setFuture(fileData.folder->vfs().hydrateFile(fileId, targetPath)); + hydJob->deleteLater(); + }); + connect(hydJob, &HydrationJob::error, this, [job, hydJob](const QString& err) { + job->success({{QStringLiteral("status"), QStringLiteral("ERROR")}, {QStringLiteral("error"), err}}); + hydJob->deleteLater(); + }); + hydJob->start(); + } else { + qCDebug(lcSocketApi) << "Hydration job for" << fileId << "already running"; + } } else { job->failure(QStringLiteral("cannot hydrate unknown file")); } diff --git a/src/libsync/CMakeLists.txt b/src/libsync/CMakeLists.txt index d5478d3757..b72a2aa6cb 100644 --- a/src/libsync/CMakeLists.txt +++ b/src/libsync/CMakeLists.txt @@ -7,7 +7,8 @@ set_package_properties(LibreGraphAPI PROPERTIES configure_file(config.h.in ${CMAKE_CURRENT_BINARY_DIR}/config.h) -set(libsync_SRCS + +add_library(libsync SHARED account.cpp bandwidthmanager.cpp capabilities.cpp @@ -45,8 +46,9 @@ set(libsync_SRCS syncoptions.cpp theme.cpp - creds/credentialmanager.cpp creds/abstractcredentials.cpp + creds/credentialmanager.cpp + creds/httpcredentials.cpp creds/oauth.cpp creds/jwt.cpp creds/webfinger.cpp @@ -65,17 +67,16 @@ set(libsync_SRCS ) if(WIN32) - list(APPEND libsync_SRCS platform_win.cpp) + target_sources(libsync PRIVATE platform_win.cpp) elseif(UNIX) + target_sources(libsync PRIVATE xattr.cpp) if (APPLE) - list(APPEND libsync_SRCS platform_mac.mm) + target_sources(libsync PRIVATE platform_mac.mm) else() - list(APPEND libsync_SRCS platform_unix.cpp) + target_sources(libsync PRIVATE platform_unix.cpp) endif() endif() -set(libsync_SRCS ${libsync_SRCS} creds/httpcredentials.cpp) - # These headers are installed for libopencloudsync to be used by 3rd party apps INSTALL( FILES @@ -85,7 +86,6 @@ INSTALL( DESTINATION ${KDE_INSTALL_INCLUDEDIR}/${APPLICATION_SHORTNAME}/libsync ) -add_library(libsync SHARED ${libsync_SRCS}) set_target_properties(libsync PROPERTIES EXPORT_NAME SyncCore) target_link_libraries(libsync diff --git a/src/libsync/filesystem.cpp b/src/libsync/filesystem.cpp index e27854a314..b2acd2f98d 100644 --- a/src/libsync/filesystem.cpp +++ b/src/libsync/filesystem.cpp @@ -17,6 +17,7 @@ #include "common/asserts.h" #include "common/utility.h" #include "libsync/discoveryinfo.h" +#include "libsync/xattr.h" #include #include @@ -28,10 +29,6 @@ #include -#if defined(Q_OS_MAC) || defined(Q_OS_LINUX) -#include -#endif - #ifdef Q_OS_WIN32 #include "common/utility_win.h" #include @@ -250,52 +247,25 @@ std::optional FileSystem::getInode(const std::filesystem::path &filena return info.inode(); } -namespace { - -#ifdef Q_OS_LINUX - Q_ALWAYS_INLINE ssize_t getxattr(const char *path, const char *name, void *value, size_t size, u_int32_t, int) - { - return ::getxattr(path, name, value, size); - } - - Q_ALWAYS_INLINE int setxattr(const char *path, const char *name, const void *value, size_t size, u_int32_t, int) - { - return ::setxattr(path, name, value, size, 0); - } - - Q_ALWAYS_INLINE int removexattr(const char *path, const char *name, int) - { - return ::removexattr(path, name); - } -#endif // Q_OS_LINUX - -} // anonymous namespace - -std::optional FileSystem::Tags::get(const QString &path, const QString &key) +std::optional FileSystem::Tags::get(const QString &path, const QString &key) { #if defined(Q_OS_MAC) || defined(Q_OS_LINUX) QString platformKey = key; if (Utility::isLinux()) { platformKey = QStringLiteral("user.") + platformKey; } - - QByteArray value(MaxValueSize + 1, '\0'); // Add a NUL character to terminate a string - auto size = getxattr(path.toUtf8().constData(), platformKey.toUtf8().constData(), value.data(), MaxValueSize, 0, 0); - if (size != -1) { - value.truncate(size); - return value; - } + return Xattr::getxattr(toFilesystemPath(path), platformKey); #elif defined(Q_OS_WIN) QFile file(QStringLiteral("%1:%2").arg(path, key)); if (file.open(QIODevice::ReadOnly)) { - return file.readAll(); + return QString::fromUtf8(file.readAll()); } #endif // Q_OS_MAC || Q_OS_LINUX return {}; } -OCC::Result FileSystem::Tags::set(const QString &path, const QString &key, const QByteArray &value) +OCC::Result FileSystem::Tags::set(const QString &path, const QString &key, const QString &value) { OC_ASSERT(value.size() < MaxValueSize) @@ -304,30 +274,25 @@ OCC::Result FileSystem::Tags::set(const QString &path, const QStr if (Utility::isLinux()) { platformKey = QStringLiteral("user.") + platformKey; } - - auto result = setxattr(path.toUtf8().constData(), platformKey.toUtf8().constData(), value.constData(), value.size(), 0, 0); - if (result != 0) { - return QString::fromUtf8(strerror(errno)); - } - - return {}; + return Xattr::setxattr(toFilesystemPath(path), platformKey, value); #elif defined(Q_OS_WIN) QFile file(QStringLiteral("%1:%2").arg(path, key)); if (!file.open(QIODevice::WriteOnly)) { return file.errorString(); } - auto bytesWritten = file.write(value); - if (bytesWritten != value.size()) { - return QStringLiteral("wrote %1 out of %2 bytes").arg(QString::number(bytesWritten), QString::number(value.size())); + const auto data = value.toUtf8(); + auto bytesWritten = file.write(data); + if (bytesWritten != data.size()) { + return QStringLiteral("wrote %1 out of %2 bytes").arg(QString::number(bytesWritten), QString::number(data.size())); } return {}; #else - return QStringLiteral("function not implemented"); + return u"Not implemented"_s; #endif // Q_OS_MAC || Q_OS_LINUX } -bool FileSystem::Tags::remove(const QString &path, const QString &key) +OCC::Result FileSystem::Tags::remove(const QString &path, const QString &key) { #if defined(Q_OS_MAC) || defined(Q_OS_LINUX) QString platformKey = key; @@ -335,31 +300,18 @@ bool FileSystem::Tags::remove(const QString &path, const QString &key) platformKey = QStringLiteral("user.%1").arg(platformKey); } - auto result = removexattr(path.toUtf8().constData(), platformKey.toUtf8().constData(), 0); - if (result == 0) { - return true; - } -#ifdef Q_OS_MAC - if (errno == ENOATTR) { -#else - if (errno == ENODATA) { -#endif - qCWarning(lcFileSystem) << u"Failed to remove tag" << key << u"from" << path << u"tag doesn't exist"; - return true; - } - qCWarning(lcFileSystem) << u"Failed to remove tag" << key << u"from" << path << u":" << strerror(errno); - return false; + return Xattr::removexattr(toFilesystemPath(path), platformKey); #elif defined(Q_OS_WIN) const auto fsPath = toFilesystemPath(u"%1:%2"_s.arg(path, key)); std::error_code fileError; std::filesystem::remove(fsPath, fileError); if (fileError) { qCWarning(lcFileSystem) << u"Failed to remove tag" << key << u"from" << path << u":" << fsPath << u"error:" << fileError.message(); - return false; + return QString::fromStdString(fileError.message()); } - return true; + return {}; #else - return false; + return u"Not implemented"_s; #endif // Q_OS_MAC || Q_OS_LINUX } } // namespace OCC diff --git a/src/libsync/filesystem.h b/src/libsync/filesystem.h index c45497d801..0a9ce0d75e 100644 --- a/src/libsync/filesystem.h +++ b/src/libsync/filesystem.h @@ -128,9 +128,9 @@ namespace FileSystem { bool OPENCLOUD_SYNC_EXPORT removeRecursively(const QString &path, RemoveEntryList *success, RemoveEntryList *locked, RemoveErrorList *errors); namespace Tags { - std::optional OPENCLOUD_SYNC_EXPORT get(const QString &path, const QString &key); - OCC::Result OPENCLOUD_SYNC_EXPORT set(const QString &path, const QString &key, const QByteArray &value); - bool OPENCLOUD_SYNC_EXPORT remove(const QString &path, const QString &key); + std::optional OPENCLOUD_SYNC_EXPORT get(const QString &path, const QString &key); + OCC::Result OPENCLOUD_SYNC_EXPORT set(const QString &path, const QString &key, const QString &value); + OCC::Result OPENCLOUD_SYNC_EXPORT remove(const QString &path, const QString &key); } } diff --git a/src/libsync/vfs/hydrationjob.cpp b/src/libsync/vfs/hydrationjob.cpp index a7b4a9c0b5..b6188ca309 100644 --- a/src/libsync/vfs/hydrationjob.cpp +++ b/src/libsync/vfs/hydrationjob.cpp @@ -17,6 +17,16 @@ HydrationJob::HydrationJob(Vfs *vfs, const QByteArray &fileId, std::unique_ptrparams().journal->getFileRecordsByFileId(_fileId, [this](const SyncJournalFileRecord &record) { diff --git a/src/libsync/vfs/hydrationjob.h b/src/libsync/vfs/hydrationjob.h index 68516d9c88..bcb52dc79e 100644 --- a/src/libsync/vfs/hydrationjob.h +++ b/src/libsync/vfs/hydrationjob.h @@ -22,10 +22,16 @@ class OPENCLOUD_SYNC_EXPORT HydrationJob : public QObject void start(); void abort(); + // In case the device to write to is a file, it can be passed here to the result slots + void setTargetFile(const QString& fileName); + QString targetFileName() const; + Vfs *vfs() const; SyncJournalFileRecord record() const; + QByteArray fileId() const { return _fileId; } + Q_SIGNALS: void finished(); void error(const QString &error); @@ -34,6 +40,7 @@ class OPENCLOUD_SYNC_EXPORT HydrationJob : public QObject Vfs *_vfs; QByteArray _fileId; std::unique_ptr _device; + QString _fileName; SyncJournalFileRecord _record; GETFileJob *_job = nullptr; }; diff --git a/src/libsync/vfs/vfs.cpp b/src/libsync/vfs/vfs.cpp index 225a2bf43c..c63491501a 100644 --- a/src/libsync/vfs/vfs.cpp +++ b/src/libsync/vfs/vfs.cpp @@ -51,6 +51,8 @@ Optional Vfs::modeFromString(const QString &str) return Off; } else if (str == QLatin1String("cfapi")) { return WindowsCfApi; + } else if (str == QLatin1String("xattr")) { + return XAttr; } return {}; } @@ -65,6 +67,8 @@ QString Utility::enumToString(Vfs::Mode mode) return QStringLiteral("cfapi"); case Vfs::Mode::Off: return QStringLiteral("off"); + case Vfs::Mode::XAttr: + return QStringLiteral("xattr"); } Q_UNREACHABLE(); } @@ -144,10 +148,10 @@ void Vfs::wipeDehydratedVirtualFiles() // But hydrated placeholders may still be around. } -QFuture> Vfs::hydrateFile(const QByteArray &, const QString &) +HydrationJob* Vfs::hydrateFile(const QByteArray&, const QString&) { // nothing to do - return QtFuture::makeReadyValueFuture(Result{}); + return nullptr; } Q_LOGGING_CATEGORY(lcPlugin, "sync.plugins", QtInfoMsg) @@ -207,6 +211,8 @@ Vfs::Mode OCC::VfsPluginManager::bestAvailableVfsMode() const { if (isVfsPluginAvailable(Vfs::WindowsCfApi)) { return Vfs::WindowsCfApi; + } else if (isVfsPluginAvailable(Vfs::XAttr)) { + return Vfs::XAttr; } else if (isVfsPluginAvailable(Vfs::Off)) { return Vfs::Off; } diff --git a/src/libsync/vfs/vfs.h b/src/libsync/vfs/vfs.h index 4404cc7fe1..8009577c05 100644 --- a/src/libsync/vfs/vfs.h +++ b/src/libsync/vfs/vfs.h @@ -37,6 +37,7 @@ class Account; class SyncJournalDb; class SyncFileItem; class SyncEngine; +class HydrationJob; /** Collection of parameters for initializing a Vfs instance. */ struct OPENCLOUD_SYNC_EXPORT VfsSetupParams @@ -97,7 +98,7 @@ class OPENCLOUD_SYNC_EXPORT Vfs : public QObject * Currently plugins and modes are one-to-one but that's not required. * The raw integer values are used in Qml */ - enum Mode : uint8_t { Off = 0, WindowsCfApi = 1 }; + enum Mode : uint8_t { Off = 0, WindowsCfApi = 1, XAttr = 2 }; Q_ENUM(Mode) enum class ConvertToPlaceholderResult : uint8_t { Ok, Locked }; Q_ENUM(ConvertToPlaceholderResult) @@ -211,7 +212,7 @@ class OPENCLOUD_SYNC_EXPORT Vfs : public QObject * * Returns a QFuture void if successful and QFuture QString if an error occurs. */ - [[nodiscard]] virtual QFuture> hydrateFile(const QByteArray &fileId, const QString &targetPath); + [[nodiscard]] virtual HydrationJob* hydrateFile(const QByteArray &fileId, const QString &targetPath); public Q_SLOTS: /** Update in-sync state based on SyncFileStatusTracker signal. diff --git a/src/libsync/xattr.cpp b/src/libsync/xattr.cpp new file mode 100644 index 0000000000..07a8cb808e --- /dev/null +++ b/src/libsync/xattr.cpp @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2025 Hannah von Reth + +#include "xattr.h" + +#include + +#include "common/filesystembase.h" + +namespace OCC { +namespace FileSystem { + std::optional Xattr::getxattr(const std::filesystem::path &path, const QString &name) + { + QByteArray value; + ssize_t res = 0; + do { + value.resize(value.size() + 255); +#ifdef Q_OS_MAC + res = ::getxattr(path.c_str(), name.toUtf8().constData(), value.data(), value.size(), 0, XATTR_NOFOLLOW); +#else + res = ::lgetxattr(path.c_str(), name.toUtf8().constData(), value.data(), value.size()); +#endif + } while (res == -1 && errno == ERANGE); + if (res > 0) { + value.resize(res); + return QString::fromUtf8(value); + } else { + return {}; + } + } + + Result Xattr::setxattr(const std::filesystem::path &path, const QString &name, const QString &value) + { + const auto data = value.toUtf8(); +#ifdef Q_OS_MAC + const auto result = ::setxattr(path.c_str(), name.toUtf8().constData(), data.constData(), data.size(), 0, XATTR_NOFOLLOW); +#else + const auto result = ::lsetxattr(path.c_str(), name.toUtf8().constData(), data.constData(), data.size(), 0); +#endif + if (result != 0) { + return QString::fromUtf8(strerror(errno)); + } + return {}; + } + + Result Xattr::removexattr(const std::filesystem::path &path, const QString &name) + { +#ifdef Q_OS_MAC + const auto result = ::removexattr(path.c_str(), name.toUtf8().constData(), 0); +#else + const auto result = ::lremovexattr(path.c_str(), name.toUtf8().constData()); +#endif + +#ifdef Q_OS_MAC + if (errno == ENOATTR) { +#else + if (errno == ENODATA) { +#endif + qCWarning(lcFileSystem) << u"Failed to remove tag" << name << u"from" << path.native() << u"tag doesn't exist"; + return {}; + } + if (result != 0) { + return QString::fromUtf8(strerror(errno)); + } + return {}; + } +} +} diff --git a/src/libsync/xattr.h b/src/libsync/xattr.h new file mode 100644 index 0000000000..a99358fb1d --- /dev/null +++ b/src/libsync/xattr.h @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: 2025 Hannah von Reth + +#pragma once + +#include "libsync/common/result.h" +#include "libsync/opencloudsynclib.h" + +#include +#include + +#include + +namespace OCC { +namespace FileSystem { + namespace Xattr { + OPENCLOUD_SYNC_EXPORT std::optional getxattr(const std::filesystem::path &path, const QString &name); + OPENCLOUD_SYNC_EXPORT Result setxattr(const std::filesystem::path &path, const QString &name, const QString &value); + + OPENCLOUD_SYNC_EXPORT Result removexattr(const std::filesystem::path &path, const QString &name); + } +} +} \ No newline at end of file diff --git a/src/plugins/vfs/xattr/CMakeLists.txt b/src/plugins/vfs/xattr/CMakeLists.txt new file mode 100644 index 0000000000..3e1df3a3bb --- /dev/null +++ b/src/plugins/vfs/xattr/CMakeLists.txt @@ -0,0 +1,6 @@ +if (NOT WIN32) + add_vfs_plugin(NAME xattr + SRC + vfs_xattr.cpp + ) +endif() diff --git a/src/plugins/vfs/xattr/vfs_xattr.cpp b/src/plugins/vfs/xattr/vfs_xattr.cpp new file mode 100644 index 0000000000..135634ba20 --- /dev/null +++ b/src/plugins/vfs/xattr/vfs_xattr.cpp @@ -0,0 +1,453 @@ +/* + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2025 OpenCloud GmbH and OpenCloud contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "vfs_xattr.h" + +#include "account.h" +#include "common/syncjournaldb.h" +#include "filesystem.h" +#include "libsync/xattr.h" +#include "syncfileitem.h" +#include "vfs/hydrationjob.h" +#include "theme.h" + +#include + +#include +#include +#include +#include +#include +#include + +using namespace Qt::StringLiterals; +using namespace xattr; + +Q_LOGGING_CATEGORY(lcVfsXAttr, "sync.vfs.xattr", QtInfoMsg) + + +namespace { +const QString ownerXAttrName = u"user.openvfs.owner"_s; +const QString etagXAttrName = u"user.openvfs.etag"_s; +const QString fileidXAttrName = u"user.openvfs.fileid"_s; +const QString modtimeXAttrName = u"user.openvfs.modtime"_s; +const QString fileSizeXAttrName = u"user.openvfs.fsize"_s; +const QString actionXAttrName = u"user.openvfs.action"_s; +const QString stateXAttrName = u"user.openvfs.state"_s; +const QString pinstateXAttrName = u"user.openvfs.pinstate"_s; + +const QString fileStateVirtual = u"virtual"_s; +const QString fileStateHydrate = u"hydrate"_s; +const QString fileStateDehydrate = u"dehydrate"_s; +const QString fileStateHydrated = u"hydrated"_s; + +} + +namespace OCC { + +VfsXAttr::VfsXAttr(QObject *parent) + : Vfs(parent) +{ +} + +VfsXAttr::~VfsXAttr() = default; + +Vfs::Mode VfsXAttr::mode() const +{ + return XAttr; +} + +QString VfsXAttr::xattrOwnerString() const +{ + return u"%1:%2"_s.arg(Theme::instance()->appName(), params().account->uuid().toString(QUuid::WithoutBraces)); +} + +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); + + auto owner = FileSystem::Xattr::getxattr(path, ownerXAttrName); + QString err; + + if (!owner) { + // set the owner to opencloud to claim it + if (!FileSystem::Xattr::setxattr(path, ownerXAttrName, xattrOwnerString() )) { + err = tr("Unable to claim the sync root for files on demand"); + } + } else if (owner.value() != xattrOwnerString()) { + // owner is set. See if it is us + qCDebug(lcVfsXAttr) << "Root-FS has a different owner" << owner.value() << "Not our vfs!"; + err = tr("The sync path is claimed by a different cloud, please check your setup"); + } + if (err.isEmpty()) { + Q_EMIT started(); + } else { + Q_EMIT error(err); + } +} + +void VfsXAttr::stop() +{ +} + +void VfsXAttr::unregisterFolder() +{ +} + +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 {}; +} + +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; + } + 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; + } 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); + + // remove the action marker again + FileSystem::Xattr::removexattr(localPath, actionXAttrName); + + return res; +} + +void VfsXAttr::slotHydrateJobFinished() +{ + HydrationJob *hydration = qobject_cast(sender()); + + const auto targetPath = FileSystem::toFilesystemPath(hydration->targetFileName()); + Q_ASSERT(!targetPath.empty()); + + qCInfo(lcVfsXAttr) << u"Hydration Job finished for" << targetPath; + + if (std::filesystem::exists(targetPath)) { + auto item = OCC::SyncFileItem::fromSyncJournalFileRecord(hydration->record()); + // the file is now downloaded + item->_type = ItemTypeFile; + + if (auto inode = FileSystem::getInode(targetPath)) { + item->_inode = inode.value(); + } else { + qCWarning(lcVfsXAttr) << u"Failed to get inode for" << targetPath; + } + + // set the xattrs + // the file is not virtual any more, remove the xattrs. No state xattr means local available data + 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) { + qCWarning(lcVfsXAttr) << u"Error when setting the file record to the database" << result.error(); + } else { + qCInfo(lcVfsXAttr) << u"Hydration succeeded" << targetPath; + } + } else { + qCWarning(lcVfsXAttr) << u"Hydration succeeded but the file appears to be moved" << targetPath; + } + + hydration->deleteLater(); + this->_hydrationJobs.remove(hydration->fileId()); +} + +Result VfsXAttr::createPlaceholder(const SyncFileItem &item) +{ + 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"); + } + + 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 {}; +} + +HydrationJob* VfsXAttr::hydrateFile(const QByteArray &fileId, const QString &targetPath) +{ + qCInfo(lcVfsXAttr) << u"Requesting hydration for" << fileId; + if (_hydrationJobs.contains(fileId)) { + qCWarning(lcVfsXAttr) << u"Ignoring hydration request for running hydration for fileId" << fileId; + return {}; + } + + HydrationJob *hydration = new HydrationJob(this, fileId, std::make_unique(targetPath), nullptr); + hydration->setTargetFile(targetPath); + _hydrationJobs.insert(fileId, hydration); + + // set an action attrib + addPlaceholderAttribute(targetPath, actionXAttrName, 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()); + hydration->deleteLater(); + }); + + return hydration; +} + +bool VfsXAttr::needsMetadataUpdate(const SyncFileItem &item) +{ + const QString path = params().filesystemPath + item.localName(); + + return QFileInfo::exists(path); +} + +bool VfsXAttr::isDehydratedPlaceholder(const QString &filePath) +{ + + if (QFileInfo::exists(filePath)) { + const auto attribs = placeHolderAttributes(filePath); + return (attribs.state() == fileStateVirtual); + } + 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) { + type = ItemTypeVirtualFile; + if (attribs.pinState() == pinStateToString(PinState::AlwaysLocal)) { + type = ItemTypeVirtualFileDownload; + } + } else { + if (attribs.pinState() == pinStateToString(PinState::OnlineOnly)) { + type = ItemTypeVirtualFileDehydration; + } + } + } + qCDebug(lcVfsXAttr) << p << 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); + 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); + } + 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; + } + qCDebug(lcVfsXAttr) << folderPath << pState; + + return pState; +} + +Vfs::AvailabilityResult VfsXAttr::availability(const QString &folderPath) +{ + + const auto basePinState = pinState(folderPath); + Vfs::AvailabilityResult res {VfsItemAvailability::Mixed}; + + if (basePinState) { + switch (*basePinState) { + case OCC::PinState::AlwaysLocal: + res = VfsItemAvailability::AlwaysLocal; + break; + case OCC::PinState::Inherited: + break; + case OCC::PinState::OnlineOnly: + res = VfsItemAvailability::OnlineOnly; + break; + case OCC::PinState::Unspecified: + break; + case OCC::PinState::Excluded: + break; + }; + res = VfsItemAvailability::Mixed; + } else { + res = AvailabilityError::NoSuchItem; + } + qCDebug(lcVfsXAttr) << folderPath << res.get(); + + return res; +} + +void VfsXAttr::fileStatusChanged(const QString& systemFileName, SyncFileStatus fileStatus) +{ + if (fileStatus.tag() == SyncFileStatus::StatusExcluded) { + setPinState(systemFileName, 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 new file mode 100644 index 0000000000..6ea1132034 --- /dev/null +++ b/src/plugins/vfs/xattr/vfs_xattr.h @@ -0,0 +1,101 @@ +/* + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2025 OpenCloud GmbH and OpenCloud contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ +#pragma once + +#include +#include + +#include "vfs/vfs.h" +#include "common/plugin.h" +#include "common/result.h" + +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; + +class VfsXAttr : public Vfs +{ + Q_OBJECT + +public: + explicit VfsXAttr(QObject *parent = nullptr); + ~VfsXAttr() override; + + [[nodiscard]] Mode mode() const override; + + void stop() override; + void unregisterFolder() override; + + [[nodiscard]] bool socketApiPinStateActionsShown() const override; + + Result updateMetadata(const SyncFileItem &syncItem, const QString &filePath, const QString &replacesFile) override; + // [[nodiscard]] bool isPlaceHolderInSync(const QString &filePath) const override { Q_UNUSED(filePath) return true; } + + Result createPlaceholder(const SyncFileItem &item) override; + + bool needsMetadataUpdate(const SyncFileItem &item) override; + bool isDehydratedPlaceholder(const QString &filePath) override; + LocalInfo statTypeVirtualFile(const std::filesystem::directory_entry &path, ItemType type) override; + + bool setPinState(const QString &folderPath, PinState state) override; + Optional pinState(const QString &folderPath) override; + AvailabilityResult availability(const QString &folderPath) override; + + HydrationJob* hydrateFile(const QByteArray &fileId, const QString& targetPath) override; + + QString pinStateToString(PinState) const; + PinState stringToPinState(const QString&) const; + +Q_SIGNALS: + void finished(Result); + +public Q_SLOTS: + void fileStatusChanged(const QString &systemFileName, OCC::SyncFileStatus fileStatus) override; + + void slotHydrateJobFinished(); + +protected: + void startImpl(const VfsSetupParams ¶ms) override; + +private: + QString xattrOwnerString() const; + 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; +}; + +class XattrVfsPluginFactory : public QObject, public DefaultPluginFactory +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "eu.opencloud.PluginFactory" FILE "libsync/vfs/vfspluginmetadata.json") + Q_INTERFACES(OCC::PluginFactory) +}; + +} // namespace OCC diff --git a/src/resources/fonticon.h b/src/resources/fonticon.h index 388526efc1..c1b7594e5f 100644 --- a/src/resources/fonticon.h +++ b/src/resources/fonticon.h @@ -13,6 +13,7 @@ class OPENCLOUD_RESOURCES_EXPORT FontIcon : public QIcon { Q_GADGET QML_VALUE_TYPE(fontIcon) + public: enum class DefaultGlyphes : char16_t { Question = u'', @@ -20,13 +21,13 @@ class OPENCLOUD_RESOURCES_EXPORT FontIcon : public QIcon Info = u'' }; - Q_ENUM(DefaultGlyphes); + Q_ENUM(DefaultGlyphes) enum class FontFamily : uint8_t { FontAwesome, RemixIcon, }; - Q_ENUM(FontFamily); + Q_ENUM(FontFamily) enum class Size : uint8_t { // fullsized icon @@ -34,7 +35,8 @@ class OPENCLOUD_RESOURCES_EXPORT FontIcon : public QIcon // hafl sized, centered icon Half }; - Q_ENUM(Size); + Q_ENUM(Size) + FontIcon(); // defaults to fontawesoem FontIcon(QChar glyphe, Size size = Size::Normal, const QColor &color = {}); diff --git a/test/testutility.cpp b/test/testutility.cpp index 63ba0616a7..1b7ec35d96 100644 --- a/test/testutility.cpp +++ b/test/testutility.cpp @@ -315,7 +315,7 @@ private Q_SLOTS: const auto fn = tempFile.fileName(); const QString testKey = QStringLiteral("testKey"); - const QByteArray testValue("testValue"); + const auto testValue(u"testValue"_s); QVERIFY(!Tags::get(fn, testKey).has_value()); QVERIFY(Tags::set(fn, testKey, testValue)); @@ -333,7 +333,7 @@ private Q_SLOTS: const auto fn = tempDir.path(); const QString testKey = QStringLiteral("testKey"); - const QByteArray testValue("testValue"); + const QString testValue(u"testValue"_s); QVERIFY(!Tags::get(fn, testKey).has_value()); QVERIFY(Tags::set(fn, testKey, testValue));