From 3e39a89d4767aff97f3a93dca193e8f5eb295225 Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Wed, 14 May 2025 09:30:07 +0200 Subject: [PATCH 1/2] feat(dbus): use KF6 DBusAddons to offer DBus API should help to build end-to-end integration tests with desktop files client and a Nextcloud server Signed-off-by: Matthieu Gallien --- CMakeLists.txt | 4 ++++ NEXTCLOUD.cmake | 2 +- config.h.in | 2 ++ src/gui/CMakeLists.txt | 4 ++++ src/gui/main.cpp | 8 ++++++++ 5 files changed, 19 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e9962ebe55a99..1584995dfbdf9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -257,6 +257,10 @@ if(BUILD_CLIENT) endif() endif() +if (LINUX) + find_package(KF6DBusAddons) +endif() + option(BUILD_WITH_WEBENGINE "BUILD_WITH_WEBENGINE" ON) if (BUILD_WITH_WEBENGINE) find_package(Qt${QT_VERSION_MAJOR}WebEngineCore ${REQUIRED_QT_VERSION} CONFIG QUIET) diff --git a/NEXTCLOUD.cmake b/NEXTCLOUD.cmake index 054b303d80fc1..dba98af3739f9 100644 --- a/NEXTCLOUD.cmake +++ b/NEXTCLOUD.cmake @@ -30,7 +30,7 @@ endif() set( APPLICATION_ICON_SET "SVG" ) set( APPLICATION_SERVER_URL "" CACHE STRING "URL for the server to use. If entered, the UI field will be pre-filled with it" ) set( APPLICATION_SERVER_URL_ENFORCE ON ) # If set and APPLICATION_SERVER_URL is defined, the server can only connect to the pre-defined URL -set( APPLICATION_REV_DOMAIN "com.nextcloud.desktopclient" ) +set( APPLICATION_REV_DOMAIN "nextcloudgmbh.com" ) set( APPLICATION_VIRTUALFILE_SUFFIX "nextcloud" CACHE STRING "Virtual file suffix (not including the .)") set( APPLICATION_OCSP_STAPLING_ENABLED OFF ) set( APPLICATION_FORBID_BAD_SSL OFF ) diff --git a/config.h.in b/config.h.in index 1f8ec57476bf9..f131699fea2dd 100644 --- a/config.h.in +++ b/config.h.in @@ -63,6 +63,8 @@ #cmakedefine01 NEXTCLOUD_DEV +#cmakedefine01 KF6DBusAddons_FOUND + #cmakedefine WITH_WEBENGINE #cmakedefine01 CLIENTSIDEENCRYPTION_ENFORCE_USB_TOKEN diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index a92ee0424f64e..c0b3e3302462b 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -731,6 +731,10 @@ install(TARGETS nextcloud BUNDLE DESTINATION "." ) +if(KF6DBusAddons_FOUND) + target_link_libraries(nextcloud PRIVATE KF6::DBusAddons) +endif() + if (WIN32) install(FILES $ DESTINATION bin OPTIONAL) endif() diff --git a/src/gui/main.cpp b/src/gui/main.cpp index 414a8b8f10df9..199d1937f0548 100644 --- a/src/gui/main.cpp +++ b/src/gui/main.cpp @@ -23,6 +23,10 @@ #include "updater/updater.h" #endif +#if defined KF6DBusAddons_FOUND && KF6DBusAddons_FOUND +#include +#endif + #include #include #include @@ -155,6 +159,10 @@ int main(int argc, char **argv) return 0; } +#if defined KF6DBusAddons_FOUND && KF6DBusAddons_FOUND + auto dbusService = KDBusService{KDBusService::StartupOption::NoExitOnFailure | KDBusService::StartupOption::Unique}; +#endif + // We can't call isSystemTrayAvailable with appmenu-qt5 begause it hides the systemtray // (issue #4693) if (qgetenv("QT_QPA_PLATFORMTHEME") != "appmenu-qt5") From e22fc51d3e53d552fc03890db16291ba6c130428 Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Wed, 14 May 2025 10:32:31 +0200 Subject: [PATCH 2/2] feat(vfs): skeleton of a new linux VFS plugin should allow building a new linux VFS plugin to integrate within desktop environment via a DBus API for reuse in KIO or GIO subsystems Signed-off-by: Matthieu Gallien --- src/common/vfs.cpp | 8 + src/common/vfs.h | 1 + src/gui/wizard/owncloudadvancedsetuppage.cpp | 2 + src/gui/wizard/owncloudwizard.cpp | 3 + src/libsync/vfs/CMakeLists.txt | 2 +- src/libsync/vfs/dbusapi/CMakeLists.txt | 46 +++ src/libsync/vfs/dbusapi/hydrationjob.cpp | 368 +++++++++++++++++++ src/libsync/vfs/dbusapi/hydrationjob.h | 120 ++++++ src/libsync/vfs/dbusapi/vfs_dbusapi.cpp | 230 ++++++++++++ src/libsync/vfs/dbusapi/vfs_dbusapi.h | 90 +++++ 10 files changed, 869 insertions(+), 1 deletion(-) create mode 100644 src/libsync/vfs/dbusapi/CMakeLists.txt create mode 100644 src/libsync/vfs/dbusapi/hydrationjob.cpp create mode 100644 src/libsync/vfs/dbusapi/hydrationjob.h create mode 100644 src/libsync/vfs/dbusapi/vfs_dbusapi.cpp create mode 100644 src/libsync/vfs/dbusapi/vfs_dbusapi.h diff --git a/src/common/vfs.cpp b/src/common/vfs.cpp index 9ae456aa734bd..e6b5c58ec2bf1 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 DBusApi: + return QStringLiteral("dbusapi"); } return QStringLiteral("off"); } @@ -50,6 +52,8 @@ Optional Vfs::modeFromString(const QString &str) return WithSuffix; } else if (str == QLatin1String("wincfapi")) { return WindowsCfApi; + } else if (str == QLatin1String("dbusapi")) { + return DBusApi; } return {}; } @@ -195,6 +199,10 @@ Vfs::Mode OCC::bestAvailableVfsMode() return Vfs::WindowsCfApi; } + if (isVfsPluginAvailable(Vfs::DBusApi)) { + return Vfs::DBusApi; + } + if (isVfsPluginAvailable(Vfs::WithSuffix)) { return Vfs::WithSuffix; } diff --git a/src/common/vfs.h b/src/common/vfs.h index 183c3013f9595..c5838fcb4df67 100644 --- a/src/common/vfs.h +++ b/src/common/vfs.h @@ -96,6 +96,7 @@ class OCSYNC_EXPORT Vfs : public QObject WithSuffix, WindowsCfApi, XAttr, + DBusApi, }; Q_ENUM(Mode) enum class ConvertToPlaceholderResult { diff --git a/src/gui/wizard/owncloudadvancedsetuppage.cpp b/src/gui/wizard/owncloudadvancedsetuppage.cpp index aa18cab4555f1..02cf711812568 100644 --- a/src/gui/wizard/owncloudadvancedsetuppage.cpp +++ b/src/gui/wizard/owncloudadvancedsetuppage.cpp @@ -107,6 +107,8 @@ OwncloudAdvancedSetupPage::OwncloudAdvancedSetupPage(OwncloudWizard *wizard) bestAvailableVfsMode() == Vfs::WindowsCfApi #elif defined(BUILD_FILE_PROVIDER_MODULE) Mac::FileProvider::fileProviderAvailable() +#elif defined Q_OS_LINUX + bestAvailableVfsMode() == Vfs::DBusApi #else false #endif diff --git a/src/gui/wizard/owncloudwizard.cpp b/src/gui/wizard/owncloudwizard.cpp index 536fb37be1110..bf26ae8005065 100644 --- a/src/gui/wizard/owncloudwizard.cpp +++ b/src/gui/wizard/owncloudwizard.cpp @@ -476,6 +476,9 @@ void OwncloudWizard::askExperimentalVirtualFilesFeature(QWidget *receiver, const case Vfs::WindowsCfApi: callback(true); return; + case Vfs::DBusApi: + callback(true); + return; case Vfs::WithSuffix: msgBox = new QMessageBox( QMessageBox::Warning, diff --git a/src/libsync/vfs/CMakeLists.txt b/src/libsync/vfs/CMakeLists.txt index de48e10ee9bd6..786ac0a21f593 100644 --- a/src/libsync/vfs/CMakeLists.txt +++ b/src/libsync/vfs/CMakeLists.txt @@ -5,7 +5,7 @@ # 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") +list(APPEND VIRTUAL_FILE_SYSTEM_PLUGINS "suffix" "cfapi" "xattr" "dbusapi") message("list of plugins ${VIRTUAL_FILE_SYSTEM_PLUGINS}") diff --git a/src/libsync/vfs/dbusapi/CMakeLists.txt b/src/libsync/vfs/dbusapi/CMakeLists.txt new file mode 100644 index 0000000000000..658e0bf626724 --- /dev/null +++ b/src/libsync/vfs/dbusapi/CMakeLists.txt @@ -0,0 +1,46 @@ +# SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: GPL-2.0-or-later + +if (LINUX) + add_library(nextcloudsync_vfs_dbus SHARED + hydrationjob.h + hydrationjob.cpp + vfs_dbusapi.h + vfs_dbusapi.cpp + ) + + target_link_libraries(nextcloudsync_vfs_dbus PRIVATE + Nextcloud::sync + ) + + set_target_properties(nextcloudsync_vfs_dbus + PROPERTIES + LIBRARY_OUTPUT_DIRECTORY + ${BIN_OUTPUT_DIRECTORY} + RUNTIME_OUTPUT_DIRECTORY + ${BIN_OUTPUT_DIRECTORY} + PREFIX + "" + AUTOMOC + TRUE + LIBRARY_OUTPUT_NAME + ${APPLICATION_EXECUTABLE}sync_vfs_dbus + RUNTIME_OUTPUT_NAME + ${APPLICATION_EXECUTABLE}sync_vfs_dbus + ) + + target_include_directories(nextcloudsync_vfs_dbus BEFORE PUBLIC ${CMAKE_CURRENT_BINARY_DIR} INTERFACE ${CMAKE_BINARY_DIR}) + + set(vfs_installdir "${PLUGINDIR}") + + generate_export_header(nextcloudsync_vfs_dbus + BASE_NAME nextcloudsync_vfs_dbus + EXPORT_MACRO_NAME NEXTCLOUD_CFAPI_EXPORT + EXPORT_FILE_NAME cfapiexport.h + ) + + install(TARGETS nextcloudsync_vfs_dbus + LIBRARY DESTINATION "${vfs_installdir}" + RUNTIME DESTINATION "${vfs_installdir}" + ) +endif() diff --git a/src/libsync/vfs/dbusapi/hydrationjob.cpp b/src/libsync/vfs/dbusapi/hydrationjob.cpp new file mode 100644 index 0000000000000..05800f631c81c --- /dev/null +++ b/src/libsync/vfs/dbusapi/hydrationjob.cpp @@ -0,0 +1,368 @@ +/* + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "hydrationjob.h" + +#include "common/syncjournaldb.h" +#include "propagatedownload.h" +#include "vfs/cfapi/vfs_cfapi.h" +#include +#include "encryptedfoldermetadatahandler.h" +#include "foldermetadata.h" + +#include "filesystem.h" + +#include +#include + +Q_LOGGING_CATEGORY(lcHydration, "nextcloud.sync.vfs.hydrationjob", QtInfoMsg) + +OCC::HydrationJob::HydrationJob(QObject *parent) + : QObject(parent) +{ +} + +OCC::HydrationJob::~HydrationJob() = default; + +OCC::AccountPtr OCC::HydrationJob::account() const +{ + return _account; +} + +void OCC::HydrationJob::setAccount(const AccountPtr &account) +{ + _account = account; +} + +QString OCC::HydrationJob::remoteSyncRootPath() const +{ + return _remoteSyncRootPath; +} + +void OCC::HydrationJob::setRemoteSyncRootPath(const QString &path) +{ + _remoteSyncRootPath = Utility::noLeadingSlashPath(path); +} + +QString OCC::HydrationJob::localPath() const +{ + return _localPath; +} + +void OCC::HydrationJob::setLocalPath(const QString &localPath) +{ + _localPath = localPath; +} + +OCC::SyncJournalDb *OCC::HydrationJob::journal() const +{ + return _journal; +} + +void OCC::HydrationJob::setJournal(SyncJournalDb *journal) +{ + _journal = journal; +} + +QString OCC::HydrationJob::requestId() const +{ + return _requestId; +} + +void OCC::HydrationJob::setRequestId(const QString &requestId) +{ + _requestId = requestId; +} + +QString OCC::HydrationJob::folderPath() const +{ + return _folderPath; +} + +void OCC::HydrationJob::setFolderPath(const QString &folderPath) +{ + _folderPath = folderPath; +} + +bool OCC::HydrationJob::isEncryptedFile() const +{ + return _isEncryptedFile; +} + +void OCC::HydrationJob::setIsEncryptedFile(bool isEncrypted) +{ + _isEncryptedFile = isEncrypted; +} + +QString OCC::HydrationJob::e2eMangledName() const +{ + return _e2eMangledName; +} + +void OCC::HydrationJob::setE2eMangledName(const QString &e2eMangledName) +{ + _e2eMangledName = e2eMangledName; +} + +OCC::HydrationJob::Status OCC::HydrationJob::status() const +{ + return _status; +} + +int OCC::HydrationJob::errorCode() const +{ + return _errorCode; +} + +int OCC::HydrationJob::statusCode() const +{ + return _statusCode; +} + +QString OCC::HydrationJob::errorString() const +{ + return _errorString; +} + +void OCC::HydrationJob::start() +{ + Q_ASSERT(_account); + Q_ASSERT(_journal); + Q_ASSERT(!_remoteSyncRootPath.isEmpty() && !_localPath.isEmpty()); + Q_ASSERT(!_requestId.isEmpty() && !_folderPath.isEmpty()); + + Q_ASSERT(_remoteSyncRootPath.endsWith('/')); + Q_ASSERT(_localPath.endsWith('/')); + Q_ASSERT(!_folderPath.startsWith('/')); + + const auto startServer = [this](const QString &serverName) -> QLocalServer * { + const auto server = new QLocalServer(this); + const auto listenResult = server->listen(serverName); + if (!listenResult) { + qCCritical(lcHydration) << "Couldn't get server to listen" << serverName + << _localPath << _folderPath; + if (!_isCancelled) { + emitFinished(Error); + } + return nullptr; + } + qCInfo(lcHydration) << "Server ready, waiting for connections" << serverName + << _localPath << _folderPath; + return server; + }; + + // Start cancellation server + _signalServer = startServer(_requestId + ":cancellation"); + Q_ASSERT(_signalServer); + if (!_signalServer) { + return; + } + connect(_signalServer, &QLocalServer::newConnection, this, &HydrationJob::onCancellationServerNewConnection); + + // Start transfer data server + _transferDataServer = startServer(_requestId); + Q_ASSERT(_transferDataServer); + if (!_transferDataServer) { + return; + } + connect(_transferDataServer, &QLocalServer::newConnection, this, &HydrationJob::onNewConnection); +} + +void OCC::HydrationJob::cancel() +{ + _isCancelled = true; + if (_job) { + _job->cancel(); + } + + if (_signalSocket) { + _signalSocket->write("cancelled"); + _signalSocket->close(); + } + + if (_transferDataSocket) { + _transferDataSocket->close(); + } + emitFinished(Cancelled); +} + +void OCC::HydrationJob::emitFinished(Status status) +{ + _status = status; + if (_signalSocket) { + _signalSocket->close(); + } + + if (status == Success) { + connect(_transferDataSocket, &QLocalSocket::disconnected, this, [=, this] { + _transferDataSocket->close(); + emit finished(this); + }); + _transferDataSocket->disconnectFromServer(); + return; + } + + if (_transferDataSocket) { + _transferDataSocket->close(); + } + + emit finished(this); +} + +void OCC::HydrationJob::onCancellationServerNewConnection() +{ + Q_ASSERT(!_signalSocket); + + qCInfo(lcHydration) << "Got new connection on cancellation server" << _requestId << _folderPath; + _signalSocket = _signalServer->nextPendingConnection(); +} + +void OCC::HydrationJob::onNewConnection() +{ + Q_ASSERT(!_transferDataSocket); + Q_ASSERT(!_job); + + if (isEncryptedFile()) { + handleNewConnectionForEncryptedFile(); + } else { + handleNewConnection(); + } +} + +void OCC::HydrationJob::finalize(OCC::VfsCfApi *vfs) +{ + // Mark the file as hydrated in the sync journal + SyncJournalFileRecord record; + if (!_journal->getFileRecord(_folderPath, &record)) { + qCWarning(lcHydration) << "could not get file from local DB" << _folderPath; + return; + } + Q_ASSERT(record.isValid()); + if (!record.isValid()) { + qCWarning(lcHydration) << "Couldn't find record to update after hydration" << _requestId << _folderPath; + // emitFinished(Error); + return; + } + + if (_isCancelled) { + // Remove placeholder file because there might be already pumped + // some data into it + QFile::remove(_localPath + _folderPath); + // Create a new placeholder file + const auto item = SyncFileItem::fromSyncJournalFileRecord(record); + vfs->createPlaceholder(*item); + return; + } + + switch(_status) { + case Success: + record._type = ItemTypeFile; + break; + case Error: + case Cancelled: + record._type = CSyncEnums::ItemTypeVirtualFile; + break; + }; + + // store the actual size of a file that has been decrypted as we will need its actual size when dehydrating it if requested + record._fileSize = FileSystem::getSize(localPath() + folderPath()); + + const auto result = _journal->setFileRecord(record); + if (!result) { + qCWarning(lcHydration) << "Error when setting the file record to the database" << record._path << result.error(); + } +} + +void OCC::HydrationJob::slotFetchMetadataJobFinished(int statusCode, const QString &message) +{ + if (statusCode != 200) { + qCCritical(lcHydration) << "Failed to find encrypted metadata information of remote file" << e2eMangledName() << message; + emitFinished(Error); + return; + } + + // TODO: the following code is borrowed from PropagateDownloadEncrypted (see HydrationJob::onNewConnection() for explanation of next steps) + qCDebug(lcHydration) << "Metadata Received reading" << e2eMangledName(); + const auto metadata = _encryptedFolderMetadataHandler->folderMetadata(); + if (!metadata->isValid()) { + qCCritical(lcHydration) << "Failed to find encrypted metadata information of a remote file" << e2eMangledName(); + emitFinished(Error); + return; + } + + const auto files = metadata->files(); + const QString encryptedFileExactName = e2eMangledName().section(QLatin1Char('/'), -1); + for (const FolderMetadata::EncryptedFile &file : files) { + if (encryptedFileExactName == file.encryptedFilename) { + qCDebug(lcHydration) << "Found matching encrypted metadata for file, starting download" << _requestId << _folderPath; + _transferDataSocket = _transferDataServer->nextPendingConnection(); + _job = new GETEncryptedFileJob(_account, Utility::trailingSlashPath(_remoteSyncRootPath) + e2eMangledName(), _transferDataSocket, {}, {}, 0, file, this); + + connect(qobject_cast(_job), &GETEncryptedFileJob::finishedSignal, this, &HydrationJob::onGetFinished); + _job->start(); + return; + } + } +} + +void OCC::HydrationJob::onGetFinished() +{ + _errorCode = _job->reply()->error(); + _statusCode = _job->reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + if (_errorCode != 0 || (_statusCode != 200 && _statusCode != 204)) { + _errorString = _job->reply()->errorString(); + } + + if (!_errorString.isEmpty()) { + qCInfo(lcHydration) << "GETFileJob finished" << _requestId << _folderPath << _errorCode << _statusCode << _errorString; + } else { + qCInfo(lcHydration) << "GETFileJob finished" << _requestId << _folderPath; + } + // GETFileJob deletes itself after this signal was handled + _job = nullptr; + if (_isCancelled) { + _errorCode = 0; + _statusCode = 0; + _errorString.clear(); + return; + } + + if (_errorCode) { + emitFinished(Error); + return; + } + emitFinished(Success); +} + +void OCC::HydrationJob::handleNewConnection() +{ + qCInfo(lcHydration) << "Got new connection starting GETFileJob" << _requestId << _folderPath; + _transferDataSocket = _transferDataServer->nextPendingConnection(); + _job = new GETFileJob(_account, Utility::trailingSlashPath(_remoteSyncRootPath) + _folderPath, _transferDataSocket, {}, {}, 0, this); + connect(_job, &GETFileJob::finishedSignal, this, &HydrationJob::onGetFinished); + _job->start(); +} + +void OCC::HydrationJob::handleNewConnectionForEncryptedFile() +{ + // TODO: the following code is borrowed from PropagateDownloadEncrypted (should we factor it out and reuse? YES! Should we do it now? Probably not, as, this would imply modifying PropagateDownloadEncrypted, so we need a separate PR) + qCInfo(lcHydration) << "Got new connection for encrypted file. Getting required info for decryption..."; + const auto remoteFilename = e2eMangledName(); + const QString fullRemotePath = Utility::trailingSlashPath(_remoteSyncRootPath) + remoteFilename; + const auto containingFolderFullRemotePath = fullRemotePath.left(fullRemotePath.lastIndexOf('/')); + + SyncJournalFileRecord rec; + if (!_journal->getRootE2eFolderRecord(Utility::fullRemotePathToRemoteSyncRootRelative(containingFolderFullRemotePath, _remoteSyncRootPath), &rec) || !rec.isValid()) { + emitFinished(Error); + return; + } + _encryptedFolderMetadataHandler.reset( + new EncryptedFolderMetadataHandler(_account, containingFolderFullRemotePath, _remoteSyncRootPath, _journal, rec.path())); + connect(_encryptedFolderMetadataHandler.data(), + &EncryptedFolderMetadataHandler::fetchFinished, + this, + &HydrationJob::slotFetchMetadataJobFinished); + _encryptedFolderMetadataHandler->fetchMetadata(); +} diff --git a/src/libsync/vfs/dbusapi/hydrationjob.h b/src/libsync/vfs/dbusapi/hydrationjob.h new file mode 100644 index 0000000000000..d371e2454c634 --- /dev/null +++ b/src/libsync/vfs/dbusapi/hydrationjob.h @@ -0,0 +1,120 @@ +/* + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ +#pragma once + +#include + +#include "account.h" + +class QLocalServer; +class QLocalSocket; + +namespace OCC { +class EncryptedFolderMetadataHandler; +class GETFileJob; +class SyncJournalDb; +class VfsCfApi; + +namespace EncryptionHelper { + class StreamingDecryptor; +}; + +class HydrationJob : public QObject +{ + Q_OBJECT +public: + enum Status { + Success = 0, + Error, + Cancelled, + }; + Q_ENUM(Status) + + explicit HydrationJob(QObject *parent = nullptr); + + ~HydrationJob() override; + + AccountPtr account() const; + void setAccount(const AccountPtr &account); + + [[nodiscard]] QString remoteSyncRootPath() const; + void setRemoteSyncRootPath(const QString &path); + + QString localPath() const; + void setLocalPath(const QString &localPath); + + SyncJournalDb *journal() const; + void setJournal(SyncJournalDb *journal); + + QString requestId() const; + void setRequestId(const QString &requestId); + + QString folderPath() const; + void setFolderPath(const QString &folderPath); + + bool isEncryptedFile() const; + void setIsEncryptedFile(bool isEncrypted); + + QString e2eMangledName() const; + void setE2eMangledName(const QString &e2eMangledName); + + qint64 fileTotalSize() const; + void setFileTotalSize(qint64 totalSize); + + Status status() const; + + [[nodiscard]] int errorCode() const; + [[nodiscard]] int statusCode() const; + [[nodiscard]] QString errorString() const; + + void start(); + void cancel(); + void finalize(OCC::VfsCfApi *vfs); + +signals: + void finished(HydrationJob *job); + +private slots: + void slotFetchMetadataJobFinished(int statusCode, const QString &message); + +private: + void emitFinished(Status status); + + void onNewConnection(); + void onCancellationServerNewConnection(); + void onGetFinished(); + + void handleNewConnection(); + void handleNewConnectionForEncryptedFile(); + + void startServerAndWaitForConnections(); + + AccountPtr _account; + QString _remoteSyncRootPath; + QString _localPath; + SyncJournalDb *_journal = nullptr; + bool _isCancelled = false; + + QString _requestId; + QString _folderPath; + + bool _isEncryptedFile = false; + QString _e2eMangledName; + + QLocalServer *_transferDataServer = nullptr; + QLocalServer *_signalServer = nullptr; + QLocalSocket *_transferDataSocket = nullptr; + QLocalSocket *_signalSocket = nullptr; + GETFileJob *_job = nullptr; + Status _status = Success; + int _errorCode = 0; + int _statusCode = 0; + QString _errorString; + QString _remoteParentPath; + + QScopedPointer _encryptedFolderMetadataHandler; +}; + +} // namespace OCC diff --git a/src/libsync/vfs/dbusapi/vfs_dbusapi.cpp b/src/libsync/vfs/dbusapi/vfs_dbusapi.cpp new file mode 100644 index 0000000000000..deeb16ac77982 --- /dev/null +++ b/src/libsync/vfs/dbusapi/vfs_dbusapi.cpp @@ -0,0 +1,230 @@ +/* + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "vfs_dbusapi.h" + +#include +#include + +#include "syncfileitem.h" +#include "filesystem.h" +#include "common/filesystembase.h" +#include "common/syncjournaldb.h" +#include "config.h" + +#include + +Q_LOGGING_CATEGORY(lcDBusApi, "nextcloud.sync.vfs.dbusapi", QtInfoMsg) + +namespace OCC { + +class VfsDBusApiPrivate +{ +public: + QList hydrationJobs; +}; + +VfsDBusApi::VfsDBusApi(QObject *parent) + : Vfs(parent) + , d(new VfsDBusApiPrivate) +{ +} + +VfsDBusApi::~VfsDBusApi() = default; + +Vfs::Mode VfsDBusApi::mode() const +{ + return DBusApi; +} + +QString VfsDBusApi::fileSuffix() const +{ + return {}; +} + +void VfsDBusApi::startImpl(const VfsSetupParams ¶ms) +{ +} + +void VfsDBusApi::stop() +{ +} + +void VfsDBusApi::unregisterFolder() +{ +} + +bool VfsDBusApi::socketApiPinStateActionsShown() const +{ + return true; +} + +bool VfsDBusApi::isHydrating() const +{ + return !d->hydrationJobs.isEmpty(); +} + +Result VfsDBusApi::updateMetadata(const QString &filePath, time_t modtime, qint64 size, const QByteArray &fileId) +{ +} + +Result VfsDBusApi::updatePlaceholderMarkInSync(const QString &filePath, const QByteArray &fileId) +{ + return ConvertToPlaceholderResult::Error; +} + +bool VfsDBusApi::isPlaceHolderInSync(const QString &filePath) const +{ + return false; +} + +Result VfsDBusApi::createPlaceholder(const SyncFileItem &item) +{ + return {}; +} + +Result VfsDBusApi::dehydratePlaceholder(const SyncFileItem &item) +{ + return {}; +} + +Result VfsDBusApi::convertToPlaceholder(const QString &filename, const SyncFileItem &item, const QString &replacesFile, UpdateMetadataTypes updateType) +{ + return ConvertToPlaceholderResult::Error; +} + +bool VfsDBusApi::needsMetadataUpdate(const SyncFileItem &item) +{ + return false; +} + +bool VfsDBusApi::isDehydratedPlaceholder(const QString &filePath) +{ + return false; +} + +bool VfsDBusApi::statTypeVirtualFile(csync_file_stat_t *stat, void *statData) +{ + return false; +} + +bool VfsDBusApi::setPinState(const QString &folderPath, PinState state) +{ + qCDebug(lcDBusApi()) << "setPinState" << folderPath << state; + return setPinStateInDb(folderPath, state); +} + +Optional VfsDBusApi::pinState(const QString &folderPath) +{ + return pinStateInDb(folderPath); +} + +Vfs::AvailabilityResult VfsDBusApi::availability(const QString &folderPath, const AvailabilityRecursivity recursiveCheck) +{ + return AvailabilityError::NoSuchItem; +} + +void VfsDBusApi::cancelHydration(const QString &requestId, const QString & /*path*/) +{ +} + +void VfsDBusApi::requestHydration(const QString &requestId, const QString &path) +{ + qCInfo(lcDBusApi) << "Received request to hydrate" << path << requestId; + const auto root = QDir::toNativeSeparators(params().filesystemPath); + Q_ASSERT(path.startsWith(root)); + + const auto relativePath = QDir::fromNativeSeparators(path.mid(root.length())); + const auto journal = params().journal; + + // Set in the database that we should download the file + SyncJournalFileRecord record; + if (!journal->getFileRecord(relativePath, &record) || !record.isValid()) { + qCInfo(lcDBusApi) << "Couldn't hydrate, did not find file in db"; + emit hydrationRequestFailed(requestId); + return; + } + + bool isNotVirtualFileFailure = false; + if (!record.isVirtualFile()) { + if (isDehydratedPlaceholder(path)) { + qCWarning(lcDBusApi) << "Hydration requested for a placeholder file not marked as virtual in local DB. Attempting to fix it..."; + record._type = ItemTypeVirtualFileDownload; + isNotVirtualFileFailure = !journal->setFileRecord(record); + } else { + isNotVirtualFileFailure = true; + } + } + + if (isNotVirtualFileFailure) { + qCWarning(lcDBusApi) << "Couldn't hydrate, the file is not virtual"; + emit hydrationRequestFailed(requestId); + return; + } +} + +void VfsDBusApi::fileStatusChanged(const QString &systemFileName, SyncFileStatus fileStatus) +{ + Q_UNUSED(systemFileName); + Q_UNUSED(fileStatus); +} + +int VfsDBusApi::finalizeHydrationJob(const QString &requestId) +{ + return -1; +} + +VfsDBusApi::HydratationAndPinStates VfsDBusApi::computeRecursiveHydrationAndPinStates(const QString &folderPath, const Optional &basePinState) +{ + Q_ASSERT(!folderPath.endsWith('/')); + const auto fullPath = QString{params().filesystemPath + folderPath}; + QFileInfo info(params().filesystemPath + folderPath); + + if (!FileSystem::fileExists(fullPath)) { + return {}; + } + const auto effectivePin = pinState(folderPath); + const auto pinResult = (!effectivePin && !basePinState) ? Optional() + : (!effectivePin || !basePinState) ? PinState::Inherited + : (*effectivePin == *basePinState) ? *effectivePin + : PinState::Inherited; + + if (FileSystem::isDir(fullPath)) { + const auto dirState = HydratationAndPinStates { + pinResult, + {} + }; + const auto dir = QDir(info.absoluteFilePath()); + Q_ASSERT(dir.exists()); + const auto children = dir.entryList(); + return std::accumulate(std::cbegin(children), std::cend(children), dirState, [=, this](const HydratationAndPinStates ¤tState, const QString &name) { + if (name == QStringLiteral("..") || name == QStringLiteral(".")) { + return currentState; + } + + // if the folderPath.isEmpty() we don't want to end up having path "/example.file" because this will lead to double slash later, when appending to "SyncFolder/" + const auto path = folderPath.isEmpty() ? name : folderPath + '/' + name; + const auto states = computeRecursiveHydrationAndPinStates(path, currentState.pinState); + return HydratationAndPinStates { + states.pinState, + { + states.hydrationStatus.hasHydrated || currentState.hydrationStatus.hasHydrated, + states.hydrationStatus.hasDehydrated || currentState.hydrationStatus.hasDehydrated, + } + }; + }); + } else { // file case + const auto isDehydrated = isDehydratedPlaceholder(info.absoluteFilePath()); + return { + pinResult, + { + !isDehydrated, + isDehydrated + } + }; + } +} + +} // namespace OCC diff --git a/src/libsync/vfs/dbusapi/vfs_dbusapi.h b/src/libsync/vfs/dbusapi/vfs_dbusapi.h new file mode 100644 index 0000000000000..49bdb21a19c73 --- /dev/null +++ b/src/libsync/vfs/dbusapi/vfs_dbusapi.h @@ -0,0 +1,90 @@ +/* + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-2.0-or-later + */ +#pragma once + +#include +#include + +#include "common/vfs.h" +#include "common/plugin.h" + +namespace OCC { +class HydrationJob; +class VfsDBusApiPrivate; +class SyncJournalFileRecord; + +class VfsDBusApi : public Vfs +{ + Q_OBJECT + +public: + explicit VfsDBusApi(QObject *parent = nullptr); + ~VfsDBusApi(); + + Mode mode() const override; + QString fileSuffix() const override; + + void stop() override; + void unregisterFolder() override; + + bool socketApiPinStateActionsShown() const override; + bool isHydrating() const override; + + Result updateMetadata(const QString &filePath, time_t modtime, qint64 size, const QByteArray &fileId) override; + + Result updatePlaceholderMarkInSync(const QString &filePath, const QByteArray &fileId) override; + + [[nodiscard]] bool isPlaceHolderInSync(const QString &filePath) const override; + + Result createPlaceholder(const SyncFileItem &item) override; + Result dehydratePlaceholder(const SyncFileItem &item) override; + Result convertToPlaceholder(const QString &filename, const SyncFileItem &item, const QString &replacesFile, UpdateMetadataTypes updateType) override; + + bool needsMetadataUpdate(const SyncFileItem &) override; + bool isDehydratedPlaceholder(const QString &filePath) override; + bool statTypeVirtualFile(csync_file_stat_t *stat, void *statData) override; + + bool setPinState(const QString &folderPath, PinState state) override; + Optional pinState(const QString &folderPath) override; + AvailabilityResult availability(const QString &folderPath, const AvailabilityRecursivity recursiveCheck) override; + + void cancelHydration(const QString &requestId, const QString &path); + + int finalizeHydrationJob(const QString &requestId); + +public slots: + void requestHydration(const QString &requestId, const QString &path); + void fileStatusChanged(const QString &systemFileName, OCC::SyncFileStatus fileStatus) override; + +signals: + void hydrationRequestReady(const QString &requestId); + void hydrationRequestFailed(const QString &requestId); + void hydrationRequestFinished(const QString &requestId); + +protected: + void startImpl(const VfsSetupParams ¶ms) override; + +private: + struct HasHydratedDehydrated { + bool hasHydrated = false; + bool hasDehydrated = false; + }; + struct HydratationAndPinStates { + Optional pinState; + HasHydratedDehydrated hydrationStatus; + }; + HydratationAndPinStates computeRecursiveHydrationAndPinStates(const QString &path, const Optional &basePinState); + + QScopedPointer d; +}; + +class DBusApiVfsPluginFactory : public QObject, public DefaultPluginFactory +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.owncloud.PluginFactory" FILE "vfspluginmetadata.json") + Q_INTERFACES(OCC::PluginFactory) +}; + +} // namespace OCC