2016-10-02 21:59:28 +02:00
# include "./application.h"
# include "./helper.h"
2018-04-07 22:01:54 +02:00
# include "./jsconsole.h"
# include "./jsdefs.h"
# include "./jsincludes.h"
2016-10-02 21:59:28 +02:00
2021-01-25 19:48:11 +01:00
# include <syncthingconnector/syncthingconfig.h>
# include <syncthingconnector/utils.h>
2016-10-02 21:59:28 +02:00
2017-09-30 18:55:06 +02:00
// use header-only functions waitForSignals() and signalInfo() from test utilities; disable assertions via macro
# define SYNCTHINGTESTHELPER_FOR_CLI
# include "../testhelper/helper.h"
2018-04-07 22:01:54 +02:00
# include "resources/config.h"
2016-10-02 21:59:28 +02:00
# include <c++utilities/chrono/timespan.h>
2018-07-05 17:39:35 +02:00
# include <c++utilities/conversion/stringbuilder.h>
2016-10-02 21:59:28 +02:00
# include <c++utilities/conversion/stringconversion.h>
2017-05-01 03:34:43 +02:00
# include <c++utilities/io/ansiescapecodes.h>
2019-06-10 22:48:26 +02:00
# include <c++utilities/misc/parseerror.h>
2016-10-02 21:59:28 +02:00
2017-01-02 23:25:58 +01:00
# include <qtutilities/misc/conversion.h>
2016-10-02 21:59:28 +02:00
# include <QCoreApplication>
2017-03-29 23:17:53 +02:00
# include <QDir>
2018-04-01 20:21:51 +02:00
# include <QJsonDocument>
2017-05-01 03:34:43 +02:00
# include <QNetworkAccessManager>
2018-04-01 22:34:59 +02:00
# include <QProcess>
2017-10-16 19:43:38 +02:00
# include <QStringBuilder>
2018-04-01 22:34:59 +02:00
# include <QTemporaryFile>
2017-09-30 18:55:06 +02:00
# include <QTimer>
2016-10-02 21:59:28 +02:00
# include <functional>
# include <iostream>
using namespace std ;
using namespace std : : placeholders ;
2019-06-10 22:48:26 +02:00
using namespace CppUtilities ;
using namespace CppUtilities : : EscapeCodes ;
using namespace QtUtilities ;
2016-10-02 21:59:28 +02:00
using namespace Data ;
namespace Cli {
2017-01-03 00:29:55 +01:00
static bool terminated = false ;
static int statusCode = 0 ;
2016-11-11 12:44:11 +01:00
void exitApplication ( int statusCode )
{
2021-03-20 22:39:40 +01:00
: : Cli : : statusCode = statusCode ;
2016-11-11 12:44:11 +01:00
terminated = true ;
}
2017-03-22 21:22:30 +01:00
inline QString argToQString ( const char * arg , int size = - 1 )
2017-01-02 23:25:58 +01:00
{
# if !defined(PLATFORM_WINDOWS)
2017-03-22 21:22:30 +01:00
return QString : : fromLocal8Bit ( arg , size ) ;
2017-01-02 23:25:58 +01:00
# else
// under Windows args are converted to UTF-8
2017-03-22 21:22:30 +01:00
return QString : : fromUtf8 ( arg , size ) ;
2017-01-02 23:25:58 +01:00
# endif
}
2017-05-01 03:34:43 +02:00
Application : : Application ( )
: m_expectedResponse ( 0 )
, m_preventDisconnect ( false )
, m_callbacksInvoked ( false )
2018-04-01 20:21:51 +02:00
, m_requiresMainEventLoop ( true )
2017-09-30 18:55:06 +02:00
, m_idleDuration ( 0 )
, m_idleTimeout ( 0 )
2017-09-30 18:51:29 +02:00
, m_argsRead ( false )
2016-10-02 21:59:28 +02:00
{
// take ownership over the global QNetworkAccessManager
networkAccessManager ( ) . setParent ( this ) ;
2019-03-13 19:12:23 +01:00
// setup callbacks
m_args . parser . setExitFunction ( & exitApplication ) ;
2016-10-02 21:59:28 +02:00
m_args . status . setCallback ( bind ( & Application : : printStatus , this , _1 ) ) ;
m_args . log . setCallback ( bind ( & Application : : requestLog , this , _1 ) ) ;
2016-10-02 22:16:43 +02:00
m_args . stop . setCallback ( bind ( & Application : : requestShutdown , this , _1 ) ) ;
2016-10-02 21:59:28 +02:00
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 ) ) ;
2017-02-23 17:47:19 +01:00
m_args . pause . setCallback ( bind ( & Application : : requestPauseResume , this , true ) ) ;
m_args . resume . setCallback ( bind ( & Application : : requestPauseResume , this , false ) ) ;
2017-09-30 18:55:06 +02:00
m_args . waitForIdle . setCallback ( bind ( & Application : : waitForIdle , this , _1 ) ) ;
2017-04-06 00:08:24 +02:00
m_args . pwd . setCallback ( bind ( & Application : : checkPwdOperationPresent , this , _1 ) ) ;
2018-04-01 20:21:51 +02:00
m_args . cat . setCallback ( bind ( & Application : : printConfig , this , _1 ) ) ;
2018-04-01 22:34:59 +02:00
m_args . edit . setCallback ( bind ( & Application : : editConfig , this , _1 ) ) ;
2017-04-06 00:08:24 +02:00
m_args . statusPwd . setCallback ( bind ( & Application : : printPwdStatus , this , _1 ) ) ;
m_args . rescanPwd . setCallback ( bind ( & Application : : requestRescanPwd , this , _1 ) ) ;
m_args . pausePwd . setCallback ( bind ( & Application : : requestPausePwd , this , _1 ) ) ;
m_args . resumePwd . setCallback ( bind ( & Application : : requestResumePwd , this , _1 ) ) ;
2018-01-24 23:07:53 +01:00
m_args . dir . setCallback ( bind ( & Application : : initDirCompletion , this , ref ( m_args . dir ) , _1 ) ) ;
m_args . dev . setCallback ( bind ( & Application : : initDevCompletion , this , ref ( m_args . dev ) , _1 ) ) ;
2016-10-02 21:59:28 +02:00
// connect signals and slots
connect ( & m_connection , & SyncthingConnection : : statusChanged , this , & Application : : handleStatusChanged ) ;
connect ( & m_connection , & SyncthingConnection : : error , this , & Application : : handleError ) ;
}
Application : : ~ Application ( )
2017-05-01 03:34:43 +02:00
{
}
2016-10-02 21:59:28 +02:00
2017-05-01 03:34:43 +02:00
int Application : : exec ( int argc , const char * const * argv )
2016-10-02 21:59:28 +02:00
{
try {
// parse arguments
m_args . parser . readArgs ( argc , argv ) ;
2016-11-11 12:44:11 +01:00
// check whether application needs to be terminated due to --bash-completion argument
2017-05-01 03:34:43 +02:00
if ( terminated ) {
2016-11-11 12:44:11 +01:00
return statusCode ;
}
2016-10-02 21:59:28 +02:00
m_args . parser . checkConstraints ( ) ;
2017-09-30 18:51:29 +02:00
m_argsRead = true ;
2016-10-02 21:59:28 +02:00
2019-06-10 22:48:26 +02:00
} catch ( const ParseError & failure ) {
2017-09-30 18:51:29 +02:00
cerr < < failure ;
return 1 ;
}
2016-10-02 21:59:28 +02:00
2017-09-30 18:51:29 +02:00
// handle help argument
2019-03-13 19:12:23 +01:00
if ( m_args . parser . helpArg ( ) . isPresent ( ) ) {
2017-09-30 18:51:29 +02:00
m_args . parser . printHelp ( cout ) ;
return 0 ;
}
2016-10-02 21:59:28 +02:00
2017-09-30 18:51:29 +02:00
// load configuration
if ( const int res = loadConfig ( ) ) {
return res ;
}
// finally do the request or establish connection
2018-01-24 23:07:53 +01:00
if ( m_args . status . isPresent ( ) | | m_args . rescan . isPresent ( ) | | m_args . rescanAll . isPresent ( ) | | m_args . pause . isPresent ( )
2017-10-16 19:43:38 +02:00
| | m_args . resume . isPresent ( ) | | m_args . waitForIdle . isPresent ( ) | | m_args . pwd . isPresent ( ) ) {
2018-04-01 20:21:51 +02:00
// those arguments require establishing a connection first, the actual handler is called by handleStatusChanged() when
2017-09-30 18:51:29 +02:00
// the connection has been established
m_connection . reconnect ( m_settings ) ;
2018-04-02 12:48:27 +02:00
cerr < < Phrases : : Info < < " Connecting to " < < m_settings . syncthingUrl . toLocal8Bit ( ) . data ( ) < < " ... " < < TextAttribute : : Reset < < flush ;
2017-09-30 18:51:29 +02:00
} else {
// call handler for any other arguments directly
m_connection . applySettings ( m_settings ) ;
m_args . parser . invokeCallbacks ( ) ;
}
2018-04-01 20:21:51 +02:00
// enter main event loop
if ( ! m_requiresMainEventLoop ) {
return 0 ;
}
2017-09-30 18:51:29 +02:00
return QCoreApplication : : exec ( ) ;
}
2017-09-30 18:55:06 +02:00
2023-07-04 18:47:06 +02:00
static int assignIntegerFromArg ( const Argument & arg , int & integer )
2017-09-30 18:55:06 +02:00
{
2018-04-01 20:21:51 +02:00
if ( ! arg . isPresent ( ) ) {
return 0 ;
}
try {
integer = stringToNumber < int > ( arg . firstValue ( ) ) ;
if ( integer < 0 ) {
throw ConversionException ( ) ;
2016-10-02 21:59:28 +02:00
}
2018-04-01 20:21:51 +02:00
} catch ( const ConversionException & ) {
cerr < < Phrases : : Error < < " The specified number of milliseconds \" " < < arg . firstValue ( ) < < " \" is no unsigned integer. " < < Phrases : : EndFlush ;
return - 4 ;
2017-09-30 18:55:06 +02:00
}
return 0 ;
}
2016-10-02 21:59:28 +02:00
2017-09-30 18:51:29 +02:00
int Application : : loadConfig ( )
{
// locate and read Syncthing config file
QString configFile ;
const char * configFileArgValue = m_args . configFile . firstValue ( ) ;
if ( configFileArgValue ) {
configFile = fromNativeFileName ( configFileArgValue ) ;
} else {
configFile = SyncthingConfig : : locateConfigFile ( ) ;
}
SyncthingConfig config ;
const char * apiKeyArgValue = m_args . apiKey . firstValue ( ) ;
if ( ! config . restore ( configFile ) ) {
if ( configFileArgValue ) {
2018-04-01 20:21:51 +02:00
cerr < < Phrases : : Error < < " Unable to locate specified Syncthing config file \" " < < configFileArgValue < < " \" " < < Phrases : : EndFlush ;
2017-09-30 18:51:29 +02:00
return - 1 ;
} else if ( ! apiKeyArgValue ) {
2018-04-01 20:21:51 +02:00
cerr < < Phrases : : Error < < " Unable to locate Syncthing config file and no API key specified " < < Phrases : : EndFlush ;
2017-09-30 18:51:29 +02:00
return - 2 ;
2016-10-02 21:59:28 +02:00
}
2017-09-30 18:51:29 +02:00
}
2016-10-02 21:59:28 +02:00
2017-09-30 18:51:29 +02:00
// apply settings for connection
if ( const char * urlArgValue = m_args . url . firstValue ( ) ) {
m_settings . syncthingUrl = argToQString ( urlArgValue ) ;
} else if ( ! config . guiAddress . isEmpty ( ) ) {
m_settings . syncthingUrl = config . syncthingUrl ( ) ;
} else {
m_settings . syncthingUrl = QStringLiteral ( " http://localhost:8080 " ) ;
}
if ( m_args . credentials . isPresent ( ) ) {
m_settings . authEnabled = true ;
m_settings . userName = argToQString ( m_args . credentials . values ( 0 ) [ 0 ] ) ;
m_settings . password = argToQString ( m_args . credentials . values ( 0 ) [ 1 ] ) ;
}
if ( apiKeyArgValue ) {
m_settings . apiKey . append ( apiKeyArgValue ) ;
} else {
2020-09-04 01:09:18 +02:00
m_settings . apiKey . append ( config . guiApiKey . toUtf8 ( ) ) ;
2017-09-30 18:51:29 +02:00
}
if ( const char * certArgValue = m_args . certificate . firstValue ( ) ) {
m_settings . httpsCertPath = argToQString ( certArgValue ) ;
if ( m_settings . httpsCertPath . isEmpty ( ) | | ! m_settings . loadHttpsCert ( ) ) {
cerr < < Phrases : : Error < < " Unable to load specified certificate \" " < < m_args . certificate . firstValue ( ) < < ' \" ' < < Phrases : : End < < flush ;
return - 3 ;
}
2017-09-30 18:55:06 +02:00
}
2016-10-02 21:59:28 +02:00
2017-09-30 18:55:06 +02:00
// read idle duration and timeout
if ( const int res = assignIntegerFromArg ( m_args . atLeast , m_idleDuration ) ) {
return res ;
}
if ( const int res = assignIntegerFromArg ( m_args . timeout , m_idleTimeout ) ) {
return res ;
2016-10-02 21:59:28 +02:00
}
2017-09-30 18:51:29 +02:00
2023-07-04 19:04:20 +02:00
// read request timeout
if ( const int res = assignIntegerFromArg ( m_args . requestTimeout , m_settings . requestTimeout ) ) {
return res ;
}
2018-10-25 18:22:07 +02:00
// disable polling for information which is not used by any CLI operation so far
m_settings . trafficPollInterval = 0 ;
m_settings . devStatsPollInterval = 0 ;
m_settings . errorsPollInterval = 0 ;
2017-09-30 18:51:29 +02:00
return 0 ;
}
2017-09-30 18:55:06 +02:00
2018-02-19 03:22:47 +01:00
bool Application : : waitForConnected ( int timeout )
2017-09-30 18:55:06 +02:00
{
bool isConnected = m_connection . isConnected ( ) ;
const function < void ( SyncthingStatus ) > checkStatus ( [ this , & isConnected ] ( SyncthingStatus ) { isConnected = m_connection . isConnected ( ) ; } ) ;
2018-02-19 03:22:47 +01:00
return waitForSignalsOrFail ( bind ( static_cast < void ( SyncthingConnection : : * ) ( SyncthingConnectionSettings & ) > ( & SyncthingConnection : : reconnect ) ,
ref ( m_connection ) , ref ( m_settings ) ) ,
timeout , signalInfo ( & m_connection , & SyncthingConnection : : error ) ,
signalInfo ( & m_connection , & SyncthingConnection : : statusChanged , checkStatus , & isConnected ) ) ;
}
bool Application : : waitForConfig ( int timeout )
{
2018-02-24 02:53:28 +01:00
m_connection . applySettings ( m_settings ) ;
2018-02-19 03:22:47 +01:00
return waitForSignalsOrFail ( bind ( & SyncthingConnection : : requestConfig , ref ( m_connection ) ) , timeout ,
signalInfo ( & m_connection , & SyncthingConnection : : error ) , signalInfo ( & m_connection , & SyncthingConnection : : newConfig ) ,
signalInfo ( & m_connection , & SyncthingConnection : : newDirs ) , signalInfo ( & m_connection , & SyncthingConnection : : newDevices ) ) ;
2016-10-02 21:59:28 +02:00
}
2018-04-07 22:01:54 +02:00
bool Application : : waitForConfigAndStatus ( int timeout )
{
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 ) ) ;
}
2016-10-02 21:59:28 +02:00
void Application : : handleStatusChanged ( SyncthingStatus newStatus )
{
Q_UNUSED ( newStatus )
2017-09-30 18:55:06 +02:00
// skip when callbacks have already been invoked, when doing shell completion or not connected yet
if ( ! m_argsRead | | m_callbacksInvoked | | ! m_connection . isConnected ( ) ) {
2017-03-29 23:17:53 +02:00
return ;
}
2017-09-30 18:55:06 +02:00
// erase current line
2018-04-01 22:34:15 +02:00
cerr < < Phrases : : Override ;
2017-09-30 18:55:06 +02:00
// invoke callbacks
m_callbacksInvoked = true ;
m_args . parser . invokeCallbacks ( ) ;
// disconnect, except when m_preventDisconnect has been set in callbacks
if ( ! m_preventDisconnect ) {
m_connection . disconnect ( ) ;
2016-10-02 21:59:28 +02:00
}
}
void Application : : handleResponse ( )
{
2018-04-01 22:34:15 +02:00
if ( ! m_expectedResponse ) {
2017-09-26 15:43:34 +02:00
cerr < < Phrases : : Error < < " Unexpected response " < < Phrases : : End < < flush ;
2016-10-02 21:59:28 +02:00
QCoreApplication : : exit ( - 4 ) ;
2018-04-01 22:34:15 +02:00
return ;
}
if ( ! - - m_expectedResponse ) {
QCoreApplication : : quit ( ) ;
2016-10-02 21:59:28 +02:00
}
}
2017-08-20 01:20:47 +02:00
void Application : : handleError (
const QString & message , SyncthingErrorCategory category , int networkError , const QNetworkRequest & request , const QByteArray & response )
2016-10-02 21:59:28 +02:00
{
2019-06-12 21:00:49 +02:00
CPP_UTILITIES_UNUSED ( category )
CPP_UTILITIES_UNUSED ( networkError )
2017-09-30 18:55:06 +02:00
// skip error handling for shell completion
if ( ! m_argsRead ) {
return ;
}
// print error message and relevant request and response if present
2018-04-01 22:34:15 +02:00
cerr < < Phrases : : Override < < Phrases : : Error < < message . toLocal8Bit ( ) . data ( ) < < Phrases : : End ;
const auto url ( request . url ( ) ) ;
2017-08-20 01:20:47 +02:00
if ( ! url . isEmpty ( ) ) {
2017-09-26 15:44:07 +02:00
cerr < < " \n Request: " < < url . toString ( QUrl : : PrettyDecoded ) . toLocal8Bit ( ) . data ( ) < < ' \n ' ;
2017-08-20 01:20:47 +02:00
}
if ( ! response . isEmpty ( ) ) {
2017-09-26 15:44:07 +02:00
cerr < < " \n Response: \n " < < response . data ( ) < < ' \n ' ;
2017-08-20 01:20:47 +02:00
}
2017-09-26 15:44:07 +02:00
cerr < < flush ;
2016-10-02 21:59:28 +02:00
QCoreApplication : : exit ( - 3 ) ;
}
void Application : : requestLog ( const ArgumentOccurrence & )
{
2018-10-20 22:08:25 +02:00
connect ( & m_connection , & SyncthingConnection : : logAvailable , printLog ) ;
m_connection . requestLog ( ) ;
2016-10-02 21:59:28 +02:00
cerr < < " Request log from " < < m_settings . syncthingUrl . toLocal8Bit ( ) . data ( ) < < " ... " ;
cerr . flush ( ) ;
}
2016-10-02 22:16:43 +02:00
void Application : : requestShutdown ( const ArgumentOccurrence & )
{
connect ( & m_connection , & SyncthingConnection : : shutdownTriggered , & QCoreApplication : : quit ) ;
m_connection . shutdown ( ) ;
cerr < < " Request shutdown " < < m_settings . syncthingUrl . toLocal8Bit ( ) . data ( ) < < " ... " ;
cerr . flush ( ) ;
}
2016-10-02 21:59:28 +02:00
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 )
{
2017-09-30 18:51:29 +02:00
if ( ! m_argsRead ) {
initDirCompletion ( m_args . rescan , occurrence ) ;
return ;
}
2017-10-16 19:43:38 +02:00
m_expectedResponse = 0 ;
2016-10-02 21:59:28 +02:00
connect ( & m_connection , & SyncthingConnection : : rescanTriggered , this , & Application : : handleResponse ) ;
2017-05-01 03:34:43 +02:00
for ( const char * value : occurrence . values ) {
2017-10-16 19:43:38 +02:00
const QString dirIdentifier ( argToQString ( value ) ) ;
const RelevantDir relevantDir ( findDirectory ( dirIdentifier ) ) ;
if ( ! relevantDir . dirObj ) {
continue ;
2017-03-22 21:22:30 +01:00
}
2017-10-16 19:43:38 +02:00
relevantDir . notifyAboutRescan ( ) ;
m_connection . rescan ( relevantDir . dirObj - > id , relevantDir . subDir ) ;
+ + m_expectedResponse ;
2016-10-02 21:59:28 +02:00
}
2017-10-16 19:43:38 +02:00
if ( ! m_expectedResponse ) {
2023-09-17 20:33:31 +02:00
cerr < < Phrases : : Error < < " No (valid) folders specified. " < < Phrases : : End < < flush ;
2017-10-16 19:43:38 +02:00
exit ( 1 ) ;
}
cerr < < flush ;
2016-10-02 21:59:28 +02:00
}
void Application : : requestRescanAll ( const ArgumentOccurrence & )
{
m_expectedResponse = m_connection . dirInfo ( ) . size ( ) ;
connect ( & m_connection , & SyncthingConnection : : rescanTriggered , this , & Application : : handleResponse ) ;
2023-09-17 20:33:31 +02:00
cerr < < " Request rescanning all folders ... " < < endl ;
2016-10-02 21:59:28 +02:00
m_connection . rescanAllDirs ( ) ;
}
2017-02-23 17:47:19 +01:00
void Application : : requestPauseResume ( bool pause )
2016-10-02 21:59:28 +02:00
{
2017-02-23 17:47:19 +01:00
findRelevantDirsAndDevs ( OperationType : : PauseResume ) ;
2017-09-26 16:48:47 +02:00
m_expectedResponse = 0 ;
2017-05-01 03:34:43 +02:00
if ( pause ) {
2017-02-23 17:47:19 +01:00
connect ( & m_connection , & SyncthingConnection : : devicePauseTriggered , this , & Application : : handleResponse ) ;
connect ( & m_connection , & SyncthingConnection : : directoryPauseTriggered , this , & Application : : handleResponse ) ;
} else {
connect ( & m_connection , & SyncthingConnection : : deviceResumeTriggered , this , & Application : : handleResponse ) ;
connect ( & m_connection , & SyncthingConnection : : directoryResumeTriggered , this , & Application : : handleResponse ) ;
}
2017-09-26 16:48:47 +02:00
if ( m_relevantDirs . empty ( ) & & m_relevantDevs . empty ( ) ) {
2023-09-17 20:33:31 +02:00
cerr < < Phrases : : Error < < " No folders or devices specified. " < < Phrases : : End < < flush ;
2017-09-26 16:48:47 +02:00
exit ( 1 ) ;
}
2017-05-01 03:34:43 +02:00
if ( ! m_relevantDirs . empty ( ) ) {
2017-02-23 17:47:19 +01:00
QStringList dirIds ;
2018-04-01 22:34:15 +02:00
dirIds . reserve ( trQuandity ( m_relevantDirs . size ( ) ) ) ;
2017-10-16 19:43:38 +02:00
for ( const RelevantDir & dir : m_relevantDirs ) {
dirIds < < dir . dirObj - > id ;
2017-02-23 17:47:19 +01:00
}
2017-05-01 03:34:43 +02:00
if ( pause ) {
2023-09-17 20:33:31 +02:00
cerr < < " Request pausing folders " ;
2017-02-23 17:47:19 +01:00
} else {
2023-09-17 20:33:31 +02:00
cerr < < " Request resuming folders " ;
2017-02-23 17:47:19 +01:00
}
cerr < < dirIds . join ( QStringLiteral ( " , " ) ) . toLocal8Bit ( ) . data ( ) < < " ... \n " ;
2017-05-01 03:34:43 +02:00
if ( pause ? m_connection . pauseDirectories ( dirIds ) : m_connection . resumeDirectories ( dirIds ) ) {
2017-02-23 17:47:19 +01:00
+ + m_expectedResponse ;
}
}
2017-07-02 21:47:23 +02:00
if ( ! m_relevantDevs . empty ( ) ) {
QStringList devIds ;
2018-04-01 22:34:15 +02:00
devIds . reserve ( trQuandity ( m_relevantDirs . size ( ) ) ) ;
2017-07-02 21:47:23 +02:00
for ( const SyncthingDev * dev : m_relevantDevs ) {
devIds < < dev - > id ;
}
2017-05-01 03:34:43 +02:00
if ( pause ) {
2017-07-02 21:47:23 +02:00
cerr < < " Request pausing devices " ;
} else {
cerr < < " Request resuming devices " ;
}
cerr < < devIds . join ( QStringLiteral ( " , " ) ) . toLocal8Bit ( ) . data ( ) < < " ... \n " ;
2018-01-24 23:08:09 +01:00
if ( pause ? m_connection . pauseDevice ( devIds ) : m_connection . resumeDevice ( devIds ) ) {
2017-07-02 21:47:23 +02:00
+ + m_expectedResponse ;
2017-02-23 17:47:19 +01:00
}
2016-10-02 21:59:28 +02:00
}
2017-09-26 16:48:47 +02:00
if ( ! m_expectedResponse ) {
2023-09-17 20:33:31 +02:00
cerr < < Phrases : : Warning < < " No folders or devices altered. " < < Phrases : : End < < flush ;
2017-09-26 16:48:47 +02:00
exit ( 0 ) ;
}
cerr < < flush ;
2016-10-02 21:59:28 +02:00
}
2017-02-23 17:47:19 +01:00
void Application : : findRelevantDirsAndDevs ( OperationType operationType )
2016-10-02 21:59:28 +02:00
{
int dummy ;
2017-02-23 17:47:19 +01:00
2018-01-24 23:07:53 +01:00
// find relevant dirs
const bool allDirs = m_args . allDirs . isPresent ( ) ;
if ( ! allDirs ) {
const Argument & dirArg = m_args . dir ;
if ( dirArg . isPresent ( ) ) {
m_relevantDirs . reserve ( dirArg . occurrences ( ) ) ;
for ( size_t i = 0 ; i ! = dirArg . occurrences ( ) ; + + i ) {
const QString dirIdentifier ( argToQString ( dirArg . values ( i ) . front ( ) ) ) ;
const RelevantDir relevantDir ( findDirectory ( dirIdentifier ) ) ;
if ( relevantDir . dirObj ) {
2022-11-04 16:51:01 +01:00
m_relevantDirs . emplace_back ( std : : move ( relevantDir ) ) ;
2018-01-24 23:07:53 +01:00
}
2016-10-02 21:59:28 +02:00
}
}
}
2018-01-24 23:07:53 +01:00
// find relevant devs
const bool allDevs = m_args . allDevs . isPresent ( ) ;
if ( ! allDevs ) {
Argument & devArg = m_args . dev ;
if ( devArg . isPresent ( ) ) {
m_relevantDevs . reserve ( devArg . occurrences ( ) ) ;
for ( size_t i = 0 ; i ! = devArg . occurrences ( ) ; + + i ) {
const SyncthingDev * dev = m_connection . findDevInfo ( argToQString ( devArg . values ( i ) . front ( ) ) , dummy ) ;
if ( ! dev ) {
dev = m_connection . findDevInfoByName ( argToQString ( devArg . values ( i ) . front ( ) ) , dummy ) ;
}
2018-04-01 22:34:15 +02:00
if ( ! dev ) {
2018-01-24 23:07:53 +01:00
cerr < < Phrases : : Warning < < " Specified device \" " < < devArg . values ( i ) . front ( ) < < " \" does not exist and will be ignored. "
< < Phrases : : End ;
2018-04-01 22:34:15 +02:00
continue ;
2018-01-24 23:07:53 +01:00
}
2018-04-01 22:34:15 +02:00
m_relevantDevs . emplace_back ( dev ) ;
2016-10-02 21:59:28 +02:00
}
}
}
2018-01-24 23:07:53 +01:00
2018-07-05 17:39:35 +02:00
// when displaying status information and no stats and no dirs/devs have been specified, just print information for all
const bool displayEverything
= operationType = = OperationType : : Status & & ! m_args . stats . isPresent ( ) & & m_relevantDirs . empty ( ) & & m_relevantDevs . empty ( ) ;
2018-01-24 23:07:53 +01:00
if ( allDirs | | ( ! allDevs & & displayEverything ) ) {
m_relevantDirs . reserve ( m_connection . dirInfo ( ) . size ( ) ) ;
for ( const SyncthingDir & dir : m_connection . dirInfo ( ) ) {
m_relevantDirs . emplace_back ( & dir , QString ( ) ) ;
}
}
if ( allDevs | | ( ! allDirs & & displayEverything ) ) {
m_relevantDevs . reserve ( m_connection . devInfo ( ) . size ( ) ) ;
for ( const SyncthingDev & dev : m_connection . devInfo ( ) ) {
m_relevantDevs . emplace_back ( & dev ) ;
2016-10-02 21:59:28 +02:00
}
}
2016-10-17 22:49:22 +02:00
}
2017-04-06 00:08:24 +02:00
bool Application : : findPwd ( )
{
const QString pwd ( QDir : : currentPath ( ) ) ;
2017-10-16 19:43:38 +02:00
// find directory for working directory
int dummy ;
m_pwd . dirObj = m_connection . findDirInfoByPath ( pwd , m_pwd . subDir , dummy ) ;
if ( m_pwd ) {
return true ;
2017-04-06 00:08:24 +02:00
}
2017-10-16 19:43:38 +02:00
// handle error
2023-09-17 20:33:31 +02:00
cerr < < Phrases : : Error < < " The current working directory \" " < < pwd . toLocal8Bit ( ) . data ( ) < < " \" is not (part of) a Syncthing folder. " ;
2017-09-26 15:43:34 +02:00
cerr < < Phrases : : End < < flush ;
2017-04-06 00:08:24 +02:00
QCoreApplication : : exit ( 2 ) ;
return false ;
}
2018-01-25 00:03:31 +01:00
void Application : : printDir ( const RelevantDir & relevantDir ) const
2017-03-29 23:17:53 +02:00
{
2017-10-16 19:43:38 +02:00
const SyncthingDir * const dir = relevantDir . dirObj ;
2023-01-28 17:05:49 +01:00
cout < < " - " < < TextAttribute : : Bold ;
if ( dir - > label . isEmpty ( ) ) {
2023-01-28 20:19:43 +01:00
cout < < dir - > id . toLocal8Bit ( ) . data ( ) < < ' \n ' < < TextAttribute : : Reset ;
2023-01-28 17:05:49 +01:00
} else {
cout < < dir - > label . toLocal8Bit ( ) . data ( ) < < ' \n ' < < TextAttribute : : Reset ;
if ( dir - > id ! = dir - > label ) {
printProperty ( " ID " , dir - > id ) ;
}
}
2017-03-29 23:17:53 +02:00
printProperty ( " Path " , dir - > path ) ;
printProperty ( " Status " , dir - > statusString ( ) ) ;
2019-02-16 00:34:18 +01:00
if ( ! dir - > paused ) {
printProperty ( " Global " , directoryStatusString ( dir - > globalStats ) , nullptr , 6 ) ;
printProperty ( " Local " , directoryStatusString ( dir - > localStats ) , nullptr , 6 ) ;
}
2017-03-29 23:17:53 +02:00
printProperty ( " Last scan time " , dir - > lastScanTime ) ;
printProperty ( " Last file time " , dir - > lastFileTime ) ;
printProperty ( " Last file name " , dir - > lastFileName ) ;
2017-08-22 19:45:07 +02:00
printProperty ( " Shared with " , dir - > deviceNames . isEmpty ( ) ? dir - > deviceIds : dir - > deviceNames ) ;
2018-01-25 00:03:31 +01:00
printProperty ( " Download progress " , dir - > downloadLabel ) ;
if ( ! dir - > completionByDevice . empty ( ) ) {
printProperty ( " Remote progress " , dir - > areRemotesUpToDate ( ) ? " all up-to-date " : " some need bytes " ) ;
for ( const auto & completionForDev : dir - > completionByDevice ) {
printProperty ( m_connection . deviceNameOrId ( completionForDev . first ) . toLocal8Bit ( ) . data ( ) ,
2018-03-29 00:38:21 +02:00
argsToString ( dataSizeToString ( completionForDev . second . globalBytes - completionForDev . second . needed . bytes ) , ' ' , ' / ' , ' ' ,
2018-01-25 00:03:31 +01:00
dataSizeToString ( completionForDev . second . globalBytes ) , ' ' , ' ( ' , static_cast < int > ( completionForDev . second . percentage ) , " %) " )
. data ( ) ,
nullptr , 6 ) ;
}
}
2018-05-12 23:08:57 +02:00
printProperty ( " Type " , dir - > dirTypeString ( ) ) ;
2017-03-29 23:17:53 +02:00
printProperty ( " Ignore permissions " , dir - > ignorePermissions ) ;
printProperty ( " Auto-normalize " , dir - > autoNormalize ) ;
2018-09-15 18:18:15 +02:00
printProperty ( " Rescan interval " , rescanIntervalString ( dir - > rescanInterval , dir - > fileSystemWatcherEnabled ) ) ;
2018-09-15 18:28:44 +02:00
if ( dir - > fileSystemWatcherEnabled ) {
printProperty ( " File watcher delay " , dir - > fileSystemWatcherDelay , " seconds " ) ;
}
2017-03-29 23:17:53 +02:00
printProperty ( " Min. free disk percentage " , dir - > minDiskFreePercentage ) ;
2017-07-14 20:14:15 +02:00
printProperty ( " Error " , dir - > globalError ) ;
if ( ! dir - > itemErrors . empty ( ) ) {
cout < < " Failed items \n " ;
for ( const SyncthingItemError & error : dir - > itemErrors ) {
2017-03-29 23:17:53 +02:00
printProperty ( " - Message " , error . message ) ;
printProperty ( " File " , error . path ) ;
}
}
cout < < ' \n ' ;
}
2018-01-25 00:03:31 +01:00
void Application : : printDev ( const SyncthingDev * dev ) const
2017-03-29 23:17:53 +02:00
{
2018-04-01 22:34:15 +02:00
cout < < " - " < < TextAttribute : : Bold < < dev - > name . toLocal8Bit ( ) . data ( ) < < ' \n ' < < TextAttribute : : Reset ;
2017-03-29 23:17:53 +02:00
printProperty ( " ID " , dev - > id ) ;
printProperty ( " Status " , dev - > statusString ( ) ) ;
printProperty ( " Addresses " , dev - > addresses ) ;
printProperty ( " Compression " , dev - > compression ) ;
printProperty ( " Cert name " , dev - > certName ) ;
printProperty ( " Connection address " , dev - > connectionAddress ) ;
printProperty ( " Connection type " , dev - > connectionType ) ;
2023-05-11 23:44:10 +02:00
printProperty ( " Connection local " , dev - > connectionLocal ) ;
2017-03-29 23:17:53 +02:00
printProperty ( " Client version " , dev - > clientVersion ) ;
printProperty ( " Last seen " , dev - > lastSeen ) ;
2017-05-01 03:34:43 +02:00
if ( dev - > totalIncomingTraffic > 0 ) {
2019-03-13 19:12:23 +01:00
printProperty ( " Incoming traffic " , dataSizeToString ( static_cast < std : : uint64_t > ( dev - > totalIncomingTraffic ) ) . data ( ) ) ;
2017-03-29 23:17:53 +02:00
}
2017-05-01 03:34:43 +02:00
if ( dev - > totalOutgoingTraffic > 0 ) {
2019-03-13 19:12:23 +01:00
printProperty ( " Outgoing traffic " , dataSizeToString ( static_cast < std : : uint64_t > ( dev - > totalOutgoingTraffic ) ) . data ( ) ) ;
2017-03-29 23:17:53 +02:00
}
cout < < ' \n ' ;
}
2016-10-17 22:49:22 +02:00
void Application : : printStatus ( const ArgumentOccurrence & )
{
2018-01-24 23:16:35 +01:00
findRelevantDirsAndDevs ( OperationType : : Status ) ;
2016-10-02 21:59:28 +02:00
2018-07-05 17:39:35 +02:00
// display stats
if ( m_args . stats . isPresent ( ) | | ( ! m_args . dir . isPresent ( ) & & ! m_args . dev . isPresent ( ) ) ) {
cout < < TextAttribute : : Bold < < " Overall statistics \n " < < TextAttribute : : Reset ;
2018-07-30 23:01:04 +02:00
const auto & overallStats ( m_connection . computeOverallDirStatistics ( ) ) ;
2018-10-20 23:42:18 +02:00
const auto * statusString = " idle " ;
const auto * statusColor = " 32 " ;
2020-12-31 02:48:18 +01:00
if ( m_connection . hasOutOfSyncDirs ( ) ) {
2018-10-20 23:42:18 +02:00
statusString = " out-of-sync " ;
statusColor = " 31 " ;
2020-12-31 02:48:18 +01:00
} else {
switch ( m_connection . status ( ) ) {
case SyncthingStatus : : Synchronizing :
statusString = " synchronizing " ;
statusColor = " 34 " ;
break ;
case SyncthingStatus : : RemoteNotInSync :
statusString = " remote synchronizing " ;
statusColor = " 34 " ;
break ;
case SyncthingStatus : : Scanning :
statusString = " scanning " ;
statusColor = " 34 " ;
break ;
default : ;
}
2018-07-30 23:01:04 +02:00
}
2018-10-20 23:42:18 +02:00
if ( ! EscapeCodes : : enabled ) {
printProperty ( " Status " , statusString ) ;
} else {
2023-05-11 23:43:18 +02:00
printProperty ( " Status " , argsToString ( ' \033 ' , ' [ ' , statusColor , ' m ' , statusString , " \033 [0m " ) ) ;
2018-10-20 23:42:18 +02:00
}
2018-07-30 23:01:04 +02:00
printProperty ( " Global " , directoryStatusString ( overallStats . global ) , nullptr , 6 ) ;
printProperty ( " Local " , directoryStatusString ( overallStats . local ) , nullptr , 6 ) ;
2018-07-05 17:39:35 +02:00
printProperty ( " Incoming traffic " , trafficString ( m_connection . totalIncomingTraffic ( ) , m_connection . totalIncomingRate ( ) ) ) ;
printProperty ( " Outgoing traffic " , trafficString ( m_connection . totalOutgoingTraffic ( ) , m_connection . totalOutgoingRate ( ) ) ) ;
const auto & connectedDevices ( m_connection . connectedDevices ( ) ) ;
2018-07-30 23:01:04 +02:00
if ( connectedDevices . empty ( ) ) {
printProperty ( " Connected to " , " no other devices " ) ;
} else {
printProperty ( " Connected to " , argsToString ( connectedDevices . size ( ) , ' ' , connectedDevices . size ( ) = = 1 ? " device " : " devices " , ' : ' ) ) ;
printProperty ( " " , displayNames ( connectedDevices ) , nullptr , 6 ) ;
}
printProperty ( " Uptime " , m_connection . uptime ( ) . toString ( TimeSpanOutputFormat : : WithMeasures , true ) ) ;
printProperty ( " Version " , m_connection . syncthingVersion ( ) ) ;
2018-07-05 17:39:35 +02:00
cout < < ' \n ' ;
}
2016-10-02 21:59:28 +02:00
// display dirs
2017-05-01 03:34:43 +02:00
if ( ! m_relevantDirs . empty ( ) ) {
2023-09-17 20:33:31 +02:00
cout < < TextAttribute : : Bold < < " Folders \n " < < TextAttribute : : Reset ;
2021-01-11 18:49:35 +01:00
std : : sort ( m_relevantDirs . begin ( ) , m_relevantDirs . end ( ) ,
[ ] ( const RelevantDir & lhs , const RelevantDir & rhs ) { return lhs . dirObj - > displayName ( ) < rhs . dirObj - > displayName ( ) ; } ) ;
std : : for_each ( m_relevantDirs . cbegin ( ) , m_relevantDirs . cend ( ) , bind ( & Application : : printDir , this , std : : placeholders : : _1 ) ) ;
2016-10-02 21:59:28 +02:00
}
// display devs
2017-05-01 03:34:43 +02:00
if ( ! m_relevantDevs . empty ( ) ) {
2018-04-01 22:34:15 +02:00
cout < < TextAttribute : : Bold < < " Devices \n " < < TextAttribute : : Reset ;
2021-01-11 18:49:35 +01:00
std : : sort ( m_relevantDevs . begin ( ) , m_relevantDevs . end ( ) , [ ] ( const SyncthingDev * lhs , const SyncthingDev * rhs ) {
2023-09-16 22:28:35 +02:00
const auto lhsIsOwn = lhs - > status = = SyncthingDevStatus : : ThisDevice , rhsIsOwn = rhs - > status = = SyncthingDevStatus : : ThisDevice ;
2021-01-11 18:49:35 +01:00
return lhsIsOwn ! = rhsIsOwn ? lhsIsOwn : lhs - > displayName ( ) < rhs - > displayName ( ) ;
} ) ;
std : : for_each ( m_relevantDevs . cbegin ( ) , m_relevantDevs . cend ( ) , bind ( & Application : : printDev , this , std : : placeholders : : _1 ) ) ;
2016-10-02 21:59:28 +02:00
}
cout . flush ( ) ;
QCoreApplication : : exit ( ) ;
}
void Application : : printLog ( const std : : vector < SyncthingLogEntry > & logEntries )
{
2018-04-01 22:34:15 +02:00
cerr < < Phrases : : Override ;
2016-10-02 21:59:28 +02:00
2017-05-01 03:34:43 +02:00
for ( const SyncthingLogEntry & entry : logEntries ) {
2021-05-18 00:04:46 +02:00
const auto when = entry . when . toUtf8 ( ) ;
try {
cout < < DateTime : : fromIsoStringLocal ( when . data ( ) ) . toString ( DateTimeOutputFormat : : DateAndTime , true ) ;
2023-06-08 12:33:11 +02:00
} catch ( const ConversionException & ) {
2021-05-18 00:04:46 +02:00
cout < < when . data ( ) ;
}
cout < < ' : ' < < ' ' < < entry . message . toLocal8Bit ( ) . data ( ) < < ' \n ' ;
2016-10-02 21:59:28 +02:00
}
cout . flush ( ) ;
QCoreApplication : : exit ( ) ;
}
2018-04-01 20:21:51 +02:00
void Application : : printConfig ( const ArgumentOccurrence & )
{
2018-04-01 22:34:59 +02:00
// 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
2018-04-01 20:21:51 +02:00
m_requiresMainEventLoop = false ;
2018-04-01 22:34:59 +02:00
if ( ! waitForConfig ( ) ) {
return ;
}
cerr < < Phrases : : Override ;
cout < < QJsonDocument ( m_connection . rawConfig ( ) ) . toJson ( QJsonDocument : : Indented ) . data ( ) < < flush ;
}
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
m_requiresMainEventLoop = false ;
2018-04-07 22:01:54 +02:00
// wait until config is available
2018-04-07 22:28:21 +02:00
const bool viaJavaScript ( m_args . script . isPresent ( ) | | m_args . jsLines . isPresent ( ) ) ;
if ( ! ( viaJavaScript ? waitForConfigAndStatus ( ) : waitForConfig ( ) ) ) {
2018-04-07 22:01:54 +02:00
return ;
}
cerr < < Phrases : : Override ;
2018-04-07 22:28:21 +02:00
const auto newConfig ( viaJavaScript ? editConfigViaScript ( ) : editConfigViaEditor ( ) ) ;
2018-04-07 22:01:54 +02:00
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
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
{
2018-04-01 22:34:59 +02:00
// read editor command and options
const auto * const editorArgValue ( m_args . editor . firstValue ( ) ) ;
const auto editorCommand ( editorArgValue ? QString : : fromLocal8Bit ( editorArgValue ) : QString ( ) ) ;
if ( editorCommand . isEmpty ( ) ) {
cerr < < Phrases : : Error < < " No editor command specified. It must be either passed via --editor argument or EDITOR environment variable. "
< < Phrases : : EndFlush ;
2018-04-07 22:01:54 +02:00
return QByteArray ( ) ;
2018-04-01 22:34:59 +02:00
}
QStringList editorOptions ;
if ( m_args . editor . isPresent ( ) ) {
const auto & editorArgValues ( m_args . editor . values ( ) ) ;
if ( ! editorArgValues . empty ( ) ) {
editorOptions . reserve ( trQuandity ( editorArgValues . size ( ) ) ) ;
for ( auto i = editorArgValues . cbegin ( ) + 1 , end = editorArgValues . cend ( ) ; i ! = end ; + + i ) {
editorOptions < < QString : : fromLocal8Bit ( * i ) ;
}
}
}
// write config to temporary file
QTemporaryFile tempFile ( QStringLiteral ( " syncthing-config-XXXXXX.json " ) ) ;
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 ;
2018-04-07 22:01:54 +02:00
return QByteArray ( ) ;
2018-04-01 22:34:59 +02:00
}
editorOptions < < tempFile . fileName ( ) ;
tempFile . close ( ) ;
// open editor and wait until it has finished
cerr < < Phrases : : Info < < " Waiting till editor closed ... " < < TextAttribute : : Reset < < flush ;
QProcess editor ;
editor . setProcessChannelMode ( QProcess : : ForwardedChannels ) ;
editor . setInputChannelMode ( QProcess : : ForwardedInputChannel ) ;
editor . start ( editorCommand , editorOptions ) ;
editor . waitForFinished ( - 1 ) ;
cerr < < Phrases : : Override ;
// handle editor crash
if ( editor . exitStatus ( ) = = QProcess : : CrashExit ) {
cerr < < Phrases : : Error < < " Editor crashed with exit code " < < editor . exitCode ( ) < < Phrases : : End < < " invocation command: " < < editorArgValue ;
if ( m_args . editor . isPresent ( ) ) {
const auto & editorArgValues ( m_args . editor . values ( ) ) ;
if ( ! editorArgValues . empty ( ) ) {
for ( auto i = editorArgValues . cbegin ( ) + 1 , end = editorArgValues . cend ( ) ; i ! = end ; + + i ) {
cerr < < ' ' < < * i ;
}
}
}
cerr < < endl ;
2018-04-07 22:01:54 +02:00
return QByteArray ( ) ;
2018-04-01 22:34:59 +02:00
}
// read (altered) configuration again
QFile tempFile2 ( editorOptions . back ( ) ) ;
if ( ! tempFile2 . open ( QIODevice : : ReadOnly ) ) {
cerr < < Phrases : : Error < < " Unable to open temporary file containing the configuration again. " < < Phrases : : EndFlush ;
2018-04-07 22:01:54 +02:00
return QByteArray ( ) ;
2018-04-01 22:34:59 +02:00
}
const auto newConfig ( tempFile2 . readAll ( ) ) ;
if ( newConfig . isEmpty ( ) ) {
cerr < < Phrases : : Error < < " Unable to read any bytes from temporary file containing the configuration. " < < Phrases : : EndFlush ;
2018-04-07 22:01:54 +02:00
return QByteArray ( ) ;
2018-04-01 22:34:59 +02:00
}
// 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)
QJsonParseError error ;
const auto configDoc ( QJsonDocument : : fromJson ( newConfig , & error ) ) ;
if ( error . error ! = QJsonParseError : : NoError ) {
cerr < < Phrases : : Error < < " Unable to parse new configuration " < < Phrases : : End < < " reason: " < < error . errorString ( ) . toLocal8Bit ( ) . data ( )
< < " at character " < < error . offset < < endl ;
2018-04-07 22:01:54 +02:00
return QByteArray ( ) ;
2018-04-01 22:34:59 +02:00
}
2018-04-07 18:30:35 +02:00
// perform at least some checks before sending the configuration
2018-04-01 22:34:59 +02:00
const auto configObj ( configDoc . object ( ) ) ;
if ( configObj . isEmpty ( ) ) {
cerr < < Phrases : : Error < < " New config object seems empty. " < < Phrases : : EndFlush ;
2018-04-07 22:01:54 +02:00
return QByteArray ( ) ;
2018-04-01 22:34:59 +02:00
}
2018-09-15 18:42:14 +02:00
if ( configObj = = m_connection . rawConfig ( ) ) {
cerr < < Phrases : : Warning < < " Editing aborted; config hasn't changed. " < < Phrases : : EndFlush ;
return QByteArray ( ) ;
}
2018-04-07 22:01:54 +02:00
for ( const auto & arrayName : { QStringLiteral ( " devices " ) , QStringLiteral ( " folders " ) } ) {
2018-04-07 18:30:35 +02:00
if ( ! configObj . value ( arrayName ) . isArray ( ) ) {
cerr < < Phrases : : Error < < " Array \" " < < arrayName . toLocal8Bit ( ) . data ( ) < < " \" is not present. " < < Phrases : : EndFlush ;
2018-04-07 22:01:54 +02:00
return QByteArray ( ) ;
2018-04-07 18:30:35 +02:00
}
}
2018-04-07 22:01:54 +02:00
for ( const auto & objectName : { QStringLiteral ( " options " ) , QStringLiteral ( " gui " ) } ) {
2018-04-07 18:30:35 +02:00
if ( ! configObj . value ( objectName ) . isObject ( ) ) {
cerr < < Phrases : : Error < < " Object \" " < < objectName . toLocal8Bit ( ) . data ( ) < < " \" is not present. " < < Phrases : : EndFlush ;
2018-04-07 22:01:54 +02:00
return QByteArray ( ) ;
2018-04-07 18:30:35 +02:00
}
}
2018-04-07 22:01:54 +02:00
return newConfig ;
}
2018-04-07 18:30:35 +02:00
2018-04-07 22:01:54 +02:00
QByteArray Application : : editConfigViaScript ( ) const
{
# if defined(SYNCTHINGCTL_USE_SCRIPT) || defined(SYNCTHINGCTL_USE_JSENGINE)
2018-04-07 22:28:21 +02:00
// get script
QByteArray script ;
QString scriptFileName ;
if ( m_args . script . isPresent ( ) ) {
// 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 ( ) ;
}
script = scriptFile . readAll ( ) ;
scriptFileName = scriptFile . fileName ( ) ;
if ( script . isEmpty ( ) ) {
cerr < < Phrases : : Error < < " Unable to read any bytes from specified script file \" " < < m_args . script . firstValue ( ) < < " \" . "
< < Phrases : : EndFlush ;
return QByteArray ( ) ;
}
} else if ( m_args . jsLines . isPresent ( ) ) {
// construct script from CLI arguments
2021-03-20 22:39:40 +01:00
auto requiredSize = QString : : size_type ( 0 ) ;
for ( const auto * const line : m_args . jsLines . values ( ) ) {
requiredSize + = static_cast < QString : : size_type > ( std : : strlen ( line ) ) ;
2018-04-07 22:28:21 +02:00
requiredSize + = 1 ;
}
script . reserve ( requiredSize ) ;
2021-03-20 22:39:40 +01:00
for ( const auto * const line : m_args . jsLines . values ( ) ) {
2018-04-07 22:28:21 +02:00
script + = line ;
script + = ' \n ' ;
}
2018-04-07 22:01:54 +02:00
}
// define function to print error
const auto printError ( [ ] ( const auto & object ) {
cerr < < object . toString ( ) . toLocal8Bit ( ) . data ( ) < < " \n in 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 ;
2018-04-08 13:03:05 +02:00
globalObject . setProperty ( QStringLiteral ( " console " ) , engine . newQObject ( & console ) ) ;
2018-04-07 22:01:54 +02:00
2018-04-07 22:57:36 +02:00
// provide helper
2018-04-08 13:03:05 +02:00
QFile helperFile ( QStringLiteral ( " :/js/helper.js " ) ) ;
2018-04-07 22:57:36 +02:00
helperFile . open ( QFile : : ReadOnly ) ;
const auto helperScript ( helperFile . readAll ( ) ) ;
if ( helperScript . isEmpty ( ) ) {
cerr < < Phrases : : Error < < " Unable to load internal helper script. " < < Phrases : : EndFlush ;
return QByteArray ( ) ;
}
const auto helperRes ( engine . evaluate ( QString : : fromUtf8 ( helperScript ) ) ) ;
if ( helperRes . isError ( ) ) {
cerr < < Phrases : : Error < < " Unable to evaluate internal helper script. " < < Phrases : : End ;
printError ( helperRes ) ;
return QByteArray ( ) ;
}
2018-04-07 22:01:54 +02:00
// evaluate the user provided script
2018-04-07 22:28:21 +02:00
const auto res ( engine . evaluate ( QString : : fromUtf8 ( script ) , scriptFileName ) ) ;
2018-04-07 22:01:54 +02:00
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 ( ) ;
}
2018-04-07 18:30:14 +02:00
}
2018-04-01 22:34:59 +02:00
2018-04-07 22:01:54 +02:00
// serilaize the altered configuration via JSON.stringify()
const auto newConfigJson ( engine . evaluate ( QStringLiteral ( " JSON.stringify(config, null, 4) " ))) ;
if ( ! newConfigJson . isString ( ) ) {
cerr < < Phrases : : Error < < " Unable to convert the config object to JSON via JSON.stringify(). " < < Phrases : : End ;
cerr < < configObj . toString ( ) . toLocal8Bit ( ) . data ( ) < < endl ;
return QByteArray ( ) ;
2018-04-01 22:34:59 +02:00
}
2018-04-07 22:01:54 +02:00
return newConfigJson . toString ( ) . toUtf8 ( ) ;
# else
cerr < < Phrases : : Error < < PROJECT_NAME " has not been built with JavaScript support. " < < Phrases : : EndFlush ;
return QByteArray ( ) ;
# endif
2018-04-01 20:21:51 +02:00
}
2017-09-30 18:55:06 +02:00
void Application : : waitForIdle ( const ArgumentOccurrence & )
2016-10-17 22:49:22 +02:00
{
2017-03-29 23:17:53 +02:00
m_preventDisconnect = true ;
2023-09-20 18:12:55 +02:00
// setup timer for handling minimum idle duration
auto idleTime = QTimer ( ) ;
2017-09-30 18:55:06 +02:00
idleTime . setSingleShot ( true ) ;
idleTime . setInterval ( m_idleDuration ) ;
2016-10-17 22:49:22 +02:00
2023-09-20 18:12:55 +02:00
// define event handlers
auto isLongEnoughIdle = false , dirsOrDevsChanged = true , newDirsOrDevs = true ;
const auto handleStatusChange = [ & dirsOrDevsChanged ] { dirsOrDevsChanged = true ; } ;
const auto handleNewDirsOrDevs = [ & newDirsOrDevs ] { newDirsOrDevs = true ; } ;
const auto handleAllEventsProcessed = [ this , & newDirsOrDevs , & dirsOrDevsChanged , & idleTime ] {
if ( newDirsOrDevs ) {
findRelevantDirsAndDevs ( OperationType : : WaitForIdle ) ;
}
if ( newDirsOrDevs | | dirsOrDevsChanged ) {
if ( ! checkWhetherIdle ( ) ) {
idleTime . stop ( ) ;
return ;
}
if ( ! idleTime . isActive ( ) ) {
idleTime . start ( ) ;
}
}
newDirsOrDevs = dirsOrDevsChanged = false ;
} ;
const auto handleIdleTimer = [ this , & isLongEnoughIdle ] {
2017-09-30 18:55:06 +02:00
if ( checkWhetherIdle ( ) ) {
isLongEnoughIdle = true ;
}
2023-09-20 18:12:55 +02:00
} ;
2017-09-30 18:55:06 +02:00
// invoke handler manually because Syncthing could already be idling
2023-09-20 18:12:55 +02:00
handleAllEventsProcessed ( ) ;
2017-09-30 18:55:06 +02:00
waitForSignals ( & noop , m_idleTimeout , signalInfo ( & m_connection , & SyncthingConnection : : dirStatusChanged , handleStatusChange , & isLongEnoughIdle ) ,
signalInfo ( & m_connection , & SyncthingConnection : : devStatusChanged , handleStatusChange , & isLongEnoughIdle ) ,
signalInfo ( & m_connection , & SyncthingConnection : : newDirs , handleNewDirsOrDevs , & isLongEnoughIdle ) ,
signalInfo ( & m_connection , & SyncthingConnection : : newDevices , handleNewDirsOrDevs , & isLongEnoughIdle ) ,
2023-09-20 18:12:55 +02:00
signalInfo ( & m_connection , & SyncthingConnection : : allEventsProcessed , handleAllEventsProcessed , & isLongEnoughIdle ) ,
signalInfo ( & idleTime , & QTimer : : timeout , handleIdleTimer , & isLongEnoughIdle ) ) ;
2017-09-30 18:55:06 +02:00
if ( ! isLongEnoughIdle ) {
cerr < < Phrases : : Warning < < " Exiting after timeout " < < Phrases : : End < < flush ;
}
QCoreApplication : : exit ( isLongEnoughIdle ? 0 : 1 ) ;
2016-10-17 22:49:22 +02:00
}
2017-09-30 18:55:06 +02:00
bool Application : : checkWhetherIdle ( ) const
2016-10-17 22:49:22 +02:00
{
2017-10-16 19:43:38 +02:00
for ( const RelevantDir & dir : m_relevantDirs ) {
switch ( dir . dirObj - > status ) {
2016-10-17 22:49:22 +02:00
case SyncthingDirStatus : : Unknown :
case SyncthingDirStatus : : Idle :
break ;
default :
2017-09-30 18:55:06 +02:00
return false ;
2016-10-17 22:49:22 +02:00
}
}
2017-05-01 03:34:43 +02:00
for ( const SyncthingDev * dev : m_relevantDevs ) {
switch ( dev - > status ) {
2016-10-17 22:49:22 +02:00
case SyncthingDevStatus : : Unknown :
case SyncthingDevStatus : : Disconnected :
2023-08-27 00:09:06 +02:00
if ( m_args . requireDevsConnected . isPresent ( ) ) {
return false ;
}
break ;
2023-09-16 22:28:35 +02:00
case SyncthingDevStatus : : ThisDevice :
2016-10-17 22:49:22 +02:00
case SyncthingDevStatus : : Idle :
break ;
default :
2017-09-30 18:55:06 +02:00
return false ;
2016-10-17 22:49:22 +02:00
}
}
2017-09-30 18:55:06 +02:00
return true ;
2016-10-17 22:49:22 +02:00
}
2017-04-06 00:08:24 +02:00
void Application : : checkPwdOperationPresent ( const ArgumentOccurrence & occurrence )
2017-03-29 23:17:53 +02:00
{
2017-09-26 16:48:09 +02:00
// FIXME: implement default operation in argument parser
if ( m_args . pwd . specifiedOperation ( ) ) {
return ;
2017-03-29 23:17:53 +02:00
}
2017-04-06 00:08:24 +02:00
// print status when no operation specified
printPwdStatus ( occurrence ) ;
}
void Application : : printPwdStatus ( const ArgumentOccurrence & )
{
2017-05-01 03:34:43 +02:00
if ( ! findPwd ( ) ) {
2017-03-29 23:17:53 +02:00
return ;
}
2017-10-16 19:43:38 +02:00
printDir ( RelevantDir { m_pwd } ) ;
2017-04-06 00:08:24 +02:00
QCoreApplication : : quit ( ) ;
}
2017-03-29 23:17:53 +02:00
2017-04-06 00:08:24 +02:00
void Application : : requestRescanPwd ( const ArgumentOccurrence & )
{
2017-05-01 03:34:43 +02:00
if ( ! findPwd ( ) ) {
2017-03-29 23:17:53 +02:00
return ;
2017-04-06 00:08:24 +02:00
}
2017-10-16 19:43:38 +02:00
m_pwd . notifyAboutRescan ( ) ;
m_connection . rescan ( m_pwd . dirObj - > id , m_pwd . subDir ) ;
2017-04-06 00:08:24 +02:00
connect ( & m_connection , & SyncthingConnection : : rescanTriggered , this , & Application : : handleResponse ) ;
m_expectedResponse = 1 ;
}
void Application : : requestPausePwd ( const ArgumentOccurrence & )
{
2017-05-01 03:34:43 +02:00
if ( ! findPwd ( ) ) {
2017-03-29 23:17:53 +02:00
return ;
}
2017-10-16 19:43:38 +02:00
if ( m_connection . pauseDirectories ( QStringList ( m_pwd . dirObj - > id ) ) ) {
2023-09-17 20:33:31 +02:00
cerr < < " Request pausing folder \" " < < m_pwd . dirObj - > path . toLocal8Bit ( ) . data ( ) < < " \" ... " < < endl ;
2017-04-06 00:08:24 +02:00
connect ( & m_connection , & SyncthingConnection : : directoryPauseTriggered , this , & Application : : handleResponse ) ;
m_preventDisconnect = true ;
m_expectedResponse = 1 ;
} else {
2023-09-17 20:33:31 +02:00
cerr < < " Folder \" " < < m_pwd . dirObj - > path . toLocal8Bit ( ) . data ( ) < < " already paused " < < endl ;
2017-04-06 00:08:24 +02:00
QCoreApplication : : quit ( ) ;
}
}
2017-03-29 23:17:53 +02:00
2017-04-06 00:08:24 +02:00
void Application : : requestResumePwd ( const ArgumentOccurrence & )
{
2017-05-01 03:34:43 +02:00
if ( ! findPwd ( ) ) {
2017-04-06 00:08:24 +02:00
return ;
}
2017-10-16 19:43:38 +02:00
if ( m_connection . resumeDirectories ( QStringList ( m_pwd . dirObj - > id ) ) ) {
2023-09-17 20:33:31 +02:00
cerr < < " Request resuming folder \" " < < m_pwd . dirObj - > path . toLocal8Bit ( ) . data ( ) < < " \" ... " < < endl ;
2017-04-06 00:08:24 +02:00
connect ( & m_connection , & SyncthingConnection : : directoryResumeTriggered , this , & Application : : handleResponse ) ;
m_preventDisconnect = true ;
m_expectedResponse = 1 ;
return ;
} else {
2023-09-17 20:33:31 +02:00
cerr < < " Folder \" " < < m_pwd . dirObj - > path . toLocal8Bit ( ) . data ( ) < < " not paused " < < endl ;
2017-04-06 00:08:24 +02:00
QCoreApplication : : quit ( ) ;
}
2017-03-29 23:17:53 +02:00
}
2017-09-30 18:51:29 +02:00
void Application : : initDirCompletion ( Argument & arg , const ArgumentOccurrence & )
{
2018-01-10 19:50:21 +01:00
// prevent this initialization if we're not in shell completion mode
if ( m_argsRead ) {
return ;
}
2017-09-30 18:51:29 +02:00
// load config and wait for connected
2023-07-04 18:49:03 +02:00
if ( loadConfig ( ) ) {
return ;
}
2023-07-04 19:07:03 +02:00
m_settings . requestTimeout = 5000 ; // avoid blocking shell for too long
2018-04-01 20:21:51 +02:00
waitForConfig ( ) ;
2017-09-30 18:51:29 +02:00
// set directory IDs as completion values
2018-02-19 03:22:47 +01:00
m_dirCompletion = m_connection . directoryIds ( ) . join ( QChar ( ' ' ) ) . toUtf8 ( ) ;
arg . setPreDefinedCompletionValues ( m_dirCompletion . data ( ) ) ;
2017-09-30 18:51:29 +02:00
}
void Application : : initDevCompletion ( Argument & arg , const ArgumentOccurrence & )
{
2018-01-10 19:50:21 +01:00
// prevent this initialization if we're not in shell completion mode
if ( m_argsRead ) {
return ;
}
2017-09-30 18:51:29 +02:00
// load config and wait for connected
2023-07-04 18:49:03 +02:00
if ( loadConfig ( ) ) {
return ;
}
2023-07-04 19:07:03 +02:00
m_settings . requestTimeout = 5000 ; // avoid blocking shell for too long
2018-04-01 20:21:51 +02:00
waitForConfig ( ) ;
2017-09-30 18:51:29 +02:00
// set device IDs and names as completion values
QStringList completionValues ;
const size_t valueCount = m_connection . devInfo ( ) . size ( ) < < 2 ;
if ( valueCount > numeric_limits < int > : : max ( ) ) {
return ;
}
completionValues . reserve ( static_cast < int > ( valueCount ) ) ;
for ( const SyncthingDev & dev : m_connection . devInfo ( ) ) {
completionValues < < dev . id < < dev . name ;
}
2018-02-19 03:22:47 +01:00
m_devCompletion = completionValues . join ( QChar ( ' ' ) ) . toUtf8 ( ) ;
arg . setPreDefinedCompletionValues ( m_devCompletion . data ( ) ) ;
2017-09-30 18:51:29 +02:00
}
2017-10-16 19:43:38 +02:00
RelevantDir Application : : findDirectory ( const QString & dirIdentifier )
{
int dummy ;
RelevantDir relevantDir ;
// check whether the specified identifier is a known Syncthing directory or a relative path to an item in a
// known Syncthing directory
2021-11-04 00:16:27 +01:00
auto firstSlash = dirIdentifier . indexOf ( QChar ( ' / ' ) ) ;
2023-01-28 17:17:34 +01:00
relevantDir . dirObj = m_connection . findDirInfoConsideringLabels ( firstSlash > = 0 ? dirIdentifier . mid ( 0 , firstSlash ) : dirIdentifier , dummy ) ;
2017-10-16 19:43:38 +02:00
if ( relevantDir ) {
if ( firstSlash > = 0 ) {
relevantDir . subDir = dirIdentifier . mid ( firstSlash + 1 ) ;
}
return relevantDir ;
}
// check whether the specified identifier is an absolute or relative path of an item inside a known Syncthing directory
relevantDir . dirObj = m_connection . findDirInfoByPath (
QDir : : isRelativePath ( dirIdentifier ) ? QDir : : currentPath ( ) % QChar ( ' / ' ) % dirIdentifier : dirIdentifier , relevantDir . subDir , dummy ) ;
if ( relevantDir ) {
return relevantDir ;
}
2023-09-17 20:33:31 +02:00
cerr < < Phrases : : Warning < < " Specified folder \" " < < dirIdentifier . toLocal8Bit ( ) . data ( ) < < " \" is no Syncthing folder (or not part of any). "
2017-10-16 19:43:38 +02:00
< < Phrases : : End ;
return relevantDir ;
}
void RelevantDir : : notifyAboutRescan ( ) const
{
2018-04-02 12:48:27 +02:00
cerr < < Phrases : : Info ;
2017-10-16 19:43:38 +02:00
if ( subDir . isEmpty ( ) ) {
2023-09-17 20:33:31 +02:00
cerr < < " Request rescanning folder \" " < < dirObj - > path . toLocal8Bit ( ) . data ( ) < < " \" ... " ;
2017-10-16 19:43:38 +02:00
} else {
2018-04-02 12:48:27 +02:00
cerr < < " Request rescanning item \" " < < subDir . toLocal8Bit ( ) . data ( ) < < " \" in directory \" " < < dirObj - > path . toLocal8Bit ( ) . data ( ) < < " \" ... " ;
2017-10-16 19:43:38 +02:00
}
2018-04-02 12:48:27 +02:00
cerr < < Phrases : : EndFlush ;
2017-10-16 19:43:38 +02:00
}
2016-10-02 21:59:28 +02:00
} // namespace Cli