diff --git a/CMakeLists.txt b/CMakeLists.txt index eed6d81ad9..0a93c0364a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -103,7 +103,7 @@ add_feature_info(AppImageUpdate WITH_APPIMAGEUPDATER "Built-in libappimageupdate option(WITH_EXTERNAL_BRANDING "A URL to an external branding repo" "") # specify additional vfs plugins -set(VIRTUAL_FILE_SYSTEM_PLUGINS off cfapi CACHE STRING "Name of internal plugin in src/libsync/vfs or the locations of virtual file plugins") +set(VIRTUAL_FILE_SYSTEM_PLUGINS xattr off cfapi CACHE STRING "Name of internal plugin in src/libsync/vfs or the locations of virtual file plugins") if(APPLE) set( SOCKETAPI_TEAM_IDENTIFIER_PREFIX "" CACHE STRING "SocketApi prefix (including a following dot) that must match the codesign key's TeamIdentifier/Organizational Unit" ) diff --git a/src/gui/folder.cpp b/src/gui/folder.cpp index 4424be96f9..64fb1c3b1b 100644 --- a/src/gui/folder.cpp +++ b/src/gui/folder.cpp @@ -1116,6 +1116,7 @@ void Folder::registerFolderWatcher() _folderWatcher.reset(new FolderWatcher(this)); connect(_folderWatcher.data(), &FolderWatcher::pathChanged, this, [this](const QSet &paths) { slotWatchedPathsChanged(paths, Folder::ChangeReason::Other); }); + connect(_folderWatcher.data(), &FolderWatcher::changesDetected, this, [this] { // don't set to not yet started if a sync is already running if (!isSyncRunning()) { diff --git a/src/gui/folder.h b/src/gui/folder.h index fac94a4459..69e899b07a 100644 --- a/src/gui/folder.h +++ b/src/gui/folder.h @@ -66,7 +66,8 @@ class OPENCLOUD_GUI_EXPORT Folder : public QObject public: enum class ChangeReason { Other, - UnLock + UnLock, + XAttr }; Q_ENUM(ChangeReason) diff --git a/src/gui/folderwatcher.cpp b/src/gui/folderwatcher.cpp index 31768f7445..53e8ba2778 100644 --- a/src/gui/folderwatcher.cpp +++ b/src/gui/folderwatcher.cpp @@ -48,7 +48,8 @@ FolderWatcher::FolderWatcher(Folder *folder) _timer.setInterval(notificationTimeoutC); _timer.setSingleShot(true); connect(&_timer, &QTimer::timeout, this, [this] { - auto paths = popChangeSet(); + _timer.stop(); + auto paths = std::move(_changeSet); Q_ASSERT(!paths.empty()); if (!paths.isEmpty()) { qCInfo(lcFolderWatcher) << u"Detected changes in paths:" << paths; diff --git a/src/gui/folderwatcher.h b/src/gui/folderwatcher.h index 1aaed61fbc..9953960397 100644 --- a/src/gui/folderwatcher.h +++ b/src/gui/folderwatcher.h @@ -80,15 +80,19 @@ class OPENCLOUD_GUI_EXPORT FolderWatcher : public QObject /// For testing linux behavior only int testLinuxWatchCount() const; + // pop the accumulated changes QSet popChangeSet(); - Q_SIGNALS: /** Emitted when one of the watched directories or one * of the contained files is changed. */ void pathChanged(const QSet &path); + /** Emitted when an extended file attribute changed on one + * of the files in the list */ + void xattrChanged(const QSet &path); + /** * We detected a file change, this signal can be used to trigger the prepareSync state */ diff --git a/src/gui/folderwatcher_linux.cpp b/src/gui/folderwatcher_linux.cpp index 0751e7cf52..bd6bd60cdc 100644 --- a/src/gui/folderwatcher_linux.cpp +++ b/src/gui/folderwatcher_linux.cpp @@ -173,7 +173,8 @@ void FolderWatcherPrivate::slotReceivedNotification(int fd) // Filter out journal changes - redundant with filtering in FolderWatcher::pathIsIgnored. if (fileName.startsWith("._sync_") || fileName.startsWith(".csync_journal.db") - || fileName.startsWith(".sync_")) { + || fileName.startsWith(".sync_") + || fileName == QByteArrayLiteral(".OpenCloudSync.log") ) { continue; } diff --git a/src/gui/folderwizard/folderwizard.cpp b/src/gui/folderwizard/folderwizard.cpp index c414f98078..ce619a9e0c 100644 --- a/src/gui/folderwizard/folderwizard.cpp +++ b/src/gui/folderwizard/folderwizard.cpp @@ -121,7 +121,12 @@ const AccountStatePtr &FolderWizardPrivate::accountState() bool FolderWizardPrivate::useVirtualFiles() const { - return VfsPluginManager::instance().bestAvailableVfsMode() == Vfs::WindowsCfApi; + const auto bavm = VfsPluginManager::instance().bestAvailableVfsMode(); + if (Utility::isWindows()) + return bavm == Vfs::WindowsCfApi; + if (Utility::isLinux()) + return bavm == Vfs::XAttr; + return false; } FolderWizard::FolderWizard(const AccountStatePtr &account, QWidget *parent) diff --git a/src/gui/socketapi/socketapi.cpp b/src/gui/socketapi/socketapi.cpp index 4a66e70054..f7425e68c5 100644 --- a/src/gui/socketapi/socketapi.cpp +++ b/src/gui/socketapi/socketapi.cpp @@ -158,6 +158,13 @@ SocketApi::SocketApi(QObject *parent) unregisterAccount(accountState->account()); } }); + + const QJsonObject args { { QStringLiteral("size"), 5 } }; + const QJsonObject obj { { QStringLiteral("id"), QString::number(17) }, { QStringLiteral("arguments"), args } }; + const auto json = QJsonDocument(obj).toJson(QJsonDocument::Indented); + + qCDebug(lcSocketApi) << u"JSON:" << json; + } SocketApi::~SocketApi() @@ -283,7 +290,7 @@ void SocketApi::slotReadSocket() } } else if (command.startsWith(QLatin1String("V2/"))) { QJsonParseError error; - const auto json = QJsonDocument::fromJson(argument.toUtf8(), &error).object(); + const auto json = QJsonDocument::fromJson(argument.trimmed().toUtf8(), &error).object(); if (error.error != QJsonParseError::NoError) { qCWarning(lcSocketApi()) << u"Invalid json" << argument << error.errorString(); listener->sendError(error.errorString()); @@ -709,6 +716,21 @@ void SocketApi::command_V2_GET_CLIENT_ICON(const QSharedPointer job->success({ { QStringLiteral("png"), QString::fromUtf8(data) } }); } +void SocketApi::command_V2_HYDRATE_FILE(const QSharedPointer &job) const +{ + OC_ASSERT(job); + const auto &arguments = job->arguments(); + + const auto &file = arguments[QStringLiteral("file")].toString(); + + auto fileData = FileData::get(file); + + if (fileData.folder) { + // call the getfile job to hydrate + } + job->success({ {QStringLiteral("hydration"), QStringLiteral("STARTED") } }); +} + void SocketApi::emailPrivateLink(const QUrl &link) { Utility::openEmailComposer( diff --git a/src/gui/socketapi/socketapi.h b/src/gui/socketapi/socketapi.h index 0113d70933..3084597905 100644 --- a/src/gui/socketapi/socketapi.h +++ b/src/gui/socketapi/socketapi.h @@ -120,6 +120,9 @@ private Q_SLOTS: Q_INVOKABLE void command_OPEN_APP_LINK(const QString &localFile, SocketListener *listener); // External sync Q_INVOKABLE void command_V2_LIST_ACCOUNTS(const QSharedPointer &job) const; + // VFS + Q_INVOKABLE void command_V2_HYDRATE_FILE(const QSharedPointer &job) const; + // Sends the id and the client icon as PNG image (base64 encoded) in Json key "png" // e.g. { "id" : "1", "arguments" : { "png" : "hswehs343dj8..." } } or an error message in key "error" diff --git a/src/libsync/common/CMakeLists.txt b/src/libsync/common/CMakeLists.txt index 9832e0652e..494f09f97a 100644 --- a/src/libsync/common/CMakeLists.txt +++ b/src/libsync/common/CMakeLists.txt @@ -12,6 +12,7 @@ target_sources(libsync PRIVATE utility.cpp remotepermissions.cpp vfs.cpp + hydrationjob.cpp pinstate.cpp plugin.cpp restartmanager.cpp diff --git a/src/plugins/vfs/cfapi/hydrationjob.cpp b/src/libsync/common/hydrationjob.cpp similarity index 84% rename from src/plugins/vfs/cfapi/hydrationjob.cpp rename to src/libsync/common/hydrationjob.cpp index fad56cbd1e..b5ad0a522a 100644 --- a/src/plugins/vfs/cfapi/hydrationjob.cpp +++ b/src/libsync/common/hydrationjob.cpp @@ -3,12 +3,10 @@ * SPDX-License-Identifier: GPL-2.0-or-later */ -#include "plugins/vfs/cfapi/hydrationjob.h" - -#include "plugins/vfs/cfapi/cfapiwrapper.h" -#include "plugins/vfs/cfapi/vfs_cfapi.h" +#include "hydrationjob.h" #include "libsync/common/syncjournaldb.h" +#include "libsync/common/vfs.h" #include "libsync/filesystem.h" #include "libsync/networkjobs/getfilejob.h" @@ -19,7 +17,7 @@ using namespace Qt::Literals::StringLiterals; Q_LOGGING_CATEGORY(lcHydration, "sync.vfs.hydrationjob", QtDebugMsg) -OCC::HydrationJob::HydrationJob(const CfApiWrapper::CallBackContext &context) +OCC::HydrationJob::HydrationJob(const CallBackContext &context) : QObject(context.vfs) , _context(context) { @@ -103,7 +101,7 @@ OCC::HydrationJob::Status OCC::HydrationJob::status() const return _status; } -const OCC::CfApiWrapper::CallBackContext OCC::HydrationJob::context() const +const OCC::CallBackContext OCC::HydrationJob::context() const { return _context; } @@ -132,21 +130,22 @@ void OCC::HydrationJob::start() Q_ASSERT(_localRoot.endsWith('/'_L1)); const auto startServer = [this](const QString &serverName) -> QLocalServer * { + QLocalServer::removeServer(serverName); const auto server = new QLocalServer(this); const auto listenResult = server->listen(serverName); if (!listenResult) { - qCCritical(lcHydration) << u"Couldn't get server to listen" << serverName << _localRoot << _context; + //qCCritical(lcHydration) << u"Couldn't get server to listen" << serverName << _localRoot << _context; if (!_isCancelled) { emitFinished(Status::Error); } return nullptr; } - qCInfo(lcHydration) << u"Server ready, waiting for connections" << serverName << _localRoot << _context; + // qCInfo(lcHydration) << u"Server ready, waiting for connections" << serverName << _localRoot << _context; return server; }; // Start cancellation server - _signalServer = startServer(_context.requestHexId() + u":cancellation"_s); + _signalServer = startServer(_context.requestHexId() + QStringLiteral(":cancellation")); Q_ASSERT(_signalServer); if (!_signalServer) { return; @@ -209,7 +208,7 @@ void OCC::HydrationJob::onCancellationServerNewConnection() { Q_ASSERT(!_signalSocket); - qCInfo(lcHydration) << u"Got new connection on cancellation server" << _context; + // qCInfo(lcHydration) << u"Got new connection on cancellation server" << _context; _signalSocket = _signalServer->nextPendingConnection(); } @@ -220,7 +219,7 @@ void OCC::HydrationJob::onNewConnection() handleNewConnection(); } -void OCC::HydrationJob::finalize(OCC::VfsCfApi *vfs) +void OCC::HydrationJob::finalize(OCC::Vfs *vfs) { auto item = SyncFileItem::fromSyncJournalFileRecord(_record); if (_isCancelled) { @@ -246,10 +245,10 @@ void OCC::HydrationJob::finalize(OCC::VfsCfApi *vfs) FileSystem::getInode(FileSystem::toFilesystemPath(localFilePathAbs()), &item->_inode); const auto result = _journal->setFileRecord(SyncJournalFileRecord::fromSyncFileItem(*item)); if (!result) { - qCWarning(lcHydration) << u"Error when setting the file record to the database" << _context << result.error(); + // qCWarning(lcHydration) << u"Error when setting the file record to the database" << _context << result.error(); } } else { - qCWarning(lcHydration) << u"Hydration succeeded but the file appears to be moved" << _context; + // qCWarning(lcHydration) << u"Hydration succeeded but the file appears to be moved" << _context; } } @@ -271,9 +270,9 @@ void OCC::HydrationJob::onGetFinished() } } if (!_errorString.isEmpty()) { - qCInfo(lcHydration) << u"GETFileJob finished" << _context << _errorCode << _statusCode << _errorString; + // qCInfo(lcHydration) << u"GETFileJob finished" << _context << _errorCode << _statusCode << _errorString; } else { - qCInfo(lcHydration) << u"GETFileJob finished" << _context; + // qCInfo(lcHydration) << u"GETFileJob finished" << _context; } if (_isCancelled) { _errorCode = QNetworkReply::NoError; @@ -292,7 +291,8 @@ void OCC::HydrationJob::onGetFinished() void OCC::HydrationJob::handleNewConnection() { - qCInfo(lcHydration) << u"Got new connection starting GETFileJob" << _context; + // FIXME: Fix all the loggings which require the operator<< from vfs.cpp for CallBackContext + // qCInfo(lcHydration) << u"Got new connection starting GETFileJob" << _context; _transferDataSocket = _transferDataServer->nextPendingConnection(); _job = new GETFileJob(_account, _remoteSyncRootPath, _remoteFilePathRel, _transferDataSocket, {}, {}, 0, this); _job->setExpectedContentLength(_record.size()); diff --git a/src/plugins/vfs/cfapi/hydrationjob.h b/src/libsync/common/hydrationjob.h similarity index 84% rename from src/plugins/vfs/cfapi/hydrationjob.h rename to src/libsync/common/hydrationjob.h index baf6330262..9aa7510e7c 100644 --- a/src/plugins/vfs/cfapi/hydrationjob.h +++ b/src/libsync/common/hydrationjob.h @@ -4,7 +4,6 @@ */ #pragma once -#include "cfapiwrapper.h" #include "libsync/account.h" #include "libsync/common/syncjournalfilerecord.h" @@ -16,7 +15,18 @@ class QLocalSocket; namespace OCC { class GETFileJob; class SyncJournalDb; -class VfsCfApi; +class Vfs; + +struct CallBackContext +{ + OCC::Vfs *vfs; + QString path; + int64_t requestId; + QByteArray fileId; + QMap extraArgs; + + inline QString requestHexId() const { return QString::number(requestId, 16); } +}; // TODO: check checksums class HydrationJob : public QObject @@ -30,7 +40,7 @@ class HydrationJob : public QObject }; Q_ENUM(Status) - explicit HydrationJob(const CfApiWrapper::CallBackContext &context); + explicit HydrationJob(const CallBackContext &context); ~HydrationJob() override; @@ -58,7 +68,7 @@ class HydrationJob : public QObject Status status() const; - const CfApiWrapper::CallBackContext context() const; + const CallBackContext context() const; [[nodiscard]] int errorCode() const; [[nodiscard]] int statusCode() const; @@ -66,7 +76,7 @@ class HydrationJob : public QObject void start(); void cancel(); - void finalize(OCC::VfsCfApi *vfs); + void finalize(OCC::Vfs *vfs); Q_SIGNALS: void finished(HydrationJob *job); @@ -89,7 +99,7 @@ class HydrationJob : public QObject SyncJournalDb *_journal = nullptr; bool _isCancelled = false; - CfApiWrapper::CallBackContext _context; + OCC::CallBackContext _context; QString _remoteFilePathRel; SyncJournalFileRecord _record; diff --git a/src/libsync/common/pinstate.cpp b/src/libsync/common/pinstate.cpp index 6d3570298b..956a70d6fc 100644 --- a/src/libsync/common/pinstate.cpp +++ b/src/libsync/common/pinstate.cpp @@ -19,6 +19,24 @@ using namespace OCC; +template <> +QString Utility::enumToDisplayName(PinState state) +{ + switch (state) { + case PinState::AlwaysLocal: + return QStringLiteral("AlwaysLocal"); + case PinState::Excluded: + return QStringLiteral("Excluded"); + case PinState::Inherited: + return QStringLiteral("Inherited"); + case PinState::OnlineOnly: + return QStringLiteral("OnlineOnly"); + case PinState::Unspecified: + return QStringLiteral("Unspecified"); + } + Q_UNREACHABLE(); +} + template <> QString Utility::enumToDisplayName(VfsItemAvailability availability) { diff --git a/src/libsync/common/pinstate.h b/src/libsync/common/pinstate.h index 48a7ebbb05..dfeb470c7f 100644 --- a/src/libsync/common/pinstate.h +++ b/src/libsync/common/pinstate.h @@ -134,6 +134,8 @@ namespace PinStateEnums { Q_ENUM_NS(VfsItemAvailability) } using namespace PinStateEnums; +template <> +OPENCLOUD_SYNC_EXPORT QString Utility::enumToDisplayName(PinState state); template <> OPENCLOUD_SYNC_EXPORT QString Utility::enumToDisplayName(VfsItemAvailability availability); diff --git a/src/libsync/common/vfs.cpp b/src/libsync/common/vfs.cpp index e519ad8b2d..11a3ecef59 100644 --- a/src/libsync/common/vfs.cpp +++ b/src/libsync/common/vfs.cpp @@ -21,6 +21,7 @@ #include "common/version.h" #include "plugin.h" #include "syncjournaldb.h" +#include "syncfileitem.h" #include #include @@ -35,9 +36,29 @@ using namespace Qt::Literals::StringLiterals; Q_LOGGING_CATEGORY(lcVfs, "sync.vfs", QtInfoMsg) +QDebug operator<<(QDebug debug, const OCC::CallBackContext &context) +{ + QDebugStateSaver saver(debug); + debug.setAutoInsertSpaces(false); + debug << u"cfapiCallback(" << context.path << u", " << context.requestHexId(); + for (const auto &[k, v] : context.extraArgs.asKeyValueRange()) { + debug << u", "; + debug.noquote() << k << u"="; + debug.quote() << v; + }; + debug << u")"; + return debug.maybeSpace(); +} + +class OCC::VfsApiPrivate +{ +public: + QMap hydrationJobs; +}; Vfs::Vfs(QObject *parent) : QObject(parent) + , d(new VfsApiPrivate) { } @@ -51,6 +72,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 +88,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(); } @@ -110,7 +135,6 @@ void Vfs::start(const VfsSetupParams ¶ms) startImpl(this->params()); } - void Vfs::wipeDehydratedVirtualFiles() { if (mode() == Vfs::Mode::Off) { @@ -144,6 +168,128 @@ void Vfs::wipeDehydratedVirtualFiles() // But hydrated placeholders may still be around. } +// ======================= Hydration ================ + +HydrationJob *Vfs::findHydrationJob(int64_t requestId) const +{ + // Find matching hydration job for request id + return d->hydrationJobs.value(requestId); +} + +void Vfs::cancelHydration(const OCC::CallBackContext &context) +{ + // Find matching hydration job for request id + const auto hydrationJob = findHydrationJob(context.requestId); + // If found, cancel it + if (hydrationJob) { + qCInfo(lcVfs) << u"Cancel hydration" << hydrationJob->context(); + hydrationJob->cancel(); + } +} + +void Vfs::requestHydration(const OCC::CallBackContext &context, qint64 requestedFileSize) +{ + qCInfo(lcVfs) << u"Received request to hydrate" << context.path << context.fileId; + const auto root = QDir::toNativeSeparators(params().filesystemPath); + Q_ASSERT(context.path.startsWith(root)); + + + // Set in the database that we should download the file + SyncJournalFileRecord record; + params().journal->getFileRecordsByFileId(context.fileId, [&record](const auto &r) { + Q_ASSERT(!record.isValid()); + record = r; + }); + if (!record.isValid()) { + qCInfo(lcVfs) << u"Couldn't hydrate, did not find file in db"; + Q_ASSERT(false); // how did we end up here if it's not a cloud file + Q_EMIT hydrationRequestFailed(context.requestId); + Q_EMIT needSync(); + return; + } + + bool isNotVirtualFileFailure = false; + if (!record.isVirtualFile()) { + if (isDehydratedPlaceholder(context.path)) { + qCWarning(lcVfs) << u"Hydration requested for a placeholder file that is incorrectly not marked as a virtual file in the local database. " + u"Attempting to correct this inconsistency..."; + auto item = SyncFileItem::fromSyncJournalFileRecord(record); + item->_type = ItemTypeVirtualFileDownload; + isNotVirtualFileFailure = !params().journal->setFileRecord(SyncJournalFileRecord::fromSyncFileItem(*item)); + } else { + isNotVirtualFileFailure = true; + } + } + if (requestedFileSize != record.size()) { + // we are out of sync + qCWarning(lcVfs) << u"The db size and the placeholder meta data are out of sync, request resync"; + Q_ASSERT(false); // this should not happen + Q_EMIT hydrationRequestFailed(context.requestId); + Q_EMIT needSync(); + return; + } + + if (isNotVirtualFileFailure) { + qCWarning(lcVfs) << u"Couldn't hydrate, the file is not virtual"; + Q_ASSERT(false); // this should not happen + Q_EMIT hydrationRequestFailed(context.requestId); + Q_EMIT needSync(); + return; + } + + // All good, let's hydrate now + scheduleHydrationJob(context, std::move(record)); +} + +void Vfs::scheduleHydrationJob(const OCC::CallBackContext &context, SyncJournalFileRecord &&record) +{ + // after a local move, the remotePath and the targetPath might not match + if (findHydrationJob(context.requestId)) { + qCWarning(lcVfs) << u"The OS submitted again a hydration request which is already on-going" << context; + Q_EMIT hydrationRequestFailed(context.requestId); + return; + } + Q_ASSERT(!std::any_of(std::cbegin(d->hydrationJobs), std::cend(d->hydrationJobs), + [=](HydrationJob *job) { return job->requestId() == context.requestId || job->localFilePathAbs() == context.path; })); + auto job = new HydrationJob(context); + job->setAccount(params().account); + job->setRemoteSyncRootPath(params().baseUrl()); + job->setLocalRoot(params().filesystemPath); + job->setJournal(params().journal); + job->setRemoteFilePathRel(record.path()); + job->setRecord(std::move(record)); + connect(job, &HydrationJob::finished, this, &Vfs::onHydrationJobFinished); + d->hydrationJobs.insert(context.requestId, job); + job->start(); + Q_EMIT hydrationRequestReady(context.requestId); +} + +void Vfs::onHydrationJobFinished(HydrationJob *job) +{ + Q_ASSERT(findHydrationJob(job->requestId())); + qCInfo(lcVfs) << u"Hydration job finished" << job->requestId() << job->localFilePathAbs() << job->status(); + Q_EMIT hydrationRequestFinished(job->requestId()); + if (!job->errorString().isEmpty()) { + qCWarning(lcVfs) << job->errorString(); + } +} + +HydrationJob::Status Vfs::finalizeHydrationJob(int64_t requestId) +{ + // Find matching hydration job for request id + if (const auto hydrationJob = findHydrationJob(requestId)) { + qCDebug(lcVfs) << u"Finalize hydration job" << hydrationJob->context(); + hydrationJob->finalize(this); + d->hydrationJobs.take(hydrationJob->requestId()); + hydrationJob->deleteLater(); + return hydrationJob->status(); + } + qCCritical(lcVfs) << u"Failed to finalize hydration job" << requestId << u". Job not found."; + return HydrationJob::Status::Error; +} + +// =============================================================== + Q_LOGGING_CATEGORY(lcPlugin, "sync.plugins", QtInfoMsg) OCC::VfsPluginManager *OCC::VfsPluginManager::_instance = nullptr; @@ -201,6 +347,8 @@ Vfs::Mode OCC::VfsPluginManager::bestAvailableVfsMode() const { if (isVfsPluginAvailable(Vfs::WindowsCfApi)) { return Vfs::WindowsCfApi; + } else if (isVfsPluginAvailable(Vfs::XAttr)) { + return Vfs::XAttr; } else if (isVfsPluginAvailable(Vfs::Off)) { return Vfs::Off; } diff --git a/src/libsync/common/vfs.h b/src/libsync/common/vfs.h index f830e7fcda..5ec8499ae1 100644 --- a/src/libsync/common/vfs.h +++ b/src/libsync/common/vfs.h @@ -21,6 +21,7 @@ #include "result.h" #include "syncfilestatus.h" #include "utility.h" +#include "hydrationjob.h" #include #include @@ -36,6 +37,7 @@ class Account; class SyncJournalDb; class SyncFileItem; class SyncEngine; +class VfsApiPrivate; /** Collection of parameters for initializing a Vfs instance. */ struct OPENCLOUD_SYNC_EXPORT VfsSetupParams @@ -96,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) @@ -202,7 +204,19 @@ class OPENCLOUD_SYNC_EXPORT Vfs : public QObject */ void wipeDehydratedVirtualFiles(); + // === Hydration + HydrationJob *findHydrationJob(int64_t requestId) const; + void cancelHydration(const OCC::CallBackContext &context); + + void requestHydration(const OCC::CallBackContext &context, qint64 requestedFileSize); + + void scheduleHydrationJob(const OCC::CallBackContext &context, SyncJournalFileRecord &&record); + public Q_SLOTS: + void onHydrationJobFinished(HydrationJob *job); + HydrationJob::Status finalizeHydrationJob(int64_t requestId); + + /** Update in-sync state based on SyncFileStatusTracker signal. * * For some vfs plugins the icons aren't based on SocketAPI but rather on data shared @@ -221,6 +235,11 @@ public Q_SLOTS: /// The vfs plugin detected that the meta data are out of sync and requests a sync with the server void needSync(); +Q_SIGNALS: + void hydrationRequestReady(int64_t requestId); + void hydrationRequestFailed(int64_t requestId); + void hydrationRequestFinished(int64_t requestId); + protected: /** Update placeholder metadata during discovery. * @@ -241,9 +260,10 @@ public Q_SLOTS: */ virtual void startImpl(const VfsSetupParams ¶ms) = 0; + std::unique_ptr _setupParams; private: // the parameters passed to start() - std::unique_ptr _setupParams; + QScopedPointer d; friend class OwncloudPropagator; }; diff --git a/src/plugins/vfs/CMakeLists.txt b/src/plugins/vfs/CMakeLists.txt index 6a2935120a..b835455ea1 100644 --- a/src/plugins/vfs/CMakeLists.txt +++ b/src/plugins/vfs/CMakeLists.txt @@ -5,6 +5,7 @@ include(OCAddVfsPlugin) foreach(vfsPlugin ${VIRTUAL_FILE_SYSTEM_PLUGINS}) + message("checking vfs ${vfsPlugin}") set(vfsPluginPath ${vfsPlugin}) set(vfsPluginPathOut ${vfsPlugin}) get_filename_component(vfsPluginName ${vfsPlugin} NAME) diff --git a/src/plugins/vfs/cfapi/CMakeLists.txt b/src/plugins/vfs/cfapi/CMakeLists.txt index ba00122505..cac580a1e4 100644 --- a/src/plugins/vfs/cfapi/CMakeLists.txt +++ b/src/plugins/vfs/cfapi/CMakeLists.txt @@ -2,7 +2,6 @@ if (WIN32) add_vfs_plugin(NAME cfapi SRC cfapiwrapper.cpp - hydrationjob.cpp vfs_cfapi.cpp LIBS cldapi diff --git a/src/plugins/vfs/cfapi/cfapiwrapper.cpp b/src/plugins/vfs/cfapi/cfapiwrapper.cpp index bd0c5e99c7..e214e499d5 100644 --- a/src/plugins/vfs/cfapi/cfapiwrapper.cpp +++ b/src/plugins/vfs/cfapi/cfapiwrapper.cpp @@ -311,6 +311,7 @@ void CALLBACK cfApiFetchDataCallback(const CF_CALLBACK_INFO *callbackInfo, const if (hydrationJobResult != OCC::HydrationJob::Status::Success) { sendTransferError(); } + } OCC::Result updatePlaceholderState( @@ -826,16 +827,4 @@ bool OCC::CfApiWrapper::isPlaceHolderInSync(const QString &filePath) return true; } -QDebug operator<<(QDebug debug, const OCC::CfApiWrapper::CallBackContext &context) -{ - QDebugStateSaver saver(debug); - debug.setAutoInsertSpaces(false); - debug << u"cfapiCallback(" << context.path << u", " << context.requestHexId(); - for (const auto &[k, v] : context.extraArgs.asKeyValueRange()) { - debug << u", "; - debug.noquote() << k << u"="; - debug.quote() << v; - }; - debug << u")"; - return debug.maybeSpace(); -} + diff --git a/src/plugins/vfs/cfapi/cfapiwrapper.h b/src/plugins/vfs/cfapi/cfapiwrapper.h index 293b3ea3da..a1d8775cd0 100644 --- a/src/plugins/vfs/cfapi/cfapiwrapper.h +++ b/src/plugins/vfs/cfapi/cfapiwrapper.h @@ -22,16 +22,6 @@ namespace OCC { class VfsCfApi; namespace CfApiWrapper { - struct CallBackContext - { - OCC::VfsCfApi *vfs; - QString path; - int64_t requestId; - QByteArray fileId; - QMap extraArgs; - - inline QString requestHexId() const { return QString::number(requestId, 16); } - }; template class PlaceHolderInfo diff --git a/src/plugins/vfs/cfapi/vfs_cfapi.cpp b/src/plugins/vfs/cfapi/vfs_cfapi.cpp index 90a43cbad1..996ee6df0d 100644 --- a/src/plugins/vfs/cfapi/vfs_cfapi.cpp +++ b/src/plugins/vfs/cfapi/vfs_cfapi.cpp @@ -103,7 +103,6 @@ namespace OCC { class VfsCfApiPrivate { public: - QMap hydrationJobs; CF_CONNECTION_KEY connectionKey = {}; }; @@ -187,6 +186,11 @@ Result VfsCfApi::updateMetadata(const } } +bool VfsCfApi::handleXAttrChange(const QSet &) +{ + return false; +} + Result VfsCfApi::createPlaceholder(const SyncFileItem &item) { Q_ASSERT(params().filesystemPath.endsWith('/'_L1)); @@ -290,77 +294,6 @@ Vfs::AvailabilityResult VfsCfApi::availability(const QString &folderPath) } } -HydrationJob *VfsCfApi::findHydrationJob(int64_t requestId) const -{ - // Find matching hydration job for request id - return d->hydrationJobs.value(requestId); -} - -void VfsCfApi::cancelHydration(const OCC::CfApiWrapper::CallBackContext &context) -{ - // Find matching hydration job for request id - const auto hydrationJob = findHydrationJob(context.requestId); - // If found, cancel it - if (hydrationJob) { - qCInfo(lcCfApi) << u"Cancel hydration" << hydrationJob->context(); - hydrationJob->cancel(); - } -} - -void VfsCfApi::requestHydration(const OCC::CfApiWrapper::CallBackContext &context, qint64 requestedFileSize) -{ - qCInfo(lcCfApi) << u"Received request to hydrate" << context; - const auto root = QDir::toNativeSeparators(params().filesystemPath); - Q_ASSERT(context.path.startsWith(root)); - - - // Set in the database that we should download the file - SyncJournalFileRecord record; - params().journal->getFileRecordsByFileId(context.fileId, [&record](const auto &r) { - Q_ASSERT(!record.isValid()); - record = r; - }); - if (!record.isValid()) { - qCInfo(lcCfApi) << u"Couldn't hydrate, did not find file in db"; - Q_ASSERT(false); // how did we end up here if it's not a cloud file - Q_EMIT hydrationRequestFailed(context.requestId); - Q_EMIT needSync(); - return; - } - - bool isNotVirtualFileFailure = false; - if (!record.isVirtualFile()) { - if (isDehydratedPlaceholder(context.path)) { - qCWarning(lcCfApi) << u"Hydration requested for a placeholder file that is incorrectly not marked as a virtual file in the local database. " - u"Attempting to correct this inconsistency..."; - auto item = SyncFileItem::fromSyncJournalFileRecord(record); - item->_type = ItemTypeVirtualFileDownload; - isNotVirtualFileFailure = !params().journal->setFileRecord(SyncJournalFileRecord::fromSyncFileItem(*item)); - } else { - isNotVirtualFileFailure = true; - } - } - if (requestedFileSize != record.size()) { - // we are out of sync - qCWarning(lcCfApi) << u"The db size and the placeholder meta data are out of sync, request resync"; - Q_ASSERT(false); // this should not happen - Q_EMIT hydrationRequestFailed(context.requestId); - Q_EMIT needSync(); - return; - } - - if (isNotVirtualFileFailure) { - qCWarning(lcCfApi) << u"Couldn't hydrate, the file is not virtual"; - Q_ASSERT(false); // this should not happen - Q_EMIT hydrationRequestFailed(context.requestId); - Q_EMIT needSync(); - return; - } - - // All good, let's hydrate now - scheduleHydrationJob(context, std::move(record)); -} - void VfsCfApi::fileStatusChanged(const QString &systemFileName, SyncFileStatus fileStatus) { if (!QFileInfo::exists(systemFileName)) { @@ -380,52 +313,5 @@ void VfsCfApi::fileStatusChanged(const QString &systemFileName, SyncFileStatus f } } -void VfsCfApi::scheduleHydrationJob(const OCC::CfApiWrapper::CallBackContext &context, SyncJournalFileRecord &&record) -{ - // after a local move, the remotePath and the targetPath might not match - if (findHydrationJob(context.requestId)) { - qCWarning(lcCfApi) << u"The OS submitted again a hydration request which is already on-going" << context; - Q_EMIT hydrationRequestFailed(context.requestId); - return; - } - Q_ASSERT(!std::any_of(std::cbegin(d->hydrationJobs), std::cend(d->hydrationJobs), - [=](HydrationJob *job) { return job->requestId() == context.requestId || job->localFilePathAbs() == context.path; })); - auto job = new HydrationJob(context); - job->setAccount(params().account); - job->setRemoteSyncRootPath(params().baseUrl()); - job->setLocalRoot(params().filesystemPath); - job->setJournal(params().journal); - job->setRemoteFilePathRel(record.path()); - job->setRecord(std::move(record)); - connect(job, &HydrationJob::finished, this, &VfsCfApi::onHydrationJobFinished); - d->hydrationJobs.insert(context.requestId, job); - job->start(); - Q_EMIT hydrationRequestReady(context.requestId); -} - -void VfsCfApi::onHydrationJobFinished(HydrationJob *job) -{ - Q_ASSERT(findHydrationJob(job->requestId())); - qCInfo(lcCfApi) << u"Hydration job finished" << job->requestId() << job->localFilePathAbs() << job->status(); - Q_EMIT hydrationRequestFinished(job->requestId()); - if (!job->errorString().isEmpty()) { - qCWarning(lcCfApi) << job->errorString(); - } -} - -HydrationJob::Status VfsCfApi::finalizeHydrationJob(int64_t requestId) -{ - // Find matching hydration job for request id - if (const auto hydrationJob = findHydrationJob(requestId)) { - qCDebug(lcCfApi) << u"Finalize hydration job" << hydrationJob->context(); - hydrationJob->finalize(this); - d->hydrationJobs.take(hydrationJob->requestId()); - hydrationJob->deleteLater(); - return hydrationJob->status(); - } - qCCritical(lcCfApi) << u"Failed to finalize hydration job" << requestId << u". Job not found."; - return HydrationJob::Status::Error; -} - } // namespace OCC diff --git a/src/plugins/vfs/cfapi/vfs_cfapi.h b/src/plugins/vfs/cfapi/vfs_cfapi.h index 263656f23b..8309813ebd 100644 --- a/src/plugins/vfs/cfapi/vfs_cfapi.h +++ b/src/plugins/vfs/cfapi/vfs_cfapi.h @@ -40,31 +40,18 @@ class VfsCfApi : public Vfs bool setPinState(const QString &folderPath, PinState state) override; Optional pinState(const QString &folderPath) override; AvailabilityResult availability(const QString &folderPath) override; - - void cancelHydration(const OCC::CfApiWrapper::CallBackContext &context); - - HydrationJob::Status finalizeHydrationJob(int64_t requestId); + bool handleXAttrChange(const QSet &) override; LocalInfo statTypeVirtualFile(const std::filesystem::directory_entry &path, ItemType type) override; public Q_SLOTS: - void requestHydration(const CfApiWrapper::CallBackContext &context, qint64 requestedFileSize); void fileStatusChanged(const QString &systemFileName, OCC::SyncFileStatus fileStatus) override; -Q_SIGNALS: - void hydrationRequestReady(int64_t requestId); - void hydrationRequestFailed(int64_t requestId); - void hydrationRequestFinished(int64_t requestId); - protected: Result updateMetadata(const SyncFileItem &, const QString &, const QString &) override; void startImpl(const VfsSetupParams ¶ms) override; private: - void scheduleHydrationJob(const OCC::CfApiWrapper::CallBackContext &context, SyncJournalFileRecord &&record); - void onHydrationJobFinished(HydrationJob *job); - HydrationJob *findHydrationJob(int64_t requestId) const; - QScopedPointer d; }; diff --git a/src/plugins/vfs/xattr/CMakeLists.txt b/src/plugins/vfs/xattr/CMakeLists.txt new file mode 100644 index 0000000000..c035267321 --- /dev/null +++ b/src/plugins/vfs/xattr/CMakeLists.txt @@ -0,0 +1,6 @@ + add_vfs_plugin(NAME xattr + SRC + vfs_xattr.cpp + ) + + diff --git a/src/plugins/vfs/xattr/vfs_xattr.cpp b/src/plugins/vfs/xattr/vfs_xattr.cpp new file mode 100644 index 0000000000..2993939ef9 --- /dev/null +++ b/src/plugins/vfs/xattr/vfs_xattr.cpp @@ -0,0 +1,470 @@ +/* + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2025 OpenCloud GmbH and OpenCloud contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "vfs_xattr.h" + +#include "syncfileitem.h" +#include "filesystem.h" +#include "common/syncjournaldb.h" + +#include +#include +#include +#include + +#include + +Q_LOGGING_CATEGORY(lcVfsXAttr, "sync.vfs.xattr", QtInfoMsg) + +QDebug operator<<(QDebug debug, const OCC::CallBackContext &context) +{ + QDebugStateSaver saver(debug); + debug.setAutoInsertSpaces(false); + debug << u"cfapiCallback(" << context.path << u", " << context.requestHexId(); + for (const auto &[k, v] : context.extraArgs.asKeyValueRange()) { + debug << u", "; + debug.noquote() << k << u"="; + debug.quote() << v; + }; + debug << u")"; + return debug.maybeSpace(); +} + +namespace { +std::atomic id{1}; + +int requestId() { + return id++; +} + +} + +namespace xattr { +constexpr auto ownerXAttrName = "user.openvfs.owner"; +constexpr auto etagXAttrName = "user.openvfs.etag"; +constexpr auto fileidXAttrName = "user.openvfs.fileid"; +constexpr auto modtimeXAttrName = "user.openvfs.modtime"; +constexpr auto fileSizeXAttrName = "user.openvfs.fsize"; +constexpr auto actionXAttrName = "user.openvfs.action"; +constexpr auto stateXAttrName = "user.openvfs.state"; +constexpr auto pinstateXAttrName = "user.openvfs.pinstate"; + +OCC::Optional get(const QByteArray &path, const QByteArray &name) +{ + QByteArray result(512, Qt::Initialization::Uninitialized); + auto count = getxattr(path.constData(), name.constData(), result.data(), result.size()); + if (count > 0) { + // xattr is special. It does not store C-Strings, but blobs. + // So it needs to be checked, if a trailing \0 was added when writing + // (as this software does) or not as the standard setfattr-tool + // the following will handle both cases correctly. + if (result[count-1] == '\0') { + count--; + } + result.truncate(count); + return result; + } else { + return {}; + } +} + +bool set(const QByteArray &path, const QByteArray &name, const QByteArray &value) +{ + const auto returnCode = setxattr(path.constData(), name.constData(), value.constData(), value.size()+1, 0); + return returnCode == 0; +} +} + +namespace OCC { + +using namespace xattr; + +VfsXAttr::VfsXAttr(QObject *parent) + : Vfs(parent) +{ +} + +VfsXAttr::~VfsXAttr() = default; + +Vfs::Mode VfsXAttr::mode() const +{ + return XAttr; +} + +void VfsXAttr::startImpl(const VfsSetupParams &) +{ + qCDebug(lcVfsXAttr(), "Start XAttr VFS"); + + Q_EMIT started(); +} + +void VfsXAttr::stop() +{ +} + +void VfsXAttr::unregisterFolder() +{ +} + +bool VfsXAttr::socketApiPinStateActionsShown() const +{ + return true; +} + +QByteArray VfsXAttr::xattrOwnerString() const +{ + auto s = QByteArray(APPLICATION_EXECUTABLE); + s.append(":"); + s.append(_setupParams->account->uuid().toByteArray(QUuid::WithoutBraces)); + return s; +} + +PlaceHolderAttribs VfsXAttr::placeHolderAttributes(const QString& path) +{ + PlaceHolderAttribs attribs; + + // lambda to handle the Optional return val of xattrGet + auto xattr = [](const QByteArray& p, const QByteArray& name) { + const auto value = xattr::get(p, name); + if (value) { + return *value; + } else { + return QByteArray(); + } + }; + + const auto p = path.toUtf8(); + + attribs._owner = xattr(p, ownerXAttrName); + if (attribs._owner.isEmpty()) { + // lets claim it + attribs._owner = xattrOwnerString(); + } else { + if (attribs._owner != xattrOwnerString()) { + qCDebug(lcVfsXAttr) << "XAttributes not from our instance"; + attribs._owner.clear(); + return attribs; + } + } + + attribs._etag = QString::fromUtf8(xattr(p, etagXAttrName)); + attribs._fileId = xattr(p, fileidXAttrName); + + const QByteArray& tt = xattr(p, modtimeXAttrName); + attribs._modtime = tt.toLongLong(); + + attribs._action = xattr(p, actionXAttrName); + attribs._size = xattr(p, fileSizeXAttrName).toLongLong(); + attribs._state = xattr(p, stateXAttrName); + attribs._pinState = xattr(p, pinstateXAttrName); + + return attribs; +} + +OCC::Result VfsXAttr::addPlaceholderAttribute(const QString &path, const QByteArray& name, const QByteArray& value) +{ + const PlaceHolderAttribs attribs = placeHolderAttributes(path); + + if (! attribs.validOwner()) { + return QStringLiteral("Can not overwrite attributes - not our placeholder"); + } + + // FIXME: this always sets the name, can be optimized + auto success = xattr::set(path.toUtf8(), ownerXAttrName, xattrOwnerString()); + if (!success) { + return QStringLiteral("Failed to set the extended attribute for owner"); + } + + if (!name.isEmpty()) { + auto success = xattr::set(path.toUtf8(), name, value); + if (!success) { + return QStringLiteral("Failed to set the extended attribute"); + } + } + + return {}; +} + +OCC::Result VfsXAttr::updateMetadata(const SyncFileItem &syncItem, const QString &filePath, const QString &replacesFile) +{ + const auto localPath = QDir::toNativeSeparators(filePath); + const auto replacesPath = QDir::toNativeSeparators(replacesFile); + + qCDebug(lcVfsXAttr()) << localPath; + + PlaceHolderAttribs attribs = placeHolderAttributes(localPath); + OCC::Vfs::ConvertToPlaceholderResult res{OCC::Vfs::ConvertToPlaceholderResult::Ok}; + + if (attribs.validOwner() && attribs.state().isEmpty()) { // No status + // There is no state, so it is a normal, hydrated file + } + + if (syncItem._type == ItemTypeVirtualFileDehydration) { // + addPlaceholderAttribute(localPath, actionXAttrName, "dehydrate"); + // FIXME: Error handling + auto r = createPlaceholder(syncItem); + if (!r) { + res = OCC::Vfs::ConvertToPlaceholderResult::Locked; + } + + } else if (syncItem._type == ItemTypeVirtualFileDownload) { + addPlaceholderAttribute(localPath, actionXAttrName, "hydrate"); + // start to download? FIXME + } else if (syncItem._type == ItemTypeVirtualFile) { + FileSystem::setModTime(localPath, syncItem._modtime); + + // FIXME only write attribs if they're different, and/or all together + addPlaceholderAttribute(localPath, fileSizeXAttrName, QByteArray::number(syncItem._size)); + addPlaceholderAttribute(localPath, stateXAttrName, "dehydrated"); + addPlaceholderAttribute(localPath, fileidXAttrName, syncItem._fileId); + addPlaceholderAttribute(localPath, etagXAttrName, syncItem._etag.toUtf8()); + } else { + // FIXME anything to check for other types? + qCDebug(lcVfsXAttr) << "Unexpected syncItem Type"; + } + + // FIXME Errorhandling + return res; +} + +Result VfsXAttr::createPlaceholder(const SyncFileItem &item) +{ + if (item._modtime <= 0) { + return {tr("Error updating metadata due to invalid modification time")}; + } + + const auto path = QDir::toNativeSeparators(params().filesystemPath + item.localName()); + + qCDebug(lcVfsXAttr()) << path; + + QFile file(path); + // FIXME: Check to not overwrite an existing file + // if (file.exists() && file.size() > 1 + // && !FileSystem::verifyFileUnchanged(path, item._size, item._modtime)) { + // return QStringLiteral("Cannot create a placeholder because a file with the placeholder name already exist"); + // } + + if (!file.open(QFile::ReadWrite | QFile::Truncate)) { + return file.errorString(); + } + + file.write(" "); + file.close(); + + /* + * Only write the state and the executor, the rest is added in the updateMetadata() method + */ + addPlaceholderAttribute(path, stateXAttrName, "dehydrated"); + + + // Ensure the pin state isn't contradictory + const auto pin = pinState(item.localName()); + if (pin && *pin == PinState::AlwaysLocal) { + setPinState(item._renameTarget, PinState::Unspecified); + } + return {}; +} + + +OCC::Result VfsXAttr::convertToPlaceholder( + const QString &path, time_t modtime, qint64 size, const QByteArray &fileId, const QString &replacesPath) +{ + Q_UNUSED(modtime) + Q_UNUSED(size) + Q_UNUSED(fileId) + Q_UNUSED(replacesPath) + + // Nothing necessary - no idea why, taken from previews... + qCDebug(lcVfsXAttr) << "empty function returning ok, DOUBLECHECK" << path ; + return {ConvertToPlaceholderResult::Ok}; +} + +bool VfsXAttr::handleAction(const QString& path, const PlaceHolderAttribs& attribs) +{ + bool re{false}; + + const auto sendTransferError = [=] { + qCWarning(lcVfsXAttr) << u"Transfer ERROR detected"; + }; + + const auto sendTransferInfo = [=](qint64 size) { + qCInfo(lcVfsXAttr) << u"Received" << size << u"bytes"; + }; + + if (attribs.action() == QByteArrayLiteral("hydrate")) { + qCInfo(lcVfsXAttr) << u"Received request to hydrate"; + const auto root = QDir::toNativeSeparators(params().filesystemPath); + Q_ASSERT(path.startsWith(root)); + + SyncJournalFileRecord record; + params().journal->getFileRecordsByFileId(attribs.fileId(), [&record](const auto &r) { + Q_ASSERT(!record.isValid()); + record = r; + }); + if (!record.isValid()) { + qCInfo(lcVfsXAttr) << u"Couldn't hydrate, did not find file in db"; + Q_ASSERT(false); // how did we end up here if it's not a cloud file + Q_EMIT hydrationRequestFailed(-1); + Q_EMIT needSync(); + return false; + } + + bool isNotVirtualFileFailure = false; + if (!record.isVirtualFile()) { + if (isDehydratedPlaceholder(path)) { + qCWarning(lcVfsXAttr) << u"Hydration requested for a placeholder file that is incorrectly not marked as a virtual file in the local database. " + u"Attempting to correct this inconsistency..."; + auto item = SyncFileItem::fromSyncJournalFileRecord(record); + item->_type = ItemTypeVirtualFileDownload; + isNotVirtualFileFailure = !params().journal->setFileRecord(SyncJournalFileRecord::fromSyncFileItem(*item)); + } else { + isNotVirtualFileFailure = true; + } + } + if (isNotVirtualFileFailure) { + qCWarning(lcVfsXAttr) << u"Couldn't hydrate, the file is not virtual"; + Q_ASSERT(false); // this should not happen + Q_EMIT hydrationRequestFailed(-1); + Q_EMIT needSync(); + return false; + } + + OCC::CallBackContext context{.vfs = this, + .path = path, + .requestId = requestId(), + .fileId = attribs.fileId(), + .extraArgs = {}}; + // .extraArgs = std::move(extraArgs)}; + // All good, let's hydrate now + + sendTransferInfo(0); + + OCC::HydrationJob::Status hydrationJobResult = OCC::HydrationJob::Status::Success; + + if (hydrationJobResult != OCC::HydrationJob::Status::Success) { + sendTransferError(); + } + } + + return re; +} + +bool VfsXAttr::needsMetadataUpdate(const SyncFileItem &) +{ + qCDebug(lcVfsXAttr()) << "returns false by default DOUBLECHECK"; + return false; +} + +bool VfsXAttr::isDehydratedPlaceholder(const QString &filePath) +{ + const auto fi = QFileInfo(filePath); + if (fi.exists()) { + const auto attribs = placeHolderAttributes(filePath); + return (attribs.validOwner() && + attribs.state() == QByteArrayLiteral("dehydrated")); + } + return false; +} + +LocalInfo VfsXAttr::statTypeVirtualFile(const std::filesystem::directory_entry &path, ItemType type) +{ + if (type == ItemTypeFile) { + const QString p = QString::fromUtf8(path.path().c_str()); //FIXME? + qCDebug(lcVfsXAttr()) << p; + + auto attribs = placeHolderAttributes(p); + if (attribs.validOwner()) { + bool shouldDownload{false}; + if (attribs.pinState() == QByteArrayLiteral("alwayslocal")) { + shouldDownload = true; + } + + // const auto shouldDownload = pin && (*pin == PinState::AlwaysLocal); + if (shouldDownload) { + type = ItemTypeVirtualFileDownload; + } else { + type = ItemTypeVirtualFile; + } + } + } + + return LocalInfo(path, type); +} + +bool VfsXAttr::setPinState(const QString &folderPath, PinState state) +{ + qCDebug(lcVfsXAttr()) << folderPath << state; + auto stateStr = Utility::enumToDisplayName(state); + auto res = addPlaceholderAttribute(folderPath, pinstateXAttrName, stateStr.toUtf8()); + if (!res) { + qCDebug(lcVfsXAttr()) << "Failed to set pin state"; + return false; + } + return true; +} + +Optional VfsXAttr::pinState(const QString &folderPath) +{ + qCDebug(lcVfsXAttr()) << folderPath; + + PlaceHolderAttribs attribs = placeHolderAttributes(folderPath); + + PinState pState{PinState::Unspecified}; + if (attribs.validOwner()) { + const QString pin = QString::fromUtf8(attribs.pinState()); + + if (pin == Utility::enumToDisplayName(PinState::AlwaysLocal)) { + pState = PinState::AlwaysLocal; + } else if (pin == Utility::enumToDisplayName(PinState::Excluded)) { + pState = PinState::Excluded; + } else if (pin.isEmpty() || pin == Utility::enumToDisplayName(PinState::Inherited)) { + pState = PinState::Inherited; + } else if (pin == Utility::enumToDisplayName(PinState::OnlineOnly)) { + pState = PinState::OnlineOnly; + } + } + + return pState; +} + +Vfs::AvailabilityResult VfsXAttr::availability(const QString &folderPath) +{ + qCDebug(lcVfsXAttr()) << folderPath; + + const auto basePinState = pinState(folderPath); + + if (basePinState) { + switch (*basePinState) { + case OCC::PinState::AlwaysLocal: + return VfsItemAvailability::AlwaysLocal; + break; + case OCC::PinState::Inherited: + break; + case OCC::PinState::OnlineOnly: + return VfsItemAvailability::OnlineOnly; + break; + case OCC::PinState::Unspecified: + break; + case OCC::PinState::Excluded: + break; + }; + return VfsItemAvailability::Mixed; + } else { + return AvailabilityError::NoSuchItem; + } +} + +void VfsXAttr::fileStatusChanged(const QString& systemFileName, SyncFileStatus fileStatus) +{ + if (fileStatus.tag() == SyncFileStatus::StatusExcluded) { + setPinState(systemFileName, PinState::Excluded); + return; + } + + qCDebug(lcVfsXAttr()) << systemFileName << fileStatus; +} + +} // namespace OCC diff --git a/src/plugins/vfs/xattr/vfs_xattr.h b/src/plugins/vfs/xattr/vfs_xattr.h new file mode 100644 index 0000000000..9ffa393521 --- /dev/null +++ b/src/plugins/vfs/xattr/vfs_xattr.h @@ -0,0 +1,105 @@ +/* + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2025 OpenCloud GmbH and OpenCloud contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ +#pragma once + +#include +#include + +#include "common/vfs.h" +#include "common/plugin.h" +#include "common/result.h" + +#include "config.h" + +namespace xattr { + +struct PlaceHolderAttribs { +public: + qint64 size() const { return _size; } + QByteArray fileId() const { return _fileId; } + time_t modTime() const {return _modtime; } + QString eTag() const { return _etag; } + QByteArray pinState() const { return _pinState; } + QByteArray action() const { return _action; } + QByteArray state() const { return _state; } + QByteArray owner() const { return _owner; } + + // the owner must not be empty but set to the ownerString, that consists + // of the app name and an instance ID + // If no xattrs are set at all, the method @placeHolderAttributes sets it + // to our name and claims the space + + // Always check if we're the valid owner before accessing the xattrs. + bool validOwner() const { return !_owner.isEmpty(); } + + qint64 _size; + QByteArray _fileId; + time_t _modtime; + QString _etag; + QByteArray _owner; + QByteArray _pinState; + QByteArray _action; + QByteArray _state; + +}; +} + +namespace OCC { + +using namespace xattr; + +class VfsXAttr : public Vfs +{ + Q_OBJECT + +public: + explicit VfsXAttr(QObject *parent = nullptr); + ~VfsXAttr() override; + + [[nodiscard]] Mode mode() const override; + + void stop() override; + void unregisterFolder() override; + + [[nodiscard]] bool socketApiPinStateActionsShown() const override; + + Result updateMetadata(const SyncFileItem &syncItem, const QString &filePath, const QString &replacesFile) override; + // [[nodiscard]] bool isPlaceHolderInSync(const QString &filePath) const override { Q_UNUSED(filePath) return true; } + + Result createPlaceholder(const SyncFileItem &item) override; + OCC::Result convertToPlaceholder( + const QString &path, time_t modtime, qint64 size, const QByteArray &fileId, const QString &replacesPath); + + bool handleAction(const QString& path, const PlaceHolderAttribs &attribs); + bool needsMetadataUpdate(const SyncFileItem &item) override; + bool isDehydratedPlaceholder(const QString &filePath) override; + LocalInfo statTypeVirtualFile(const std::filesystem::directory_entry &path, ItemType type) override; + + bool setPinState(const QString &folderPath, PinState state) override; + Optional pinState(const QString &folderPath) override; + AvailabilityResult availability(const QString &folderPath) override; + +public Q_SLOTS: + void fileStatusChanged(const QString &systemFileName, OCC::SyncFileStatus fileStatus) override; + +protected: + void startImpl(const VfsSetupParams ¶ms) override; + +private: + QByteArray xattrOwnerString() const; + PlaceHolderAttribs placeHolderAttributes(const QString& path); + OCC::Result addPlaceholderAttribute(const QString &path, const QByteArray &name = {}, const QByteArray &val = {}); + +}; + +class XattrVfsPluginFactory : public QObject, public DefaultPluginFactory +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "eu.opencloud.PluginFactory" FILE "libsync/common/vfspluginmetadata.json") + Q_INTERFACES(OCC::PluginFactory) +}; + +} // namespace OCC