From 1f21c2dc52e6347a10eaf200834979054c287617 Mon Sep 17 00:00:00 2001 From: Martchus Date: Sun, 2 Oct 2016 21:59:28 +0200 Subject: [PATCH] Add syncthingctl, see README.md --- CMakeLists.txt | 5 +- README.md | 2 +- cli/CMakeLists.txt | 44 +++ cli/application.cpp | 385 ++++++++++++++++++++++ cli/application.h | 48 +++ cli/args.cpp | 45 +++ cli/args.h | 22 ++ cli/helper.h | 70 ++++ cli/main.cpp | 15 + connector/CMakeLists.txt | 1 + connector/syncthingconnection.cpp | 198 +++++++++-- connector/syncthingconnection.h | 72 +--- connector/syncthingconnectionsettings.cpp | 2 +- model/CMakeLists.txt | 1 + tray/CMakeLists.txt | 1 + tray/README.md | 1 + tray/application/main.cpp | 5 +- 17 files changed, 827 insertions(+), 90 deletions(-) create mode 100644 cli/CMakeLists.txt create mode 100644 cli/application.cpp create mode 100644 cli/application.h create mode 100644 cli/args.cpp create mode 100644 cli/args.h create mode 100644 cli/helper.h create mode 100644 cli/main.cpp create mode 120000 tray/README.md diff --git a/CMakeLists.txt b/CMakeLists.txt index e076468..fb748b3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,7 +3,6 @@ cmake_minimum_required(VERSION 3.1.0 FATAL_ERROR) # metadata set(META_PROJECT_NAME syncthingtray) set(META_PROJECT_TYPE application) -set(META_APP_NAME "Syncthing Tray") set(META_APP_AUTHOR "Martchus") set(META_APP_URL "https://github.com/${META_APP_AUTHOR}/${META_PROJECT_NAME}") set(META_APP_DESCRIPTION "Tray application for Syncthing") @@ -15,12 +14,16 @@ set(META_VERSION_PATCH 2) project(${META_PROJECT_NAME}) # options for partial build +option(NO_CLI "specifies whether building CLI should be skipped" OFF) option(NO_TRAY "specifies whether building the tray should be skipped" OFF) option(NO_MODEL "specifies whether building models should be skipped, implies NO_TRAY" OFF) # add subdirectories add_subdirectory(connector) link_directories(${LIB_SYNCTHING_CONNECTOR_BINARY_DIR}) +if(NOT NO_CLI) + add_subdirectory(cli) +endif() if(NOT NO_MODEL) add_subdirectory(model) link_directories(${LIB_SYNCTHING_MODEL_BINARY_DIR}) diff --git a/README.md b/README.md index 31241ed..72d6ed6 100644 --- a/README.md +++ b/README.md @@ -31,12 +31,12 @@ support * Utilizes either Qt WebKit or Qt WebEngine * Can be built without web view support as well (then the web UI is opened in the regular browser) * Allows quickly switching between multiple Syncthing instances +* Features a simple command line utility `syncthingctl` to check Syncthing status and trigger rescan/pause/resume/restart ## Planned features The tray is still under development; the following features are planned: * Show recently processed items * Improve notification handling -* Create simple command line application * Create Plasmoid for Plasma 5 desktop ## Screenshots diff --git a/cli/CMakeLists.txt b/cli/CMakeLists.txt new file mode 100644 index 0000000..3dc4daa --- /dev/null +++ b/cli/CMakeLists.txt @@ -0,0 +1,44 @@ +cmake_minimum_required(VERSION 3.1.0 FATAL_ERROR) + +# metadata +set(META_PROJECT_NAME syncthingctl) +set(META_APP_NAME "Syncthing control") +set(META_APP_DESCRIPTION "Command line app to control Syncthing") +set(META_PROJECT_TYPE application) +set(META_GUI_OPTIONAL false) + +# add project files +set(HEADER_FILES + helper.h + args.h + application.h +) +set(SRC_FILES + main.cpp + args.cpp + application.cpp +) + +# find c++utilities +find_package(c++utilities 4.1.0 REQUIRED) +use_cpp_utilities() + +# find qtutilities +find_package(qtutilities 5.0.0 REQUIRED) +use_qt_utilities() + +# find backend libraries +find_package(syncthingconnector ${META_APP_VERSION} REQUIRED) +use_syncthingconnector() + +# link also explicitely against the following Qt 5 modules +list(APPEND ADDITIONAL_QT_MODULES Network) + +# include modules to apply configuration +include(BasicConfig) +include(QtConfig) +include(WindowsResources) +include(AppTarget) +include(ShellCompletion) +include(Doxygen) +include(ConfigHeader) diff --git a/cli/application.cpp b/cli/application.cpp new file mode 100644 index 0000000..070faa9 --- /dev/null +++ b/cli/application.cpp @@ -0,0 +1,385 @@ +#include "./application.h" +#include "./helper.h" + +#include "../connector/syncthingconfig.h" + +#include +#include +#include +#include + +#include +#include +#include + +#include +#include + +using namespace std; +using namespace std::placeholders; +using namespace ApplicationUtilities; +using namespace EscapeCodes; +using namespace ChronoUtilities; +using namespace ConversionUtilities; +using namespace Data; + +namespace Cli { + +Application::Application() : + m_expectedResponse(0) +{ + // take ownership over the global QNetworkAccessManager + networkAccessManager().setParent(this); + + // setup argument callbacks + m_args.status.setCallback(bind(&Application::printStatus, this, _1)); + m_args.log.setCallback(bind(&Application::requestLog, this, _1)); + m_args.restart.setCallback(bind(&Application::requestRestart, this, _1)); + m_args.rescan.setCallback(bind(&Application::requestRescan, this, _1)); + m_args.rescanAll.setCallback(bind(&Application::requestRescanAll, this, _1)); + m_args.pause.setCallback(bind(&Application::requestPause, this, _1)); + m_args.pauseAll.setCallback(bind(&Application::requestPauseAll, this, _1)); + m_args.resume.setCallback(bind(&Application::requestResume, this, _1)); + m_args.resumeAll.setCallback(bind(&Application::requestResumeAll, this, _1)); + + // connect signals and slots + connect(&m_connection, &SyncthingConnection::statusChanged, this, &Application::handleStatusChanged); + connect(&m_connection, &SyncthingConnection::error, this, &Application::handleError); +} + +Application::~Application() +{} + +int Application::exec(int argc, const char * const *argv) +{ + try { + // parse arguments + m_args.parser.readArgs(argc, argv); + m_args.parser.checkConstraints(); + + // handle help argument + if(m_args.help.isPresent()) { + m_args.parser.printHelp(cout); + return 0; + } + + // locate and read Syncthing config file + QString configFile; + const char *configFileArgValue = m_args.configFile.firstValue(); + if(configFileArgValue) { + configFile = QString::fromLocal8Bit(configFileArgValue); + } else { + configFile = SyncthingConfig::locateConfigFile(); + } + SyncthingConfig config; + const char *apiKeyArgValue = m_args.apiKey.firstValue(); + if(!config.restore(configFile)) { + if(configFileArgValue) { + cerr << "Error: Unable to locate specified Syncthing config file \"" << configFileArgValue << "\"" << endl; + return -1; + } else if(!apiKeyArgValue) { + cerr << "Error: Unable to locate Syncthing config file and no API key specified" << endl; + return -2; + } + } + + // apply settings for connection + if(const char *urlArgValue = m_args.url.firstValue()) { + m_settings.syncthingUrl = QString::fromLocal8Bit(urlArgValue); + } else if(!config.guiAddress.isEmpty()) { + m_settings.syncthingUrl = (config.guiEnforcesSecureConnection || !QHostAddress(config.guiAddress.mid(0, config.guiAddress.indexOf(QChar(':')))).isLoopback() ? QStringLiteral("https://") : QStringLiteral("http://")) + config.guiAddress; + } else { + m_settings.syncthingUrl = QStringLiteral("http://localhost:8080"); + } + if(m_args.credentials.isPresent()) { + m_settings.authEnabled = true; + m_settings.userName = QString::fromLocal8Bit(m_args.credentials.values(0)[0]); + m_settings.password = QString::fromLocal8Bit(m_args.credentials.values(0)[1]); + } + if(apiKeyArgValue) { + m_settings.apiKey.append(apiKeyArgValue); + } else { + m_settings.apiKey.append(config.guiApiKey); + } + if(const char *certArgValue = m_args.certificate.firstValue()) { + m_settings.httpsCertPath = QString::fromLocal8Bit(certArgValue); + if(m_settings.httpsCertPath.isEmpty() || !m_settings.loadHttpsCert()) { + cerr << "Error: Unable to load specified certificate \"" << m_args.certificate.firstValue() << "\"" << endl; + return -3; + } + } + + // finally to request / establish connection + if(m_args.status.isPresent() || m_args.rescanAll.isPresent() || m_args.pauseAll.isPresent() || m_args.resumeAll.isPresent()) { + // those arguments rquire establishing a connection first, the actual handler is called by handleStatusChanged() when + // the connection has been established + m_connection.reconnect(m_settings); + cerr << "Connecting to " << m_settings.syncthingUrl.toLocal8Bit().data() << " ..."; + cerr.flush(); + } else { + // call handler for any other arguments directly + m_connection.applySettings(m_settings); + m_args.parser.invokeCallbacks(); + } + + // enter event loop + return QCoreApplication::exec(); + + } catch(const Failure &ex) { + cerr << "Unable to parse arguments. " << ex.what() << "\nSee --help for available commands." << endl; + return 1; + } +} + +void Application::handleStatusChanged(SyncthingStatus newStatus) +{ + Q_UNUSED(newStatus) + if(m_connection.isConnected()) { + eraseLine(cout); + cout << '\r'; + m_args.parser.invokeCallbacks(); + m_connection.disconnect(); + } +} + +void Application::handleResponse() +{ + if(m_expectedResponse) { + if(!--m_expectedResponse) { + QCoreApplication::quit(); + } + } else { + cerr << "Error: Unexpected response" << endl; + QCoreApplication::exit(-4); + } +} + +void Application::handleError(const QString &message) +{ + eraseLine(cout); + cerr << "\rError: " << message.toLocal8Bit().data() << endl; + QCoreApplication::exit(-3); +} + +void Application::requestLog(const ArgumentOccurrence &) +{ + m_connection.requestLog(bind(&Application::printLog, this, _1)); + cerr << "Request log from " << m_settings.syncthingUrl.toLocal8Bit().data() << " ..."; + cerr.flush(); +} + +void Application::requestRestart(const ArgumentOccurrence &) +{ + connect(&m_connection, &SyncthingConnection::restartTriggered, &QCoreApplication::quit); + m_connection.restart(); + cerr << "Request restart " << m_settings.syncthingUrl.toLocal8Bit().data() << " ..."; + cerr.flush(); +} + +void Application::requestRescan(const ArgumentOccurrence &occurrence) +{ + m_expectedResponse = occurrence.values.size(); + connect(&m_connection, &SyncthingConnection::rescanTriggered, this, &Application::handleResponse); + for(const char *value : occurrence.values) { + cerr << "Request rescanning " << value << " ...\n"; + m_connection.rescan(QString::fromLocal8Bit(value)); + } + cerr.flush(); +} + +void Application::requestRescanAll(const ArgumentOccurrence &) +{ + m_expectedResponse = m_connection.dirInfo().size(); + connect(&m_connection, &SyncthingConnection::rescanTriggered, this, &Application::handleResponse); + cerr << "Request rescanning all directories ..." << endl; + m_connection.rescanAllDirs(); +} + +void Application::requestPause(const ArgumentOccurrence &occurrence) +{ + m_expectedResponse = occurrence.values.size(); + connect(&m_connection, &SyncthingConnection::pauseTriggered, this, &Application::handleResponse); + for(const char *value : occurrence.values) { + cerr << "Request pausing " << value << " ...\n"; + m_connection.pause(QString::fromLocal8Bit(value)); + } + cerr.flush(); +} + +void Application::requestPauseAll(const ArgumentOccurrence &) +{ + m_expectedResponse = m_connection.devInfo().size(); + connect(&m_connection, &SyncthingConnection::pauseTriggered, this, &Application::handleResponse); + cerr << "Request pausing all devices ..." << endl; + m_connection.pauseAllDevs(); +} + +void Application::requestResume(const ArgumentOccurrence &occurrence) +{ + m_expectedResponse = occurrence.values.size(); + connect(&m_connection, &SyncthingConnection::resumeTriggered, this, &Application::handleResponse); + for(const char *value : occurrence.values) { + cerr << "Request resuming " << value << " ...\n"; + m_connection.resume(QString::fromLocal8Bit(value)); + } + cerr.flush(); +} + +void Application::requestResumeAll(const ArgumentOccurrence &) +{ + m_expectedResponse = m_connection.devInfo().size(); + connect(&m_connection, &SyncthingConnection::resumeTriggered, this, &Application::handleResponse); + cerr << "Request resuming all devices ..." << endl; + m_connection.resumeAllDevs(); +} + +void Application::printStatus(const ArgumentOccurrence &) +{ + // find relevant dirs and devs + std::vector relevantDirs; + std::vector relevantDevs; + int dummy; + if(m_args.dir.isPresent()) { + relevantDirs.reserve(m_args.dir.occurrences()); + for(size_t i = 0; i != m_args.dir.occurrences(); ++i) { + if(const SyncthingDir *dir = m_connection.findDirInfo(QString::fromLocal8Bit(m_args.dir.values(i).front()), dummy)) { + relevantDirs.emplace_back(dir); + } else { + cerr << "Warning: Specified directory \"" << m_args.dir.values(i).front() << "\" does not exist" << endl; + } + } + } + if(m_args.dev.isPresent()) { + relevantDevs.reserve(m_args.dev.occurrences()); + for(size_t i = 0; i != m_args.dev.occurrences(); ++i) { + const SyncthingDev *dev = m_connection.findDevInfo(QString::fromLocal8Bit(m_args.dev.values(i).front()), dummy); + if(!dev) { + dev = m_connection.findDevInfoByName(QString::fromLocal8Bit(m_args.dev.values(i).front()), dummy); + } + if(dev) { + relevantDevs.emplace_back(dev); + } else { + cerr << "Warning: Specified device \"" << m_args.dev.values(i).front() << "\" does not exist" << endl; + } + } + } + if(relevantDirs.empty() && relevantDevs.empty()) { + relevantDirs.reserve(m_connection.dirInfo().size()); + for(const SyncthingDir &dir : m_connection.dirInfo()) { + relevantDirs.emplace_back(&dir); + } + relevantDevs.reserve(m_connection.devInfo().size()); + for(const SyncthingDev &dev : m_connection.devInfo()) { + relevantDevs.emplace_back(&dev); + } + } + + // display dirs + if(!relevantDirs.empty()) { + setStyle(cout, TextAttribute::Bold); + cout << "Directories\n"; + setStyle(cout); + for(const SyncthingDir *dir : relevantDirs) { + cout << " - "; + setStyle(cout, TextAttribute::Bold); + cout << dir->id.toLocal8Bit().data() << '\n'; + setStyle(cout); + printProperty("Label", dir->label); + printProperty("Path", dir->path); + const char *status; + switch(dir->status) { + case DirStatus::Idle: + status = "idle"; break; + case DirStatus::Scanning: + status = "scanning"; break; + case DirStatus::Synchronizing: + status = "synchronizing"; break; + case DirStatus::Paused: + status = "paused"; break; + case DirStatus::OutOfSync: + status = "out of sync"; break; + default: + status = "unknown"; + } + printProperty("Status", status); + printProperty("Last scan time", dir->lastScanTime); + printProperty("Last file time", dir->lastFileTime); + printProperty("Last file name", dir->lastFileName); + printProperty("Download progress", dir->downloadLabel); + printProperty("Devices", dir->devices); + printProperty("Read-only", dir->readOnly); + printProperty("Ignore permissions", dir->ignorePermissions); + printProperty("Auto-normalize", dir->autoNormalize); + printProperty("Rescan interval", TimeSpan::fromSeconds(dir->rescanInterval)); + printProperty("Min. free disk percentage", dir->minDiskFreePercentage); + cout << '\n'; + } + } + + // display devs + if(!relevantDevs.empty()) { + setStyle(cout, TextAttribute::Bold); + cout << "Devices\n"; + setStyle(cout); + for(const SyncthingDev *dev : relevantDevs) { + cout << " - "; + setStyle(cout, TextAttribute::Bold); + cout << dev->name.toLocal8Bit().data() << '\n'; + setStyle(cout); + printProperty("ID", dev->id); + const char *status; + if(dev->paused) { + status = "paused"; + } else { + switch(dev->status) { + case DevStatus::Disconnected: + status = "disconnected"; break; + case DevStatus::OwnDevice: + status = "own device"; break; + case DevStatus::Idle: + status = "idle"; break; + case DevStatus::Synchronizing: + status = "synchronizing"; break; + case DevStatus::OutOfSync: + status = "out of sync"; break; + case DevStatus::Rejected: + status = "rejected"; break; + default: + status = "unknown"; + } + } + printProperty("Status", status); + printProperty("Addresses", dev->addresses); + printProperty("Compression", dev->compression); + printProperty("Cert name", dev->certName); + printProperty("Connection address", dev->connectionAddress); + printProperty("Connection type", dev->connectionType); + printProperty("Client version", dev->clientVersion); + printProperty("Last seen", dev->lastSeen); + if(dev->totalIncomingTraffic > 0) { + printProperty("Incoming traffic", dataSizeToString(static_cast(dev->totalIncomingTraffic)).data()); + } + if(dev->totalOutgoingTraffic > 0) { + printProperty("Outgoing traffic", dataSizeToString(static_cast(dev->totalOutgoingTraffic)).data()); + } + cout << '\n'; + } + } + + cout.flush(); + QCoreApplication::exit(); +} + +void Application::printLog(const std::vector &logEntries) +{ + eraseLine(cout); + cout << '\r'; + + for(const SyncthingLogEntry &entry : logEntries) { + cout << DateTime::fromIsoStringLocal(entry.when.toLocal8Bit().data()).toString(DateTimeOutputFormat::DateAndTime, true).data() << ':' << ' ' << entry.message.toLocal8Bit().data() << '\n'; + } + cout.flush(); + QCoreApplication::exit(); +} + +} // namespace Cli diff --git a/cli/application.h b/cli/application.h new file mode 100644 index 0000000..b4c8477 --- /dev/null +++ b/cli/application.h @@ -0,0 +1,48 @@ +#ifndef CLI_APPLICATION_H +#define CLI_APPLICATION_H + +#include "./args.h" + +#include "../connector/syncthingconnection.h" +#include "../connector/syncthingconnectionsettings.h" + +#include + +namespace Cli { + +class Application : public QObject +{ + Q_OBJECT + +public: + Application(); + ~Application(); + + int exec(int argc, const char *const *argv); + +private slots: + void handleStatusChanged(Data::SyncthingStatus newStatus); + void handleResponse(); + void handleError(const QString &message); + +private: + void requestLog(const ArgumentOccurrence &); + void requestRestart(const ArgumentOccurrence &); + void requestRescan(const ArgumentOccurrence &occurrence); + void requestRescanAll(const ArgumentOccurrence &); + void requestPause(const ArgumentOccurrence &occurrence); + void requestPauseAll(const ArgumentOccurrence &); + void requestResume(const ArgumentOccurrence &); + void requestResumeAll(const ArgumentOccurrence &); + void printStatus(const ArgumentOccurrence &); + void printLog(const std::vector &logEntries); + + Args m_args; + Data::SyncthingConnectionSettings m_settings; + Data::SyncthingConnection m_connection; + size_t m_expectedResponse; +}; + +} // namespace Cli + +#endif // CLI_APPLICATION_H diff --git a/cli/args.cpp b/cli/args.cpp new file mode 100644 index 0000000..338c4ed --- /dev/null +++ b/cli/args.cpp @@ -0,0 +1,45 @@ +#include "./args.h" + +namespace Cli { + +Args::Args() : + help(parser), + status("status", 's', "shows the status"), + log("log", 'l', "shows the Syncthing log"), + restart("restart", '\0', "restarts Syncthing"), + rescan("rescan", 'r', "rescans the specified directories"), + rescanAll("rescan-all", '\0', "rescans all directories"), + pause("pause", '\0', "pauses the specified devices"), + pauseAll("pause-all", '\0', "pauses all devices"), + resume("resume", '\0', "resumes the specified devices"), + resumeAll("resume-all", '\0', "resumes all devices"), + dir("dir", 'd', "specifies the directory to display status info for (default is all dirs)", {"ID"}), + dev("dev", '\0', "specifies the device to display status info for (default is all devs)", {"ID"}), + configFile("config-file", 'f', "specifies the Syncthing config file", {"path"}), + apiKey("api-key", 'k', "specifies the API key", {"key"}), + url("url", 'u', "specifies the Syncthing URL, default is http://localhost:8080", {"URL"}), + credentials("credentials", 'c', "specifies user name and password", {"user name", "password"}), + certificate("cert", '\0', "specifies the certificate used by the Syncthing instance", {"path"}) +{ + dir.setConstraints(0, -1), dev.setConstraints(0, -1); + status.setSubArguments({&dir, &dev}); + + rescan.setValueNames({"dir ID"}); + rescan.setRequiredValueCount(-1); + pause.setValueNames({"dev ID"}); + pause.setRequiredValueCount(-1); + resume.setValueNames({"dev ID"}); + resume.setRequiredValueCount(-1); + + parser.setMainArguments({&status, &log, &restart, &rescan, &rescanAll, &pause, &pauseAll, + &resume, &resumeAll, + &configFile, &apiKey, &url, &credentials, &certificate, &help}); + + // allow setting default values via environment + configFile.setEnvironmentVariable("SYNCTHING_CTL_CONFIG_FILE"); + apiKey.setEnvironmentVariable("SYNCTHING_CTL_API_KEY"); + url.setEnvironmentVariable("SYNCTHING_CTL_URL"); + certificate.setEnvironmentVariable("SYNCTHING_CTL_CERTIFICATE"); +} + +} // namespace Cli diff --git a/cli/args.h b/cli/args.h new file mode 100644 index 0000000..a82aa49 --- /dev/null +++ b/cli/args.h @@ -0,0 +1,22 @@ +#ifndef CLI_ARGS_H +#define CLI_ARGS_H + +#include + +namespace Cli { + +using namespace ApplicationUtilities; + +struct Args +{ + Args(); + ArgumentParser parser; + HelpArgument help; + OperationArgument status, log, restart, rescan, rescanAll, pause, pauseAll, resume, resumeAll; + ConfigValueArgument dir, dev; + ConfigValueArgument configFile, apiKey, url, credentials, certificate; +}; + +} // namespace Cli + +#endif // CLI_ARGS_H diff --git a/cli/helper.h b/cli/helper.h new file mode 100644 index 0000000..e4bb8ee --- /dev/null +++ b/cli/helper.h @@ -0,0 +1,70 @@ +#ifndef SYNCTHINGCTL_HELPER +#define SYNCTHINGCTL_HELPER + +#include +#include +#include +#include + +#include +#include + +#include +#include + +namespace Cli { + +inline void printProperty(const char *propName, const char *value, const char *suffix = nullptr, ApplicationUtilities::Indentation indentation = 3) +{ + if(*value) { + std::cout << indentation << propName << ApplicationUtilities::Indentation(30 - strlen(propName)) << value; + if(suffix) { + std::cout << ' ' << suffix; + } + std::cout << '\n'; + } +} + +inline void printProperty(const char *propName, const QString &value, const char *suffix = nullptr, ApplicationUtilities::Indentation indentation = 3) +{ + printProperty(propName, value.toLocal8Bit().data(), suffix, indentation); +} + +inline void printProperty(const char *propName, const QStringList &value, const char *suffix = nullptr, ApplicationUtilities::Indentation indentation = 3) +{ + for(const QString &str : value) { + printProperty(propName, str, suffix, indentation); + propName = ""; + } +} + +inline void printProperty(const char *propName, ChronoUtilities::TimeSpan timeSpan, const char *suffix = nullptr, ApplicationUtilities::Indentation indentation = 3) +{ + if(!timeSpan.isNull()) { + printProperty(propName, timeSpan.toString(ChronoUtilities::TimeSpanOutputFormat::WithMeasures).data(), suffix, indentation); + } +} + +inline void printProperty(const char *propName, ChronoUtilities::DateTime dateTime, const char *suffix = nullptr, ApplicationUtilities::Indentation indentation = 3) +{ + if(!dateTime.isNull()) { + printProperty(propName, dateTime.toString().data(), suffix, indentation); + } +} + +inline void printProperty(const char *propName, bool value, const char *suffix = nullptr, ApplicationUtilities::Indentation indentation = 3) +{ + printProperty(propName, value ? "yes" : "no", suffix, indentation); +} + +template +inline void printProperty(const char *propName, const intType value, const char *suffix = nullptr, bool force = false, ApplicationUtilities::Indentation indentation = 3) +{ + if(value != 0 || force) { + printProperty(propName, ConversionUtilities::numberToString(value).data(), suffix, indentation); + } +} + +} + +#endif // SYNCTHINGCTL_HELPER diff --git a/cli/main.cpp b/cli/main.cpp new file mode 100644 index 0000000..a6fa5ac --- /dev/null +++ b/cli/main.cpp @@ -0,0 +1,15 @@ +#include "./application.h" + +#include "resources/config.h" + +#include + +#include + +int main(int argc, char *argv[]) +{ + SET_APPLICATION_INFO; + QCoreApplication coreApp(argc, argv); + Cli::Application cliApp; + return cliApp.exec(argc, argv); +} diff --git a/connector/CMakeLists.txt b/connector/CMakeLists.txt index 40c8490..2722bca 100644 --- a/connector/CMakeLists.txt +++ b/connector/CMakeLists.txt @@ -3,6 +3,7 @@ cmake_minimum_required(VERSION 3.1.0 FATAL_ERROR) # metadata set(META_PROJECT_NAME syncthingconnector) set(META_PROJECT_TYPE library) +set(META_APP_NAME "Connection backend of Syncthing Tray") set(META_APP_DESCRIPTION "Connection backend of Syncthing Tray") set(META_PROJECT_VARNAME_UPPER LIB_SYNCTHING_CONNECTOR) diff --git a/connector/syncthingconnection.cpp b/connector/syncthingconnection.cpp index 8ad34c4..c2024de 100644 --- a/connector/syncthingconnection.cpp +++ b/connector/syncthingconnection.cpp @@ -30,8 +30,8 @@ namespace Data { */ QNetworkAccessManager &networkAccessManager() { - static QNetworkAccessManager networkAccessManager; - return networkAccessManager; + static auto networkAccessManager = new QNetworkAccessManager; + return *networkAccessManager; } /*! @@ -226,24 +226,12 @@ void SyncthingConnection::reconnect() } /*! - * \brief Applies the specifies configuration and tries to reconnect via reconnect(). + * \brief Applies the specified configuration and tries to reconnect via reconnect(). * \remarks The expected SSL errors of the specified configuration are updated accordingly. */ void SyncthingConnection::reconnect(SyncthingConnectionSettings &connectionSettings) { - setSyncthingUrl(connectionSettings.syncthingUrl); - setApiKey(connectionSettings.apiKey); - if(connectionSettings.authEnabled) { - setCredentials(connectionSettings.userName, connectionSettings.password); - } else { - setCredentials(QString(), QString()); - } - setTrafficPollInterval(connectionSettings.trafficPollInterval); - setDevStatsPollInterval(connectionSettings.devStatsPollInterval); - loadSelfSignedCertificate(); - if(connectionSettings.expectedSslErrors.isEmpty()) { - connectionSettings.expectedSslErrors = expectedSslErrors(); - } + applySettings(connectionSettings); reconnect(); } @@ -290,7 +278,10 @@ void SyncthingConnection::pause(const QString &devId) { QUrlQuery query; query.addQueryItem(QStringLiteral("device"), devId); - QObject::connect(postData(QStringLiteral("system/pause"), query), &QNetworkReply::finished, this, &SyncthingConnection::readPauseResume); + QNetworkReply *reply = postData(QStringLiteral("system/pause"), query); + reply->setProperty("devId", devId); + reply->setProperty("resume", false); + QObject::connect(reply, &QNetworkReply::finished, this, &SyncthingConnection::readPauseResume); } /*! @@ -314,7 +305,10 @@ void SyncthingConnection::resume(const QString &devId) { QUrlQuery query; query.addQueryItem(QStringLiteral("device"), devId); - QObject::connect(postData(QStringLiteral("system/resume"), query), &QNetworkReply::finished, this, &SyncthingConnection::readPauseResume); + QNetworkReply *reply = postData(QStringLiteral("system/resume"), query); + reply->setProperty("devId", devId); + reply->setProperty("resume", true); + QObject::connect(reply, &QNetworkReply::finished, this, &SyncthingConnection::readPauseResume); } /*! @@ -338,7 +332,9 @@ void SyncthingConnection::rescan(const QString &dirId) { QUrlQuery query; query.addQueryItem(QStringLiteral("folder"), dirId); - QObject::connect(postData(QStringLiteral("db/scan"), query), &QNetworkReply::finished, this, &SyncthingConnection::readRescan); + QNetworkReply *reply = postData(QStringLiteral("db/scan"), query); + reply->setProperty("dirId", dirId); + QObject::connect(reply, &QNetworkReply::finished, this, &SyncthingConnection::readRescan); } /*! @@ -383,6 +379,7 @@ QNetworkRequest SyncthingConnection::prepareRequest(const QString &path, const Q url.setPassword(password()); url.setQuery(query); QNetworkRequest request(url); + request.setHeader(QNetworkRequest::ContentTypeHeader, QByteArray("application/x-www-form-urlencoded")); request.setRawHeader("X-API-Key", m_apiKey); return request; } @@ -441,6 +438,23 @@ SyncthingDev *SyncthingConnection::findDevInfo(const QString &devId, int &row) return nullptr; // TODO: dev is unknown, trigger refreshing the config } +/*! + * \brief Returns the device info object for the first device with the specified name. + * \returns Returns a pointer to the object or nullptr if not found. + * \remarks The returned object becomes invalid when the newConfig() signal is emitted or the connection is destroyed. + */ +SyncthingDev *SyncthingConnection::findDevInfoByName(const QString &devName, int &row) +{ + row = 0; + for(SyncthingDev &d : m_devs) { + if(d.name == devName) { + return &d; + } + ++row; + } + return nullptr; +} + /*! * \brief Continues connecting if both - config and status - have been parsed yet and continuous polling is enabled. */ @@ -612,12 +626,18 @@ void SyncthingConnection::loadSelfSignedCertificate() // ensure current exceptions for self-signed certificates are cleared m_expectedSslErrors.clear(); - // only possible if the Syncthing instance is running on the local machine - const QString host(QUrl(syncthingUrl()).host()); - if(host.compare(QLatin1String("localhost"), Qt::CaseInsensitive) != 0 && !QHostAddress(host).isLoopback()) { + // not required when not using secure connection + const QUrl syncthingUrl(m_syncthingUrl); + if(!syncthingUrl.scheme().endsWith(QChar('s'))) { return; } + // only possible if the Syncthing instance is running on the local machine + const QString host(syncthingUrl.host()); + if(host.compare(QLatin1String("localhost"), Qt::CaseInsensitive) != 0 && !QHostAddress(host).isLoopback()) { + return; + } + // find cert const QString certPath = !m_configDir.isEmpty() ? (m_configDir + QStringLiteral("/https-cert.pem")) : SyncthingConfig::locateHttpsCertificate(); if(certPath.isEmpty()) { @@ -637,6 +657,32 @@ void SyncthingConnection::loadSelfSignedCertificate() m_expectedSslErrors << QSslError(QSslError::HostNameMismatch, cert.at(0)); } +/*! + * \brief Applies the specified configuration. + * \remarks + * - The expected SSL errors of the specified configuration are updated accordingly. + * - The configuration is not used instantly. It will be used on the next reconnect. + * \sa reconnect() + */ +void SyncthingConnection::applySettings(SyncthingConnectionSettings &connectionSettings) +{ + setSyncthingUrl(connectionSettings.syncthingUrl); + setApiKey(connectionSettings.apiKey); + if(connectionSettings.authEnabled) { + setCredentials(connectionSettings.userName, connectionSettings.password); + } else { + setCredentials(QString(), QString()); + } + setTrafficPollInterval(connectionSettings.trafficPollInterval); + setDevStatsPollInterval(connectionSettings.devStatsPollInterval); + if(connectionSettings.expectedSslErrors.isEmpty()) { + loadSelfSignedCertificate(); + connectionSettings.expectedSslErrors = expectedSslErrors(); + } else { + m_expectedSslErrors = connectionSettings.expectedSslErrors; + } +} + /*! * \brief Reads results of requestConfig(). */ @@ -855,7 +901,6 @@ void SyncthingConnection::readConnections() /*! * \brief Reads results of requestDirStatistics(). - * \remarks TODO */ void SyncthingConnection::readDirStatistics() { @@ -916,7 +961,6 @@ void SyncthingConnection::readDirStatistics() /*! * \brief Reads results of requestDeviceStatistics(). - * \remarks TODO */ void SyncthingConnection::readDeviceStatistics() { @@ -957,6 +1001,9 @@ void SyncthingConnection::readDeviceStatistics() } } +/*! + * \brief Reads results of requestErrors(). + */ void SyncthingConnection::readErrors() { auto *reply = static_cast(sender()); @@ -1257,6 +1304,7 @@ void SyncthingConnection::readDeviceEvent(DateTime eventTime, const QString &eve /*! * \brief Reads results of requestEvents(). + * \remarks TODO */ void SyncthingConnection::readItemStarted(DateTime eventTime, const QJsonObject &eventData) { @@ -1305,6 +1353,7 @@ void SyncthingConnection::readRescan() reply->deleteLater(); switch(reply->error()) { case QNetworkReply::NoError: + emit rescanTriggered(reply->property("dirId").toString()); break; default: emit error(tr("Unable to request rescan: ") + reply->errorString()); @@ -1320,6 +1369,11 @@ void SyncthingConnection::readPauseResume() reply->deleteLater(); switch(reply->error()) { case QNetworkReply::NoError: + if(reply->property("resume").toBool()) { + emit resumeTriggered(reply->property("devId").toString()); + } else { + emit pauseTriggered(reply->property("devId").toString()); + } break; default: emit error(tr("Unable to request pause/resume: ") + reply->errorString()); @@ -1335,6 +1389,7 @@ void SyncthingConnection::readRestart() reply->deleteLater(); switch(reply->error()) { case QNetworkReply::NoError: + emit restartTriggered(); break; default: emit error(tr("Unable to request restart: ") + reply->errorString()); @@ -1404,4 +1459,99 @@ void SyncthingConnection::emitNotification(DateTime when, const QString &message emit newNotification(when, message); } +/*! + * \fn SyncthingConnection::newConfig() + * \brief Indicates new configuration (dirs, devs, ...) is available. + * \remarks + * - Configuration is requested automatically when connecting. + * - Previous directories (and directory info objects!) are invalidated. + * - Previous devices (and device info objects!) are invalidated. + */ + +/*! + * \fn SyncthingConnection::newDirs() + * \brief Indicates new directories are available. + * \remarks Always emitted after newConfig() as soon as new directory info objects become available. + */ + +/*! + * \fn SyncthingConnection::newDevices() + * \brief Indicates new devices are available. + * \remarks Always emitted after newConfig() as soon as new device info objects become available. + */ + +/*! + * \fn SyncthingConnection::newEvents() + * \brief Indicates new events (dir status changed, ...) are available. + * \remarks New events are automatically polled when connected. + */ + +/*! + * \fn SyncthingConnection::dirStatusChanged() + * \brief Indicates the status of the specified \a dir changed. + */ + +/*! + * \fn SyncthingConnection::devStatusChanged() + * \brief Indicates the status of the specified \a dev changed. + */ + +/*! + * \fn SyncthingConnection::downloadProgressChanged() + * \brief Indicates the download progress changed. + */ + +/*! + * \fn SyncthingConnection::newNotification() + * \brief Indicates a new Syncthing notification is available. + */ + +/*! + * \fn SyncthingConnection::error() + * \brief Indicates a request (for configuration, events, ...) failed. + */ + +/*! + * \fn SyncthingConnection::statusChanged() + * \brief Indicates the status of the connection changed. + */ + +/*! + * \fn SyncthingConnection::configDirChanged() + * \brief Indicates the Syncthing home/configuration directory changed. + */ + +/*! + * \fn SyncthingConnection::myIdChanged() + * \brief Indicates ID of the own Syncthing device changed. + */ + +/*! + * \fn SyncthingConnection::trafficChanged() + * \brief Indicates totalIncomingTraffic() or totalOutgoingTraffic() has changed. + */ + +/*! + * \fn SyncthingConnection::rescanTriggered() + * \brief Indicates a rescan has been triggered sucessfully. + * \remarks Only emitted for rescans triggered internally via rescan() or rescanAll(). + */ + +/*! + * \fn SyncthingConnection::pauseTriggered() + * \brief Indicates a device has been paused sucessfully. + * \remarks Only emitted for pausing triggered internally via pause() or pauseAll(). + */ + +/*! + * \fn SyncthingConnection::resumeTriggered() + * \brief Indicates a device has been resumed sucessfully. + * \remarks Only emitted for resuming triggered internally via resume() or resumeAll(). + */ + +/*! + * \fn SyncthingConnection::restartTriggered() + * \brief Indicates a restart has been successfully triggered via restart(). + */ + } diff --git a/connector/syncthingconnection.h b/connector/syncthingconnection.h index 14fd1da..3ec224c 100644 --- a/connector/syncthingconnection.h +++ b/connector/syncthingconnection.h @@ -194,9 +194,13 @@ public: QMetaObject::Connection requestQrCode(const QString &text, std::function callback); QMetaObject::Connection requestLog(std::function &)> callback); const QList &expectedSslErrors(); + SyncthingDir *findDirInfo(const QString &dirId, int &row); + SyncthingDev *findDevInfo(const QString &devId, int &row); + SyncthingDev *findDevInfoByName(const QString &devName, int &row); public Q_SLOTS: void loadSelfSignedCertificate(); + void applySettings(SyncthingConnectionSettings &connectionSettings); void connect(); void disconnect(); void reconnect(); @@ -211,77 +215,23 @@ public Q_SLOTS: void considerAllNotificationsRead(); Q_SIGNALS: - /*! - * \brief Indicates new configuration (dirs, devs, ...) is available. - * \remarks - * - Configuration is requested automatically when connecting. - * - Previous directories (and directory info objects!) are invalidated. - * - Previous devices (and device info objects!) are invalidated. - */ void newConfig(const QJsonObject &config); - - /*! - * \brief Indicates new directories are available. - * \remarks Always emitted after newConfig() as soon as new directory info objects become available. - */ void newDirs(const std::vector &dirs); - - /*! - * \brief Indicates new devices are available. - * \remarks Always emitted after newConfig() as soon as new device info objects become available. - */ void newDevices(const std::vector &devs); - - /*! - * \brief Indicates new events (dir status changed, ...) are available. - * \remarks New events are automatically polled when connected. - */ void newEvents(const QJsonArray &events); - - /*! - * \brief Indicates the status of the specified \a dir changed. - */ void dirStatusChanged(const SyncthingDir &dir, int index); - - /*! - * \brief Indicates the status of the specified \a dev changed. - */ void devStatusChanged(const SyncthingDev &dev, int index); - - /*! - * \brief Indicates the download progress changed. - */ void downloadProgressChanged(); - - /*! - * \brief Indicates a new Syncthing notification is available. - */ void newNotification(ChronoUtilities::DateTime when, const QString &message); - - /*! - * \brief Indicates a request (for configuration, events, ...) failed. - */ void error(const QString &errorMessage); - - /*! - * \brief Indicates the status of the connection changed. - */ void statusChanged(SyncthingStatus newStatus); - - /*! - * \brief Indicates the Syncthing home/configuration directory changed. - */ void configDirChanged(const QString &newConfigDir); - - /*! - * \brief Indicates ID of the own Syncthing device changed. - */ void myIdChanged(const QString &myNewId); - - /*! - * \brief Indicates totalIncomingTraffic() or totalOutgoingTraffic() has changed. - */ void trafficChanged(int totalIncomingTraffic, int totalOutgoingTraffic); + void rescanTriggered(const QString &dirId); + void pauseTriggered(const QString &devId); + void resumeTriggered(const QString &devId); + void restartTriggered(); private Q_SLOTS: void requestConfig(); @@ -322,8 +272,6 @@ private: QNetworkRequest prepareRequest(const QString &path, const QUrlQuery &query, bool rest = true); QNetworkReply *requestData(const QString &path, const QUrlQuery &query, bool rest = true); QNetworkReply *postData(const QString &path, const QUrlQuery &query, const QByteArray &data = QByteArray()); - SyncthingDir *findDirInfo(const QString &dirId, int &row); - SyncthingDev *findDevInfo(const QString &devId, int &row); QString m_syncthingUrl; QByteArray m_apiKey; @@ -536,8 +484,8 @@ inline const std::vector &SyncthingConnection::devInfo() const } /*! - * \brief Returns a list of all expected certificate errors. - * \remarks This list is shared by all instances and updated via loadSelfSignedCertificate(). + * \brief Returns a list of all expected certificate errors. This is meant to allow self-signed certificates. + * \remarks This list is updated via loadSelfSignedCertificate(). */ inline const QList &SyncthingConnection::expectedSslErrors() { diff --git a/connector/syncthingconnectionsettings.cpp b/connector/syncthingconnectionsettings.cpp index afd3058..f6658f6 100644 --- a/connector/syncthingconnectionsettings.cpp +++ b/connector/syncthingconnectionsettings.cpp @@ -4,12 +4,12 @@ namespace Data { bool SyncthingConnectionSettings::loadHttpsCert() { + expectedSslErrors.clear(); if(!httpsCertPath.isEmpty()) { const QList cert = QSslCertificate::fromPath(httpsCertPath); if(cert.isEmpty()) { return false; } - expectedSslErrors.clear(); expectedSslErrors.reserve(4); expectedSslErrors << QSslError(QSslError::UnableToGetLocalIssuerCertificate, cert.at(0)); expectedSslErrors << QSslError(QSslError::UnableToVerifyFirstCertificate, cert.at(0)); diff --git a/model/CMakeLists.txt b/model/CMakeLists.txt index 9d80bd3..9995b5a 100644 --- a/model/CMakeLists.txt +++ b/model/CMakeLists.txt @@ -3,6 +3,7 @@ cmake_minimum_required(VERSION 3.1.0 FATAL_ERROR) # metadata set(META_PROJECT_NAME syncthingmodel) set(META_PROJECT_TYPE library) +set(META_APP_NAME "Data models of Syncthing Tray") set(META_APP_DESCRIPTION "Data models of Syncthing Tray") set(META_PROJECT_VARNAME_UPPER LIB_SYNCTHING_MODEL) diff --git a/tray/CMakeLists.txt b/tray/CMakeLists.txt index fb8732e..8ae294f 100644 --- a/tray/CMakeLists.txt +++ b/tray/CMakeLists.txt @@ -3,6 +3,7 @@ cmake_minimum_required(VERSION 3.1.0 FATAL_ERROR) # metadata set(META_PROJECT_TYPE application) set(META_GUI_OPTIONAL false) +set(META_APP_NAME "Syncthing Tray") # add project files set(WIDGETS_HEADER_FILES diff --git a/tray/README.md b/tray/README.md new file mode 120000 index 0000000..32d46ee --- /dev/null +++ b/tray/README.md @@ -0,0 +1 @@ +../README.md \ No newline at end of file diff --git a/tray/application/main.cpp b/tray/application/main.cpp index a2339ef..9014bd1 100644 --- a/tray/application/main.cpp +++ b/tray/application/main.cpp @@ -18,6 +18,7 @@ #include #include +#include #include #include @@ -94,6 +95,7 @@ int runApplication(int argc, const char *const *argv) QApplication application(argc, const_cast(argv)); QGuiApplication::setQuitOnLastWindowClosed(false); SingleInstance singleInstance(argc, argv); + networkAccessManager().setParent(&singleInstance); QObject::connect(&singleInstance, &SingleInstance::newInstance, &runApplication); Settings::restore(); @@ -125,7 +127,8 @@ int runApplication(int argc, const char *const *argv) } } catch(const Failure &ex) { CMD_UTILS_START_CONSOLE; - cout << "Unable to parse arguments. " << ex.what() << "\nSee --help for available commands." << endl; + cerr << "Unable to parse arguments. " << ex.what() << "\nSee --help for available commands." << endl; + return 1; } return 0;