Allow editing Syncthing config via JavaScript
This commit is contained in:
parent
bf4b26c6f8
commit
b61592fbbd
|
@ -5,6 +5,7 @@ set(META_PROJECT_NAME syncthingctl)
|
||||||
set(META_APP_NAME "Syncthing control")
|
set(META_APP_NAME "Syncthing control")
|
||||||
set(META_APP_DESCRIPTION "Command line app to control Syncthing")
|
set(META_APP_DESCRIPTION "Command line app to control Syncthing")
|
||||||
set(META_PROJECT_TYPE application)
|
set(META_PROJECT_TYPE application)
|
||||||
|
set(META_JS_SRC_DIR .)
|
||||||
|
|
||||||
# add project files
|
# add project files
|
||||||
set(HEADER_FILES
|
set(HEADER_FILES
|
||||||
|
@ -32,6 +33,11 @@ use_syncthingconnector()
|
||||||
|
|
||||||
# include modules to apply configuration
|
# include modules to apply configuration
|
||||||
include(BasicConfig)
|
include(BasicConfig)
|
||||||
|
include(JsProviderConfig)
|
||||||
|
if(JS_PROVIDER)
|
||||||
|
list(APPEND HEADER_FILES jsconsole.h)
|
||||||
|
list(APPEND SRC_FILES jsconsole.cpp)
|
||||||
|
endif()
|
||||||
include(QtConfig)
|
include(QtConfig)
|
||||||
include(WindowsResources)
|
include(WindowsResources)
|
||||||
include(AppTarget)
|
include(AppTarget)
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
#include "./application.h"
|
#include "./application.h"
|
||||||
#include "./helper.h"
|
#include "./helper.h"
|
||||||
|
#include "./jsconsole.h"
|
||||||
|
#include "./jsdefs.h"
|
||||||
|
#include "./jsincludes.h"
|
||||||
|
|
||||||
#include "../connector/syncthingconfig.h"
|
#include "../connector/syncthingconfig.h"
|
||||||
#include "../connector/utils.h"
|
#include "../connector/utils.h"
|
||||||
|
@ -8,6 +11,8 @@
|
||||||
#define SYNCTHINGTESTHELPER_FOR_CLI
|
#define SYNCTHINGTESTHELPER_FOR_CLI
|
||||||
#include "../testhelper/helper.h"
|
#include "../testhelper/helper.h"
|
||||||
|
|
||||||
|
#include "resources/config.h"
|
||||||
|
|
||||||
#include <c++utilities/application/failure.h>
|
#include <c++utilities/application/failure.h>
|
||||||
#include <c++utilities/chrono/timespan.h>
|
#include <c++utilities/chrono/timespan.h>
|
||||||
#include <c++utilities/conversion/stringconversion.h>
|
#include <c++utilities/conversion/stringconversion.h>
|
||||||
|
@ -244,6 +249,16 @@ bool Application::waitForConfig(int timeout)
|
||||||
signalInfo(&m_connection, &SyncthingConnection::newDirs), signalInfo(&m_connection, &SyncthingConnection::newDevices));
|
signalInfo(&m_connection, &SyncthingConnection::newDirs), signalInfo(&m_connection, &SyncthingConnection::newDevices));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool Application::waitForConfigAndStatus(int timeout)
|
||||||
|
{
|
||||||
|
using namespace TestUtilities;
|
||||||
|
m_connection.applySettings(m_settings);
|
||||||
|
return waitForSignalsOrFail(bind(&SyncthingConnection::requestConfigAndStatus, ref(m_connection)), timeout,
|
||||||
|
signalInfo(&m_connection, &SyncthingConnection::error), signalInfo(&m_connection, &SyncthingConnection::newConfig),
|
||||||
|
signalInfo(&m_connection, &SyncthingConnection::newDirs), signalInfo(&m_connection, &SyncthingConnection::newDevices),
|
||||||
|
signalInfo(&m_connection, &SyncthingConnection::myIdChanged));
|
||||||
|
}
|
||||||
|
|
||||||
void Application::handleStatusChanged(SyncthingStatus newStatus)
|
void Application::handleStatusChanged(SyncthingStatus newStatus)
|
||||||
{
|
{
|
||||||
Q_UNUSED(newStatus)
|
Q_UNUSED(newStatus)
|
||||||
|
@ -586,7 +601,6 @@ void Application::printConfig(const ArgumentOccurrence &)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
cerr << Phrases::Override;
|
cerr << Phrases::Override;
|
||||||
|
|
||||||
cout << QJsonDocument(m_connection.rawConfig()).toJson(QJsonDocument::Indented).data() << flush;
|
cout << QJsonDocument(m_connection.rawConfig()).toJson(QJsonDocument::Indented).data() << flush;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -595,13 +609,47 @@ void Application::editConfig(const ArgumentOccurrence &)
|
||||||
// disable main event loop since this method is invoked directly as argument callback and we're doing all required async operations during the waitForConfig() call already
|
// disable main event loop since this method is invoked directly as argument callback and we're doing all required async operations during the waitForConfig() call already
|
||||||
m_requiresMainEventLoop = false;
|
m_requiresMainEventLoop = false;
|
||||||
|
|
||||||
|
// wait until config is available
|
||||||
|
if (!(m_args.script.isPresent() ? waitForConfigAndStatus() : waitForConfig())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cerr << Phrases::Override;
|
||||||
|
|
||||||
|
const auto newConfig(m_args.script.isPresent() ? editConfigViaScript() : editConfigViaEditor());
|
||||||
|
if (newConfig.isEmpty()) {
|
||||||
|
// just return here; an error message should have already been printed by editConfigVia*()
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle "dry-run" case
|
||||||
|
if (m_args.dryRun.isPresent()) {
|
||||||
|
cout << newConfig.data();
|
||||||
|
if (!newConfig.endsWith('\n')) {
|
||||||
|
cout << '\n';
|
||||||
|
}
|
||||||
|
cout << flush;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// post new config
|
||||||
|
using namespace TestUtilities;
|
||||||
|
cerr << Phrases::Info << "Posting new configuration ..." << TextAttribute::Reset << flush;
|
||||||
|
if (!waitForSignalsOrFail(bind(&SyncthingConnection::postConfigFromByteArray, ref(m_connection), ref(newConfig)), 0,
|
||||||
|
signalInfo(&m_connection, &SyncthingConnection::error), signalInfo(&m_connection, &SyncthingConnection::newConfigTriggered))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cerr << Phrases::Override << Phrases::Info << "Configuration posted successfully" << Phrases::EndFlush;
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray Application::editConfigViaEditor() const
|
||||||
|
{
|
||||||
// read editor command and options
|
// read editor command and options
|
||||||
const auto *const editorArgValue(m_args.editor.firstValue());
|
const auto *const editorArgValue(m_args.editor.firstValue());
|
||||||
const auto editorCommand(editorArgValue ? QString::fromLocal8Bit(editorArgValue) : QString());
|
const auto editorCommand(editorArgValue ? QString::fromLocal8Bit(editorArgValue) : QString());
|
||||||
if (editorCommand.isEmpty()) {
|
if (editorCommand.isEmpty()) {
|
||||||
cerr << Phrases::Error << "No editor command specified. It must be either passed via --editor argument or EDITOR environment variable."
|
cerr << Phrases::Error << "No editor command specified. It must be either passed via --editor argument or EDITOR environment variable."
|
||||||
<< Phrases::EndFlush;
|
<< Phrases::EndFlush;
|
||||||
return;
|
return QByteArray();
|
||||||
}
|
}
|
||||||
QStringList editorOptions;
|
QStringList editorOptions;
|
||||||
if (m_args.editor.isPresent()) {
|
if (m_args.editor.isPresent()) {
|
||||||
|
@ -614,17 +662,11 @@ void Application::editConfig(const ArgumentOccurrence &)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// wait until config is available
|
|
||||||
if (!waitForConfig()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
cerr << Phrases::Override;
|
|
||||||
|
|
||||||
// write config to temporary file
|
// write config to temporary file
|
||||||
QTemporaryFile tempFile(QStringLiteral("syncthing-config-XXXXXX.json"));
|
QTemporaryFile tempFile(QStringLiteral("syncthing-config-XXXXXX.json"));
|
||||||
if (!tempFile.open() || !tempFile.write(QJsonDocument(m_connection.rawConfig()).toJson(QJsonDocument::Indented))) {
|
if (!tempFile.open() || !tempFile.write(QJsonDocument(m_connection.rawConfig()).toJson(QJsonDocument::Indented))) {
|
||||||
cerr << Phrases::Error << "Unable to write the configuration to a temporary file." << Phrases::EndFlush;
|
cerr << Phrases::Error << "Unable to write the configuration to a temporary file." << Phrases::EndFlush;
|
||||||
return;
|
return QByteArray();
|
||||||
}
|
}
|
||||||
editorOptions << tempFile.fileName();
|
editorOptions << tempFile.fileName();
|
||||||
tempFile.close();
|
tempFile.close();
|
||||||
|
@ -650,19 +692,19 @@ void Application::editConfig(const ArgumentOccurrence &)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cerr << endl;
|
cerr << endl;
|
||||||
return;
|
return QByteArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
// read (altered) configuration again
|
// read (altered) configuration again
|
||||||
QFile tempFile2(editorOptions.back());
|
QFile tempFile2(editorOptions.back());
|
||||||
if (!tempFile2.open(QIODevice::ReadOnly)) {
|
if (!tempFile2.open(QIODevice::ReadOnly)) {
|
||||||
cerr << Phrases::Error << "Unable to open temporary file containing the configuration again." << Phrases::EndFlush;
|
cerr << Phrases::Error << "Unable to open temporary file containing the configuration again." << Phrases::EndFlush;
|
||||||
return;
|
return QByteArray();
|
||||||
}
|
}
|
||||||
const auto newConfig(tempFile2.readAll());
|
const auto newConfig(tempFile2.readAll());
|
||||||
if (newConfig.isEmpty()) {
|
if (newConfig.isEmpty()) {
|
||||||
cerr << Phrases::Error << "Unable to read any bytes from temporary file containing the configuration." << Phrases::EndFlush;
|
cerr << Phrases::Error << "Unable to read any bytes from temporary file containing the configuration." << Phrases::EndFlush;
|
||||||
return;
|
return QByteArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
// convert the config to JSON again (could send it to Syncthing as it is, but this allows us to check whether the JSON is valid)
|
// convert the config to JSON again (could send it to Syncthing as it is, but this allows us to check whether the JSON is valid)
|
||||||
|
@ -671,42 +713,113 @@ void Application::editConfig(const ArgumentOccurrence &)
|
||||||
if (error.error != QJsonParseError::NoError) {
|
if (error.error != QJsonParseError::NoError) {
|
||||||
cerr << Phrases::Error << "Unable to parse new configuration" << Phrases::End << "reason: " << error.errorString().toLocal8Bit().data()
|
cerr << Phrases::Error << "Unable to parse new configuration" << Phrases::End << "reason: " << error.errorString().toLocal8Bit().data()
|
||||||
<< " at character " << error.offset << endl;
|
<< " at character " << error.offset << endl;
|
||||||
return;
|
return QByteArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
// perform at least some checks before sending the configuration
|
// perform at least some checks before sending the configuration
|
||||||
const auto configObj(configDoc.object());
|
const auto configObj(configDoc.object());
|
||||||
if (configObj.isEmpty()) {
|
if (configObj.isEmpty()) {
|
||||||
cerr << Phrases::Error << "New config object seems empty." << Phrases::EndFlush;
|
cerr << Phrases::Error << "New config object seems empty." << Phrases::EndFlush;
|
||||||
return;
|
return QByteArray();
|
||||||
}
|
}
|
||||||
for (const auto &arrayName : {QStringLiteral("devices"), QStringLiteral("folders")}) {
|
for (const auto &arrayName : { QStringLiteral("devices"), QStringLiteral("folders") }) {
|
||||||
if (!configObj.value(arrayName).isArray()) {
|
if (!configObj.value(arrayName).isArray()) {
|
||||||
cerr << Phrases::Error << "Array \"" << arrayName.toLocal8Bit().data() << "\" is not present." << Phrases::EndFlush;
|
cerr << Phrases::Error << "Array \"" << arrayName.toLocal8Bit().data() << "\" is not present." << Phrases::EndFlush;
|
||||||
return;
|
return QByteArray();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const auto &objectName : {QStringLiteral("options"), QStringLiteral("gui")}) {
|
for (const auto &objectName : { QStringLiteral("options"), QStringLiteral("gui") }) {
|
||||||
if (!configObj.value(objectName).isObject()) {
|
if (!configObj.value(objectName).isObject()) {
|
||||||
cerr << Phrases::Error << "Object \"" << objectName.toLocal8Bit().data() << "\" is not present." << Phrases::EndFlush;
|
cerr << Phrases::Error << "Object \"" << objectName.toLocal8Bit().data() << "\" is not present." << Phrases::EndFlush;
|
||||||
return;
|
return QByteArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray Application::editConfigViaScript() const
|
||||||
|
{
|
||||||
|
#if defined(SYNCTHINGCTL_USE_SCRIPT) || defined(SYNCTHINGCTL_USE_JSENGINE)
|
||||||
|
// read script file
|
||||||
|
QFile scriptFile(QString::fromLocal8Bit(m_args.script.firstValue()));
|
||||||
|
if (!scriptFile.open(QFile::ReadOnly)) {
|
||||||
|
cerr << Phrases::Error << "Unable to open specified script file \"" << m_args.script.firstValue() << "\"." << Phrases::EndFlush;
|
||||||
|
return QByteArray();
|
||||||
|
}
|
||||||
|
const auto script(scriptFile.readAll());
|
||||||
|
if (script.isEmpty()) {
|
||||||
|
cerr << Phrases::Error << "Unable to read any bytes from specified script file \"" << m_args.script.firstValue() << "\"."
|
||||||
|
<< Phrases::EndFlush;
|
||||||
|
return QByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
// define function to print error
|
||||||
|
const auto printError([](const auto &object) {
|
||||||
|
cerr << object.toString().toLocal8Bit().data() << "\nin line " << SYNCTHINGCTL_JS_INT(object.property(QStringLiteral("lineNumber"))) << endl;
|
||||||
|
});
|
||||||
|
|
||||||
|
// evaluate config via JSON.parse()
|
||||||
|
SYNCTHINGCTL_JS_ENGINE engine;
|
||||||
|
auto globalObject(engine.globalObject());
|
||||||
|
const auto configString(QJsonDocument(m_connection.rawConfig()).toJson(QJsonDocument::Indented));
|
||||||
|
globalObject.setProperty(QStringLiteral("configStr"), SYNCTHINGCTL_JS_VALUE(QString::fromUtf8(configString)) SYNCTHINGCTL_JS_READONLY);
|
||||||
|
const auto configObj(engine.evaluate(QStringLiteral("JSON.parse(configStr)")));
|
||||||
|
if (configObj.isError()) {
|
||||||
|
cerr << Phrases::Error << "Unable to evaluate the current Syncthing configuration." << Phrases::End;
|
||||||
|
printError(configObj);
|
||||||
|
cerr << "Syncthing configuration: " << configString.data() << flush;
|
||||||
|
return QByteArray();
|
||||||
|
}
|
||||||
|
globalObject.setProperty(QStringLiteral("config"), configObj SYNCTHINGCTL_JS_UNDELETABLE);
|
||||||
|
|
||||||
|
// provide additional values
|
||||||
|
globalObject.setProperty(QStringLiteral("ownID"), m_connection.myId() SYNCTHINGCTL_JS_UNDELETABLE);
|
||||||
|
globalObject.setProperty(QStringLiteral("url"), m_connection.syncthingUrl() SYNCTHINGCTL_JS_UNDELETABLE);
|
||||||
|
|
||||||
|
// provide console.log() which is not available in QJSEngine and QScriptEngine by default (note that print() is only available when using Qt Script)
|
||||||
|
JSConsole console;
|
||||||
|
engine.globalObject().setProperty("console", engine.newQObject(&console));
|
||||||
|
|
||||||
|
// evaluate the user provided script
|
||||||
|
const auto res(engine.evaluate(QString::fromUtf8(script), scriptFile.fileName()));
|
||||||
|
if (res.isError()) {
|
||||||
|
cerr << Phrases::Error << "Unable to evaluate the specified script file \"" << m_args.script.firstValue() << "\"." << Phrases::End;
|
||||||
|
printError(res);
|
||||||
|
return QByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate the altered configuration
|
||||||
|
const auto newConfigObj(globalObject.property(QStringLiteral("config")));
|
||||||
|
if (!newConfigObj.isObject()) {
|
||||||
|
cerr << Phrases::Error << "New config object seems empty." << Phrases::EndFlush;
|
||||||
|
return QByteArray();
|
||||||
|
}
|
||||||
|
for (const auto &arrayName : { QStringLiteral("devices"), QStringLiteral("folders") }) {
|
||||||
|
if (!newConfigObj.property(arrayName).isArray()) {
|
||||||
|
cerr << Phrases::Error << "Array \"" << arrayName.toLocal8Bit().data() << "\" is not present." << Phrases::EndFlush;
|
||||||
|
return QByteArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const auto &objectName : { QStringLiteral("options"), QStringLiteral("gui") }) {
|
||||||
|
if (!newConfigObj.property(objectName).isObject()) {
|
||||||
|
cerr << Phrases::Error << "Object \"" << objectName.toLocal8Bit().data() << "\" is not present." << Phrases::EndFlush;
|
||||||
|
return QByteArray();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle "dry-run" case
|
// serilaize the altered configuration via JSON.stringify()
|
||||||
if (m_args.dryRun.isPresent()) {
|
const auto newConfigJson(engine.evaluate(QStringLiteral("JSON.stringify(config, null, 4)")));
|
||||||
cout << newConfig.data() << flush;
|
if (!newConfigJson.isString()) {
|
||||||
return;
|
cerr << Phrases::Error << "Unable to convert the config object to JSON via JSON.stringify()." << Phrases::End;
|
||||||
|
cerr << configObj.toString().toLocal8Bit().data() << endl;
|
||||||
|
return QByteArray();
|
||||||
}
|
}
|
||||||
|
return newConfigJson.toString().toUtf8();
|
||||||
|
|
||||||
// post new config
|
#else
|
||||||
using namespace TestUtilities;
|
cerr << Phrases::Error << PROJECT_NAME " has not been built with JavaScript support." << Phrases::EndFlush;
|
||||||
cerr << Phrases::Info << "Posting new configuration ..." << TextAttribute::Reset << flush;
|
return QByteArray();
|
||||||
if (!waitForSignalsOrFail(bind(&SyncthingConnection::postConfig, ref(m_connection), ref(configObj)), 0,
|
#endif
|
||||||
signalInfo(&m_connection, &SyncthingConnection::error), signalInfo(&m_connection, &SyncthingConnection::newConfigTriggered))) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
cerr << Phrases::Override << Phrases::Info << "Configuration posted successfully" << Phrases::EndFlush;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Application::waitForIdle(const ArgumentOccurrence &)
|
void Application::waitForIdle(const ArgumentOccurrence &)
|
||||||
|
|
|
@ -55,6 +55,7 @@ private:
|
||||||
int loadConfig();
|
int loadConfig();
|
||||||
bool waitForConnected(int timeout = 2000);
|
bool waitForConnected(int timeout = 2000);
|
||||||
bool waitForConfig(int timeout = 2000);
|
bool waitForConfig(int timeout = 2000);
|
||||||
|
bool waitForConfigAndStatus(int timeout = 2000);
|
||||||
void requestLog(const ArgumentOccurrence &);
|
void requestLog(const ArgumentOccurrence &);
|
||||||
void requestShutdown(const ArgumentOccurrence &);
|
void requestShutdown(const ArgumentOccurrence &);
|
||||||
void requestRestart(const ArgumentOccurrence &);
|
void requestRestart(const ArgumentOccurrence &);
|
||||||
|
@ -67,6 +68,8 @@ private:
|
||||||
static void printLog(const std::vector<Data::SyncthingLogEntry> &logEntries);
|
static void printLog(const std::vector<Data::SyncthingLogEntry> &logEntries);
|
||||||
void printConfig(const ArgumentOccurrence &);
|
void printConfig(const ArgumentOccurrence &);
|
||||||
void editConfig(const ArgumentOccurrence &);
|
void editConfig(const ArgumentOccurrence &);
|
||||||
|
QByteArray editConfigViaEditor() const;
|
||||||
|
QByteArray editConfigViaScript() const;
|
||||||
void waitForIdle(const ArgumentOccurrence &);
|
void waitForIdle(const ArgumentOccurrence &);
|
||||||
bool checkWhetherIdle() const;
|
bool checkWhetherIdle() const;
|
||||||
void checkPwdOperationPresent(const ArgumentOccurrence &occurrence);
|
void checkPwdOperationPresent(const ArgumentOccurrence &occurrence);
|
||||||
|
|
|
@ -22,6 +22,7 @@ Args::Args()
|
||||||
, rescanPwd("rescan", 'r', "rescans the current working directory")
|
, rescanPwd("rescan", 'r', "rescans the current working directory")
|
||||||
, pausePwd("pause", 'p', "pauses the current working directory")
|
, pausePwd("pause", 'p', "pauses the current working directory")
|
||||||
, resumePwd("resume", '\0', "resumes the current working directory")
|
, resumePwd("resume", '\0', "resumes the current working directory")
|
||||||
|
, script("script", '\0', "runs the specified UTF-8 encoded ECMAScript on the configuration rather than opening an editor", { "path" })
|
||||||
, dryRun("dry-run", '\0', "writes the altered configuration to stdout instead of posting it to Syncthing")
|
, dryRun("dry-run", '\0', "writes the altered configuration to stdout instead of posting it to Syncthing")
|
||||||
, dir("dir", 'd', "specifies a directory by ID", { "ID" })
|
, dir("dir", 'd', "specifies a directory by ID", { "ID" })
|
||||||
, dev("dev", '\0', "specifies a device by ID or name", { "ID/name" })
|
, dev("dev", '\0', "specifies a device by ID or name", { "ID/name" })
|
||||||
|
@ -48,7 +49,7 @@ Args::Args()
|
||||||
waitForIdle.setExample(PROJECT_NAME " wait-for-idle --timeout 1800000 --at-least 5000 && systemctl poweroff\n" PROJECT_NAME
|
waitForIdle.setExample(PROJECT_NAME " wait-for-idle --timeout 1800000 --at-least 5000 && systemctl poweroff\n" PROJECT_NAME
|
||||||
" wait-for-idle --dir dir1 --dir dir2 --dev dev1 --dev dev2 --at-least 5000");
|
" wait-for-idle --dir dir1 --dir dir2 --dev dev1 --dev dev2 --at-least 5000");
|
||||||
pwd.setSubArguments({ &statusPwd, &rescanPwd, &pausePwd, &resumePwd });
|
pwd.setSubArguments({ &statusPwd, &rescanPwd, &pausePwd, &resumePwd });
|
||||||
edit.setSubArguments({ &editor, &dryRun });
|
edit.setSubArguments({ &editor, &script, &dryRun });
|
||||||
|
|
||||||
rescan.setValueNames({ "dir ID" });
|
rescan.setValueNames({ "dir ID" });
|
||||||
rescan.setRequiredValueCount(Argument::varValueCount);
|
rescan.setRequiredValueCount(Argument::varValueCount);
|
||||||
|
|
|
@ -14,7 +14,7 @@ struct Args {
|
||||||
NoColorArgument noColor;
|
NoColorArgument noColor;
|
||||||
OperationArgument status, log, stop, restart, rescan, rescanAll, pause, resume, waitForIdle, pwd, cat, edit;
|
OperationArgument status, log, stop, restart, rescan, rescanAll, pause, resume, waitForIdle, pwd, cat, edit;
|
||||||
OperationArgument statusPwd, rescanPwd, pausePwd, resumePwd;
|
OperationArgument statusPwd, rescanPwd, pausePwd, resumePwd;
|
||||||
ConfigValueArgument dryRun;
|
ConfigValueArgument script, dryRun;
|
||||||
ConfigValueArgument dir, dev, allDirs, allDevs;
|
ConfigValueArgument dir, dev, allDirs, allDevs;
|
||||||
ConfigValueArgument atLeast, timeout;
|
ConfigValueArgument atLeast, timeout;
|
||||||
ConfigValueArgument editor;
|
ConfigValueArgument editor;
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
#include "./jsconsole.h"
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
using namespace std;
|
||||||
|
|
||||||
|
JSConsole::JSConsole(QObject *parent) : QObject(parent)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void JSConsole::log(const QString &msg) const
|
||||||
|
{
|
||||||
|
cerr << "script: "<< msg.toLocal8Bit().data() << endl;
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
#ifndef CLI_JS_CONSOLE_H
|
||||||
|
#define CLI_JS_CONSOLE_H
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
|
||||||
|
class JSConsole : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit JSConsole(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
void log(const QString &msg) const;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // CLI_JS_CONSOLE_H
|
|
@ -0,0 +1,36 @@
|
||||||
|
// Created via CMake from template jsdefs.h.in
|
||||||
|
// WARNING! Any changes to this file will be overwritten by the next CMake run!
|
||||||
|
|
||||||
|
#ifndef SYNCTHINGCTL_JAVA_SCRIPT_DEFINES
|
||||||
|
#define SYNCTHINGCTL_JAVA_SCRIPT_DEFINES
|
||||||
|
|
||||||
|
#include <QtGlobal>
|
||||||
|
|
||||||
|
#if defined(SYNCTHINGCTL_USE_JSENGINE)
|
||||||
|
# define SYNCTHINGCTL_JS_ENGINE QJSEngine
|
||||||
|
# define SYNCTHINGCTL_JS_VALUE QJSValue
|
||||||
|
# define SYNCTHINGCTL_JS_READONLY
|
||||||
|
# define SYNCTHINGCTL_JS_UNDELETABLE
|
||||||
|
# define SYNCTHINGCTL_JS_QOBJECT(engine, obj) engine.newQObject(obj)
|
||||||
|
# define SYNCTHINGCTL_JS_INT(value) value.toInt()
|
||||||
|
# define SYNCTHINGCTL_JS_IS_VALID_PROG(program) (!program.isError() && program.isCallable())
|
||||||
|
#elif defined(SYNCTHINGCTL_USE_SCRIPT)
|
||||||
|
# define SYNCTHINGCTL_JS_ENGINE QScriptEngine
|
||||||
|
# define SYNCTHINGCTL_JS_VALUE QScriptValue
|
||||||
|
# define SYNCTHINGCTL_JS_READONLY ,QScriptValue::ReadOnly
|
||||||
|
# define SYNCTHINGCTL_JS_UNDELETABLE ,QScriptValue::Undeletable
|
||||||
|
# define SYNCTHINGCTL_JS_QOBJECT(engine, obj) engine.newQObject(obj, QScriptEngine::ScriptOwnership)
|
||||||
|
# define SYNCTHINGCTL_JS_INT(value) value.toInt32()
|
||||||
|
# define SYNCTHINGCTL_JS_IS_VALID_PROG(program) (!program.isError() && program.isFunction())
|
||||||
|
#elif !defined(SYNCTHINGCTL_NO_JSENGINE)
|
||||||
|
# error "No definition for JavaScript provider present."
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef SYNCTHINGCTL_JS_ENGINE
|
||||||
|
QT_FORWARD_DECLARE_CLASS(SYNCTHINGCTL_JS_ENGINE)
|
||||||
|
#endif
|
||||||
|
#ifdef SYNCTHINGCTL_JS_VALUE
|
||||||
|
QT_FORWARD_DECLARE_CLASS(SYNCTHINGCTL_JS_VALUE)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif // SYNCTHINGCTL_JAVA_SCRIPT_DEFINES
|
|
@ -0,0 +1,19 @@
|
||||||
|
// Created via CMake from template jsincludes.h.in
|
||||||
|
// WARNING! Any changes to this file will be overwritten by the next CMake run!
|
||||||
|
|
||||||
|
#ifndef SYNCTHINGCTL_JAVA_SCRIPT_INCLUDES
|
||||||
|
#define SYNCTHINGCTL_JAVA_SCRIPT_INCLUDES
|
||||||
|
|
||||||
|
#include <QtGlobal>
|
||||||
|
|
||||||
|
#if defined(SYNCTHINGCTL_USE_JSENGINE)
|
||||||
|
# include <QJSEngine>
|
||||||
|
# include <QJSValue>
|
||||||
|
#elif defined(SYNCTHINGCTL_USE_SCRIPT)
|
||||||
|
# include <QScriptEngine>
|
||||||
|
# include <QScriptValue>
|
||||||
|
#elif !defined(SYNCTHINGCTL_NO_JSENGINE)
|
||||||
|
# error "No definition for JavaScript provider present."
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif // SYNCTHINGCTL_JAVA_SCRIPT_INCLUDES
|
|
@ -0,0 +1,27 @@
|
||||||
|
// example script for changing configuration with syncthingctl
|
||||||
|
// can be executed like: syncthingctl edit --script example.js
|
||||||
|
|
||||||
|
// alter some options
|
||||||
|
config.gui.useTLS = true;
|
||||||
|
config.options.relaysEnabled = false;
|
||||||
|
|
||||||
|
// enable file system watcher for all folders starting with "docs-"
|
||||||
|
var folders = config.folders;
|
||||||
|
for (var i = 0, count = folders.length; i !== count; ++i) {
|
||||||
|
var folder = folders[i];
|
||||||
|
if (folder.id.indexOf("docs-") === 0) {
|
||||||
|
//folder.fsWatcherDelayS = 50;
|
||||||
|
//folder.fsWatcherEnabled = true;
|
||||||
|
console.log("enabling file system watcher for folder " + folder.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure all devices are enabled
|
||||||
|
var devices = config.devices;
|
||||||
|
for (var i = 0, count = devices.length; i !== count; ++i) {
|
||||||
|
var device = devices[i];
|
||||||
|
if (device.paused) {
|
||||||
|
device.paused = false;
|
||||||
|
console.log("unpausing device " + (device.name ? device.name : device.deviceID));
|
||||||
|
}
|
||||||
|
}
|
|
@ -756,7 +756,7 @@ void SyncthingConnection::requestConfig()
|
||||||
/*!
|
/*!
|
||||||
* \brief Requests the Syncthing status asynchronously.
|
* \brief Requests the Syncthing status asynchronously.
|
||||||
*
|
*
|
||||||
* The signal configDirChanged() and myIdChanged() emitted when those values have changed; error() is emitted in the error case.
|
* The signals configDirChanged() and myIdChanged() are emitted when those values have changed; error() is emitted in the error case.
|
||||||
*/
|
*/
|
||||||
void SyncthingConnection::requestStatus()
|
void SyncthingConnection::requestStatus()
|
||||||
{
|
{
|
||||||
|
@ -764,6 +764,17 @@ void SyncthingConnection::requestStatus()
|
||||||
m_statusReply = requestData(QStringLiteral("system/status"), QUrlQuery()), &QNetworkReply::finished, this, &SyncthingConnection::readStatus);
|
m_statusReply = requestData(QStringLiteral("system/status"), QUrlQuery()), &QNetworkReply::finished, this, &SyncthingConnection::readStatus);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief Requests the Syncthing configuration and status asynchronously.
|
||||||
|
*
|
||||||
|
* \sa requestConfig() and requestStatus() for emitted signals.
|
||||||
|
*/
|
||||||
|
void SyncthingConnection::requestConfigAndStatus()
|
||||||
|
{
|
||||||
|
requestConfig();
|
||||||
|
requestStatus();
|
||||||
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* \brief Requests current connections asynchronously.
|
* \brief Requests current connections asynchronously.
|
||||||
*
|
*
|
||||||
|
@ -846,12 +857,24 @@ void SyncthingConnection::requestDeviceStatistics()
|
||||||
* \remarks The signal newConfigTriggered() is emitted when the config has been posted sucessfully. In the error case, error() is emitted.
|
* \remarks The signal newConfigTriggered() is emitted when the config has been posted sucessfully. In the error case, error() is emitted.
|
||||||
* Besides, the newConfig() signal should be emitted as well, indicating Syncthing has actually applied the new configuration.
|
* Besides, the newConfig() signal should be emitted as well, indicating Syncthing has actually applied the new configuration.
|
||||||
*/
|
*/
|
||||||
void SyncthingConnection::postConfig(const QJsonObject &rawConfig)
|
void SyncthingConnection::postConfigFromJsonObject(const QJsonObject &rawConfig)
|
||||||
{
|
{
|
||||||
QObject::connect(postData(QStringLiteral("system/config"), QUrlQuery(), QJsonDocument(rawConfig).toJson(QJsonDocument::Compact)),
|
QObject::connect(postData(QStringLiteral("system/config"), QUrlQuery(), QJsonDocument(rawConfig).toJson(QJsonDocument::Compact)),
|
||||||
&QNetworkReply::finished, this, &SyncthingConnection::readPostConfig);
|
&QNetworkReply::finished, this, &SyncthingConnection::readPostConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief Posts the specified \a rawConfig.
|
||||||
|
* \param rawConfig A valid JSON document containing the configuration. It is directly passed to Syncthing.
|
||||||
|
* \remarks The signal newConfigTriggered() is emitted when the config has been posted sucessfully. In the error case, error() is emitted.
|
||||||
|
* Besides, the newConfig() signal should be emitted as well, indicating Syncthing has actually applied the new configuration.
|
||||||
|
*/
|
||||||
|
void SyncthingConnection::postConfigFromByteArray(const QByteArray &rawConfig)
|
||||||
|
{
|
||||||
|
QObject::connect(
|
||||||
|
postData(QStringLiteral("system/config"), QUrlQuery(), rawConfig), &QNetworkReply::finished, this, &SyncthingConnection::readPostConfig);
|
||||||
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* \brief Requests the Syncthing events (since the last successful call) asynchronously.
|
* \brief Requests the Syncthing events (since the last successful call) asynchronously.
|
||||||
*
|
*
|
||||||
|
|
|
@ -165,6 +165,7 @@ public Q_SLOTS:
|
||||||
|
|
||||||
void requestConfig();
|
void requestConfig();
|
||||||
void requestStatus();
|
void requestStatus();
|
||||||
|
void requestConfigAndStatus();
|
||||||
void requestErrors();
|
void requestErrors();
|
||||||
void requestConnections();
|
void requestConnections();
|
||||||
void requestClearingErrors();
|
void requestClearingErrors();
|
||||||
|
@ -172,7 +173,8 @@ public Q_SLOTS:
|
||||||
void requestDirStatus(const QString &dirId);
|
void requestDirStatus(const QString &dirId);
|
||||||
void requestCompletion(const QString &devId, const QString &dirId);
|
void requestCompletion(const QString &devId, const QString &dirId);
|
||||||
void requestDeviceStatistics();
|
void requestDeviceStatistics();
|
||||||
void postConfig(const QJsonObject &rawConfig);
|
void postConfigFromJsonObject(const QJsonObject &rawConfig);
|
||||||
|
void postConfigFromByteArray(const QByteArray &rawConfig);
|
||||||
|
|
||||||
Q_SIGNALS:
|
Q_SIGNALS:
|
||||||
void newConfig(const QJsonObject &rawConfig);
|
void newConfig(const QJsonObject &rawConfig);
|
||||||
|
|
Loading…
Reference in New Issue