Add syncthingctl, see README.md

This commit is contained in:
Martchus 2016-10-02 21:59:28 +02:00
parent 6470038fc7
commit 1f21c2dc52
17 changed files with 827 additions and 90 deletions

View File

@ -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})

View File

@ -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

44
cli/CMakeLists.txt Normal file
View File

@ -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)

385
cli/application.cpp Normal file
View File

@ -0,0 +1,385 @@
#include "./application.h"
#include "./helper.h"
#include "../connector/syncthingconfig.h"
#include <c++utilities/application/failure.h>
#include <c++utilities/io/ansiescapecodes.h>
#include <c++utilities/chrono/timespan.h>
#include <c++utilities/conversion/stringconversion.h>
#include <QCoreApplication>
#include <QNetworkAccessManager>
#include <QHostAddress>
#include <functional>
#include <iostream>
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<const SyncthingDir *> relevantDirs;
std::vector<const SyncthingDev *> 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<uint64>(dev->totalIncomingTraffic)).data());
}
if(dev->totalOutgoingTraffic > 0) {
printProperty("Outgoing traffic", dataSizeToString(static_cast<uint64>(dev->totalOutgoingTraffic)).data());
}
cout << '\n';
}
}
cout.flush();
QCoreApplication::exit();
}
void Application::printLog(const std::vector<SyncthingLogEntry> &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

48
cli/application.h Normal file
View File

@ -0,0 +1,48 @@
#ifndef CLI_APPLICATION_H
#define CLI_APPLICATION_H
#include "./args.h"
#include "../connector/syncthingconnection.h"
#include "../connector/syncthingconnectionsettings.h"
#include <QObject>
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<Data::SyncthingLogEntry> &logEntries);
Args m_args;
Data::SyncthingConnectionSettings m_settings;
Data::SyncthingConnection m_connection;
size_t m_expectedResponse;
};
} // namespace Cli
#endif // CLI_APPLICATION_H

45
cli/args.cpp Normal file
View File

@ -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

22
cli/args.h Normal file
View File

@ -0,0 +1,22 @@
#ifndef CLI_ARGS_H
#define CLI_ARGS_H
#include <c++utilities/application/argumentparser.h>
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

70
cli/helper.h Normal file
View File

@ -0,0 +1,70 @@
#ifndef SYNCTHINGCTL_HELPER
#define SYNCTHINGCTL_HELPER
#include <c++utilities/application/commandlineutils.h>
#include <c++utilities/chrono/datetime.h>
#include <c++utilities/chrono/timespan.h>
#include <c++utilities/conversion/stringconversion.h>
#include <QString>
#include <QStringList>
#include <iostream>
#include <cstring>
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<typename intType>
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<intType>(value).data(), suffix, indentation);
}
}
}
#endif // SYNCTHINGCTL_HELPER

15
cli/main.cpp Normal file
View File

@ -0,0 +1,15 @@
#include "./application.h"
#include "resources/config.h"
#include <c++utilities/application/argumentparser.h>
#include <QCoreApplication>
int main(int argc, char *argv[])
{
SET_APPLICATION_INFO;
QCoreApplication coreApp(argc, argv);
Cli::Application cliApp;
return cliApp.exec(argc, argv);
}

View File

@ -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)

View File

@ -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<QNetworkReply *>(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().
*/
}

View File

@ -194,9 +194,13 @@ public:
QMetaObject::Connection requestQrCode(const QString &text, std::function<void (const QByteArray &)> callback);
QMetaObject::Connection requestLog(std::function<void (const std::vector<SyncthingLogEntry> &)> callback);
const QList<QSslError> &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<SyncthingDir> &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<SyncthingDev> &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<SyncthingDev> &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<QSslError> &SyncthingConnection::expectedSslErrors()
{

View File

@ -4,12 +4,12 @@ namespace Data {
bool SyncthingConnectionSettings::loadHttpsCert()
{
expectedSslErrors.clear();
if(!httpsCertPath.isEmpty()) {
const QList<QSslCertificate> 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));

View File

@ -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)

View File

@ -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

1
tray/README.md Symbolic link
View File

@ -0,0 +1 @@
../README.md

View File

@ -18,6 +18,7 @@
#include <qtutilities/settingsdialog/qtsettings.h>
#include <QApplication>
#include <QNetworkAccessManager>
#include <QMessageBox>
#include <iostream>
@ -94,6 +95,7 @@ int runApplication(int argc, const char *const *argv)
QApplication application(argc, const_cast<char **>(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;