Add socket command to upload a selection of files based on a regex
authorHannah von Reth <hannah.vonreth@owncloud.com>
Wed, 17 Feb 2021 14:41:32 +0000 (15:41 +0100)
committerMatthieu Gallien <matthieu.gallien@nextcloud.com>
Mon, 23 Aug 2021 07:44:34 +0000 (09:44 +0200)
(cherry picked from commit 0ded3a56a9f3470a951b18eaa9d3c1b5e8db1135)

23 files changed:
src/gui/CMakeLists.txt
src/gui/application.cpp
src/gui/folder.cpp
src/gui/folderman.cpp
src/gui/socketapi.cpp [deleted file]
src/gui/socketapi.h [deleted file]
src/gui/socketapi/CMakeLists.txt [new file with mode: 0644]
src/gui/socketapi/socketapi.cpp [new file with mode: 0644]
src/gui/socketapi/socketapi.h [new file with mode: 0644]
src/gui/socketapi/socketapi_p.h [new file with mode: 0644]
src/gui/socketapi/socketapisocket_mac.h [new file with mode: 0644]
src/gui/socketapi/socketapisocket_mac.mm [new file with mode: 0644]
src/gui/socketapi/socketuploadjob.cpp [new file with mode: 0644]
src/gui/socketapi/socketuploadjob.h [new file with mode: 0644]
src/gui/socketapi_p.h [deleted file]
src/gui/socketapisocket_mac.h [deleted file]
src/gui/socketapisocket_mac.mm [deleted file]
src/libsync/discovery.cpp
src/libsync/owncloudpropagator.cpp
src/libsync/owncloudpropagator.h
src/libsync/syncengine.cpp
src/libsync/syncoptions.cpp [new file with mode: 0644]
src/libsync/syncoptions.h

index 83ee277063031ff4db796b1f1152ef69f4c8279f..748526310b2e7d933f730f19fe42ba94168568f4 100644 (file)
@@ -87,7 +87,6 @@ set(client_SRCS
     sharemanager.cpp
     shareusergroupwidget.cpp
     sharee.cpp
-    socketapi.cpp
     sslbutton.cpp
     sslerrordialog.cpp
     syncrunfilelog.cpp
@@ -338,6 +337,8 @@ target_link_libraries(nextcloudCore
   Qt5::QuickControls2
   )
 
+add_subdirectory(socketapi)
+
 if(Qt5WebEngine_FOUND AND Qt5WebEngineWidgets_FOUND)
   target_link_libraries(nextcloudCore PUBLIC Qt5::WebEngineWidgets)
 endif()
index 97ecedba98a2a5d387d9bd76451cad84508079dc..64b2da8fa36f7f0f2f8d2b132018f53189882d2b 100644 (file)
@@ -27,7 +27,7 @@
 #include "folderman.h"
 #include "logger.h"
 #include "configfile.h"
-#include "socketapi.h"
+#include "socketapi/socketapi.h"
 #include "sslerrordialog.h"
 #include "theme.h"
 #include "clientproxy.h"
index cbb192b01d02a56850a6336c95a88e3df774a847..0cb4f472bfed906d1a0ff4691f345f7594fd8903 100644 (file)
@@ -28,7 +28,7 @@
 #include "clientproxy.h"
 #include "syncengine.h"
 #include "syncrunfilelog.h"
-#include "socketapi.h"
+#include "socketapi/socketapi.h"
 #include "theme.h"
 #include "filesystem.h"
 #include "localdiscoverytracker.h"
index 9b0f297e95cc31d51ba8dbe5a004648bba26452c..5a38814805872abd497ff9b3796d96c66afec985 100644 (file)
@@ -17,7 +17,7 @@
 #include "folder.h"
 #include "syncresult.h"
 #include "theme.h"
-#include "socketapi.h"
+#include "socketapi/socketapi.h"
 #include "account.h"
 #include "accountstate.h"
 #include "accountmanager.h"
diff --git a/src/gui/socketapi.cpp b/src/gui/socketapi.cpp
deleted file mode 100644 (file)
index bd0dc40..0000000
+++ /dev/null
@@ -1,1392 +0,0 @@
-/*
- * Copyright (C) by Dominik Schmidt <dev@dominik-schmidt.de>
- * Copyright (C) by Klaas Freitag <freitag@owncloud.com>
- * Copyright (C) by Roeland Jago Douma <roeland@famdouma.nl>
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
- * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
- * for more details.
- */
-
-#include "socketapi.h"
-#include "socketapi_p.h"
-
-#include "conflictdialog.h"
-#include "conflictsolver.h"
-#include "config.h"
-#include "configfile.h"
-#include "folderman.h"
-#include "folder.h"
-#include "theme.h"
-#include "common/syncjournalfilerecord.h"
-#include "syncengine.h"
-#include "syncfileitem.h"
-#include "filesystem.h"
-#include "version.h"
-#include "account.h"
-#include "accountstate.h"
-#include "account.h"
-#include "capabilities.h"
-#include "common/asserts.h"
-#include "guiutility.h"
-#ifndef OWNCLOUD_TEST
-#include "sharemanager.h"
-#endif
-
-#include <array>
-#include <QBitArray>
-#include <QUrl>
-#include <QMetaMethod>
-#include <QMetaObject>
-#include <QStringList>
-#include <QScopedPointer>
-#include <QFile>
-#include <QDir>
-#include <QApplication>
-#include <QLocalSocket>
-#include <QStringBuilder>
-#include <QMessageBox>
-#include <QInputDialog>
-#include <QFileDialog>
-
-
-#include <QAction>
-#include <QJsonDocument>
-#include <QJsonObject>
-#include <QWidget>
-
-#include <QClipboard>
-#include <QDesktopServices>
-
-#include <QProcess>
-#include <QStandardPaths>
-
-#ifdef Q_OS_MAC
-#include <CoreFoundation/CoreFoundation.h>
-#endif
-
-
-// This is the version that is returned when the client asks for the VERSION.
-// The first number should be changed if there is an incompatible change that breaks old clients.
-// The second number should be changed when there are new features.
-#define MIRALL_SOCKET_API_VERSION "1.1"
-
-namespace {
-
-const QLatin1Char RecordSeparator()
-{
-    return QLatin1Char('\x1e');
-}
-
-QStringList split(const QString &data)
-{
-    // TODO: string ref?
-    return data.split(RecordSeparator());
-}
-
-#if GUI_TESTING
-
-using namespace OCC;
-
-QList<QObject*> allObjects(const QList<QWidget*> &widgets) {
-    QList<QObject*> objects;
-    std::copy(widgets.constBegin(), widgets.constEnd(), std::back_inserter(objects));
-
-    objects << qApp;
-
-    return objects;
-}
-
-QObject *findWidget(const QString &queryString, const QList<QWidget*> &widgets = QApplication::allWidgets())
-{
-    auto objects = allObjects(widgets);
-
-    QList<QObject*>::const_iterator foundWidget;
-
-    if (queryString.contains('>')) {
-        qCDebug(lcSocketApi) << "queryString contains >";
-
-        auto subQueries = queryString.split('>', QString::SkipEmptyParts);
-        Q_ASSERT(subQueries.count() == 2);
-
-        auto parentQueryString = subQueries[0].trimmed();
-        qCDebug(lcSocketApi) << "Find parent: " << parentQueryString;
-        auto parent = findWidget(parentQueryString);
-
-        if(!parent) {
-            return nullptr;
-        }
-
-        auto childQueryString = subQueries[1].trimmed();
-        auto child = findWidget(childQueryString, parent->findChildren<QWidget*>());
-        qCDebug(lcSocketApi) << "found child: " << !!child;
-        return child;
-
-    } else if(queryString.startsWith('#')) {
-        auto objectName = queryString.mid(1);
-        qCDebug(lcSocketApi) << "find objectName: " << objectName;
-        foundWidget = std::find_if(objects.constBegin(), objects.constEnd(), [&](QObject *widget) {
-            return widget->objectName() == objectName;
-        });
-    } else {
-        QList<QObject*> matches;
-        std::copy_if(objects.constBegin(), objects.constEnd(), std::back_inserter(matches), [&](QObject* widget) {
-            return widget->inherits(queryString.toLatin1());
-        });
-
-        std::for_each(matches.constBegin(), matches.constEnd(), [](QObject* w) {
-            if(!w) return;
-            qCDebug(lcSocketApi) << "WIDGET: " << w->objectName() << w->metaObject()->className();
-        });
-
-        if(matches.empty()) {
-            return nullptr;
-        }
-        return matches[0];
-    }
-
-    if (foundWidget == objects.constEnd()) {
-        return nullptr;
-    }
-
-    return *foundWidget;
-}
-#endif
-
-static inline QString removeTrailingSlash(QString path)
-{
-    Q_ASSERT(path.endsWith(QLatin1Char('/')));
-    path.truncate(path.length() - 1);
-    return path;
-}
-
-static QString buildMessage(const QString &verb, const QString &path, const QString &status = QString())
-{
-    QString msg(verb);
-
-    if (!status.isEmpty()) {
-        msg.append(QLatin1Char(':'));
-        msg.append(status);
-    }
-    if (!path.isEmpty()) {
-        msg.append(QLatin1Char(':'));
-        QFileInfo fi(path);
-        msg.append(QDir::toNativeSeparators(fi.absoluteFilePath()));
-    }
-    return msg;
-}
-}
-
-namespace OCC {
-
-Q_LOGGING_CATEGORY(lcSocketApi, "nextcloud.gui.socketapi", QtInfoMsg)
-Q_LOGGING_CATEGORY(lcPublicLink, "nextcloud.gui.socketapi.publiclink", QtInfoMsg)
-
-
-void SocketListener::sendMessage(const QString &message, bool doWait) const
-{
-    if (!socket) {
-        qCWarning(lcSocketApi) << "Not sending message to dead socket:" << message;
-        return;
-    }
-
-    qCDebug(lcSocketApi) << "Sending SocketAPI message -->" << message << "to" << socket;
-    QString localMessage = message;
-    if (!localMessage.endsWith(QLatin1Char('\n'))) {
-        localMessage.append(QLatin1Char('\n'));
-    }
-
-    QByteArray bytesToSend = localMessage.toUtf8();
-    qint64 sent = socket->write(bytesToSend);
-    if (doWait) {
-        socket->waitForBytesWritten(1000);
-    }
-    if (sent != bytesToSend.length()) {
-        qCWarning(lcSocketApi) << "Could not send all data on socket for " << localMessage;
-    }
-}
-
-struct ListenerHasSocketPred
-{
-    QIODevice *socket;
-    ListenerHasSocketPred(QIODevice *socket)
-        : socket(socket)
-    {
-    }
-    bool operator()(const SocketListener &listener) const { return listener.socket == socket; }
-};
-
-SocketApi::SocketApi(QObject *parent)
-    : QObject(parent)
-{
-    QString socketPath;
-
-    qRegisterMetaType<SocketListener *>("SocketListener*");
-    qRegisterMetaType<QSharedPointer<SocketApiJob>>("QSharedPointer<SocketApiJob>");
-
-    if (Utility::isWindows()) {
-        socketPath = QLatin1String(R"(\\.\pipe\)")
-            + QLatin1String(APPLICATION_EXECUTABLE)
-            + QLatin1String("-")
-            + QString::fromLocal8Bit(qgetenv("USERNAME"));
-        // TODO: once the windows extension supports multiple
-        // client connections, switch back to the theme name
-        // See issue #2388
-        // + Theme::instance()->appName();
-    } else if (Utility::isMac()) {
-        // This must match the code signing Team setting of the extension
-        // Example for developer builds (with ad-hoc signing identity): "" "com.owncloud.desktopclient" ".socketApi"
-        // Example for official signed packages: "9B5WD74GWJ." "com.owncloud.desktopclient" ".socketApi"
-        socketPath = SOCKETAPI_TEAM_IDENTIFIER_PREFIX APPLICATION_REV_DOMAIN ".socketApi";
-#ifdef Q_OS_MAC
-        CFURLRef url = (CFURLRef)CFAutorelease((CFURLRef)CFBundleCopyBundleURL(CFBundleGetMainBundle()));
-        QString bundlePath = QUrl::fromCFURL(url).path();
-
-        auto _system = [](const QString &cmd, const QStringList &args){
-            QProcess process;
-            process.setProcessChannelMode(QProcess::MergedChannels);
-            process.start(cmd, args);
-            if (!process.waitForFinished())
-            {
-                qCWarning(lcSocketApi) << "Failed to load shell extension:" << cmd << args.join(" ") << process.errorString();
-            } else {
-                qCInfo(lcSocketApi) << (process.exitCode() != 0 ? "Failed to load" : "Loaded") <<  "shell extension:" << cmd << args.join(" ") << process.readAll();
-            }
-        };
-        // Add it again. This was needed for Mojave to trigger a load.
-        _system(QStringLiteral("pluginkit"), {QStringLiteral("-a"),QStringLiteral("%1Contents/PlugIns/FinderSyncExt.appex/").arg(bundlePath)});
-        // Tell Finder to use the Extension (checking it from System Preferences -> Extensions)
-        _system(QStringLiteral("pluginkit"), {QStringLiteral("-e"), QStringLiteral("use"), QStringLiteral("-i"), QStringLiteral(APPLICATION_REV_DOMAIN ".FinderSyncExt")});
-
-#endif
-    } else if (Utility::isLinux() || Utility::isBSD()) {
-        QString runtimeDir;
-        runtimeDir = QStandardPaths::writableLocation(QStandardPaths::RuntimeLocation);
-        socketPath = runtimeDir + "/" + Theme::instance()->appName() + "/socket";
-    } else {
-        qCWarning(lcSocketApi) << "An unexpected system detected, this probably won't work.";
-    }
-
-    SocketApiServer::removeServer(socketPath);
-    QFileInfo info(socketPath);
-    if (!info.dir().exists()) {
-        bool result = info.dir().mkpath(".");
-        qCDebug(lcSocketApi) << "creating" << info.dir().path() << result;
-        if (result) {
-            QFile::setPermissions(socketPath,
-                QFile::Permissions(QFile::ReadOwner + QFile::WriteOwner + QFile::ExeOwner));
-        }
-    }
-    if (!_localServer.listen(socketPath)) {
-        qCWarning(lcSocketApi) << "can't start server" << socketPath;
-    } else {
-        qCInfo(lcSocketApi) << "server started, listening at " << socketPath;
-    }
-
-    connect(&_localServer, &SocketApiServer::newConnection, this, &SocketApi::slotNewConnection);
-
-    // folder watcher
-    connect(FolderMan::instance(), &FolderMan::folderSyncStateChange, this, &SocketApi::slotUpdateFolderView);
-}
-
-SocketApi::~SocketApi()
-{
-    qCDebug(lcSocketApi) << "dtor";
-    _localServer.close();
-    // All remaining sockets will be destroyed with _localServer, their parent
-    ASSERT(_listeners.isEmpty() || _listeners.first().socket->parent() == &_localServer);
-    _listeners.clear();
-}
-
-void SocketApi::slotNewConnection()
-{
-    // Note that on macOS this is not actually a line-based QIODevice, it's a SocketApiSocket which is our
-    // custom message based macOS IPC.
-    QIODevice *socket = _localServer.nextPendingConnection();
-
-    if (!socket) {
-        return;
-    }
-    qCInfo(lcSocketApi) << "New connection" << socket;
-    connect(socket, &QIODevice::readyRead, this, &SocketApi::slotReadSocket);
-    connect(socket, SIGNAL(disconnected()), this, SLOT(onLostConnection()));
-    connect(socket, &QObject::destroyed, this, &SocketApi::slotSocketDestroyed);
-    ASSERT(socket->readAll().isEmpty());
-
-    _listeners.append(SocketListener(socket));
-    SocketListener &listener = _listeners.last();
-
-    foreach (Folder *f, FolderMan::instance()->map()) {
-        if (f->canSync()) {
-            QString message = buildRegisterPathMessage(removeTrailingSlash(f->path()));
-            qCInfo(lcSocketApi) << "Trying to send SocketAPI Register Path Message -->" << message << "to" << listener.socket;
-            listener.sendMessage(message);
-        }
-    }
-}
-
-void SocketApi::onLostConnection()
-{
-    qCInfo(lcSocketApi) << "Lost connection " << sender();
-    sender()->deleteLater();
-
-    auto socket = qobject_cast<QIODevice *>(sender());
-    ASSERT(socket);
-    _listeners.erase(std::remove_if(_listeners.begin(), _listeners.end(), ListenerHasSocketPred(socket)), _listeners.end());
-}
-
-void SocketApi::slotSocketDestroyed(QObject *obj)
-{
-    auto *socket = static_cast<QIODevice *>(obj);
-    _listeners.erase(std::remove_if(_listeners.begin(), _listeners.end(), ListenerHasSocketPred(socket)), _listeners.end());
-}
-
-void SocketApi::slotReadSocket()
-{
-    auto *socket = qobject_cast<QIODevice *>(sender());
-    ASSERT(socket);
-
-    // Find the SocketListener
-    //
-    // It's possible for the disconnected() signal to be triggered before
-    // the readyRead() signals are received - in that case there won't be a
-    // valid listener. We execute the handler anyway, but it will work with
-    // a SocketListener that doesn't send any messages.
-    static auto noListener = SocketListener(nullptr);
-    SocketListener *listener = &noListener;
-    auto listenerIt = std::find_if(_listeners.begin(), _listeners.end(), ListenerHasSocketPred(socket));
-    if (listenerIt != _listeners.end()) {
-        listener = &*listenerIt;
-    }
-
-    while (socket->canReadLine()) {
-        // Make sure to normalize the input from the socket to
-        // make sure that the path will match, especially on OS X.
-        const QString line = QString::fromUtf8(socket->readLine().trimmed()).normalized(QString::NormalizationForm_C);
-        qCInfo(lcSocketApi) << "Received SocketAPI message <--" << line << "from" << socket;
-        const QByteArray command = line.mid(0, line.indexOf(QLatin1Char(':'))).toUtf8();
-        const QByteArray functionWithArguments = "command_" + command + (command.startsWith("ASYNC_") ? "(QSharedPointer<SocketApiJob>)" : "(QString,SocketListener*)");
-        const int indexOfMethod = staticMetaObject.indexOfMethod(functionWithArguments);
-
-        const auto argument = line.midRef(command.length() + 1);
-        if (command.startsWith("ASYNC_")) {
-
-            auto arguments = argument.split('|');
-            if (arguments.size() != 2) {
-                listener->sendMessage(QStringLiteral("argument count is wrong"));
-                return;
-            }
-
-            auto json = QJsonDocument::fromJson(arguments[1].toUtf8()).object();
-
-            auto jobId = arguments[0];
-
-            auto socketApiJob = QSharedPointer<SocketApiJob>(
-                new SocketApiJob(jobId.toString(), listener, json), &QObject::deleteLater);
-            if (indexOfMethod != -1) {
-                staticMetaObject.method(indexOfMethod)
-                    .invoke(this, Qt::QueuedConnection,
-                            Q_ARG(QSharedPointer<SocketApiJob>, socketApiJob));
-            } else {
-                qCWarning(lcSocketApi) << "The command is not supported by this version of the client:" << command
-                      << "with argument:" << argument;
-                socketApiJob->reject(QStringLiteral("command not found"));
-            }
-        } else {
-            if (indexOfMethod != -1) {
-                // to ensure that listener is still valid we need to call it with Qt::DirectConnection
-                ASSERT(thread() == QThread::currentThread())
-                staticMetaObject.method(indexOfMethod)
-                    .invoke(this, Qt::DirectConnection, Q_ARG(QString, argument.toString()),
-                        Q_ARG(SocketListener *, listener));
-            } else {
-                qCWarning(lcSocketApi) << "The command is not supported by this version of the client:" << command << "with argument:" << argument;
-            }
-        }
-    }
-}
-
-void SocketApi::slotRegisterPath(const QString &alias)
-{
-    // Make sure not to register twice to each connected client
-    if (_registeredAliases.contains(alias))
-        return;
-
-    Folder *f = FolderMan::instance()->folder(alias);
-    if (f) {
-        QString message = buildRegisterPathMessage(removeTrailingSlash(f->path()));
-        foreach (auto &listener, _listeners) {
-            qCInfo(lcSocketApi) << "Trying to send SocketAPI Register Path Message -->" << message << "to" << listener.socket;
-            listener.sendMessage(message);
-        }
-    }
-
-    _registeredAliases.insert(alias);
-}
-
-void SocketApi::slotUnregisterPath(const QString &alias)
-{
-    if (!_registeredAliases.contains(alias))
-        return;
-
-    Folder *f = FolderMan::instance()->folder(alias);
-    if (f)
-        broadcastMessage(buildMessage(QLatin1String("UNREGISTER_PATH"), removeTrailingSlash(f->path()), QString()), true);
-
-    _registeredAliases.remove(alias);
-}
-
-void SocketApi::slotUpdateFolderView(Folder *f)
-{
-    if (_listeners.isEmpty()) {
-        return;
-    }
-
-    if (f) {
-        // do only send UPDATE_VIEW for a couple of status
-        if (f->syncResult().status() == SyncResult::SyncPrepare
-            || f->syncResult().status() == SyncResult::Success
-            || f->syncResult().status() == SyncResult::Paused
-            || f->syncResult().status() == SyncResult::Problem
-            || f->syncResult().status() == SyncResult::Error
-            || f->syncResult().status() == SyncResult::SetupError) {
-            QString rootPath = removeTrailingSlash(f->path());
-            broadcastStatusPushMessage(rootPath, f->syncEngine().syncFileStatusTracker().fileStatus(""));
-
-            broadcastMessage(buildMessage(QLatin1String("UPDATE_VIEW"), rootPath));
-        } else {
-            qCDebug(lcSocketApi) << "Not sending UPDATE_VIEW for" << f->alias() << "because status() is" << f->syncResult().status();
-        }
-    }
-}
-
-void SocketApi::broadcastMessage(const QString &msg, bool doWait)
-{
-    foreach (auto &listener, _listeners) {
-        listener.sendMessage(msg, doWait);
-    }
-}
-
-void SocketApi::processShareRequest(const QString &localFile, SocketListener *listener, ShareDialogStartPage startPage)
-{
-    auto theme = Theme::instance();
-
-    auto fileData = FileData::get(localFile);
-    auto shareFolder = fileData.folder;
-    if (!shareFolder) {
-        const QString message = QLatin1String("SHARE:NOP:") + QDir::toNativeSeparators(localFile);
-        // files that are not within a sync folder are not synced.
-        listener->sendMessage(message);
-    } else if (!shareFolder->accountState()->isConnected()) {
-        const QString message = QLatin1String("SHARE:NOTCONNECTED:") + QDir::toNativeSeparators(localFile);
-        // if the folder isn't connected, don't open the share dialog
-        listener->sendMessage(message);
-    } else if (!theme->linkSharing() && (!theme->userGroupSharing() || shareFolder->accountState()->account()->serverVersionInt() < Account::makeServerVersion(8, 2, 0))) {
-        const QString message = QLatin1String("SHARE:NOP:") + QDir::toNativeSeparators(localFile);
-        listener->sendMessage(message);
-    } else {
-        // If the file doesn't have a journal record, it might not be uploaded yet
-        if (!fileData.journalRecord().isValid()) {
-            const QString message = QLatin1String("SHARE:NOTSYNCED:") + QDir::toNativeSeparators(localFile);
-            listener->sendMessage(message);
-            return;
-        }
-
-        auto &remotePath = fileData.serverRelativePath;
-
-        // Can't share root folder
-        if (remotePath == "/") {
-            const QString message = QLatin1String("SHARE:CANNOTSHAREROOT:") + QDir::toNativeSeparators(localFile);
-            listener->sendMessage(message);
-            return;
-        }
-
-        const QString message = QLatin1String("SHARE:OK:") + QDir::toNativeSeparators(localFile);
-        listener->sendMessage(message);
-
-        emit shareCommandReceived(remotePath, fileData.localPath, startPage);
-    }
-}
-
-void SocketApi::broadcastStatusPushMessage(const QString &systemPath, SyncFileStatus fileStatus)
-{
-    QString msg = buildMessage(QLatin1String("STATUS"), systemPath, fileStatus.toSocketAPIString());
-    Q_ASSERT(!systemPath.endsWith('/'));
-    uint directoryHash = qHash(systemPath.left(systemPath.lastIndexOf('/')));
-    foreach (auto &listener, _listeners) {
-        listener.sendMessageIfDirectoryMonitored(msg, directoryHash);
-    }
-}
-
-void SocketApi::command_RETRIEVE_FOLDER_STATUS(const QString &argument, SocketListener *listener)
-{
-    // This command is the same as RETRIEVE_FILE_STATUS
-    command_RETRIEVE_FILE_STATUS(argument, listener);
-}
-
-void SocketApi::command_RETRIEVE_FILE_STATUS(const QString &argument, SocketListener *listener)
-{
-    QString statusString;
-
-    auto fileData = FileData::get(argument);
-    if (!fileData.folder) {
-        // this can happen in offline mode e.g.: nothing to worry about
-        statusString = QLatin1String("NOP");
-    } else {
-        // The user probably visited this directory in the file shell.
-        // Let the listener know that it should now send status pushes for sibblings of this file.
-        QString directory = fileData.localPath.left(fileData.localPath.lastIndexOf('/'));
-        listener->registerMonitoredDirectory(qHash(directory));
-
-        SyncFileStatus fileStatus = fileData.syncFileStatus();
-        statusString = fileStatus.toSocketAPIString();
-    }
-
-    const QString message = QLatin1String("STATUS:") % statusString % QLatin1Char(':') % QDir::toNativeSeparators(argument);
-    listener->sendMessage(message);
-}
-
-void SocketApi::command_SHARE(const QString &localFile, SocketListener *listener)
-{
-    processShareRequest(localFile, listener, ShareDialogStartPage::UsersAndGroups);
-}
-
-void SocketApi::command_MANAGE_PUBLIC_LINKS(const QString &localFile, SocketListener *listener)
-{
-    processShareRequest(localFile, listener, ShareDialogStartPage::PublicLinks);
-}
-
-void SocketApi::command_VERSION(const QString &, SocketListener *listener)
-{
-    listener->sendMessage(QLatin1String("VERSION:" MIRALL_VERSION_STRING ":" MIRALL_SOCKET_API_VERSION));
-}
-
-void SocketApi::command_SHARE_MENU_TITLE(const QString &, SocketListener *listener)
-{
-    //listener->sendMessage(QLatin1String("SHARE_MENU_TITLE:") + tr("Share with %1", "parameter is Nextcloud").arg(Theme::instance()->appNameGUI()));
-    listener->sendMessage(QLatin1String("SHARE_MENU_TITLE:") + Theme::instance()->appNameGUI());
-}
-
-void SocketApi::command_EDIT(const QString &localFile, SocketListener *listener)
-{
-    Q_UNUSED(listener)
-    auto fileData = FileData::get(localFile);
-    if (!fileData.folder) {
-        qCWarning(lcSocketApi) << "Unknown path" << localFile;
-        return;
-    }
-
-    auto record = fileData.journalRecord();
-    if (!record.isValid())
-        return;
-
-    DirectEditor* editor = getDirectEditorForLocalFile(fileData.localPath);
-    if (!editor)
-        return;
-
-    auto *job = new JsonApiJob(fileData.folder->accountState()->account(), QLatin1String("ocs/v2.php/apps/files/api/v1/directEditing/open"), this);
-
-    QUrlQuery params;
-    params.addQueryItem("path", fileData.serverRelativePath);
-    params.addQueryItem("editorId", editor->id());
-    job->addQueryParams(params);
-    job->usePOST();
-
-    QObject::connect(job, &JsonApiJob::jsonReceived, [](const QJsonDocument &json){
-        auto data = json.object().value("ocs").toObject().value("data").toObject();
-        auto url = QUrl(data.value("url").toString());
-
-        if(!url.isEmpty())
-            Utility::openBrowser(url);
-    });
-    job->start();
-}
-
-// don't pull the share manager into socketapi unittests
-#ifndef OWNCLOUD_TEST
-
-class GetOrCreatePublicLinkShare : public QObject
-{
-    Q_OBJECT
-public:
-    GetOrCreatePublicLinkShare(const AccountPtr &account, const QString &localFile,
-        std::function<void(const QString &link)> targetFun, QObject *parent)
-        : QObject(parent)
-        , _shareManager(account)
-        , _localFile(localFile)
-        , _targetFun(targetFun)
-    {
-        connect(&_shareManager, &ShareManager::sharesFetched,
-            this, &GetOrCreatePublicLinkShare::sharesFetched);
-        connect(&_shareManager, &ShareManager::linkShareCreated,
-            this, &GetOrCreatePublicLinkShare::linkShareCreated);
-        connect(&_shareManager, &ShareManager::serverError,
-            this, &GetOrCreatePublicLinkShare::serverError);
-        connect(&_shareManager, &ShareManager::linkShareRequiresPassword,
-            this, &GetOrCreatePublicLinkShare::passwordRequired);
-    }
-
-    void run()
-    {
-        qCDebug(lcPublicLink) << "Fetching shares";
-        _shareManager.fetchShares(_localFile);
-    }
-
-private slots:
-    void sharesFetched(const QList<QSharedPointer<Share>> &shares)
-    {
-        auto shareName = SocketApi::tr("Context menu share");
-        // If there already is a context menu share, reuse it
-        for (const auto &share : shares) {
-            const auto linkShare = qSharedPointerDynamicCast<LinkShare>(share);
-            if (!linkShare)
-                continue;
-
-            if (linkShare->getName() == shareName) {
-                qCDebug(lcPublicLink) << "Found existing share, reusing";
-                return success(linkShare->getLink().toString());
-            }
-        }
-
-        // otherwise create a new one
-        qCDebug(lcPublicLink) << "Creating new share";
-        _shareManager.createLinkShare(_localFile, shareName, QString());
-    }
-
-    void linkShareCreated(const QSharedPointer<LinkShare> &share)
-    {
-        qCDebug(lcPublicLink) << "New share created";
-        success(share->getLink().toString());
-    }
-
-    void passwordRequired() {
-        bool ok = false;
-        QString password = QInputDialog::getText(nullptr,
-                                                 tr("Password for share required"),
-                                                 tr("Please enter a password for your link share:"),
-                                                 QLineEdit::Normal,
-                                                 QString(),
-                                                 &ok);
-
-        if (!ok) {
-            // The dialog was canceled so no need to do anything
-            return;
-        }
-
-        // Try to create the link share again with the newly entered password
-        _shareManager.createLinkShare(_localFile, QString(), password);
-    }
-
-    void serverError(int code, const QString &message)
-    {
-        qCWarning(lcPublicLink) << "Share fetch/create error" << code << message;
-        QMessageBox::warning(
-            nullptr,
-            tr("Sharing error"),
-            tr("Could not retrieve or create the public link share. Error:\n\n%1").arg(message),
-            QMessageBox::Ok,
-            QMessageBox::NoButton);
-        deleteLater();
-    }
-
-private:
-    void success(const QString &link)
-    {
-        _targetFun(link);
-        deleteLater();
-    }
-
-    ShareManager _shareManager;
-    QString _localFile;
-    std::function<void(const QString &url)> _targetFun;
-};
-
-#else
-
-class GetOrCreatePublicLinkShare : public QObject
-{
-    Q_OBJECT
-public:
-    GetOrCreatePublicLinkShare(const AccountPtr &, const QString &,
-        std::function<void(const QString &link)>, QObject *)
-    {
-    }
-
-    void run()
-    {
-    }
-};
-
-#endif
-
-void SocketApi::command_COPY_PUBLIC_LINK(const QString &localFile, SocketListener *)
-{
-    auto fileData = FileData::get(localFile);
-    if (!fileData.folder)
-        return;
-
-    AccountPtr account = fileData.folder->accountState()->account();
-    auto job = new GetOrCreatePublicLinkShare(account, fileData.serverRelativePath, [](const QString &url) { copyUrlToClipboard(url); }, this);
-    job->run();
-}
-
-// Windows Shell / Explorer pinning fallbacks, see issue: https://github.com/nextcloud/desktop/issues/1599
-#ifdef Q_OS_WIN
-void SocketApi::command_COPYASPATH(const QString &localFile, SocketListener *)
-{
-    QApplication::clipboard()->setText(localFile);
-}
-
-void SocketApi::command_OPENNEWWINDOW(const QString &localFile, SocketListener *)
-{
-    QDesktopServices::openUrl(QUrl::fromLocalFile(localFile));
-}
-
-void SocketApi::command_OPEN(const QString &localFile, SocketListener *socketListener)
-{
-    command_OPENNEWWINDOW(localFile, socketListener);
-}
-#endif
-
-// Fetches the private link url asynchronously and then calls the target slot
-void SocketApi::fetchPrivateLinkUrlHelper(const QString &localFile, const std::function<void(const QString &url)> &targetFun)
-{
-    auto fileData = FileData::get(localFile);
-    if (!fileData.folder) {
-        qCWarning(lcSocketApi) << "Unknown path" << localFile;
-        return;
-    }
-
-    auto record = fileData.journalRecord();
-    if (!record.isValid())
-        return;
-
-    fetchPrivateLinkUrl(
-        fileData.folder->accountState()->account(),
-        fileData.serverRelativePath,
-        record.numericFileId(),
-        this,
-        targetFun);
-}
-
-void SocketApi::command_COPY_PRIVATE_LINK(const QString &localFile, SocketListener *)
-{
-    fetchPrivateLinkUrlHelper(localFile, &SocketApi::copyUrlToClipboard);
-}
-
-void SocketApi::command_EMAIL_PRIVATE_LINK(const QString &localFile, SocketListener *)
-{
-    fetchPrivateLinkUrlHelper(localFile, &SocketApi::emailPrivateLink);
-}
-
-void SocketApi::command_OPEN_PRIVATE_LINK(const QString &localFile, SocketListener *)
-{
-    fetchPrivateLinkUrlHelper(localFile, &SocketApi::openPrivateLink);
-}
-
-void SocketApi::command_MAKE_AVAILABLE_LOCALLY(const QString &filesArg, SocketListener *)
-{
-    const QStringList files = split(filesArg);
-
-    for (const auto &file : files) {
-        auto data = FileData::get(file);
-        if (!data.folder)
-            continue;
-
-        // Update the pin state on all items
-        if (!data.folder->vfs().setPinState(data.folderRelativePath, PinState::AlwaysLocal)) {
-            qCWarning(lcSocketApi) << "Could not set pin state of" << data.folderRelativePath << "to always local";
-        }
-
-        // Trigger sync
-        data.folder->schedulePathForLocalDiscovery(data.folderRelativePath);
-        data.folder->scheduleThisFolderSoon();
-    }
-}
-
-/* Go over all the files and replace them by a virtual file */
-void SocketApi::command_MAKE_ONLINE_ONLY(const QString &filesArg, SocketListener *)
-{
-    const QStringList files = split(filesArg);
-
-    for (const auto &file : files) {
-        auto data = FileData::get(file);
-        if (!data.folder)
-            continue;
-
-        // Update the pin state on all items
-        if (!data.folder->vfs().setPinState(data.folderRelativePath, PinState::OnlineOnly)) {
-            qCWarning(lcSocketApi) << "Could not set pin state of" << data.folderRelativePath << "to online only";
-        }
-
-        // Trigger sync
-        data.folder->schedulePathForLocalDiscovery(data.folderRelativePath);
-        data.folder->scheduleThisFolderSoon();
-    }
-}
-
-void SocketApi::copyUrlToClipboard(const QString &link)
-{
-    QApplication::clipboard()->setText(link);
-}
-
-void SocketApi::command_RESOLVE_CONFLICT(const QString &localFile, SocketListener *)
-{
-    const auto fileData = FileData::get(localFile);
-    if (!fileData.folder || !Utility::isConflictFile(fileData.folderRelativePath))
-        return; // should not have shown menu item
-
-    const auto conflictedRelativePath = fileData.folderRelativePath;
-    const auto baseRelativePath = fileData.folder->journalDb()->conflictFileBaseName(fileData.folderRelativePath.toUtf8());
-
-    const auto dir = QDir(fileData.folder->path());
-    const auto conflictedPath = dir.filePath(conflictedRelativePath);
-    const auto basePath = dir.filePath(baseRelativePath);
-
-    const auto baseName = QFileInfo(basePath).fileName();
-
-#ifndef OWNCLOUD_TEST
-    ConflictDialog dialog;
-    dialog.setBaseFilename(baseName);
-    dialog.setLocalVersionFilename(conflictedPath);
-    dialog.setRemoteVersionFilename(basePath);
-    if (dialog.exec() == ConflictDialog::Accepted) {
-        fileData.folder->scheduleThisFolderSoon();
-    }
-#endif
-}
-
-void SocketApi::command_DELETE_ITEM(const QString &localFile, SocketListener *)
-{
-    ConflictSolver solver;
-    solver.setLocalVersionFilename(localFile);
-    solver.exec(ConflictSolver::KeepRemoteVersion);
-}
-
-void SocketApi::command_MOVE_ITEM(const QString &localFile, SocketListener *)
-{
-    const auto fileData = FileData::get(localFile);
-    const auto parentDir = fileData.parentFolder();
-    if (!fileData.folder)
-        return; // should not have shown menu item
-
-    QString defaultDirAndName = fileData.folderRelativePath;
-
-    // If it's a conflict, we want to save it under the base name by default
-    if (Utility::isConflictFile(defaultDirAndName)) {
-        defaultDirAndName = fileData.folder->journalDb()->conflictFileBaseName(fileData.folderRelativePath.toUtf8());
-    }
-
-    // If the parent doesn't accept new files, go to the root of the sync folder
-    QFileInfo fileInfo(localFile);
-    const auto parentRecord = parentDir.journalRecord();
-    if ((fileInfo.isFile() && !parentRecord._remotePerm.hasPermission(RemotePermissions::CanAddFile))
-        || (fileInfo.isDir() && !parentRecord._remotePerm.hasPermission(RemotePermissions::CanAddSubDirectories))) {
-        defaultDirAndName = QFileInfo(defaultDirAndName).fileName();
-    }
-
-    // Add back the folder path
-    defaultDirAndName = QDir(fileData.folder->path()).filePath(defaultDirAndName);
-
-    const auto target = QFileDialog::getSaveFileName(
-        nullptr,
-        tr("Select new location â€¦"),
-        defaultDirAndName,
-        QString(), nullptr, QFileDialog::HideNameFilterDetails);
-    if (target.isEmpty())
-        return;
-
-    ConflictSolver solver;
-    solver.setLocalVersionFilename(localFile);
-    solver.setRemoteVersionFilename(target);
-}
-
-void SocketApi::emailPrivateLink(const QString &link)
-{
-    Utility::openEmailComposer(
-        tr("I shared something with you"),
-        link,
-        nullptr);
-}
-
-void OCC::SocketApi::openPrivateLink(const QString &link)
-{
-    Utility::openBrowser(link);
-}
-
-void SocketApi::command_GET_STRINGS(const QString &argument, SocketListener *listener)
-{
-    static std::array<std::pair<const char *, QString>, 5> strings { {
-        { "SHARE_MENU_TITLE", tr("Share options") },
-        { "CONTEXT_MENU_TITLE", Theme::instance()->appNameGUI() },
-        { "COPY_PRIVATE_LINK_MENU_TITLE", tr("Copy private link to clipboard") },
-        { "EMAIL_PRIVATE_LINK_MENU_TITLE", tr("Send private link by email â€¦") },
-        { "CONTEXT_MENU_ICON", APPLICATION_ICON_NAME},
-    } };
-    listener->sendMessage(QString("GET_STRINGS:BEGIN"));
-    for (const auto& key_value : strings) {
-        if (argument.isEmpty() || argument == QLatin1String(key_value.first)) {
-            listener->sendMessage(QString("STRING:%1:%2").arg(key_value.first, key_value.second));
-        }
-    }
-    listener->sendMessage(QString("GET_STRINGS:END"));
-}
-
-void SocketApi::sendSharingContextMenuOptions(const FileData &fileData, SocketListener *listener, bool enabled)
-{
-    auto record = fileData.journalRecord();
-    bool isOnTheServer = record.isValid();
-    auto flagString = isOnTheServer && enabled ? QLatin1String("::") : QLatin1String(":d:");
-
-    auto capabilities = fileData.folder->accountState()->account()->capabilities();
-    auto theme = Theme::instance();
-    if (!capabilities.shareAPI() || !(theme->userGroupSharing() || (theme->linkSharing() && capabilities.sharePublicLink())))
-        return;
-
-    // If sharing is globally disabled, do not show any sharing entries.
-    // If there is no permission to share for this file, add a disabled entry saying so
-    if (isOnTheServer && !record._remotePerm.isNull() && !record._remotePerm.hasPermission(RemotePermissions::CanReshare)) {
-        listener->sendMessage(QLatin1String("MENU_ITEM:DISABLED:d:") + (!record.isDirectory()
-            ? tr("Resharing this file is not allowed") : tr("Resharing this folder is not allowed")));
-    } else {
-        listener->sendMessage(QLatin1String("MENU_ITEM:SHARE") + flagString + tr("Share options"));
-
-        // Do we have public links?
-        bool publicLinksEnabled = theme->linkSharing() && capabilities.sharePublicLink();
-
-        // Is is possible to create a public link without user choices?
-        bool canCreateDefaultPublicLink = publicLinksEnabled
-            && !capabilities.sharePublicLinkEnforceExpireDate()
-            && !capabilities.sharePublicLinkAskOptionalPassword()
-            && !capabilities.sharePublicLinkEnforcePassword();
-
-        if (canCreateDefaultPublicLink) {
-            listener->sendMessage(QLatin1String("MENU_ITEM:COPY_PUBLIC_LINK") + flagString + tr("Copy public link"));
-        } else if (publicLinksEnabled) {
-            listener->sendMessage(QLatin1String("MENU_ITEM:MANAGE_PUBLIC_LINKS") + flagString + tr("Copy public link"));
-        }
-    }
-
-    listener->sendMessage(QLatin1String("MENU_ITEM:COPY_PRIVATE_LINK") + flagString + tr("Copy internal link"));
-
-    // Disabled: only providing email option for private links would look odd,
-    // and the copy option is more general.
-    //listener->sendMessage(QLatin1String("MENU_ITEM:EMAIL_PRIVATE_LINK") + flagString + tr("Send private link by email â€¦"));
-}
-
-SocketApi::FileData SocketApi::FileData::get(const QString &localFile)
-{
-    FileData data;
-
-    data.localPath = QDir::cleanPath(localFile);
-    if (data.localPath.endsWith(QLatin1Char('/')))
-        data.localPath.chop(1);
-
-    data.folder = FolderMan::instance()->folderForPath(data.localPath);
-    if (!data.folder)
-        return data;
-
-    data.folderRelativePath = data.localPath.mid(data.folder->cleanPath().length() + 1);
-    data.serverRelativePath = QDir(data.folder->remotePath()).filePath(data.folderRelativePath);
-    QString virtualFileExt = QStringLiteral(APPLICATION_DOTVIRTUALFILE_SUFFIX);
-    if (data.serverRelativePath.endsWith(virtualFileExt)) {
-        data.serverRelativePath.chop(virtualFileExt.size());
-    }
-    return data;
-}
-
-QString SocketApi::FileData::folderRelativePathNoVfsSuffix() const
-{
-    auto result = folderRelativePath;
-    QString virtualFileExt = QStringLiteral(APPLICATION_DOTVIRTUALFILE_SUFFIX);
-    if (result.endsWith(virtualFileExt)) {
-        result.chop(virtualFileExt.size());
-    }
-    return result;
-}
-
-SyncFileStatus SocketApi::FileData::syncFileStatus() const
-{
-    if (!folder)
-        return SyncFileStatus::StatusNone;
-    return folder->syncEngine().syncFileStatusTracker().fileStatus(folderRelativePath);
-}
-
-SyncJournalFileRecord SocketApi::FileData::journalRecord() const
-{
-    SyncJournalFileRecord record;
-    if (!folder)
-        return record;
-    folder->journalDb()->getFileRecord(folderRelativePath, &record);
-    return record;
-}
-
-SocketApi::FileData SocketApi::FileData::parentFolder() const
-{
-    return FileData::get(QFileInfo(localPath).dir().path().toUtf8());
-}
-
-void SocketApi::command_GET_MENU_ITEMS(const QString &argument, OCC::SocketListener *listener)
-{
-    listener->sendMessage(QString("GET_MENU_ITEMS:BEGIN"));
-    const QStringList files = split(argument);
-
-    // Find the common sync folder.
-    // syncFolder will be null if files are in different folders.
-    Folder *syncFolder = nullptr;
-    for (const auto &file : files) {
-        auto folder = FolderMan::instance()->folderForPath(file);
-        if (folder != syncFolder) {
-            if (!syncFolder) {
-                syncFolder = folder;
-            } else {
-                syncFolder = nullptr;
-                break;
-            }
-        }
-    }
-
-    // Sharing actions show for single files only
-    if (syncFolder && files.size() == 1 && syncFolder->accountState()->isConnected()) {
-        QString systemPath = QDir::cleanPath(argument);
-        if (systemPath.endsWith(QLatin1Char('/'))) {
-            systemPath.truncate(systemPath.length() - 1);
-        }
-
-        FileData fileData = FileData::get(argument);
-        const auto record = fileData.journalRecord();
-        const bool isOnTheServer = record.isValid();
-        const auto isE2eEncryptedPath = fileData.journalRecord()._isE2eEncrypted || !fileData.journalRecord()._e2eMangledName.isEmpty();
-        auto flagString = isOnTheServer && !isE2eEncryptedPath ? QLatin1String("::") : QLatin1String(":d:");
-
-        DirectEditor* editor = getDirectEditorForLocalFile(fileData.localPath);
-        if (editor) {
-            //listener->sendMessage(QLatin1String("MENU_ITEM:EDIT") + flagString + tr("Edit via ") + editor->name());
-            listener->sendMessage(QLatin1String("MENU_ITEM:EDIT") + flagString + tr("Edit"));
-        } else {
-            listener->sendMessage(QLatin1String("MENU_ITEM:OPEN_PRIVATE_LINK") + flagString + tr("Open in browser"));
-        }
-
-        sendSharingContextMenuOptions(fileData, listener, !isE2eEncryptedPath);
-
-        // Conflict files get conflict resolution actions
-        bool isConflict = Utility::isConflictFile(fileData.folderRelativePath);
-        if (isConflict || !isOnTheServer) {
-            // Check whether this new file is in a read-only directory
-            QFileInfo fileInfo(fileData.localPath);
-            const auto parentDir = fileData.parentFolder();
-            const auto parentRecord = parentDir.journalRecord();
-            const bool canAddToDir =
-                !parentRecord.isValid() // We're likely at the root of the sync folder, got to assume we can add there
-                || (fileInfo.isFile() && parentRecord._remotePerm.hasPermission(RemotePermissions::CanAddFile))
-                || (fileInfo.isDir() && parentRecord._remotePerm.hasPermission(RemotePermissions::CanAddSubDirectories));
-            const bool canChangeFile =
-                !isOnTheServer
-                || (record._remotePerm.hasPermission(RemotePermissions::CanDelete)
-                       && record._remotePerm.hasPermission(RemotePermissions::CanMove)
-                       && record._remotePerm.hasPermission(RemotePermissions::CanRename));
-
-            if (isConflict && canChangeFile) {
-                if (canAddToDir) {
-                    listener->sendMessage(QLatin1String("MENU_ITEM:RESOLVE_CONFLICT::") + tr("Resolve conflict â€¦"));
-                } else {
-                    if (isOnTheServer) {
-                        // Uploaded conflict file in read-only directory
-                        listener->sendMessage(QLatin1String("MENU_ITEM:MOVE_ITEM::") + tr("Move and rename â€¦"));
-                    } else {
-                        // Local-only conflict file in a read-only dir
-                        listener->sendMessage(QLatin1String("MENU_ITEM:MOVE_ITEM::") + tr("Move, rename and upload â€¦"));
-                    }
-                    listener->sendMessage(QLatin1String("MENU_ITEM:DELETE_ITEM::") + tr("Delete local changes"));
-                }
-            }
-
-            // File in a read-only directory?
-            if (!isConflict && !isOnTheServer && !canAddToDir) {
-                listener->sendMessage(QLatin1String("MENU_ITEM:MOVE_ITEM::") + tr("Move and upload â€¦"));
-                listener->sendMessage(QLatin1String("MENU_ITEM:DELETE_ITEM::") + tr("Delete"));
-            }
-        }
-    }
-
-    // File availability actions
-    if (syncFolder
-        && syncFolder->virtualFilesEnabled()
-        && syncFolder->vfs().socketApiPinStateActionsShown()) {
-        ENFORCE(!files.isEmpty());
-
-        // Determine the combined availability status of the files
-        auto combined = Optional<VfsItemAvailability>();
-        auto merge = [](VfsItemAvailability lhs, VfsItemAvailability rhs) {
-            if (lhs == rhs)
-                return lhs;
-            if (int(lhs) > int(rhs))
-                std::swap(lhs, rhs); // reduce cases ensuring lhs < rhs
-            if (lhs == VfsItemAvailability::AlwaysLocal && rhs == VfsItemAvailability::AllHydrated)
-                return VfsItemAvailability::AllHydrated;
-            if (lhs == VfsItemAvailability::AllDehydrated && rhs == VfsItemAvailability::OnlineOnly)
-                return VfsItemAvailability::AllDehydrated;
-            return VfsItemAvailability::Mixed;
-        };
-        for (const auto &file : files) {
-            auto fileData = FileData::get(file);
-            auto availability = syncFolder->vfs().availability(fileData.folderRelativePath);
-            if (!availability) {
-                if (availability.error() == Vfs::AvailabilityError::DbError)
-                    availability = VfsItemAvailability::Mixed;
-                if (availability.error() == Vfs::AvailabilityError::NoSuchItem)
-                    continue;
-            }
-            if (!combined) {
-                combined = *availability;
-            } else {
-                combined = merge(*combined, *availability);
-            }
-        }
-
-        // TODO: Should be a submenu, should use icons
-        auto makePinContextMenu = [&](bool makeAvailableLocally, bool freeSpace) {
-            listener->sendMessage(QLatin1String("MENU_ITEM:CURRENT_PIN:d:")
-                                  + Utility::vfsCurrentAvailabilityText(*combined));
-            listener->sendMessage(QLatin1String("MENU_ITEM:MAKE_AVAILABLE_LOCALLY:")
-                                  + (makeAvailableLocally ? QLatin1String(":") : QLatin1String("d:"))
-                                  + Utility::vfsPinActionText());
-            listener->sendMessage(QLatin1String("MENU_ITEM:MAKE_ONLINE_ONLY:")
-                                  + (freeSpace ? QLatin1String(":") : QLatin1String("d:"))
-                                  + Utility::vfsFreeSpaceActionText());
-        };
-
-        if (combined) {
-            switch (*combined) {
-            case VfsItemAvailability::AlwaysLocal:
-                makePinContextMenu(false, true);
-                break;
-            case VfsItemAvailability::AllHydrated:
-            case VfsItemAvailability::Mixed:
-                makePinContextMenu(true, true);
-                break;
-            case VfsItemAvailability::AllDehydrated:
-            case VfsItemAvailability::OnlineOnly:
-                makePinContextMenu(true, false);
-                break;
-            }
-        }
-    }
-
-    listener->sendMessage(QString("GET_MENU_ITEMS:END"));
-}
-
-DirectEditor* SocketApi::getDirectEditorForLocalFile(const QString &localFile)
-{
-    FileData fileData = FileData::get(localFile);
-    auto capabilities = fileData.folder->accountState()->account()->capabilities();
-
-    if (fileData.folder && fileData.folder->accountState()->isConnected()) {
-        const auto record = fileData.journalRecord();
-        const auto mimeMatchMode = record.isVirtualFile() ? QMimeDatabase::MatchExtension : QMimeDatabase::MatchDefault;
-
-        QMimeDatabase db;
-        QMimeType type = db.mimeTypeForFile(localFile, mimeMatchMode);
-
-        DirectEditor* editor = capabilities.getDirectEditorForMimetype(type);
-        if (!editor) {
-            editor = capabilities.getDirectEditorForOptionalMimetype(type);
-        }
-        return editor;
-    }
-
-    return nullptr;
-}
-
-#if GUI_TESTING
-void SocketApi::command_ASYNC_LIST_WIDGETS(const QSharedPointer<SocketApiJob> &job)
-{
-    QString response;
-    for (auto &widget : allObjects(QApplication::allWidgets())) {
-        auto objectName = widget->objectName();
-        if (!objectName.isEmpty()) {
-            response += objectName + ":" + widget->property("text").toString() + ", ";
-        }
-    }
-    job->resolve(response);
-}
-
-void SocketApi::command_ASYNC_INVOKE_WIDGET_METHOD(const QSharedPointer<SocketApiJob> &job)
-{
-    auto &arguments = job->arguments();
-
-    auto widget = findWidget(arguments["objectName"].toString());
-    if (!widget) {
-        job->reject(QLatin1String("widget not found"));
-        return;
-    }
-
-    QMetaObject::invokeMethod(widget, arguments["method"].toString().toUtf8().constData());
-    job->resolve();
-}
-
-void SocketApi::command_ASYNC_GET_WIDGET_PROPERTY(const QSharedPointer<SocketApiJob> &job)
-{
-    QString widgetName = job->arguments()[QLatin1String("objectName")].toString();
-    auto widget = findWidget(widgetName);
-    if (!widget) {
-        QString message = QString(QLatin1String("Widget not found: 2: %1")).arg(widgetName);
-        job->reject(message);
-        return;
-    }
-
-    auto propertyName = job->arguments()[QLatin1String("property")].toString();
-
-    auto segments = propertyName.split('.');
-
-    QObject* currentObject = widget;
-    QString value;
-    for(int i = 0;i<segments.count(); i++) {
-        auto segment = segments.at(i);
-        auto var = currentObject->property(segment.toUtf8().constData());
-
-        if(var.canConvert<QString>()) {
-            var.convert(QMetaType::QString);
-            value = var.value<QString>();
-            break;
-        }
-
-        auto tmpObject = var.value<QObject*>();
-        if(tmpObject) {
-            currentObject = tmpObject;
-        } else {
-            QString message = QString(QLatin1String("Widget not found: 3: %1")).arg(widgetName);
-            job->reject(message);
-            return;
-        }
-    }
-
-    job->resolve(value);
-}
-
-void SocketApi::command_ASYNC_SET_WIDGET_PROPERTY(const QSharedPointer<SocketApiJob> &job)
-{
-    auto &arguments = job->arguments();
-    QString widgetName = arguments["objectName"].toString();
-    auto widget = findWidget(widgetName);
-    if (!widget) {
-        QString message = QString(QLatin1String("Widget not found: 4: %1")).arg(widgetName);
-        job->reject(message);
-        return;
-    }
-    widget->setProperty(arguments["property"].toString().toUtf8().constData(),
-                        arguments["value"]);
-
-    job->resolve();
-}
-
-void SocketApi::command_ASYNC_WAIT_FOR_WIDGET_SIGNAL(const QSharedPointer<SocketApiJob> &job)
-{
-    auto &arguments = job->arguments();
-    QString widgetName = arguments["objectName"].toString();
-    auto widget = findWidget(arguments["objectName"].toString());
-    if (!widget) {
-        QString message = QString(QLatin1String("Widget not found: 5: %1")).arg(widgetName);
-        job->reject(message);
-        return;
-    }
-
-    ListenerClosure *closure = new ListenerClosure([job]() { job->resolve("signal emitted"); });
-
-    auto signalSignature = arguments["signalSignature"].toString();
-    signalSignature.prepend("2");
-    auto utf8 = signalSignature.toUtf8();
-    auto signalSignatureFinal = utf8.constData();
-    connect(widget, signalSignatureFinal, closure, SLOT(closureSlot()), Qt::QueuedConnection);
-}
-
-void SocketApi::command_ASYNC_TRIGGER_MENU_ACTION(const QSharedPointer<SocketApiJob> &job)
-{
-    auto &arguments = job->arguments();
-
-    auto objectName = arguments["objectName"].toString();
-    auto widget = findWidget(objectName);
-    if (!widget) {
-        QString message = QString(QLatin1String("Object not found: 1: %1")).arg(objectName);
-        job->reject(message);
-        return;
-    }
-
-    auto children = widget->findChildren<QWidget *>();
-    for (auto childWidget : children) {
-        // foo is the popupwidget!
-        auto actions = childWidget->actions();
-        for (auto action : actions) {
-            if (action->objectName() == arguments["actionName"].toString()) {
-                action->trigger();
-
-                job->resolve("action found");
-                return;
-            }
-        }
-    }
-
-    QString message = QString(QLatin1String("Action not found: 1: %1")).arg(arguments["actionName"].toString());
-    job->reject(message);
-}
-
-void SocketApi::command_ASYNC_ASSERT_ICON_IS_EQUAL(const QSharedPointer<SocketApiJob> &job)
-{
-    auto widget = findWidget(job->arguments()[QLatin1String("queryString")].toString());
-    if (!widget) {
-        QString message = QString(QLatin1String("Object not found: 6: %1")).arg(job->arguments()["queryString"].toString());
-        job->reject(message);
-        return;
-    }
-
-    auto propertyName = job->arguments()[QLatin1String("propertyPath")].toString();
-
-    auto segments = propertyName.split('.');
-
-    QObject* currentObject = widget;
-    QIcon value;
-    for(int i = 0;i<segments.count(); i++) {
-        auto segment = segments.at(i);
-        auto var = currentObject->property(segment.toUtf8().constData());
-
-        if(var.canConvert<QIcon>()) {
-            var.convert(QMetaType::QIcon);
-            value = var.value<QIcon>();
-            break;
-        }
-
-        auto tmpObject = var.value<QObject*>();
-        if(tmpObject) {
-            currentObject = tmpObject;
-        } else {
-            job->reject(QString(QLatin1String("Icon not found: %1")).arg(propertyName));
-        }
-    }
-
-    auto iconName = job->arguments()[QLatin1String("iconName")].toString();
-    if (value.name() ==  iconName) {
-        job->resolve();
-    } else {
-        job->reject("iconName " + iconName + " does not match: " + value.name());
-    }
-
-}
-#endif
-
-QString SocketApi::buildRegisterPathMessage(const QString &path)
-{
-    QFileInfo fi(path);
-    QString message = QLatin1String("REGISTER_PATH:");
-    message.append(QDir::toNativeSeparators(fi.absoluteFilePath()));
-    return message;
-}
-
-} // namespace OCC
-
-#include "socketapi.moc"
diff --git a/src/gui/socketapi.h b/src/gui/socketapi.h
deleted file mode 100644 (file)
index 1c2ef49..0000000
+++ /dev/null
@@ -1,172 +0,0 @@
-/*
- * Copyright (C) by Dominik Schmidt <dev@dominik-schmidt.de>
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
- * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
- * for more details.
- */
-
-#ifndef SOCKETAPI_H
-#define SOCKETAPI_H
-
-#include "syncfileitem.h"
-#include "common/syncfilestatus.h"
-#include "sharedialog.h" // for the ShareDialogStartPage
-#include "common/syncjournalfilerecord.h"
-
-#include "config.h"
-
-#if defined(Q_OS_MAC)
-#include "socketapisocket_mac.h"
-#else
-#include <QLocalServer>
-using SocketApiServer = QLocalServer;
-#endif
-
-class QUrl;
-class QLocalSocket;
-class QStringList;
-
-namespace OCC {
-
-class SyncFileStatus;
-class Folder;
-class SocketListener;
-class DirectEditor;
-class SocketApiJob;
-
-Q_DECLARE_LOGGING_CATEGORY(lcSocketApi)
-
-/**
- * @brief The SocketApi class
- * @ingroup gui
- */
-class SocketApi : public QObject
-{
-    Q_OBJECT
-
-public:
-    explicit SocketApi(QObject *parent = nullptr);
-    virtual ~SocketApi();
-
-public slots:
-    void slotUpdateFolderView(Folder *f);
-    void slotUnregisterPath(const QString &alias);
-    void slotRegisterPath(const QString &alias);
-    void broadcastStatusPushMessage(const QString &systemPath, SyncFileStatus fileStatus);
-
-signals:
-    void shareCommandReceived(const QString &sharePath, const QString &localPath, ShareDialogStartPage startPage);
-
-private slots:
-    void slotNewConnection();
-    void onLostConnection();
-    void slotSocketDestroyed(QObject *obj);
-    void slotReadSocket();
-
-    static void copyUrlToClipboard(const QString &link);
-    static void emailPrivateLink(const QString &link);
-    static void openPrivateLink(const QString &link);
-
-private:
-    // Helper structure for getting information on a file
-    // based on its local path - used for nearly all remote
-    // actions.
-    struct FileData
-    {
-        static FileData get(const QString &localFile);
-        SyncFileStatus syncFileStatus() const;
-        SyncJournalFileRecord journalRecord() const;
-        FileData parentFolder() const;
-
-        // Relative path of the file locally, without any vfs suffix
-        QString folderRelativePathNoVfsSuffix() const;
-
-        Folder *folder;
-        // Absolute path of the file locally. (May be a virtual file)
-        QString localPath;
-        // Relative path of the file locally, as in the DB. (May be a virtual file)
-        QString folderRelativePath;
-        // Path of the file on the server (In case of virtual file, it points to the actual file)
-        QString serverRelativePath;
-    };
-
-    void broadcastMessage(const QString &msg, bool doWait = false);
-
-    // opens share dialog, sends reply
-    void processShareRequest(const QString &localFile, SocketListener *listener, ShareDialogStartPage startPage);
-
-    Q_INVOKABLE void command_RETRIEVE_FOLDER_STATUS(const QString &argument, SocketListener *listener);
-    Q_INVOKABLE void command_RETRIEVE_FILE_STATUS(const QString &argument, SocketListener *listener);
-
-    Q_INVOKABLE void command_VERSION(const QString &argument, SocketListener *listener);
-
-    Q_INVOKABLE void command_SHARE_MENU_TITLE(const QString &argument, SocketListener *listener);
-
-    // The context menu actions
-    Q_INVOKABLE void command_SHARE(const QString &localFile, SocketListener *listener);
-    Q_INVOKABLE void command_MANAGE_PUBLIC_LINKS(const QString &localFile, SocketListener *listener);
-    Q_INVOKABLE void command_COPY_PUBLIC_LINK(const QString &localFile, SocketListener *listener);
-    Q_INVOKABLE void command_COPY_PRIVATE_LINK(const QString &localFile, SocketListener *listener);
-    Q_INVOKABLE void command_EMAIL_PRIVATE_LINK(const QString &localFile, SocketListener *listener);
-    Q_INVOKABLE void command_OPEN_PRIVATE_LINK(const QString &localFile, SocketListener *listener);
-    Q_INVOKABLE void command_MAKE_AVAILABLE_LOCALLY(const QString &filesArg, SocketListener *listener);
-    Q_INVOKABLE void command_MAKE_ONLINE_ONLY(const QString &filesArg, SocketListener *listener);
-    Q_INVOKABLE void command_RESOLVE_CONFLICT(const QString &localFile, SocketListener *listener);
-    Q_INVOKABLE void command_DELETE_ITEM(const QString &localFile, SocketListener *listener);
-    Q_INVOKABLE void command_MOVE_ITEM(const QString &localFile, SocketListener *listener);
-
-    // Windows Shell / Explorer pinning fallbacks, see issue: https://github.com/nextcloud/desktop/issues/1599
-#ifdef Q_OS_WIN
-    Q_INVOKABLE void command_COPYASPATH(const QString &localFile, SocketListener *listener);
-    Q_INVOKABLE void command_OPENNEWWINDOW(const QString &localFile, SocketListener *listener);
-    Q_INVOKABLE void command_OPEN(const QString &localFile, SocketListener *listener);
-#endif
-
-    // Fetch the private link and call targetFun
-    void fetchPrivateLinkUrlHelper(const QString &localFile, const std::function<void(const QString &url)> &targetFun);
-
-    /** Sends translated/branded strings that may be useful to the integration */
-    Q_INVOKABLE void command_GET_STRINGS(const QString &argument, SocketListener *listener);
-
-    // Sends the context menu options relating to sharing to listener
-    void sendSharingContextMenuOptions(const FileData &fileData, SocketListener *listener, bool enabled);
-
-    /** Send the list of menu item. (added in version 1.1)
-     * argument is a list of files for which the menu should be shown, separated by '\x1e'
-     * Reply with  GET_MENU_ITEMS:BEGIN
-     * followed by several MENU_ITEM:[Action]:[flag]:[Text]
-     * If flag contains 'd', the menu should be disabled
-     * and ends with GET_MENU_ITEMS:END
-     */
-    Q_INVOKABLE void command_GET_MENU_ITEMS(const QString &argument, SocketListener *listener);
-
-    /// Direct Editing
-    Q_INVOKABLE void command_EDIT(const QString &localFile, SocketListener *listener);
-    DirectEditor* getDirectEditorForLocalFile(const QString &localFile);
-
-#if GUI_TESTING
-    Q_INVOKABLE void command_ASYNC_ASSERT_ICON_IS_EQUAL(const QSharedPointer<SocketApiJob> &job);
-    Q_INVOKABLE void command_ASYNC_LIST_WIDGETS(const QSharedPointer<SocketApiJob> &job);
-    Q_INVOKABLE void command_ASYNC_INVOKE_WIDGET_METHOD(const QSharedPointer<SocketApiJob> &job);
-    Q_INVOKABLE void command_ASYNC_GET_WIDGET_PROPERTY(const QSharedPointer<SocketApiJob> &job);
-    Q_INVOKABLE void command_ASYNC_SET_WIDGET_PROPERTY(const QSharedPointer<SocketApiJob> &job);
-    Q_INVOKABLE void command_ASYNC_WAIT_FOR_WIDGET_SIGNAL(const QSharedPointer<SocketApiJob> &job);
-    Q_INVOKABLE void command_ASYNC_TRIGGER_MENU_ACTION(const QSharedPointer<SocketApiJob> &job);
-#endif
-
-    QString buildRegisterPathMessage(const QString &path);
-
-    QSet<QString> _registeredAliases;
-    QList<SocketListener> _listeners;
-    SocketApiServer _localServer;
-};
-}
-
-#endif // SOCKETAPI_H
diff --git a/src/gui/socketapi/CMakeLists.txt b/src/gui/socketapi/CMakeLists.txt
new file mode 100644 (file)
index 0000000..9290269
--- /dev/null
@@ -0,0 +1,8 @@
+target_sources(nextcloudCore PRIVATE
+    ${CMAKE_CURRENT_SOURCE_DIR}/socketapi.cpp
+    ${CMAKE_CURRENT_SOURCE_DIR}/socketuploadjob.cpp
+)
+
+if( APPLE )
+    target_sources(nextcloudCore PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/socketapisocket_mac.mm)
+endif()
diff --git a/src/gui/socketapi/socketapi.cpp b/src/gui/socketapi/socketapi.cpp
new file mode 100644 (file)
index 0000000..4335c4e
--- /dev/null
@@ -0,0 +1,1434 @@
+/*
+ * Copyright (C) by Dominik Schmidt <dev@dominik-schmidt.de>
+ * Copyright (C) by Klaas Freitag <freitag@owncloud.com>
+ * Copyright (C) by Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+
+#include "socketapi.h"
+#include "socketapi_p.h"
+#include "socketapi/socketuploadjob.h"
+
+#include "conflictdialog.h"
+#include "conflictsolver.h"
+
+#include "config.h"
+#include "configfile.h"
+#include "folderman.h"
+#include "folder.h"
+#include "theme.h"
+#include "common/syncjournalfilerecord.h"
+#include "syncengine.h"
+#include "syncfileitem.h"
+#include "filesystem.h"
+#include "version.h"
+#include "account.h"
+#include "accountstate.h"
+#include "account.h"
+#include "accountmanager.h"
+#include "capabilities.h"
+#include "common/asserts.h"
+#include "guiutility.h"
+#ifndef OWNCLOUD_TEST
+#include "sharemanager.h"
+#endif
+
+#include <array>
+#include <QBitArray>
+#include <QUrl>
+#include <QMetaMethod>
+#include <QMetaObject>
+#include <QStringList>
+#include <QScopedPointer>
+#include <QFile>
+#include <QDir>
+#include <QApplication>
+#include <QLocalSocket>
+#include <QStringBuilder>
+#include <QMessageBox>
+#include <QInputDialog>
+#include <QFileDialog>
+
+
+#include <QAction>
+#include <QJsonArray>
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QWidget>
+
+#include <QClipboard>
+#include <QDesktopServices>
+
+#include <QProcess>
+#include <QStandardPaths>
+#include <QTemporaryFile>
+#include <networkjobs.h>
+
+#ifdef Q_OS_MAC
+#include <CoreFoundation/CoreFoundation.h>
+#endif
+
+
+// This is the version that is returned when the client asks for the VERSION.
+// The first number should be changed if there is an incompatible change that breaks old clients.
+// The second number should be changed when there are new features.
+#define MIRALL_SOCKET_API_VERSION "1.1"
+
+namespace {
+
+const QLatin1Char RecordSeparator()
+{
+    return QLatin1Char('\x1e');
+}
+
+QStringList split(const QString &data)
+{
+    // TODO: string ref?
+    return data.split(RecordSeparator());
+}
+
+#if GUI_TESTING
+
+using namespace OCC;
+
+QList<QObject *> allObjects(const QList<QWidget *> &widgets)
+{
+    QList<QObject *> objects;
+    std::copy(widgets.constBegin(), widgets.constEnd(), std::back_inserter(objects));
+
+    objects << qApp;
+
+    return objects;
+}
+
+QObject *findWidget(const QString &queryString, const QList<QWidget *> &widgets = QApplication::allWidgets())
+{
+    auto objects = allObjects(widgets);
+
+    QList<QObject *>::const_iterator foundWidget;
+
+    if (queryString.contains('>')) {
+        qCDebug(lcSocketApi) << "queryString contains >";
+
+        auto subQueries = queryString.split('>', QString::SkipEmptyParts);
+        Q_ASSERT(subQueries.count() == 2);
+
+        auto parentQueryString = subQueries[0].trimmed();
+        qCDebug(lcSocketApi) << "Find parent: " << parentQueryString;
+        auto parent = findWidget(parentQueryString);
+
+        if (!parent) {
+            return nullptr;
+        }
+
+        auto childQueryString = subQueries[1].trimmed();
+        auto child = findWidget(childQueryString, parent->findChildren<QWidget *>());
+        qCDebug(lcSocketApi) << "found child: " << !!child;
+        return child;
+
+    } else if (queryString.startsWith('#')) {
+        auto objectName = queryString.mid(1);
+        qCDebug(lcSocketApi) << "find objectName: " << objectName;
+        foundWidget = std::find_if(objects.constBegin(), objects.constEnd(), [&](QObject *widget) {
+            return widget->objectName() == objectName;
+        });
+    } else {
+        QList<QObject *> matches;
+        std::copy_if(objects.constBegin(), objects.constEnd(), std::back_inserter(matches), [&](QObject *widget) {
+            return widget->inherits(queryString.toLatin1());
+        });
+
+        std::for_each(matches.constBegin(), matches.constEnd(), [](QObject *w) {
+            if (!w)
+                return;
+            qCDebug(lcSocketApi) << "WIDGET: " << w->objectName() << w->metaObject()->className();
+        });
+
+        if (matches.empty()) {
+            return nullptr;
+        }
+        return matches[0];
+    }
+
+    if (foundWidget == objects.constEnd()) {
+        return nullptr;
+    }
+
+    return *foundWidget;
+}
+#endif
+
+static inline QString removeTrailingSlash(QString path)
+{
+    Q_ASSERT(path.endsWith(QLatin1Char('/')));
+    path.truncate(path.length() - 1);
+    return path;
+}
+
+static QString buildMessage(const QString &verb, const QString &path, const QString &status = QString())
+{
+    QString msg(verb);
+
+    if (!status.isEmpty()) {
+        msg.append(QLatin1Char(':'));
+        msg.append(status);
+    }
+    if (!path.isEmpty()) {
+        msg.append(QLatin1Char(':'));
+        QFileInfo fi(path);
+        msg.append(QDir::toNativeSeparators(fi.absoluteFilePath()));
+    }
+    return msg;
+}
+}
+
+namespace OCC {
+
+Q_LOGGING_CATEGORY(lcSocketApi, "nextcloud.gui.socketapi", QtInfoMsg)
+Q_LOGGING_CATEGORY(lcPublicLink, "nextcloud.gui.socketapi.publiclink", QtInfoMsg)
+
+
+void SocketListener::sendMessage(const QString &function, const QJsonObject &obj, bool doWait) const
+{
+    sendMessage(function + QLatin1Char(':') + QJsonDocument(obj).toJson(QJsonDocument::Compact), doWait);
+}
+
+void SocketListener::sendMessage(const QString &message, bool doWait) const
+{
+    if (!socket) {
+        qCWarning(lcSocketApi) << "Not sending message to dead socket:" << message;
+        return;
+    }
+
+    qCDebug(lcSocketApi) << "Sending SocketAPI message -->" << message << "to" << socket;
+    QString localMessage = message;
+    if (!localMessage.endsWith(QLatin1Char('\n'))) {
+        localMessage.append(QLatin1Char('\n'));
+    }
+
+    QByteArray bytesToSend = localMessage.toUtf8();
+    qint64 sent = socket->write(bytesToSend);
+    if (doWait) {
+        socket->waitForBytesWritten(1000);
+    }
+    if (sent != bytesToSend.length()) {
+        qCWarning(lcSocketApi) << "Could not send all data on socket for " << localMessage;
+    }
+}
+
+struct ListenerHasSocketPred
+{
+    QIODevice *socket;
+    ListenerHasSocketPred(QIODevice *socket)
+        : socket(socket)
+    {
+    }
+    bool operator()(const SocketListener &listener) const { return listener.socket == socket; }
+};
+
+SocketApi::SocketApi(QObject *parent)
+    : QObject(parent)
+{
+    QString socketPath;
+
+    qRegisterMetaType<SocketListener *>("SocketListener*");
+    qRegisterMetaType<QSharedPointer<SocketApiJob>>("QSharedPointer<SocketApiJob>");
+
+    if (Utility::isWindows()) {
+        socketPath = QLatin1String(R"(\\.\pipe\)")
+            + QLatin1String(APPLICATION_EXECUTABLE)
+            + QLatin1String("-")
+            + QString::fromLocal8Bit(qgetenv("USERNAME"));
+        // TODO: once the windows extension supports multiple
+        // client connections, switch back to the theme name
+        // See issue #2388
+        // + Theme::instance()->appName();
+    } else if (Utility::isMac()) {
+        // This must match the code signing Team setting of the extension
+        // Example for developer builds (with ad-hoc signing identity): "" "com.owncloud.desktopclient" ".socketApi"
+        // Example for official signed packages: "9B5WD74GWJ." "com.owncloud.desktopclient" ".socketApi"
+        socketPath = SOCKETAPI_TEAM_IDENTIFIER_PREFIX APPLICATION_REV_DOMAIN ".socketApi";
+#ifdef Q_OS_MAC
+        CFURLRef url = (CFURLRef)CFAutorelease((CFURLRef)CFBundleCopyBundleURL(CFBundleGetMainBundle()));
+        QString bundlePath = QUrl::fromCFURL(url).path();
+
+        auto _system = [](const QString &cmd, const QStringList &args) {
+            QProcess process;
+            process.setProcessChannelMode(QProcess::MergedChannels);
+            process.start(cmd, args);
+            if (!process.waitForFinished()) {
+                qCWarning(lcSocketApi) << "Failed to load shell extension:" << cmd << args.join(" ") << process.errorString();
+            } else {
+                qCInfo(lcSocketApi) << (process.exitCode() != 0 ? "Failed to load" : "Loaded") << "shell extension:" << cmd << args.join(" ") << process.readAll();
+            }
+        };
+        // Add it again. This was needed for Mojave to trigger a load.
+        _system(QStringLiteral("pluginkit"), { QStringLiteral("-a"), QStringLiteral("%1Contents/PlugIns/FinderSyncExt.appex/").arg(bundlePath) });
+        // Tell Finder to use the Extension (checking it from System Preferences -> Extensions)
+        _system(QStringLiteral("pluginkit"), { QStringLiteral("-e"), QStringLiteral("use"), QStringLiteral("-i"), QStringLiteral(APPLICATION_REV_DOMAIN ".FinderSyncExt") });
+
+#endif
+    } else if (Utility::isLinux() || Utility::isBSD()) {
+        QString runtimeDir;
+        runtimeDir = QStandardPaths::writableLocation(QStandardPaths::RuntimeLocation);
+        socketPath = runtimeDir + "/" + Theme::instance()->appName() + "/socket";
+    } else {
+        qCWarning(lcSocketApi) << "An unexpected system detected, this probably won't work.";
+    }
+
+    SocketApiServer::removeServer(socketPath);
+    QFileInfo info(socketPath);
+    if (!info.dir().exists()) {
+        bool result = info.dir().mkpath(".");
+        qCDebug(lcSocketApi) << "creating" << info.dir().path() << result;
+        if (result) {
+            QFile::setPermissions(socketPath,
+                QFile::Permissions(QFile::ReadOwner + QFile::WriteOwner + QFile::ExeOwner));
+        }
+    }
+    if (!_localServer.listen(socketPath)) {
+        qCWarning(lcSocketApi) << "can't start server" << socketPath;
+    } else {
+        qCInfo(lcSocketApi) << "server started, listening at " << socketPath;
+    }
+
+    connect(&_localServer, &SocketApiServer::newConnection, this, &SocketApi::slotNewConnection);
+
+    // folder watcher
+    connect(FolderMan::instance(), &FolderMan::folderSyncStateChange, this, &SocketApi::slotUpdateFolderView);
+}
+
+SocketApi::~SocketApi()
+{
+    qCDebug(lcSocketApi) << "dtor";
+    _localServer.close();
+    // All remaining sockets will be destroyed with _localServer, their parent
+    ASSERT(_listeners.isEmpty() || _listeners.first().socket->parent() == &_localServer);
+    _listeners.clear();
+}
+
+void SocketApi::slotNewConnection()
+{
+    // Note that on macOS this is not actually a line-based QIODevice, it's a SocketApiSocket which is our
+    // custom message based macOS IPC.
+    QIODevice *socket = _localServer.nextPendingConnection();
+
+    if (!socket) {
+        return;
+    }
+    qCInfo(lcSocketApi) << "New connection" << socket;
+    connect(socket, &QIODevice::readyRead, this, &SocketApi::slotReadSocket);
+    connect(socket, SIGNAL(disconnected()), this, SLOT(onLostConnection()));
+    connect(socket, &QObject::destroyed, this, &SocketApi::slotSocketDestroyed);
+    ASSERT(socket->readAll().isEmpty());
+
+    _listeners.append(SocketListener(socket));
+    SocketListener &listener = _listeners.last();
+
+    foreach (Folder *f, FolderMan::instance()->map()) {
+        if (f->canSync()) {
+            QString message = buildRegisterPathMessage(removeTrailingSlash(f->path()));
+            qCInfo(lcSocketApi) << "Trying to send SocketAPI Register Path Message -->" << message << "to" << listener.socket;
+            listener.sendMessage(message);
+        }
+    }
+}
+
+void SocketApi::onLostConnection()
+{
+    qCInfo(lcSocketApi) << "Lost connection " << sender();
+    sender()->deleteLater();
+
+    auto socket = qobject_cast<QIODevice *>(sender());
+    ASSERT(socket);
+    _listeners.erase(std::remove_if(_listeners.begin(), _listeners.end(), ListenerHasSocketPred(socket)), _listeners.end());
+}
+
+void SocketApi::slotSocketDestroyed(QObject *obj)
+{
+    auto *socket = static_cast<QIODevice *>(obj);
+    _listeners.erase(std::remove_if(_listeners.begin(), _listeners.end(), ListenerHasSocketPred(socket)), _listeners.end());
+}
+
+void SocketApi::slotReadSocket()
+{
+    auto *socket = qobject_cast<QIODevice *>(sender());
+    ASSERT(socket);
+
+    // Find the SocketListener
+    //
+    // It's possible for the disconnected() signal to be triggered before
+    // the readyRead() signals are received - in that case there won't be a
+    // valid listener. We execute the handler anyway, but it will work with
+    // a SocketListener that doesn't send any messages.
+    static auto noListener = SocketListener(nullptr);
+    SocketListener *listener = &noListener;
+    auto listenerIt = std::find_if(_listeners.begin(), _listeners.end(), ListenerHasSocketPred(socket));
+    if (listenerIt != _listeners.end()) {
+        listener = &*listenerIt;
+    }
+
+    while (socket->canReadLine()) {
+        // Make sure to normalize the input from the socket to
+        // make sure that the path will match, especially on OS X.
+        const QString line = QString::fromUtf8(socket->readLine().trimmed()).normalized(QString::NormalizationForm_C);
+        qCInfo(lcSocketApi) << "Received SocketAPI message <--" << line << "from" << socket;
+        const QByteArray command = line.midRef(0, line.indexOf(QLatin1Char(':'))).toUtf8().toUpper().replace("/", "_");
+        const QByteArray functionWithArguments = "command_" + command + (command.startsWith("ASYNC_") ? "(QSharedPointer<SocketApiJob>)" : "(QString,SocketListener*)");
+        const int indexOfMethod = staticMetaObject.indexOfMethod(functionWithArguments);
+
+        const auto argument = line.midRef(command.length() + 1);
+        if (command.startsWith("ASYNC_")) {
+            auto arguments = argument.split('|');
+            if (arguments.size() != 2) {
+                listener->sendMessage(QStringLiteral("argument count is wrong"));
+                return;
+            }
+
+            auto json = QJsonDocument::fromJson(arguments[1].toUtf8()).object();
+
+            auto jobId = arguments[0];
+
+            auto socketApiJob = QSharedPointer<SocketApiJob>(
+                new SocketApiJob(jobId.toString(), listener, json), &QObject::deleteLater);
+            if (indexOfMethod != -1) {
+                staticMetaObject.method(indexOfMethod)
+                    .invoke(this, Qt::QueuedConnection,
+                        Q_ARG(QSharedPointer<SocketApiJob>, socketApiJob));
+            } else {
+                qCWarning(lcSocketApi) << "The command is not supported by this version of the client:" << command
+                                       << "with argument:" << argument;
+                socketApiJob->reject(QStringLiteral("command not found"));
+            }
+        } else {
+            if (indexOfMethod != -1) {
+                // to ensure that listener is still valid we need to call it with Qt::DirectConnection
+                ASSERT(thread() == QThread::currentThread())
+                staticMetaObject.method(indexOfMethod)
+                    .invoke(this, Qt::DirectConnection, Q_ARG(QString, argument.toString()),
+                        Q_ARG(SocketListener *, listener));
+            } else {
+                qCWarning(lcSocketApi) << "The command is not supported by this version of the client:" << command << "with argument:" << argument;
+            }
+        }
+    }
+}
+
+void SocketApi::slotRegisterPath(const QString &alias)
+{
+    // Make sure not to register twice to each connected client
+    if (_registeredAliases.contains(alias))
+        return;
+
+    Folder *f = FolderMan::instance()->folder(alias);
+    if (f) {
+        QString message = buildRegisterPathMessage(removeTrailingSlash(f->path()));
+        foreach (auto &listener, _listeners) {
+            qCInfo(lcSocketApi) << "Trying to send SocketAPI Register Path Message -->" << message << "to" << listener.socket;
+            listener.sendMessage(message);
+        }
+    }
+
+    _registeredAliases.insert(alias);
+}
+
+void SocketApi::slotUnregisterPath(const QString &alias)
+{
+    if (!_registeredAliases.contains(alias))
+        return;
+
+    Folder *f = FolderMan::instance()->folder(alias);
+    if (f)
+        broadcastMessage(buildMessage(QLatin1String("UNREGISTER_PATH"), removeTrailingSlash(f->path()), QString()), true);
+
+    _registeredAliases.remove(alias);
+}
+
+void SocketApi::slotUpdateFolderView(Folder *f)
+{
+    if (_listeners.isEmpty()) {
+        return;
+    }
+
+    if (f) {
+        // do only send UPDATE_VIEW for a couple of status
+        if (f->syncResult().status() == SyncResult::SyncPrepare
+            || f->syncResult().status() == SyncResult::Success
+            || f->syncResult().status() == SyncResult::Paused
+            || f->syncResult().status() == SyncResult::Problem
+            || f->syncResult().status() == SyncResult::Error
+            || f->syncResult().status() == SyncResult::SetupError) {
+            QString rootPath = removeTrailingSlash(f->path());
+            broadcastStatusPushMessage(rootPath, f->syncEngine().syncFileStatusTracker().fileStatus(""));
+
+            broadcastMessage(buildMessage(QLatin1String("UPDATE_VIEW"), rootPath));
+        } else {
+            qCDebug(lcSocketApi) << "Not sending UPDATE_VIEW for" << f->alias() << "because status() is" << f->syncResult().status();
+        }
+    }
+}
+
+void SocketApi::broadcastMessage(const QString &msg, bool doWait)
+{
+    foreach (auto &listener, _listeners) {
+        listener.sendMessage(msg, doWait);
+    }
+}
+
+void SocketApi::processShareRequest(const QString &localFile, SocketListener *listener, ShareDialogStartPage startPage)
+{
+    auto theme = Theme::instance();
+
+    auto fileData = FileData::get(localFile);
+    auto shareFolder = fileData.folder;
+    if (!shareFolder) {
+        const QString message = QLatin1String("SHARE:NOP:") + QDir::toNativeSeparators(localFile);
+        // files that are not within a sync folder are not synced.
+        listener->sendMessage(message);
+    } else if (!shareFolder->accountState()->isConnected()) {
+        const QString message = QLatin1String("SHARE:NOTCONNECTED:") + QDir::toNativeSeparators(localFile);
+        // if the folder isn't connected, don't open the share dialog
+        listener->sendMessage(message);
+    } else if (!theme->linkSharing() && (!theme->userGroupSharing() || shareFolder->accountState()->account()->serverVersionInt() < Account::makeServerVersion(8, 2, 0))) {
+        const QString message = QLatin1String("SHARE:NOP:") + QDir::toNativeSeparators(localFile);
+        listener->sendMessage(message);
+    } else {
+        // If the file doesn't have a journal record, it might not be uploaded yet
+        if (!fileData.journalRecord().isValid()) {
+            const QString message = QLatin1String("SHARE:NOTSYNCED:") + QDir::toNativeSeparators(localFile);
+            listener->sendMessage(message);
+            return;
+        }
+
+        auto &remotePath = fileData.serverRelativePath;
+
+        // Can't share root folder
+        if (remotePath == "/") {
+            const QString message = QLatin1String("SHARE:CANNOTSHAREROOT:") + QDir::toNativeSeparators(localFile);
+            listener->sendMessage(message);
+            return;
+        }
+
+        const QString message = QLatin1String("SHARE:OK:") + QDir::toNativeSeparators(localFile);
+        listener->sendMessage(message);
+
+        emit shareCommandReceived(remotePath, fileData.localPath, startPage);
+    }
+}
+
+void SocketApi::broadcastStatusPushMessage(const QString &systemPath, SyncFileStatus fileStatus)
+{
+    QString msg = buildMessage(QLatin1String("STATUS"), systemPath, fileStatus.toSocketAPIString());
+    Q_ASSERT(!systemPath.endsWith('/'));
+    uint directoryHash = qHash(systemPath.left(systemPath.lastIndexOf('/')));
+    foreach (auto &listener, _listeners) {
+        listener.sendMessageIfDirectoryMonitored(msg, directoryHash);
+    }
+}
+
+void SocketApi::command_RETRIEVE_FOLDER_STATUS(const QString &argument, SocketListener *listener)
+{
+    // This command is the same as RETRIEVE_FILE_STATUS
+    command_RETRIEVE_FILE_STATUS(argument, listener);
+}
+
+void SocketApi::command_RETRIEVE_FILE_STATUS(const QString &argument, SocketListener *listener)
+{
+    QString statusString;
+
+    auto fileData = FileData::get(argument);
+    if (!fileData.folder) {
+        // this can happen in offline mode e.g.: nothing to worry about
+        statusString = QLatin1String("NOP");
+    } else {
+        // The user probably visited this directory in the file shell.
+        // Let the listener know that it should now send status pushes for sibblings of this file.
+        QString directory = fileData.localPath.left(fileData.localPath.lastIndexOf('/'));
+        listener->registerMonitoredDirectory(qHash(directory));
+
+        SyncFileStatus fileStatus = fileData.syncFileStatus();
+        statusString = fileStatus.toSocketAPIString();
+    }
+
+    const QString message = QLatin1String("STATUS:") % statusString % QLatin1Char(':') % QDir::toNativeSeparators(argument);
+    listener->sendMessage(message);
+}
+
+void SocketApi::command_SHARE(const QString &localFile, SocketListener *listener)
+{
+    processShareRequest(localFile, listener, ShareDialogStartPage::UsersAndGroups);
+}
+
+void SocketApi::command_MANAGE_PUBLIC_LINKS(const QString &localFile, SocketListener *listener)
+{
+    processShareRequest(localFile, listener, ShareDialogStartPage::PublicLinks);
+}
+
+void SocketApi::command_VERSION(const QString &, SocketListener *listener)
+{
+    listener->sendMessage(QLatin1String("VERSION:" MIRALL_VERSION_STRING ":" MIRALL_SOCKET_API_VERSION));
+}
+
+void SocketApi::command_SHARE_MENU_TITLE(const QString &, SocketListener *listener)
+{
+    //listener->sendMessage(QLatin1String("SHARE_MENU_TITLE:") + tr("Share with %1", "parameter is Nextcloud").arg(Theme::instance()->appNameGUI()));
+    listener->sendMessage(QLatin1String("SHARE_MENU_TITLE:") + Theme::instance()->appNameGUI());
+}
+
+void SocketApi::command_EDIT(const QString &localFile, SocketListener *listener)
+{
+    Q_UNUSED(listener)
+    auto fileData = FileData::get(localFile);
+    if (!fileData.folder) {
+        qCWarning(lcSocketApi) << "Unknown path" << localFile;
+        return;
+    }
+
+    auto record = fileData.journalRecord();
+    if (!record.isValid())
+        return;
+
+    DirectEditor* editor = getDirectEditorForLocalFile(fileData.localPath);
+    if (!editor)
+        return;
+
+    auto *job = new JsonApiJob(fileData.folder->accountState()->account(), QLatin1String("ocs/v2.php/apps/files/api/v1/directEditing/open"), this);
+
+    QUrlQuery params;
+    params.addQueryItem("path", fileData.serverRelativePath);
+    params.addQueryItem("editorId", editor->id());
+    job->addQueryParams(params);
+    job->usePOST();
+
+    QObject::connect(job, &JsonApiJob::jsonReceived, [](const QJsonDocument &json){
+        auto data = json.object().value("ocs").toObject().value("data").toObject();
+        auto url = QUrl(data.value("url").toString());
+
+        if(!url.isEmpty())
+            Utility::openBrowser(url);
+    });
+    job->start();
+}
+
+// don't pull the share manager into socketapi unittests
+#ifndef OWNCLOUD_TEST
+
+class GetOrCreatePublicLinkShare : public QObject
+{
+    Q_OBJECT
+public:
+    GetOrCreatePublicLinkShare(const AccountPtr &account, const QString &localFile,
+        QObject *parent)
+        : QObject(parent)
+        , _account(account)
+        , _shareManager(account)
+        , _localFile(localFile)
+    {
+        connect(&_shareManager, &ShareManager::sharesFetched,
+            this, &GetOrCreatePublicLinkShare::sharesFetched);
+        connect(&_shareManager, &ShareManager::linkShareCreated,
+            this, &GetOrCreatePublicLinkShare::linkShareCreated);
+        connect(&_shareManager, &ShareManager::linkShareRequiresPassword,
+            this, &GetOrCreatePublicLinkShare::linkShareRequiresPassword);
+        connect(&_shareManager, &ShareManager::serverError,
+            this, &GetOrCreatePublicLinkShare::serverError);
+    }
+
+    void run()
+    {
+        qCDebug(lcPublicLink) << "Fetching shares";
+        _shareManager.fetchShares(_localFile);
+    }
+
+private slots:
+    void sharesFetched(const QList<QSharedPointer<Share>> &shares)
+    {
+        auto shareName = SocketApi::tr("Context menu share");
+
+        // If there already is a context menu share, reuse it
+        for (const auto &share : shares) {
+            const auto linkShare = qSharedPointerDynamicCast<LinkShare>(share);
+            if (!linkShare)
+                continue;
+
+            if (linkShare->getName() == shareName) {
+                qCDebug(lcPublicLink) << "Found existing share, reusing";
+                return success(linkShare->getLink().toString());
+            }
+        }
+
+        // otherwise create a new one
+        qCDebug(lcPublicLink) << "Creating new share";
+        _shareManager.createLinkShare(_localFile, shareName, QString());
+    }
+
+    void linkShareCreated(const QSharedPointer<LinkShare> &share)
+    {
+        qCDebug(lcPublicLink) << "New share created";
+        success(share->getLink().toString());
+    }
+
+    void passwordRequired() {
+        bool ok = false;
+        QString password = QInputDialog::getText(nullptr,
+                                                 tr("Password for share required"),
+                                                 tr("Please enter a password for your link share:"),
+                                                 QLineEdit::Normal,
+                                                 QString(),
+                                                 &ok);
+
+        if (!ok) {
+            // The dialog was canceled so no need to do anything
+            return;
+        }
+
+        // Try to create the link share again with the newly entered password
+        _shareManager.createLinkShare(_localFile, QString(), password);
+    }
+
+    void linkShareRequiresPassword(const QString &message)
+    {
+        qCInfo(lcPublicLink) << "Could not create link share:" << message;
+        emit error(message);
+        deleteLater();
+    }
+
+    void serverError(int code, const QString &message)
+    {
+        qCWarning(lcPublicLink) << "Share fetch/create error" << code << message;
+        QMessageBox::warning(
+            nullptr,
+            tr("Sharing error"),
+            tr("Could not retrieve or create the public link share. Error:\n\n%1").arg(message),
+            QMessageBox::Ok,
+            QMessageBox::NoButton);
+        emit error(message);
+        deleteLater();
+    }
+
+signals:
+    void done(const QString &link);
+    void error(const QString &message);
+
+private:
+    void success(const QString &link)
+    {
+        emit done(link);
+        deleteLater();
+    }
+
+    AccountPtr _account;
+    ShareManager _shareManager;
+    QString _localFile;
+};
+
+#else
+
+class GetOrCreatePublicLinkShare : public QObject
+{
+    Q_OBJECT
+public:
+    GetOrCreatePublicLinkShare(const AccountPtr &, const QString &,
+        std::function<void(const QString &link)>, QObject *)
+    {
+    }
+
+    void run()
+    {
+    }
+};
+
+#endif
+
+void SocketApi::command_COPY_PUBLIC_LINK(const QString &localFile, SocketListener *)
+{
+    auto fileData = FileData::get(localFile);
+    if (!fileData.folder)
+        return;
+
+    AccountPtr account = fileData.folder->accountState()->account();
+    auto job = new GetOrCreatePublicLinkShare(account, fileData.serverRelativePath, this);
+    connect(job, &GetOrCreatePublicLinkShare::done, this,
+        [](const QString &url) { copyUrlToClipboard(url); });
+    connect(job, &GetOrCreatePublicLinkShare::error, this,
+        [=]() { emit shareCommandReceived(fileData.serverRelativePath, fileData.localPath, ShareDialogStartPage::PublicLinks); });
+    job->run();
+}
+
+// Windows Shell / Explorer pinning fallbacks, see issue: https://github.com/nextcloud/desktop/issues/1599
+#ifdef Q_OS_WIN
+void SocketApi::command_COPYASPATH(const QString &localFile, SocketListener *)
+{
+    QApplication::clipboard()->setText(localFile);
+}
+
+void SocketApi::command_OPENNEWWINDOW(const QString &localFile, SocketListener *)
+{
+    QDesktopServices::openUrl(QUrl::fromLocalFile(localFile));
+}
+
+void SocketApi::command_OPEN(const QString &localFile, SocketListener *socketListener)
+{
+    command_OPENNEWWINDOW(localFile, socketListener);
+}
+#endif
+
+// Fetches the private link url asynchronously and then calls the target slot
+void SocketApi::fetchPrivateLinkUrlHelper(const QString &localFile, const std::function<void(const QString &url)> &targetFun)
+{
+    auto fileData = FileData::get(localFile);
+    if (!fileData.folder) {
+        qCWarning(lcSocketApi) << "Unknown path" << localFile;
+        return;
+    }
+
+    auto record = fileData.journalRecord();
+    if (!record.isValid())
+        return;
+
+    fetchPrivateLinkUrl(
+        fileData.folder->accountState()->account(),
+        fileData.serverRelativePath,
+        record.numericFileId(),
+        this,
+        targetFun);
+}
+
+void SocketApi::command_COPY_PRIVATE_LINK(const QString &localFile, SocketListener *)
+{
+    fetchPrivateLinkUrlHelper(localFile, &SocketApi::copyUrlToClipboard);
+}
+
+void SocketApi::command_EMAIL_PRIVATE_LINK(const QString &localFile, SocketListener *)
+{
+    fetchPrivateLinkUrlHelper(localFile, &SocketApi::emailPrivateLink);
+}
+
+void SocketApi::command_OPEN_PRIVATE_LINK(const QString &localFile, SocketListener *)
+{
+    fetchPrivateLinkUrlHelper(localFile, &SocketApi::openPrivateLink);
+}
+
+void SocketApi::command_MAKE_AVAILABLE_LOCALLY(const QString &filesArg, SocketListener *)
+{
+    const QStringList files = split(filesArg);
+
+    for (const auto &file : files) {
+        auto data = FileData::get(file);
+        if (!data.folder)
+            continue;
+
+        // Update the pin state on all items
+        if (!data.folder->vfs().setPinState(data.folderRelativePath, PinState::AlwaysLocal)) {
+            qCWarning(lcSocketApi) << "Could not set pin state of" << data.folderRelativePath << "to always local";
+        }
+
+        // Trigger sync
+        data.folder->schedulePathForLocalDiscovery(data.folderRelativePath);
+        data.folder->scheduleThisFolderSoon();
+    }
+}
+
+/* Go over all the files and replace them by a virtual file */
+void SocketApi::command_MAKE_ONLINE_ONLY(const QString &filesArg, SocketListener *)
+{
+    const QStringList files = split(filesArg);
+
+    for (const auto &file : files) {
+        auto data = FileData::get(file);
+        if (!data.folder)
+            continue;
+
+        // Update the pin state on all items
+        if (!data.folder->vfs().setPinState(data.folderRelativePath, PinState::OnlineOnly)) {
+            qCWarning(lcSocketApi) << "Could not set pin state of" << data.folderRelativePath << "to online only";
+        }
+
+        // Trigger sync
+        data.folder->schedulePathForLocalDiscovery(data.folderRelativePath);
+        data.folder->scheduleThisFolderSoon();
+    }
+}
+
+void SocketApi::copyUrlToClipboard(const QString &link)
+{
+    QApplication::clipboard()->setText(link);
+}
+
+void SocketApi::command_RESOLVE_CONFLICT(const QString &localFile, SocketListener *)
+{
+    const auto fileData = FileData::get(localFile);
+    if (!fileData.folder || !Utility::isConflictFile(fileData.folderRelativePath))
+        return; // should not have shown menu item
+
+    const auto conflictedRelativePath = fileData.folderRelativePath;
+    const auto baseRelativePath = fileData.folder->journalDb()->conflictFileBaseName(fileData.folderRelativePath.toUtf8());
+
+    const auto dir = QDir(fileData.folder->path());
+    const auto conflictedPath = dir.filePath(conflictedRelativePath);
+    const auto basePath = dir.filePath(baseRelativePath);
+
+    const auto baseName = QFileInfo(basePath).fileName();
+
+#ifndef OWNCLOUD_TEST
+    ConflictDialog dialog;
+    dialog.setBaseFilename(baseName);
+    dialog.setLocalVersionFilename(conflictedPath);
+    dialog.setRemoteVersionFilename(basePath);
+    if (dialog.exec() == ConflictDialog::Accepted) {
+        fileData.folder->scheduleThisFolderSoon();
+    }
+#endif
+}
+
+void SocketApi::command_DELETE_ITEM(const QString &localFile, SocketListener *)
+{
+    ConflictSolver solver;
+    solver.setLocalVersionFilename(localFile);
+    solver.exec(ConflictSolver::KeepRemoteVersion);
+}
+
+void SocketApi::command_MOVE_ITEM(const QString &localFile, SocketListener *)
+{
+    const auto fileData = FileData::get(localFile);
+    const auto parentDir = fileData.parentFolder();
+    if (!fileData.folder)
+        return; // should not have shown menu item
+
+    QString defaultDirAndName = fileData.folderRelativePath;
+
+    // If it's a conflict, we want to save it under the base name by default
+    if (Utility::isConflictFile(defaultDirAndName)) {
+        defaultDirAndName = fileData.folder->journalDb()->conflictFileBaseName(fileData.folderRelativePath.toUtf8());
+    }
+
+    // If the parent doesn't accept new files, go to the root of the sync folder
+    QFileInfo fileInfo(localFile);
+    const auto parentRecord = parentDir.journalRecord();
+    if ((fileInfo.isFile() && !parentRecord._remotePerm.hasPermission(RemotePermissions::CanAddFile))
+        || (fileInfo.isDir() && !parentRecord._remotePerm.hasPermission(RemotePermissions::CanAddSubDirectories))) {
+        defaultDirAndName = QFileInfo(defaultDirAndName).fileName();
+    }
+
+    // Add back the folder path
+    defaultDirAndName = QDir(fileData.folder->path()).filePath(defaultDirAndName);
+
+    const auto target = QFileDialog::getSaveFileName(
+        nullptr,
+        tr("Select new location â€¦"),
+        defaultDirAndName,
+        QString(), nullptr, QFileDialog::HideNameFilterDetails);
+    if (target.isEmpty())
+        return;
+
+    ConflictSolver solver;
+    solver.setLocalVersionFilename(localFile);
+    solver.setRemoteVersionFilename(target);
+}
+
+void SocketApi::command_V2_LIST_ACCOUNTS(const QString &, SocketListener *listener) const
+{
+    QJsonArray out;
+    for (auto acc : AccountManager::instance()->accounts()) {
+        // TODO: Use uuid once https://github.com/owncloud/client/pull/8397 is merged
+        out << QJsonObject({ { "name", acc->account()->displayName() }, { "id", acc->account()->id() } });
+    }
+    listener->sendMessage(QStringLiteral("V2/ACCOUNTS"), { { "accounts", out } });
+}
+
+void SocketApi::command_V2_UPLOAD_FILES_FROM(const QString &argument, SocketListener *listener) const
+{
+    auto job = new SocketUploadJob(listener, argument);
+    job->start();
+}
+
+void SocketApi::emailPrivateLink(const QString &link)
+{
+    Utility::openEmailComposer(
+        tr("I shared something with you"),
+        link,
+        nullptr);
+}
+
+void OCC::SocketApi::openPrivateLink(const QString &link)
+{
+    Utility::openBrowser(link);
+}
+
+void SocketApi::command_GET_STRINGS(const QString &argument, SocketListener *listener)
+{
+    static std::array<std::pair<const char *, QString>, 5> strings { {
+        { "SHARE_MENU_TITLE", tr("Share options") },
+        { "CONTEXT_MENU_TITLE", Theme::instance()->appNameGUI() },
+        { "COPY_PRIVATE_LINK_MENU_TITLE", tr("Copy private link to clipboard") },
+        { "EMAIL_PRIVATE_LINK_MENU_TITLE", tr("Send private link by email â€¦") },
+        { "CONTEXT_MENU_ICON", APPLICATION_ICON_NAME},
+    } };
+    listener->sendMessage(QString("GET_STRINGS:BEGIN"));
+    for (const auto& key_value : strings) {
+        if (argument.isEmpty() || argument == QLatin1String(key_value.first)) {
+            listener->sendMessage(QString("STRING:%1:%2").arg(key_value.first, key_value.second));
+        }
+    }
+    listener->sendMessage(QString("GET_STRINGS:END"));
+}
+
+void SocketApi::sendSharingContextMenuOptions(const FileData &fileData, SocketListener *listener, bool enabled)
+{
+    auto record = fileData.journalRecord();
+    bool isOnTheServer = record.isValid();
+    auto flagString = isOnTheServer && enabled ? QLatin1String("::") : QLatin1String(":d:");
+
+    auto capabilities = fileData.folder->accountState()->account()->capabilities();
+    auto theme = Theme::instance();
+    if (!capabilities.shareAPI() || !(theme->userGroupSharing() || (theme->linkSharing() && capabilities.sharePublicLink())))
+        return;
+
+    // If sharing is globally disabled, do not show any sharing entries.
+    // If there is no permission to share for this file, add a disabled entry saying so
+    if (isOnTheServer && !record._remotePerm.isNull() && !record._remotePerm.hasPermission(RemotePermissions::CanReshare)) {
+        listener->sendMessage(QLatin1String("MENU_ITEM:DISABLED:d:") + (!record.isDirectory() ? tr("Resharing this file is not allowed") : tr("Resharing this folder is not allowed")));
+    } else {
+        listener->sendMessage(QLatin1String("MENU_ITEM:SHARE") + flagString + tr("Share options"));
+
+        // Do we have public links?
+        bool publicLinksEnabled = theme->linkSharing() && capabilities.sharePublicLink();
+
+        // Is is possible to create a public link without user choices?
+        bool canCreateDefaultPublicLink = publicLinksEnabled
+            && !capabilities.sharePublicLinkEnforceExpireDate()
+            && !capabilities.sharePublicLinkAskOptionalPassword()
+            && !capabilities.sharePublicLinkEnforcePassword();
+
+        if (canCreateDefaultPublicLink) {
+            listener->sendMessage(QLatin1String("MENU_ITEM:COPY_PUBLIC_LINK") + flagString + tr("Copy public link"));
+        } else if (publicLinksEnabled) {
+            listener->sendMessage(QLatin1String("MENU_ITEM:MANAGE_PUBLIC_LINKS") + flagString + tr("Copy public link"));
+        }
+    }
+
+    listener->sendMessage(QLatin1String("MENU_ITEM:COPY_PRIVATE_LINK") + flagString + tr("Copy internal link"));
+
+    // Disabled: only providing email option for private links would look odd,
+    // and the copy option is more general.
+    //listener->sendMessage(QLatin1String("MENU_ITEM:EMAIL_PRIVATE_LINK") + flagString + tr("Send private link by email â€¦"));
+}
+
+SocketApi::FileData SocketApi::FileData::get(const QString &localFile)
+{
+    FileData data;
+
+    data.localPath = QDir::cleanPath(localFile);
+    if (data.localPath.endsWith(QLatin1Char('/')))
+        data.localPath.chop(1);
+
+    data.folder = FolderMan::instance()->folderForPath(data.localPath);
+    if (!data.folder)
+        return data;
+
+    data.folderRelativePath = data.localPath.mid(data.folder->cleanPath().length() + 1);
+    data.serverRelativePath = QDir(data.folder->remotePath()).filePath(data.folderRelativePath);
+    QString virtualFileExt = QStringLiteral(APPLICATION_DOTVIRTUALFILE_SUFFIX);
+    if (data.serverRelativePath.endsWith(virtualFileExt)) {
+        data.serverRelativePath.chop(virtualFileExt.size());
+    }
+    return data;
+}
+
+QString SocketApi::FileData::folderRelativePathNoVfsSuffix() const
+{
+    auto result = folderRelativePath;
+    QString virtualFileExt = QStringLiteral(APPLICATION_DOTVIRTUALFILE_SUFFIX);
+    if (result.endsWith(virtualFileExt)) {
+        result.chop(virtualFileExt.size());
+    }
+    return result;
+}
+
+SyncFileStatus SocketApi::FileData::syncFileStatus() const
+{
+    if (!folder)
+        return SyncFileStatus::StatusNone;
+    return folder->syncEngine().syncFileStatusTracker().fileStatus(folderRelativePath);
+}
+
+SyncJournalFileRecord SocketApi::FileData::journalRecord() const
+{
+    SyncJournalFileRecord record;
+    if (!folder)
+        return record;
+    folder->journalDb()->getFileRecord(folderRelativePath, &record);
+    return record;
+}
+
+SocketApi::FileData SocketApi::FileData::parentFolder() const
+{
+    return FileData::get(QFileInfo(localPath).dir().path().toUtf8());
+}
+
+void SocketApi::command_GET_MENU_ITEMS(const QString &argument, OCC::SocketListener *listener)
+{
+    listener->sendMessage(QString("GET_MENU_ITEMS:BEGIN"));
+    const QStringList files = split(argument);
+
+    // Find the common sync folder.
+    // syncFolder will be null if files are in different folders.
+    Folder *syncFolder = nullptr;
+    for (const auto &file : files) {
+        auto folder = FolderMan::instance()->folderForPath(file);
+        if (folder != syncFolder) {
+            if (!syncFolder) {
+                syncFolder = folder;
+            } else {
+                syncFolder = nullptr;
+                break;
+            }
+        }
+    }
+
+    // Sharing actions show for single files only
+    if (syncFolder && files.size() == 1 && syncFolder->accountState()->isConnected()) {
+        QString systemPath = QDir::cleanPath(argument);
+        if (systemPath.endsWith(QLatin1Char('/'))) {
+            systemPath.truncate(systemPath.length() - 1);
+        }
+
+        FileData fileData = FileData::get(argument);
+        const auto record = fileData.journalRecord();
+        const bool isOnTheServer = record.isValid();
+        const auto isE2eEncryptedPath = fileData.journalRecord()._isE2eEncrypted || !fileData.journalRecord()._e2eMangledName.isEmpty();
+        auto flagString = isOnTheServer && !isE2eEncryptedPath ? QLatin1String("::") : QLatin1String(":d:");
+
+        DirectEditor* editor = getDirectEditorForLocalFile(fileData.localPath);
+        if (editor) {
+            //listener->sendMessage(QLatin1String("MENU_ITEM:EDIT") + flagString + tr("Edit via ") + editor->name());
+            listener->sendMessage(QLatin1String("MENU_ITEM:EDIT") + flagString + tr("Edit"));
+        } else {
+            listener->sendMessage(QLatin1String("MENU_ITEM:OPEN_PRIVATE_LINK") + flagString + tr("Open in browser"));
+        }
+
+        sendSharingContextMenuOptions(fileData, listener, !isE2eEncryptedPath);
+
+        // Conflict files get conflict resolution actions
+        bool isConflict = Utility::isConflictFile(fileData.folderRelativePath);
+        if (isConflict || !isOnTheServer) {
+            // Check whether this new file is in a read-only directory
+            QFileInfo fileInfo(fileData.localPath);
+            const auto parentDir = fileData.parentFolder();
+            const auto parentRecord = parentDir.journalRecord();
+            const bool canAddToDir =
+                !parentRecord.isValid() // We're likely at the root of the sync folder, got to assume we can add there
+                || (fileInfo.isFile() && parentRecord._remotePerm.hasPermission(RemotePermissions::CanAddFile))
+                || (fileInfo.isDir() && parentRecord._remotePerm.hasPermission(RemotePermissions::CanAddSubDirectories));
+            const bool canChangeFile =
+                !isOnTheServer
+                || (record._remotePerm.hasPermission(RemotePermissions::CanDelete)
+                       && record._remotePerm.hasPermission(RemotePermissions::CanMove)
+                       && record._remotePerm.hasPermission(RemotePermissions::CanRename));
+
+            if (isConflict && canChangeFile) {
+                if (canAddToDir) {
+                    listener->sendMessage(QLatin1String("MENU_ITEM:RESOLVE_CONFLICT::") + tr("Resolve conflict â€¦"));
+                } else {
+                    if (isOnTheServer) {
+                        // Uploaded conflict file in read-only directory
+                        listener->sendMessage(QLatin1String("MENU_ITEM:MOVE_ITEM::") + tr("Move and rename â€¦"));
+                    } else {
+                        // Local-only conflict file in a read-only dir
+                        listener->sendMessage(QLatin1String("MENU_ITEM:MOVE_ITEM::") + tr("Move, rename and upload â€¦"));
+                    }
+                    listener->sendMessage(QLatin1String("MENU_ITEM:DELETE_ITEM::") + tr("Delete local changes"));
+                }
+            }
+
+            // File in a read-only directory?
+            if (!isConflict && !isOnTheServer && !canAddToDir) {
+                listener->sendMessage(QLatin1String("MENU_ITEM:MOVE_ITEM::") + tr("Move and upload â€¦"));
+                listener->sendMessage(QLatin1String("MENU_ITEM:DELETE_ITEM::") + tr("Delete"));
+            }
+        }
+    }
+
+    // File availability actions
+    if (syncFolder
+        && syncFolder->virtualFilesEnabled()
+        && syncFolder->vfs().socketApiPinStateActionsShown()) {
+        ENFORCE(!files.isEmpty());
+
+        // Determine the combined availability status of the files
+        auto combined = Optional<VfsItemAvailability>();
+        auto merge = [](VfsItemAvailability lhs, VfsItemAvailability rhs) {
+            if (lhs == rhs)
+                return lhs;
+            if (int(lhs) > int(rhs))
+                std::swap(lhs, rhs); // reduce cases ensuring lhs < rhs
+            if (lhs == VfsItemAvailability::AlwaysLocal && rhs == VfsItemAvailability::AllHydrated)
+                return VfsItemAvailability::AllHydrated;
+            if (lhs == VfsItemAvailability::AllDehydrated && rhs == VfsItemAvailability::OnlineOnly)
+                return VfsItemAvailability::AllDehydrated;
+            return VfsItemAvailability::Mixed;
+        };
+        for (const auto &file : files) {
+            auto fileData = FileData::get(file);
+            auto availability = syncFolder->vfs().availability(fileData.folderRelativePath);
+            if (!availability) {
+                if (availability.error() == Vfs::AvailabilityError::DbError)
+                    availability = VfsItemAvailability::Mixed;
+                if (availability.error() == Vfs::AvailabilityError::NoSuchItem)
+                    continue;
+            }
+            if (!combined) {
+                combined = *availability;
+            } else {
+                combined = merge(*combined, *availability);
+            }
+        }
+
+        // TODO: Should be a submenu, should use icons
+        auto makePinContextMenu = [&](bool makeAvailableLocally, bool freeSpace) {
+            listener->sendMessage(QLatin1String("MENU_ITEM:CURRENT_PIN:d:")
+                + Utility::vfsCurrentAvailabilityText(*combined));
+            listener->sendMessage(QLatin1String("MENU_ITEM:MAKE_AVAILABLE_LOCALLY:")
+                + (makeAvailableLocally ? QLatin1String(":") : QLatin1String("d:"))
+                + Utility::vfsPinActionText());
+            listener->sendMessage(QLatin1String("MENU_ITEM:MAKE_ONLINE_ONLY:")
+                + (freeSpace ? QLatin1String(":") : QLatin1String("d:"))
+                + Utility::vfsFreeSpaceActionText());
+        };
+
+        if (combined) {
+            switch (*combined) {
+            case VfsItemAvailability::AlwaysLocal:
+                makePinContextMenu(false, true);
+                break;
+            case VfsItemAvailability::AllHydrated:
+            case VfsItemAvailability::Mixed:
+                makePinContextMenu(true, true);
+                break;
+            case VfsItemAvailability::AllDehydrated:
+            case VfsItemAvailability::OnlineOnly:
+                makePinContextMenu(true, false);
+                break;
+            }
+        }
+    }
+
+    listener->sendMessage(QString("GET_MENU_ITEMS:END"));
+}
+
+DirectEditor* SocketApi::getDirectEditorForLocalFile(const QString &localFile)
+{
+    FileData fileData = FileData::get(localFile);
+    auto capabilities = fileData.folder->accountState()->account()->capabilities();
+
+    if (fileData.folder && fileData.folder->accountState()->isConnected()) {
+        const auto record = fileData.journalRecord();
+        const auto mimeMatchMode = record.isVirtualFile() ? QMimeDatabase::MatchExtension : QMimeDatabase::MatchDefault;
+
+        QMimeDatabase db;
+        QMimeType type = db.mimeTypeForFile(localFile, mimeMatchMode);
+
+        DirectEditor* editor = capabilities.getDirectEditorForMimetype(type);
+        if (!editor) {
+            editor = capabilities.getDirectEditorForOptionalMimetype(type);
+        }
+        return editor;
+    }
+
+    return nullptr;
+}
+
+#if GUI_TESTING
+void SocketApi::command_ASYNC_LIST_WIDGETS(const QSharedPointer<SocketApiJob> &job)
+{
+    QString response;
+    for (auto &widget : allObjects(QApplication::allWidgets())) {
+        auto objectName = widget->objectName();
+        if (!objectName.isEmpty()) {
+            response += objectName + ":" + widget->property("text").toString() + ", ";
+        }
+    }
+    job->resolve(response);
+}
+
+void SocketApi::command_ASYNC_INVOKE_WIDGET_METHOD(const QSharedPointer<SocketApiJob> &job)
+{
+    auto &arguments = job->arguments();
+
+    auto widget = findWidget(arguments["objectName"].toString());
+    if (!widget) {
+        job->reject(QLatin1String("widget not found"));
+        return;
+    }
+
+    QMetaObject::invokeMethod(widget, arguments["method"].toString().toUtf8().constData());
+    job->resolve();
+}
+
+void SocketApi::command_ASYNC_GET_WIDGET_PROPERTY(const QSharedPointer<SocketApiJob> &job)
+{
+    QString widgetName = job->arguments()[QLatin1String("objectName")].toString();
+    auto widget = findWidget(widgetName);
+    if (!widget) {
+        QString message = QString(QLatin1String("Widget not found: 2: %1")).arg(widgetName);
+        job->reject(message);
+        return;
+    }
+
+    auto propertyName = job->arguments()[QLatin1String("property")].toString();
+
+    auto segments = propertyName.split('.');
+
+    QObject *currentObject = widget;
+    QString value;
+    for (int i = 0; i < segments.count(); i++) {
+        auto segment = segments.at(i);
+        auto var = currentObject->property(segment.toUtf8().constData());
+
+        if (var.canConvert<QString>()) {
+            var.convert(QMetaType::QString);
+            value = var.value<QString>();
+            break;
+        }
+
+        auto tmpObject = var.value<QObject *>();
+        if (tmpObject) {
+            currentObject = tmpObject;
+        } else {
+            QString message = QString(QLatin1String("Widget not found: 3: %1")).arg(widgetName);
+            job->reject(message);
+            return;
+        }
+    }
+
+    job->resolve(value);
+}
+
+void SocketApi::command_ASYNC_SET_WIDGET_PROPERTY(const QSharedPointer<SocketApiJob> &job)
+{
+    auto &arguments = job->arguments();
+    QString widgetName = arguments["objectName"].toString();
+    auto widget = findWidget(widgetName);
+    if (!widget) {
+        QString message = QString(QLatin1String("Widget not found: 4: %1")).arg(widgetName);
+        job->reject(message);
+        return;
+    }
+    widget->setProperty(arguments["property"].toString().toUtf8().constData(),
+        arguments["value"]);
+
+    job->resolve();
+}
+
+void SocketApi::command_ASYNC_WAIT_FOR_WIDGET_SIGNAL(const QSharedPointer<SocketApiJob> &job)
+{
+    auto &arguments = job->arguments();
+    QString widgetName = arguments["objectName"].toString();
+    auto widget = findWidget(arguments["objectName"].toString());
+    if (!widget) {
+        QString message = QString(QLatin1String("Widget not found: 5: %1")).arg(widgetName);
+        job->reject(message);
+        return;
+    }
+
+    ListenerClosure *closure = new ListenerClosure([job]() { job->resolve("signal emitted"); });
+
+    auto signalSignature = arguments["signalSignature"].toString();
+    signalSignature.prepend("2");
+    auto utf8 = signalSignature.toUtf8();
+    auto signalSignatureFinal = utf8.constData();
+    connect(widget, signalSignatureFinal, closure, SLOT(closureSlot()), Qt::QueuedConnection);
+}
+
+void SocketApi::command_ASYNC_TRIGGER_MENU_ACTION(const QSharedPointer<SocketApiJob> &job)
+{
+    auto &arguments = job->arguments();
+
+    auto objectName = arguments["objectName"].toString();
+    auto widget = findWidget(objectName);
+    if (!widget) {
+        QString message = QString(QLatin1String("Object not found: 1: %1")).arg(objectName);
+        job->reject(message);
+        return;
+    }
+
+    auto children = widget->findChildren<QWidget *>();
+    for (auto childWidget : children) {
+        // foo is the popupwidget!
+        auto actions = childWidget->actions();
+        for (auto action : actions) {
+            if (action->objectName() == arguments["actionName"].toString()) {
+                action->trigger();
+
+                job->resolve("action found");
+                return;
+            }
+        }
+    }
+
+    QString message = QString(QLatin1String("Action not found: 1: %1")).arg(arguments["actionName"].toString());
+    job->reject(message);
+}
+
+void SocketApi::command_ASYNC_ASSERT_ICON_IS_EQUAL(const QSharedPointer<SocketApiJob> &job)
+{
+    auto widget = findWidget(job->arguments()[QLatin1String("queryString")].toString());
+    if (!widget) {
+        QString message = QString(QLatin1String("Object not found: 6: %1")).arg(job->arguments()["queryString"].toString());
+        job->reject(message);
+        return;
+    }
+
+    auto propertyName = job->arguments()[QLatin1String("propertyPath")].toString();
+
+    auto segments = propertyName.split('.');
+
+    QObject *currentObject = widget;
+    QIcon value;
+    for (int i = 0; i < segments.count(); i++) {
+        auto segment = segments.at(i);
+        auto var = currentObject->property(segment.toUtf8().constData());
+
+        if (var.canConvert<QIcon>()) {
+            var.convert(QMetaType::QIcon);
+            value = var.value<QIcon>();
+            break;
+        }
+
+        auto tmpObject = var.value<QObject *>();
+        if (tmpObject) {
+            currentObject = tmpObject;
+        } else {
+            job->reject(QString(QLatin1String("Icon not found: %1")).arg(propertyName));
+        }
+    }
+
+    auto iconName = job->arguments()[QLatin1String("iconName")].toString();
+    if (value.name() == iconName) {
+        job->resolve();
+    } else {
+        job->reject("iconName " + iconName + " does not match: " + value.name());
+    }
+}
+#endif
+
+QString SocketApi::buildRegisterPathMessage(const QString &path)
+{
+    QFileInfo fi(path);
+    QString message = QLatin1String("REGISTER_PATH:");
+    message.append(QDir::toNativeSeparators(fi.absoluteFilePath()));
+    return message;
+}
+
+} // namespace OCC
+
+#include "socketapi.moc"
diff --git a/src/gui/socketapi/socketapi.h b/src/gui/socketapi/socketapi.h
new file mode 100644 (file)
index 0000000..cd7d9ff
--- /dev/null
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) by Dominik Schmidt <dev@dominik-schmidt.de>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+
+#ifndef SOCKETAPI_H
+#define SOCKETAPI_H
+
+#include "syncfileitem.h"
+#include "common/syncfilestatus.h"
+#include "sharedialog.h" // for the ShareDialogStartPage
+#include "common/syncjournalfilerecord.h"
+
+#include "config.h"
+
+#if defined(Q_OS_MAC)
+#include "socketapisocket_mac.h"
+#else
+#include <QLocalServer>
+using SocketApiServer = QLocalServer;
+#endif
+
+class QUrl;
+class QLocalSocket;
+class QStringList;
+
+namespace OCC {
+
+class SyncFileStatus;
+class Folder;
+class SocketListener;
+class DirectEditor;
+class SocketApiJob;
+
+Q_DECLARE_LOGGING_CATEGORY(lcSocketApi)
+
+/**
+ * @brief The SocketApi class
+ * @ingroup gui
+ */
+class SocketApi : public QObject
+{
+    Q_OBJECT
+
+public:
+    explicit SocketApi(QObject *parent = nullptr);
+    virtual ~SocketApi();
+
+public slots:
+    void slotUpdateFolderView(Folder *f);
+    void slotUnregisterPath(const QString &alias);
+    void slotRegisterPath(const QString &alias);
+    void broadcastStatusPushMessage(const QString &systemPath, SyncFileStatus fileStatus);
+
+signals:
+    void shareCommandReceived(const QString &sharePath, const QString &localPath, ShareDialogStartPage startPage);
+
+private slots:
+    void slotNewConnection();
+    void onLostConnection();
+    void slotSocketDestroyed(QObject *obj);
+    void slotReadSocket();
+
+    static void copyUrlToClipboard(const QString &link);
+    static void emailPrivateLink(const QString &link);
+    static void openPrivateLink(const QString &link);
+
+private:
+    // Helper structure for getting information on a file
+    // based on its local path - used for nearly all remote
+    // actions.
+    struct FileData
+    {
+        static FileData get(const QString &localFile);
+        SyncFileStatus syncFileStatus() const;
+        SyncJournalFileRecord journalRecord() const;
+        FileData parentFolder() const;
+
+        // Relative path of the file locally, without any vfs suffix
+        QString folderRelativePathNoVfsSuffix() const;
+
+        Folder *folder;
+        // Absolute path of the file locally. (May be a virtual file)
+        QString localPath;
+        // Relative path of the file locally, as in the DB. (May be a virtual file)
+        QString folderRelativePath;
+        // Path of the file on the server (In case of virtual file, it points to the actual file)
+        QString serverRelativePath;
+    };
+
+    void broadcastMessage(const QString &msg, bool doWait = false);
+
+    // opens share dialog, sends reply
+    void processShareRequest(const QString &localFile, SocketListener *listener, ShareDialogStartPage startPage);
+
+    Q_INVOKABLE void command_RETRIEVE_FOLDER_STATUS(const QString &argument, SocketListener *listener);
+    Q_INVOKABLE void command_RETRIEVE_FILE_STATUS(const QString &argument, SocketListener *listener);
+
+    Q_INVOKABLE void command_VERSION(const QString &argument, SocketListener *listener);
+
+    Q_INVOKABLE void command_SHARE_MENU_TITLE(const QString &argument, SocketListener *listener);
+
+    // The context menu actions
+    Q_INVOKABLE void command_SHARE(const QString &localFile, SocketListener *listener);
+    Q_INVOKABLE void command_MANAGE_PUBLIC_LINKS(const QString &localFile, SocketListener *listener);
+    Q_INVOKABLE void command_COPY_PUBLIC_LINK(const QString &localFile, SocketListener *listener);
+    Q_INVOKABLE void command_COPY_PRIVATE_LINK(const QString &localFile, SocketListener *listener);
+    Q_INVOKABLE void command_EMAIL_PRIVATE_LINK(const QString &localFile, SocketListener *listener);
+    Q_INVOKABLE void command_OPEN_PRIVATE_LINK(const QString &localFile, SocketListener *listener);
+    Q_INVOKABLE void command_MAKE_AVAILABLE_LOCALLY(const QString &filesArg, SocketListener *listener);
+    Q_INVOKABLE void command_MAKE_ONLINE_ONLY(const QString &filesArg, SocketListener *listener);
+    Q_INVOKABLE void command_RESOLVE_CONFLICT(const QString &localFile, SocketListener *listener);
+    Q_INVOKABLE void command_DELETE_ITEM(const QString &localFile, SocketListener *listener);
+    Q_INVOKABLE void command_MOVE_ITEM(const QString &localFile, SocketListener *listener);
+
+    // Windows Shell / Explorer pinning fallbacks, see issue: https://github.com/nextcloud/desktop/issues/1599
+#ifdef Q_OS_WIN
+    Q_INVOKABLE void command_COPYASPATH(const QString &localFile, SocketListener *listener);
+    Q_INVOKABLE void command_OPENNEWWINDOW(const QString &localFile, SocketListener *listener);
+    Q_INVOKABLE void command_OPEN(const QString &localFile, SocketListener *listener);
+#endif
+
+    // External sync
+    Q_INVOKABLE void command_V2_LIST_ACCOUNTS(const QString &argument, SocketListener *listener) const;
+    Q_INVOKABLE void command_V2_UPLOAD_FILES_FROM(const QString &argument, SocketListener *listener) const;
+
+    // Fetch the private link and call targetFun
+    void fetchPrivateLinkUrlHelper(const QString &localFile, const std::function<void(const QString &url)> &targetFun);
+
+    /** Sends translated/branded strings that may be useful to the integration */
+    Q_INVOKABLE void command_GET_STRINGS(const QString &argument, SocketListener *listener);
+
+    // Sends the context menu options relating to sharing to listener
+    void sendSharingContextMenuOptions(const FileData &fileData, SocketListener *listener, bool enabled);
+
+    /** Send the list of menu item. (added in version 1.1)
+     * argument is a list of files for which the menu should be shown, separated by '\x1e'
+     * Reply with  GET_MENU_ITEMS:BEGIN
+     * followed by several MENU_ITEM:[Action]:[flag]:[Text]
+     * If flag contains 'd', the menu should be disabled
+     * and ends with GET_MENU_ITEMS:END
+     */
+    Q_INVOKABLE void command_GET_MENU_ITEMS(const QString &argument, SocketListener *listener);
+
+    /// Direct Editing
+    Q_INVOKABLE void command_EDIT(const QString &localFile, SocketListener *listener);
+    DirectEditor* getDirectEditorForLocalFile(const QString &localFile);
+
+#if GUI_TESTING
+    Q_INVOKABLE void command_ASYNC_ASSERT_ICON_IS_EQUAL(const QSharedPointer<SocketApiJob> &job);
+    Q_INVOKABLE void command_ASYNC_LIST_WIDGETS(const QSharedPointer<SocketApiJob> &job);
+    Q_INVOKABLE void command_ASYNC_INVOKE_WIDGET_METHOD(const QSharedPointer<SocketApiJob> &job);
+    Q_INVOKABLE void command_ASYNC_GET_WIDGET_PROPERTY(const QSharedPointer<SocketApiJob> &job);
+    Q_INVOKABLE void command_ASYNC_SET_WIDGET_PROPERTY(const QSharedPointer<SocketApiJob> &job);
+    Q_INVOKABLE void command_ASYNC_WAIT_FOR_WIDGET_SIGNAL(const QSharedPointer<SocketApiJob> &job);
+    Q_INVOKABLE void command_ASYNC_TRIGGER_MENU_ACTION(const QSharedPointer<SocketApiJob> &job);
+#endif
+
+    QString buildRegisterPathMessage(const QString &path);
+
+    QSet<QString> _registeredAliases;
+    QList<SocketListener> _listeners;
+    SocketApiServer _localServer;
+};
+}
+
+#endif // SOCKETAPI_H
diff --git a/src/gui/socketapi/socketapi_p.h b/src/gui/socketapi/socketapi_p.h
new file mode 100644 (file)
index 0000000..38738c8
--- /dev/null
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) by Dominik Schmidt <dev@dominik-schmidt.de>
+ * Copyright (C) by Klaas Freitag <freitag@owncloud.com>
+ * Copyright (C) by Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+
+#ifndef SOCKETAPI_P_H
+#define SOCKETAPI_P_H
+
+#include <functional>
+#include <QBitArray>
+#include <QPointer>
+
+#include <QJsonDocument>
+#include <QJsonObject>
+
+#include <memory>
+#include <QTimer>
+
+namespace OCC {
+
+class BloomFilter
+{
+    // Initialize with m=1024 bits and k=2 (high and low 16 bits of a qHash).
+    // For a client navigating in less than 100 directories, this gives us a probability less than
+    // (1-e^(-2*100/1024))^2 = 0.03147872136 false positives.
+    const static int NumBits = 1024;
+
+public:
+    BloomFilter()
+        : hashBits(NumBits)
+    {
+    }
+
+    void storeHash(uint hash)
+    {
+        hashBits.setBit((hash & 0xFFFF) % NumBits); // NOLINT it's uint all the way and the modulo puts us back in the 0..1023 range
+        hashBits.setBit((hash >> 16) % NumBits); // NOLINT
+    }
+    bool isHashMaybeStored(uint hash) const
+    {
+        return hashBits.testBit((hash & 0xFFFF) % NumBits) // NOLINT
+            && hashBits.testBit((hash >> 16) % NumBits); // NOLINT
+    }
+
+private:
+    QBitArray hashBits;
+};
+
+class SocketListener
+{
+public:
+    QPointer<QIODevice> socket;
+
+    explicit SocketListener(QIODevice *socket)
+        : socket(socket)
+    {
+    }
+
+    void sendMessage(const QString &message, bool doWait = false) const;
+    void sendMessage(const QString &function, const QJsonObject &obj, bool doWait = false) const;
+
+    void sendMessageIfDirectoryMonitored(const QString &message, uint systemDirectoryHash) const
+    {
+        if (_monitoredDirectoriesBloomFilter.isHashMaybeStored(systemDirectoryHash))
+            sendMessage(message, false);
+    }
+
+    void registerMonitoredDirectory(uint systemDirectoryHash)
+    {
+        _monitoredDirectoriesBloomFilter.storeHash(systemDirectoryHash);
+    }
+
+private:
+    BloomFilter _monitoredDirectoriesBloomFilter;
+};
+
+class ListenerClosure : public QObject
+{
+    Q_OBJECT
+public:
+    using CallbackFunction = std::function<void()>;
+    ListenerClosure(CallbackFunction callback)
+        : callback_(callback)
+    {
+    }
+
+public slots:
+    void closureSlot()
+    {
+        callback_();
+        deleteLater();
+    }
+
+private:
+    CallbackFunction callback_;
+};
+
+class SocketApiJob : public QObject
+{
+    Q_OBJECT
+public:
+    SocketApiJob(const QString &jobId, SocketListener *socketListener, const QJsonObject &arguments)
+        : _jobId(jobId)
+        , _socketListener(socketListener)
+        , _arguments(arguments)
+    {
+    }
+
+    void resolve(const QString &response = QString())
+    {
+        _socketListener->sendMessage(QLatin1String("RESOLVE|") + _jobId + '|' + response);
+    }
+
+    void resolve(const QJsonObject &response) { resolve(QJsonDocument { response }.toJson()); }
+
+    const QJsonObject &arguments() { return _arguments; }
+
+    void reject(const QString &response)
+    {
+        _socketListener->sendMessage(QLatin1String("REJECT|") + _jobId + '|' + response);
+    }
+
+private:
+    QString _jobId;
+    SocketListener *_socketListener;
+    QJsonObject _arguments;
+};
+}
+
+Q_DECLARE_METATYPE(OCC::SocketListener *)
+
+#endif // SOCKETAPI_P_H
diff --git a/src/gui/socketapi/socketapisocket_mac.h b/src/gui/socketapi/socketapisocket_mac.h
new file mode 100644 (file)
index 0000000..b765453
--- /dev/null
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) by Jocelyn Turcotte <jturcotte@woboq.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+
+#ifndef SOCKETAPISOCKET_OSX_H
+#define SOCKETAPISOCKET_OSX_H
+
+#include <QAbstractSocket>
+#include <QIODevice>
+
+class SocketApiServerPrivate;
+class SocketApiSocketPrivate;
+
+class SocketApiSocket : public QIODevice
+{
+    Q_OBJECT
+public:
+    SocketApiSocket(QObject *parent, SocketApiSocketPrivate *p);
+    ~SocketApiSocket();
+
+    qint64 readData(char *data, qint64 maxlen) override;
+    qint64 writeData(const char *data, qint64 len) override;
+
+    bool isSequential() const override { return true; }
+    qint64 bytesAvailable() const override;
+    bool canReadLine() const override;
+
+signals:
+    void disconnected();
+
+private:
+    // Use Qt's p-impl system to hide objective-c types from C++ code including this file
+    Q_DECLARE_PRIVATE(SocketApiSocket)
+    QScopedPointer<SocketApiSocketPrivate> d_ptr;
+    friend class SocketApiServerPrivate;
+};
+
+class SocketApiServer : public QObject
+{
+    Q_OBJECT
+public:
+    SocketApiServer();
+    ~SocketApiServer();
+
+    void close();
+    bool listen(const QString &name);
+    SocketApiSocket *nextPendingConnection();
+
+    static bool removeServer(const QString &) { return false; }
+
+signals:
+    void newConnection();
+
+private:
+    Q_DECLARE_PRIVATE(SocketApiServer)
+    QScopedPointer<SocketApiServerPrivate> d_ptr;
+};
+
+#endif // SOCKETAPISOCKET_OSX_H
diff --git a/src/gui/socketapi/socketapisocket_mac.mm b/src/gui/socketapi/socketapisocket_mac.mm
new file mode 100644 (file)
index 0000000..926c34d
--- /dev/null
@@ -0,0 +1,245 @@
+/*
+ * Copyright (C) by Jocelyn Turcotte <jturcotte@woboq.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+
+#include "socketapisocket_mac.h"
+#import <Cocoa/Cocoa.h>
+
+@protocol ChannelProtocol <NSObject>
+- (void)sendMessage:(NSData *)msg;
+@end
+
+@protocol RemoteEndProtocol <NSObject, ChannelProtocol>
+- (void)registerTransmitter:(id)tx;
+@end
+
+@interface LocalEnd : NSObject <ChannelProtocol>
+@property SocketApiSocketPrivate *wrapper;
+- (instancetype)initWithWrapper:(SocketApiSocketPrivate *)wrapper;
+@end
+
+@interface Server : NSObject
+@property SocketApiServerPrivate *wrapper;
+- (instancetype)initWithWrapper:(SocketApiServerPrivate *)wrapper;
+- (void)registerClient:(NSDistantObject<RemoteEndProtocol> *)remoteEnd;
+@end
+
+class SocketApiSocketPrivate
+{
+public:
+    SocketApiSocket *q_ptr;
+
+    SocketApiSocketPrivate(NSDistantObject<ChannelProtocol> *remoteEnd);
+    ~SocketApiSocketPrivate();
+
+    // release remoteEnd
+    void disconnectRemote();
+
+    NSDistantObject<ChannelProtocol> *remoteEnd;
+    LocalEnd *localEnd;
+    QByteArray inBuffer;
+    bool isRemoteDisconnected = false;
+};
+
+class SocketApiServerPrivate
+{
+public:
+    SocketApiServer *q_ptr;
+
+    SocketApiServerPrivate();
+    ~SocketApiServerPrivate();
+
+    QList<SocketApiSocket *> pendingConnections;
+    NSConnection *connection;
+    Server *server;
+};
+
+
+@implementation LocalEnd
+- (instancetype)initWithWrapper:(SocketApiSocketPrivate *)wrapper
+{
+    self = [super init];
+    self->_wrapper = wrapper;
+    return self;
+}
+
+- (void)sendMessage:(NSData *)msg
+{
+    if (_wrapper) {
+        _wrapper->inBuffer += QByteArray::fromRawNSData(msg);
+        emit _wrapper->q_ptr->readyRead();
+    }
+}
+
+- (void)connectionDidDie:(NSNotification *)notification
+{
+#pragma unused(notification)
+    if (_wrapper) {
+        _wrapper->disconnectRemote();
+        emit _wrapper->q_ptr->disconnected();
+    }
+}
+@end
+
+@implementation Server
+- (instancetype)initWithWrapper:(SocketApiServerPrivate *)wrapper
+{
+    self = [super init];
+    self->_wrapper = wrapper;
+    return self;
+}
+
+- (void)registerClient:(NSDistantObject<RemoteEndProtocol> *)remoteEnd
+{
+    // This saves a few mach messages that would otherwise be needed to query the interface
+    [remoteEnd setProtocolForProxy:@protocol(RemoteEndProtocol)];
+
+    SocketApiServer *server = _wrapper->q_ptr;
+    SocketApiSocketPrivate *socketPrivate = new SocketApiSocketPrivate(remoteEnd);
+    SocketApiSocket *socket = new SocketApiSocket(server, socketPrivate);
+    _wrapper->pendingConnections.append(socket);
+    emit server->newConnection();
+
+    [remoteEnd registerTransmitter:socketPrivate->localEnd];
+}
+@end
+
+
+SocketApiSocket::SocketApiSocket(QObject *parent, SocketApiSocketPrivate *p)
+    : QIODevice(parent)
+    , d_ptr(p)
+{
+    Q_D(SocketApiSocket);
+    d->q_ptr = this;
+    open(ReadWrite);
+}
+
+SocketApiSocket::~SocketApiSocket()
+{
+}
+
+qint64 SocketApiSocket::readData(char *data, qint64 maxlen)
+{
+    Q_D(SocketApiSocket);
+    qint64 len = std::min(maxlen, static_cast<qint64>(d->inBuffer.size()));
+    memcpy(data, d->inBuffer.constData(), len);
+    d->inBuffer.remove(0, len);
+    return len;
+}
+
+qint64 SocketApiSocket::writeData(const char *data, qint64 len)
+{
+    Q_D(SocketApiSocket);
+    if (d->isRemoteDisconnected)
+        return -1;
+
+    @try {
+        // FIXME: The NSConnection will make this block unless the function is marked as "oneway"
+        // in the protocol. This isn't async and reduces our performances but this currectly avoids
+        // a Mach queue deadlock during requests bursts of the legacy OwnCloudFinder extension.
+        // Since FinderSync already runs in a separate process, blocking isn't too critical.
+        [d->remoteEnd sendMessage:[NSData dataWithBytesNoCopy:const_cast<char *>(data) length:len freeWhenDone:NO]];
+        return len;
+    } @catch (NSException *e) {
+        // connectionDidDie can be notified too late, also interpret any sending exception as a disconnection.
+        d->disconnectRemote();
+        emit disconnected();
+        return -1;
+    }
+}
+
+qint64 SocketApiSocket::bytesAvailable() const
+{
+    Q_D(const SocketApiSocket);
+    return d->inBuffer.size() + QIODevice::bytesAvailable();
+}
+
+bool SocketApiSocket::canReadLine() const
+{
+    Q_D(const SocketApiSocket);
+    return d->inBuffer.indexOf('\n', int(pos())) != -1 || QIODevice::canReadLine();
+}
+
+SocketApiSocketPrivate::SocketApiSocketPrivate(NSDistantObject<ChannelProtocol> *remoteEnd)
+    : remoteEnd(remoteEnd)
+    , localEnd([[LocalEnd alloc] initWithWrapper:this])
+{
+    [remoteEnd retain];
+    // (Ab)use our objective-c object just to catch the notification
+    [[NSNotificationCenter defaultCenter] addObserver:localEnd
+                                             selector:@selector(connectionDidDie:)
+                                                 name:NSConnectionDidDieNotification
+                                               object:[remoteEnd connectionForProxy]];
+}
+
+SocketApiSocketPrivate::~SocketApiSocketPrivate()
+{
+    disconnectRemote();
+
+    // The DO vended localEnd might still be referenced by the connection
+    localEnd.wrapper = nil;
+    [localEnd release];
+}
+
+void SocketApiSocketPrivate::disconnectRemote()
+{
+    if (isRemoteDisconnected)
+        return;
+    isRemoteDisconnected = true;
+
+    [remoteEnd release];
+}
+
+SocketApiServer::SocketApiServer()
+    : d_ptr(new SocketApiServerPrivate)
+{
+    Q_D(SocketApiServer);
+    d->q_ptr = this;
+}
+
+SocketApiServer::~SocketApiServer()
+{
+}
+
+void SocketApiServer::close()
+{
+    // Assume we'll be destroyed right after
+}
+
+bool SocketApiServer::listen(const QString &name)
+{
+    Q_D(SocketApiServer);
+    // Set the name of the root object
+    return [d->connection registerName:name.toNSString()];
+}
+
+SocketApiSocket *SocketApiServer::nextPendingConnection()
+{
+    Q_D(SocketApiServer);
+    return d->pendingConnections.takeFirst();
+}
+
+SocketApiServerPrivate::SocketApiServerPrivate()
+{
+    // Create the connection and server object to vend over Disributed Objects
+    connection = [[NSConnection alloc] init];
+    server = [[Server alloc] initWithWrapper:this];
+    [connection setRootObject:server];
+}
+
+SocketApiServerPrivate::~SocketApiServerPrivate()
+{
+    [connection release];
+    server.wrapper = nil;
+    [server release];
+}
diff --git a/src/gui/socketapi/socketuploadjob.cpp b/src/gui/socketapi/socketuploadjob.cpp
new file mode 100644 (file)
index 0000000..8ade844
--- /dev/null
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+
+#include "socketuploadjob.h"
+#include "socketapi_p.h"
+
+#include "accountmanager.h"
+#include "common/syncjournaldb.h"
+#include "syncengine.h"
+
+#include <QFileInfo>
+#include <QJsonArray>
+#include <QRegularExpression>
+
+using namespace OCC;
+
+SocketUploadJob::SocketUploadJob(OCC::SocketListener *listener, const QString &argument, QObject *parent)
+    : QObject(parent)
+    , _listener(listener)
+{
+    const auto args = QJsonDocument::fromJson(argument.toUtf8()).object();
+    _localPath = args[QLatin1String("localPath")].toString();
+    _remotePath = args[QLatin1String("remotePath")].toString();
+    if (!_remotePath.startsWith("/")) {
+        _remotePath = QLatin1Char('/') + _remotePath;
+    }
+
+    _pattern = args[QLatin1String("pattern")].toString();
+    // TODO: use uuid
+    const auto accname = args[QLatin1String("account")][QLatin1String("name")].toString();
+    auto account = AccountManager::instance()->account(accname);
+
+    ENFORCE(QFileInfo(_localPath).isAbsolute())
+    ENFORCE(_tmp.open())
+
+    _db = new SyncJournalDb(_tmp.fileName(), this);
+    _engine = new SyncEngine(account->account(), _localPath.endsWith(QLatin1Char('/')) ? _localPath : _localPath + QLatin1Char('/'), _remotePath, _db);
+    _engine->setParent(_db);
+
+    connect(_engine, &OCC::SyncEngine::itemCompleted, this, [this](const OCC::SyncFileItemPtr item) {
+        _syncedFiles.append(item->_file);
+    });
+
+    connect(_engine, &OCC::SyncEngine::finished, this, [this](bool ok) {
+        if (ok) {
+            finish({});
+        }
+    });
+    connect(_engine, &OCC::SyncEngine::syncError, this, &SocketUploadJob::finish);
+}
+
+void SocketUploadJob::start()
+{
+    auto opt = _engine->syncOptions();
+    opt.setFilePattern(_pattern);
+    if (!opt.fileRegex().isValid()) {
+        finish(opt.fileRegex().errorString());
+        return;
+    }
+    _engine->setSyncOptions(opt);
+
+    // create the dir, fail if it already exists
+    auto mkdir = new OCC::MkColJob(_engine->account(), _remotePath);
+    connect(mkdir, &OCC::MkColJob::finishedWithoutError, _engine, &OCC::SyncEngine::startSync);
+    connect(mkdir, &OCC::MkColJob::finishedWithError, this, [this](QNetworkReply *reply) {
+        if (reply->error() == 202) {
+            finish(QStringLiteral("Destination %1 already exists").arg(_remotePath));
+        } else {
+            finish(reply->errorString());
+        }
+    });
+    mkdir->start();
+}
+
+void SocketUploadJob::finish(const QString &error)
+{
+    if (!_finished) {
+        _finished = true;
+        _listener->sendMessage(QStringLiteral("V2/UPLOAD_FILES_RESULT"), { { "localPath", _localPath }, { "error", error }, { "syncedFiles", QJsonArray::fromStringList(_syncedFiles) } });
+        deleteLater();
+    }
+}
diff --git a/src/gui/socketapi/socketuploadjob.h b/src/gui/socketapi/socketuploadjob.h
new file mode 100644 (file)
index 0000000..477b8bf
--- /dev/null
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+
+#pragma once
+#include <QObject>
+#include <QTemporaryFile>
+
+#include "socketapi.h"
+#include "account.h"
+
+namespace OCC {
+
+class SyncJournalDb;
+class SyncEngine;
+
+class SocketUploadJob : public QObject
+{
+    Q_OBJECT
+public:
+    SocketUploadJob(OCC::SocketListener *listener, const QString &argument, QObject *parent = nullptr);
+
+    void start();
+
+    void finish(const QString &error);
+
+private:
+    SocketListener *_listener;
+    QString _localPath;
+    QString _remotePath;
+    QString _pattern;
+    QTemporaryFile _tmp;
+    SyncJournalDb *_db;
+    SyncEngine *_engine;
+    QStringList _syncedFiles;
+
+    bool _finished = false;
+};
+}
diff --git a/src/gui/socketapi_p.h b/src/gui/socketapi_p.h
deleted file mode 100644 (file)
index da3e90b..0000000
+++ /dev/null
@@ -1,142 +0,0 @@
-/*
- * Copyright (C) by Dominik Schmidt <dev@dominik-schmidt.de>
- * Copyright (C) by Klaas Freitag <freitag@owncloud.com>
- * Copyright (C) by Roeland Jago Douma <roeland@famdouma.nl>
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
- * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
- * for more details.
- */
-
-#ifndef SOCKETAPI_P_H
-#define SOCKETAPI_P_H
-
-#include <functional>
-#include <QBitArray>
-#include <QPointer>
-
-#include <QJsonDocument>
-#include <QJsonObject>
-
-#include <memory>
-#include <QTimer>
-
-namespace OCC {
-
-class BloomFilter
-{
-    // Initialize with m=1024 bits and k=2 (high and low 16 bits of a qHash).
-    // For a client navigating in less than 100 directories, this gives us a probability less than
-    // (1-e^(-2*100/1024))^2 = 0.03147872136 false positives.
-    const static int NumBits = 1024;
-
-public:
-    BloomFilter()
-        : hashBits(NumBits)
-    {
-    }
-
-    void storeHash(uint hash)
-    {
-        hashBits.setBit((hash & 0xFFFF) % NumBits); // NOLINT it's uint all the way and the modulo puts us back in the 0..1023 range
-        hashBits.setBit((hash >> 16) % NumBits); // NOLINT
-    }
-    bool isHashMaybeStored(uint hash) const
-    {
-        return hashBits.testBit((hash & 0xFFFF) % NumBits) // NOLINT
-            && hashBits.testBit((hash >> 16) % NumBits); // NOLINT
-    }
-
-private:
-    QBitArray hashBits;
-};
-
-class SocketListener
-{
-public:
-    QPointer<QIODevice> socket;
-
-    explicit SocketListener(QIODevice *socket)
-        : socket(socket)
-    {
-    }
-
-    void sendMessage(const QString &message, bool doWait = false) const;
-
-    void sendMessageIfDirectoryMonitored(const QString &message, uint systemDirectoryHash) const
-    {
-        if (_monitoredDirectoriesBloomFilter.isHashMaybeStored(systemDirectoryHash))
-            sendMessage(message, false);
-    }
-
-    void registerMonitoredDirectory(uint systemDirectoryHash)
-    {
-        _monitoredDirectoriesBloomFilter.storeHash(systemDirectoryHash);
-    }
-
-private:
-    BloomFilter _monitoredDirectoriesBloomFilter;
-};
-
-class ListenerClosure : public QObject
-{
-    Q_OBJECT
-public:
-    using CallbackFunction = std::function<void()>;
-    ListenerClosure(CallbackFunction callback)
-        : callback_(callback)
-    {
-    }
-
-public slots:
-    void closureSlot()
-    {
-        callback_();
-        deleteLater();
-    }
-
-private:
-    CallbackFunction callback_;
-};
-
-class SocketApiJob : public QObject
-{
-    Q_OBJECT
-public:
-    SocketApiJob(const QString &jobId, SocketListener *socketListener, const QJsonObject &arguments)
-        : _jobId(jobId)
-        , _socketListener(socketListener)
-        , _arguments(arguments)
-    {
-    }
-
-    void resolve(const QString &response = QString())
-    {
-        _socketListener->sendMessage(QLatin1String("RESOLVE|") + _jobId + '|' + response);
-    }
-
-    void resolve(const QJsonObject &response) { resolve(QJsonDocument{ response }.toJson()); }
-
-    const QJsonObject &arguments() { return _arguments; }
-
-    void reject(const QString &response)
-    {
-        _socketListener->sendMessage(QLatin1String("REJECT|") + _jobId + '|' + response);
-    }
-
-private:
-    QString _jobId;
-    SocketListener *_socketListener;
-    QJsonObject _arguments;
-};
-}
-
-Q_DECLARE_METATYPE(OCC::SocketListener *)
-
-#endif // SOCKETAPI_P_H
diff --git a/src/gui/socketapisocket_mac.h b/src/gui/socketapisocket_mac.h
deleted file mode 100644 (file)
index b765453..0000000
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * Copyright (C) by Jocelyn Turcotte <jturcotte@woboq.com>
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
- * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
- * for more details.
- */
-
-#ifndef SOCKETAPISOCKET_OSX_H
-#define SOCKETAPISOCKET_OSX_H
-
-#include <QAbstractSocket>
-#include <QIODevice>
-
-class SocketApiServerPrivate;
-class SocketApiSocketPrivate;
-
-class SocketApiSocket : public QIODevice
-{
-    Q_OBJECT
-public:
-    SocketApiSocket(QObject *parent, SocketApiSocketPrivate *p);
-    ~SocketApiSocket();
-
-    qint64 readData(char *data, qint64 maxlen) override;
-    qint64 writeData(const char *data, qint64 len) override;
-
-    bool isSequential() const override { return true; }
-    qint64 bytesAvailable() const override;
-    bool canReadLine() const override;
-
-signals:
-    void disconnected();
-
-private:
-    // Use Qt's p-impl system to hide objective-c types from C++ code including this file
-    Q_DECLARE_PRIVATE(SocketApiSocket)
-    QScopedPointer<SocketApiSocketPrivate> d_ptr;
-    friend class SocketApiServerPrivate;
-};
-
-class SocketApiServer : public QObject
-{
-    Q_OBJECT
-public:
-    SocketApiServer();
-    ~SocketApiServer();
-
-    void close();
-    bool listen(const QString &name);
-    SocketApiSocket *nextPendingConnection();
-
-    static bool removeServer(const QString &) { return false; }
-
-signals:
-    void newConnection();
-
-private:
-    Q_DECLARE_PRIVATE(SocketApiServer)
-    QScopedPointer<SocketApiServerPrivate> d_ptr;
-};
-
-#endif // SOCKETAPISOCKET_OSX_H
diff --git a/src/gui/socketapisocket_mac.mm b/src/gui/socketapisocket_mac.mm
deleted file mode 100644 (file)
index f018ac7..0000000
+++ /dev/null
@@ -1,245 +0,0 @@
-/*
- * Copyright (C) by Jocelyn Turcotte <jturcotte@woboq.com>
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
- * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
- * for more details.
- */
-
-#include "socketapisocket_mac.h"
-#import <Cocoa/Cocoa.h>
-
-@protocol ChannelProtocol <NSObject>
-- (void)sendMessage:(NSData*)msg;
-@end
-
-@protocol RemoteEndProtocol <NSObject, ChannelProtocol>
-- (void)registerTransmitter:(id)tx;
-@end
-
-@interface LocalEnd : NSObject <ChannelProtocol>
-@property SocketApiSocketPrivate *wrapper;
-- (instancetype)initWithWrapper:(SocketApiSocketPrivate *)wrapper;
-@end
-
-@interface Server : NSObject
-@property SocketApiServerPrivate *wrapper;
-- (instancetype)initWithWrapper:(SocketApiServerPrivate *)wrapper;
-- (void)registerClient:(NSDistantObject <RemoteEndProtocol> *)remoteEnd;
-@end
-
-class SocketApiSocketPrivate
-{
-public:
-    SocketApiSocket *q_ptr;
-
-    SocketApiSocketPrivate(NSDistantObject <ChannelProtocol> *remoteEnd);
-    ~SocketApiSocketPrivate();
-
-    // release remoteEnd
-    void disconnectRemote();
-
-    NSDistantObject <ChannelProtocol> *remoteEnd;
-    LocalEnd *localEnd;
-    QByteArray inBuffer;
-    bool isRemoteDisconnected = false;
-};
-
-class SocketApiServerPrivate
-{
-public:
-    SocketApiServer *q_ptr;
-
-    SocketApiServerPrivate();
-    ~SocketApiServerPrivate();
-
-    QList<SocketApiSocket*> pendingConnections;
-    NSConnection *connection;
-    Server *server;
-};
-
-
-@implementation LocalEnd
-- (instancetype)initWithWrapper:(SocketApiSocketPrivate *)wrapper
-{
-    self = [super init];
-    self->_wrapper = wrapper;
-    return self;
-}
-
-- (void)sendMessage:(NSData*)msg
-{
-    if (_wrapper) {
-        _wrapper->inBuffer += QByteArray::fromRawNSData(msg);
-        emit _wrapper->q_ptr->readyRead();
-    }
-}
-
-- (void)connectionDidDie:(NSNotification*)notification
-{
-#pragma unused(notification)
-    if (_wrapper) {
-        _wrapper->disconnectRemote();
-        emit _wrapper->q_ptr->disconnected();
-    }
-}
-@end
-
-@implementation Server
-- (instancetype)initWithWrapper:(SocketApiServerPrivate *)wrapper
-{
-    self = [super init];
-    self->_wrapper = wrapper;
-    return self;
-}
-
-- (void)registerClient:(NSDistantObject <RemoteEndProtocol> *)remoteEnd
-{
-    // This saves a few mach messages that would otherwise be needed to query the interface
-    [remoteEnd setProtocolForProxy:@protocol(RemoteEndProtocol)];
-
-    SocketApiServer *server = _wrapper->q_ptr;
-    SocketApiSocketPrivate *socketPrivate = new SocketApiSocketPrivate(remoteEnd);
-    SocketApiSocket *socket = new SocketApiSocket(server, socketPrivate);
-    _wrapper->pendingConnections.append(socket);
-    emit server->newConnection();
-
-    [remoteEnd registerTransmitter:socketPrivate->localEnd];
-}
-@end
-
-
-SocketApiSocket::SocketApiSocket(QObject *parent, SocketApiSocketPrivate *p)
-    : QIODevice(parent)
-    , d_ptr(p)
-{
-    Q_D(SocketApiSocket);
-    d->q_ptr = this;
-    open(ReadWrite);
-}
-
-SocketApiSocket::~SocketApiSocket()
-{
-}
-
-qint64 SocketApiSocket::readData(char *data, qint64 maxlen)
-{
-    Q_D(SocketApiSocket);
-    qint64 len = std::min(maxlen, static_cast<qint64>(d->inBuffer.size()));
-    memcpy(data, d->inBuffer.constData(), len);
-    d->inBuffer.remove(0, len);
-    return len;
-}
-
-qint64 SocketApiSocket::writeData(const char *data, qint64 len)
-{
-    Q_D(SocketApiSocket);
-    if (d->isRemoteDisconnected)
-        return -1;
-
-    @try {
-        // FIXME: The NSConnection will make this block unless the function is marked as "oneway"
-        // in the protocol. This isn't async and reduces our performances but this currectly avoids
-        // a Mach queue deadlock during requests bursts of the legacy OwnCloudFinder extension.
-        // Since FinderSync already runs in a separate process, blocking isn't too critical.
-        [d->remoteEnd sendMessage:[NSData dataWithBytesNoCopy:const_cast<char *>(data) length:len freeWhenDone:NO]];
-        return len;
-    } @catch(NSException* e) {
-        // connectionDidDie can be notified too late, also interpret any sending exception as a disconnection.
-        d->disconnectRemote();
-        emit disconnected();
-        return -1;
-    }
-}
-
-qint64 SocketApiSocket::bytesAvailable() const
-{
-    Q_D(const SocketApiSocket);
-    return d->inBuffer.size() + QIODevice::bytesAvailable();
-}
-
-bool SocketApiSocket::canReadLine() const
-{
-    Q_D(const SocketApiSocket);
-    return d->inBuffer.indexOf('\n', int(pos())) != -1 || QIODevice::canReadLine();
-}
-
-SocketApiSocketPrivate::SocketApiSocketPrivate(NSDistantObject <ChannelProtocol> *remoteEnd)
-    : remoteEnd(remoteEnd)
-    , localEnd([[LocalEnd alloc] initWithWrapper:this])
-{
-    [remoteEnd retain];
-    // (Ab)use our objective-c object just to catch the notification
-    [[NSNotificationCenter defaultCenter] addObserver:localEnd
-        selector:@selector(connectionDidDie:)
-        name:NSConnectionDidDieNotification
-        object:[remoteEnd connectionForProxy]];
-}
-
-SocketApiSocketPrivate::~SocketApiSocketPrivate()
-{
-    disconnectRemote();
-
-    // The DO vended localEnd might still be referenced by the connection
-    localEnd.wrapper = nil;
-    [localEnd release];
-}
-
-void SocketApiSocketPrivate::disconnectRemote()
-{
-    if (isRemoteDisconnected)
-        return;
-    isRemoteDisconnected = true;
-
-    [remoteEnd release];
-}
-
-SocketApiServer::SocketApiServer()
-    : d_ptr(new SocketApiServerPrivate)
-{
-    Q_D(SocketApiServer);
-    d->q_ptr = this;
-}
-
-SocketApiServer::~SocketApiServer()
-{
-}
-
-void SocketApiServer::close()
-{
-    // Assume we'll be destroyed right after
-}
-
-bool SocketApiServer::listen(const QString &name)
-{
-    Q_D(SocketApiServer);
-    // Set the name of the root object
-    return [d->connection registerName:name.toNSString()];
-}
-
-SocketApiSocket *SocketApiServer::nextPendingConnection()
-{
-    Q_D(SocketApiServer);
-    return d->pendingConnections.takeFirst();
-}
-
-SocketApiServerPrivate::SocketApiServerPrivate()
-{
-    // Create the connection and server object to vend over Disributed Objects
-    connection = [[NSConnection alloc] init];
-    server = [[Server alloc] initWithWrapper:this];
-    [connection setRootObject:server];
-}
-
-SocketApiServerPrivate::~SocketApiServerPrivate()
-{
-    [connection release];
-    server.wrapper = nil;
-    [server release];
-}
index 8272ab2495ffe8fcbc2973c965ced7b89947da1c..7602a3bd47d991a04d41cd5113b2cf47fec56960 100644 (file)
@@ -67,8 +67,6 @@ void ProcessDirectoryJob::process()
 {
     ASSERT(_localQueryDone && _serverQueryDone);
 
-    QString localDir;
-
     // Build lookup tables for local, remote and db entries.
     // For suffix-virtual files, the key will normally be the base file name
     // without the suffix.
index ea082b9c9950643379bece6311ba3d2626ceff16..352ec32c2bd2668a114b38b834c0781e48733481 100644 (file)
@@ -40,6 +40,7 @@
 #include <QTimer>
 #include <QObject>
 #include <QTimerEvent>
+#include <QRegularExpression>
 #include <qmath.h>
 
 namespace OCC {
@@ -387,7 +388,7 @@ qint64 OwncloudPropagator::smallFileSize()
     return smallFileSize;
 }
 
-void OwncloudPropagator::start(const SyncFileItemVector &items)
+void OwncloudPropagator::start(SyncFileItemVector &&items)
 {
     Q_ASSERT(std::is_sorted(items.begin(), items.end()));
 
@@ -396,6 +397,26 @@ void OwncloudPropagator::start(const SyncFileItemVector &items)
      * In order to do that we loop over the items. (which are sorted by destination)
      * When we enter a directory, we can create the directory job and push it on the stack. */
 
+    const auto regex = syncOptions().fileRegex();
+    if (regex.isValid()) {
+        QSet<QStringRef> names;
+        for (auto &i : items) {
+            if (regex.match(i->_file).hasMatch()) {
+                int index = -1;
+                QStringRef ref;
+                do {
+                    ref = i->_file.midRef(0, index);
+                    names.insert(ref);
+                    index = ref.lastIndexOf(QLatin1Char('/'));
+                } while (index > 0);
+            }
+        }
+        items.erase(std::remove_if(items.begin(), items.end(), [&names](auto i) {
+            return !names.contains(QStringRef { &i->_file });
+        }),
+            items.end());
+    }
+
     _rootJob.reset(new PropagateRootDirectory(this));
     QStack<QPair<QString /* directory name */, PropagateDirectory * /* job */>> directories;
     directories.push(qMakePair(QString(), _rootJob.data()));
index c2df7749f97089aa61d831a8d102e51983c23990..f03ef5759325c3f061bf6d23cd56dd070ce8cb91 100644 (file)
@@ -421,7 +421,7 @@ public:
 
     ~OwncloudPropagator();
 
-    void start(const SyncFileItemVector &_syncedItems);
+    void start(SyncFileItemVector &&_syncedItems);
 
     const SyncOptions &syncOptions() const;
     void setSyncOptions(const SyncOptions &syncOptions);
index a3307f915b9087f845ca89bcab7664e55f28c383..d531fcc7d4ef8b99fa1f14c7e45614dcd36651c5 100644 (file)
@@ -434,7 +434,7 @@ void SyncEngine::startSync()
     }
 
     if (s_anySyncRunning || _syncRunning) {
-        ASSERT(false);
+        ASSERT(false)
         return;
     }
 
@@ -725,8 +725,7 @@ void SyncEngine::slotDiscoveryFinished()
         if (_needsUpdate)
             Q_EMIT started();
 
-        _propagator->start(_syncItems);
-        _syncItems.clear();
+        _propagator->start(std::move(_syncItems));
 
         qCInfo(lcEngine) << "#### Post-Reconcile end #################################################### " << _stopWatch.addLapTime(QStringLiteral("Post-Reconcile Finished")) << "ms";
     };
diff --git a/src/libsync/syncoptions.cpp b/src/libsync/syncoptions.cpp
new file mode 100644 (file)
index 0000000..588a7c6
--- /dev/null
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) by Olivier Goffart <ogoffart@woboq.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+
+#include "syncoptions.h"
+#include "common/utility.h"
+
+#include <QRegularExpression>
+
+using namespace OCC;
+
+SyncOptions::SyncOptions()
+    : _vfs(new VfsOff)
+{
+}
+
+SyncOptions::~SyncOptions()
+{
+}
+
+void SyncOptions::fillFromEnvironmentVariables()
+{
+    QByteArray chunkSizeEnv = qgetenv("OWNCLOUD_CHUNK_SIZE");
+    if (!chunkSizeEnv.isEmpty())
+        _initialChunkSize = chunkSizeEnv.toUInt();
+
+    QByteArray minChunkSizeEnv = qgetenv("OWNCLOUD_MIN_CHUNK_SIZE");
+    if (!minChunkSizeEnv.isEmpty())
+        _minChunkSize = minChunkSizeEnv.toUInt();
+
+    QByteArray maxChunkSizeEnv = qgetenv("OWNCLOUD_MAX_CHUNK_SIZE");
+    if (!maxChunkSizeEnv.isEmpty())
+        _maxChunkSize = maxChunkSizeEnv.toUInt();
+
+    QByteArray targetChunkUploadDurationEnv = qgetenv("OWNCLOUD_TARGET_CHUNK_UPLOAD_DURATION");
+    if (!targetChunkUploadDurationEnv.isEmpty())
+        _targetChunkUploadDuration = std::chrono::milliseconds(targetChunkUploadDurationEnv.toUInt());
+
+    int maxParallel = qgetenv("OWNCLOUD_MAX_PARALLEL").toInt();
+    if (maxParallel > 0)
+        _parallelNetworkJobs = maxParallel;
+}
+
+void SyncOptions::verifyChunkSizes()
+{
+    _minChunkSize = qMin(_minChunkSize, _initialChunkSize);
+    _maxChunkSize = qMax(_maxChunkSize, _initialChunkSize);
+}
+
+QRegularExpression SyncOptions::fileRegex() const
+{
+    return _fileRegex;
+}
+
+void SyncOptions::setFilePattern(const QString &pattern)
+{
+    // full match or a path ending with this pattern
+    setPathPattern(QStringLiteral("(^|/|\\\\)") + pattern + QLatin1Char('$'));
+}
+
+void SyncOptions::setPathPattern(const QString &pattern)
+{
+    _fileRegex.setPatternOptions(Utility::fsCasePreserving() ? QRegularExpression::CaseInsensitiveOption : QRegularExpression::NoPatternOption);
+    _fileRegex.setPattern(pattern);
+}
index 7dff264f06b139b5271e2cecdb4ab0b8877337e7..f109ec7fc24899c1a2b891c3f3a7114392ee333f 100644 (file)
 #pragma once
 
 #include "owncloudlib.h"
-#include <QString>
+#include "common/vfs.h"
+
+#include <QRegularExpression>
 #include <QSharedPointer>
+#include <QString>
+
 #include <chrono>
-#include "common/vfs.h"
+
 
 namespace OCC {
 
 /**
  * Value class containing the options given to the sync engine
  */
-struct OWNCLOUDSYNC_EXPORT SyncOptions
+class OWNCLOUDSYNC_EXPORT SyncOptions
 {
+public:
     SyncOptions();
     ~SyncOptions();
 
@@ -82,6 +87,29 @@ struct OWNCLOUDSYNC_EXPORT SyncOptions
      * initial chunk size value.
      */
     void verifyChunkSizes();
+
+
+    /** A regular expression to match file names
+     * If no pattern is provided the default is an invalid regular expression.
+     */
+    QRegularExpression fileRegex() const;
+
+    /**
+     * A pattern like *.txt, matching only file names
+     */
+    void setFilePattern(const QString &pattern);
+
+    /**
+     * A pattern like /own.*\/.*txt matching the full path
+     */
+    void setPathPattern(const QString &pattern);
+
+private:
+    /**
+     * Only sync files that mathc the expression
+     * Invalid pattern by default.
+     */
+    QRegularExpression _fileRegex = QRegularExpression(QStringLiteral("("));
 };
 
 }