2024-03-05 08:55:13 +01:00
// Copyright (C) 2024, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
2024-02-28 15:45:30 +01:00
using System ;
2013-02-12 21:43:14 +00:00
using System.Collections.Generic ;
2018-12-11 21:07:30 -08:00
using System.Globalization ;
2013-02-12 21:43:14 +00:00
using System.Linq ;
2019-02-14 21:50:28 +01:00
using Duplicati.Library.Common ;
using Duplicati.Library.Common.IO ;
2022-12-30 10:59:29 -08:00
using Duplicati.Library.RestAPI ;
using Duplicati.WebserverCore ;
2019-02-14 21:50:28 +01:00
2013-02-12 21:43:14 +00:00
namespace Duplicati.Server
{
public class Program
{
2019-02-07 21:57:36 +01:00
2019-02-28 17:58:26 +01:00
private static readonly List < string > AlternativeHelpStrings = new List < string > { "help" , "/help" , "usage" , "/usage" , "--help" };
2019-02-07 21:57:36 +01:00
2019-03-03 18:40:30 +01:00
private static readonly List < string > ParameterFileOptionStrings = new List < string > { "parameters-file" , "parameterfile" };
2019-02-09 16:06:40 +01:00
2018-03-12 14:07:11 +01:00
/// <summary>
/// The log tag for messages from this class
/// </summary>
2019-02-28 17:58:26 +01:00
private static readonly string LOGTAG = Library . Logging . Log . LogTagFromType < Program >();
2013-02-12 21:43:14 +00:00
/// <summary>
/// The path to the directory that contains the main executable
/// </summary>
2024-03-15 14:18:56 +01:00
public static readonly string StartupPath = Duplicati . Library . AutoUpdater . UpdaterManager . INSTALLATIONDIR ;
2013-02-12 21:43:14 +00:00
/// <summary>
/// The name of the environment variable that holds the path to the data folder used by Duplicati
/// </summary>
2019-02-28 17:58:26 +01:00
private static readonly string DATAFOLDER_ENV_NAME = Duplicati . Library . AutoUpdater . AutoUpdateSettings . AppName . ToUpper ( CultureInfo . InvariantCulture ) + "_HOME" ;
2013-02-12 21:43:14 +00:00
/// <summary>
2019-11-30 11:35:43 -08:00
/// The environment variable that holds the database key used to encrypt the SQLite database
2013-02-12 21:43:14 +00:00
/// </summary>
2019-02-28 17:58:26 +01:00
private static readonly string DB_KEY_ENV_NAME = Duplicati . Library . AutoUpdater . AutoUpdateSettings . AppName . ToUpper ( CultureInfo . InvariantCulture ) + "_DB_KEY" ;
2013-02-12 21:43:14 +00:00
/// <summary>
/// Gets the folder where Duplicati data is stored
/// </summary>
2022-12-30 10:59:29 -08:00
public static string DataFolder { get => FIXMEGlobal . DataFolder ; private set => FIXMEGlobal . DataFolder = value ; }
2013-02-12 21:43:14 +00:00
2014-03-21 10:34:34 +01:00
/// <summary>
/// The single instance
/// </summary>
2019-02-10 19:58:56 +01:00
public static SingleInstance ApplicationInstance = null ;
2018-05-22 21:49:48 +02:00
2013-02-12 21:43:14 +00:00
/// <summary>
/// This is the only access to the database
/// </summary>
2022-12-30 10:59:29 -08:00
public static Database . Connection DataConnection { get => FIXMEGlobal . DataConnection ; set => FIXMEGlobal . DataConnection = value ; }
2013-02-12 21:43:14 +00:00
/// <summary>
/// This is the lock to be used before manipulating the shared resources
/// </summary>
2022-12-30 10:59:29 -08:00
public static object MainLock { get => FIXMEGlobal . MainLock ; }
2013-02-12 21:43:14 +00:00
/// <summary>
/// This is the scheduling thread
/// </summary>
2022-12-30 10:59:29 -08:00
public static Scheduler Scheduler { get => FIXMEGlobal . Scheduler ; set => FIXMEGlobal . Scheduler = value ; }
2013-02-12 21:43:14 +00:00
/// <summary>
/// This is the working thread
/// </summary>
2022-12-30 10:59:29 -08:00
public static Duplicati . Library . Utility . WorkerThread < Runner . IRunnerData > WorkThread { get => FIXMEGlobal . WorkThread ; set => FIXMEGlobal . WorkThread = value ; }
2013-02-12 21:43:14 +00:00
2015-10-04 17:14:20 +02:00
/// <summary>
/// List of completed task results
/// </summary>
2022-12-30 10:59:29 -08:00
public static List < KeyValuePair < long , Exception >> TaskResultCache { get => FIXMEGlobal . TaskResultCache ; }
2015-10-04 17:14:20 +02:00
/// <summary>
/// The maximum number of completed task results to keep in memory
/// </summary>
2019-10-19 10:56:21 -07:00
private static readonly int MAX_TASK_RESULT_CACHE_SIZE = 100 ;
2015-10-04 17:14:20 +02:00
2014-07-25 14:09:26 +02:00
/// <summary>
/// The thread running the ping-pong handler
/// </summary>
2019-02-28 17:58:26 +01:00
private static System . Threading . Thread PingPongThread ;
2014-07-25 14:09:26 +02:00
2013-02-12 21:43:14 +00:00
/// <summary>
/// The path to the file that contains the current database
/// </summary>
2019-02-28 17:58:26 +01:00
private static string DatabasePath ;
2013-02-12 21:43:14 +00:00
/// <summary>
/// The controller interface for pause/resume and throttle options
/// </summary>
2022-12-30 10:59:29 -08:00
public static LiveControls LiveControl { get => FIXMEGlobal . LiveControl ; set => FIXMEGlobal . LiveControl = value ; }
2013-02-12 21:43:14 +00:00
/// <summary>
/// The application exit event
/// </summary>
2022-12-30 10:59:29 -08:00
public static System . Threading . ManualResetEvent ApplicationExitEvent { get => FIXMEGlobal . ApplicationExitEvent ; set => FIXMEGlobal . ApplicationExitEvent = value ; }
2013-02-12 21:43:14 +00:00
/// <summary>
/// The webserver instance
/// </summary>
2019-02-28 17:58:26 +01:00
private static WebServer . Server WebServer ;
2014-06-30 11:31:06 +02:00
2022-12-30 10:59:29 -08:00
/// <summary>
/// Callback to shutdown the modern webserver
/// </summary>
private static Action ShutdownModernWebserver ;
2014-06-30 11:31:06 +02:00
/// <summary>
/// The update poll thread.
/// </summary>
2022-12-30 10:59:29 -08:00
public static UpdatePollThread UpdatePoller { get => FIXMEGlobal . UpdatePoller ; set => FIXMEGlobal . UpdatePoller = value ; }
2018-05-22 21:49:48 +02:00
2013-02-12 21:43:14 +00:00
/// <summary>
/// An event that is set once the server is ready to respond to requests
/// </summary>
2019-10-19 10:56:21 -07:00
public static readonly System . Threading . ManualResetEvent ServerStartedEvent = new System . Threading . ManualResetEvent ( false );
2013-02-12 21:43:14 +00:00
/// <summary>
2019-11-30 11:35:43 -08:00
/// The status event signaler, used to control long polling of status updates
2013-02-12 21:43:14 +00:00
/// </summary>
2022-12-30 10:59:29 -08:00
public static EventPollNotify StatusEventNotifyer { get => FIXMEGlobal . StatusEventNotifyer ; }
2018-05-22 21:49:48 +02:00
2013-02-12 21:43:14 +00:00
/// <summary>
2014-03-10 00:16:53 +01:00
/// A delegate method for creating a copy of the current progress state
2013-02-12 21:43:14 +00:00
/// </summary>
2022-12-30 10:59:29 -08:00
public static Func < Duplicati . Server . Serialization . Interface . IProgressEventData > GenerateProgressState { get => FIXMEGlobal . GenerateProgressState ; set => FIXMEGlobal . GenerateProgressState = value ; }
2013-02-12 21:43:14 +00:00
/// <summary>
/// An event ID that increases whenever the database is updated
/// </summary>
public static long LastDataUpdateID = 0 ;
2014-08-04 01:06:29 +02:00
/// <summary>
/// An event ID that increases whenever a notification is updated
/// </summary>
public static long LastNotificationUpdateID = 0 ;
2014-08-01 11:18:10 +02:00
/// <summary>
/// The log redirect handler
/// </summary>
2022-12-30 10:59:29 -08:00
public static LogWriteHandler LogHandler { get => FIXMEGlobal . LogHandler ; }
2014-08-01 11:18:10 +02:00
2018-03-09 21:24:57 +01:00
/// <summary>
2018-05-22 21:49:48 +02:00
/// Used to check the origin of the web server (e.g. Tray icon or a stand alone Server)
2018-03-09 21:24:57 +01:00
/// </summary>
2022-12-30 10:59:29 -08:00
public static string Origin { get => FIXMEGlobal . Origin ; set => FIXMEGlobal . Origin = value ; }
2018-03-09 21:24:57 +01:00
2014-08-06 10:30:46 +02:00
private static System . Threading . Timer PurgeTempFilesTimer = null ;
2013-12-07 15:24:57 +01:00
public static int ServerPort
{
get
{
return WebServer . Port ;
}
}
2013-02-12 21:43:14 +00:00
2014-06-23 11:22:54 +02:00
public static bool IsFirstRun
{
get { return DataConnection . ApplicationSettings . IsFirstRun ; }
set { DataConnection . ApplicationSettings . IsFirstRun = value ; }
}
2018-03-09 21:24:57 +01:00
public static string StartedBy
{
get { return Origin ; }
set { Origin = value ; }
}
2014-06-23 11:22:54 +02:00
public static bool ServerPortChanged
{
get { return DataConnection . ApplicationSettings . ServerPortChanged ; }
set { DataConnection . ApplicationSettings . ServerPortChanged = value ; }
}
2022-12-30 10:59:29 -08:00
public static void IncrementLastDataUpdateID ()
{
System . Threading . Interlocked . Increment ( ref Program . LastDataUpdateID );
}
public static void IncrementLastNotificationUpdateID ()
{
System . Threading . Interlocked . Increment ( ref Program . LastNotificationUpdateID );
}
static Program ()
{
FIXMEGlobal . IncrementLastDataUpdateID = Program . IncrementLastDataUpdateID ;
FIXMEGlobal . PeekLastDataUpdateID = () => Program . LastDataUpdateID ;
FIXMEGlobal . IncrementLastNotificationUpdateID = Program . IncrementLastNotificationUpdateID ;
FIXMEGlobal . PeekLastNotificationUpdateID = () => Program . LastNotificationUpdateID ;
FIXMEGlobal . GetDatabaseConnection = Program . GetDatabaseConnection ;
FIXMEGlobal . StartOrStopUsageReporter = Program . StartOrStopUsageReporter ;
FIXMEGlobal . UpdateThrottleSpeeds = Program . UpdateThrottleSpeeds ;
}
2013-02-12 21:43:14 +00:00
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
2024-03-15 14:18:56 +01:00
public static int Main ( string [] _args )
2014-06-26 22:37:58 +02:00
{
2013-02-12 21:43:14 +00:00
//If we are on Windows, append the bundled "win-tools" programs to the search path
//We add it last, to allow the user to override with other versions
2018-11-02 22:13:25 +01:00
if ( Platform . IsClientWindows )
2013-02-12 21:43:14 +00:00
{
Environment . SetEnvironmentVariable ( "PATH" ,
Environment . GetEnvironmentVariable ( "PATH" ) +
System . IO . Path . PathSeparator . ToString () +
System . IO . Path . Combine (
System . IO . Path . GetDirectoryName ( System . Reflection . Assembly . GetExecutingAssembly (). Location ),
"win-tools" )
);
}
2013-02-27 14:25:14 +00:00
//If this executable is invoked directly, write to console, otherwise throw exceptions
2024-04-15 13:17:49 +02:00
var writeToConsole = System . Reflection . Assembly . GetEntryAssembly (). GetName (). FullName . StartsWith ( "Duplicati.Server," , StringComparison . OrdinalIgnoreCase );
2018-02-01 09:48:00 +01:00
2013-02-12 21:43:14 +00:00
//Find commandline options here for handling special startup cases
2018-02-01 09:48:00 +01:00
var args = new List < string >( _args );
2019-02-25 12:34:32 +01:00
var optionsWithFilter = Library . Utility . FilterCollector . ExtractOptions ( new List < string >( args ));
var commandlineOptions = optionsWithFilter . Item1 ;
var filter = optionsWithFilter . Item2 ;
2013-02-12 21:43:14 +00:00
2019-02-28 17:58:26 +01:00
if ( _args . Select ( s => s . ToLower ()). Intersect ( AlternativeHelpStrings . ConvertAll ( x => x . ToLower ())). Any ())
2019-02-07 21:57:36 +01:00
{
2019-02-12 21:59:43 +01:00
return ShowHelp ( writeToConsole );
2019-02-07 21:57:36 +01:00
}
2018-05-22 21:49:48 +02:00
if ( commandlineOptions . ContainsKey ( "tempdir" ) && ! string . IsNullOrEmpty ( commandlineOptions [ "tempdir" ]))
2019-02-09 16:06:40 +01:00
{
2018-05-22 21:49:48 +02:00
Library . Utility . SystemContextSettings . DefaultTempPath = commandlineOptions [ "tempdir" ];
2019-02-09 16:06:40 +01:00
}
2019-02-09 16:39:40 +01:00
2018-09-26 21:12:13 -07:00
Library . Utility . SystemContextSettings . StartSession ();
2018-05-22 21:49:48 +02:00
2019-02-09 16:39:40 +01:00
var parameterFileOption = commandlineOptions . Keys . Select ( s => s . ToLower ())
2019-02-28 17:58:26 +01:00
. Intersect ( ParameterFileOptionStrings . ConvertAll ( x => x . ToLower ())). FirstOrDefault ();
2019-02-09 16:06:40 +01:00
if ( parameterFileOption != null && ! string . IsNullOrEmpty ( commandlineOptions [ parameterFileOption ]))
2013-02-12 21:43:14 +00:00
{
2019-05-11 11:02:37 -07:00
string filename = commandlineOptions [ parameterFileOption ];
2019-02-09 16:06:40 +01:00
commandlineOptions . Remove ( parameterFileOption );
2019-05-11 11:02:37 -07:00
if (! ReadOptionsFromFile ( filename , ref filter , args , commandlineOptions ))
2019-02-09 16:06:40 +01:00
return 100 ;
2013-02-12 21:43:14 +00:00
}
2019-02-09 16:39:40 +01:00
ConfigureLogging ( commandlineOptions );
2018-05-22 21:49:48 +02:00
2013-02-12 21:43:14 +00:00
try
{
2013-02-27 14:25:14 +00:00
2017-05-21 19:25:39 +02:00
DataConnection = GetDatabaseConnection ( commandlineOptions );
2013-02-12 21:43:14 +00:00
2017-05-21 19:25:39 +02:00
if (! DataConnection . ApplicationSettings . FixedInvalidBackupId )
DataConnection . FixInvalidBackupId ();
2013-02-12 21:43:14 +00:00
2019-02-12 21:59:43 +01:00
CreateApplicationInstance ( writeToConsole );
2015-02-15 22:54:49 +01:00
2016-03-12 00:29:51 +01:00
StartOrStopUsageReporter ();
2019-02-13 22:10:00 +01:00
AdjustApplicationSettings ( commandlineOptions );
2018-08-07 00:36:12 +02:00
2013-02-12 21:43:14 +00:00
ApplicationExitEvent = new System . Threading . ManualResetEvent ( false );
2018-05-22 21:49:48 +02:00
2019-02-13 22:10:00 +01:00
Library . AutoUpdater . UpdaterManager . OnError += ( Exception obj ) =>
2014-07-25 14:09:26 +02:00
{
2019-02-10 19:58:56 +01:00
DataConnection . LogError ( null , "Error in updater" , obj );
2014-07-01 23:59:30 +02:00
};
2019-02-14 21:50:28 +01:00
2019-08-31 13:22:45 -04:00
UpdatePoller = new UpdatePollThread ();
2019-02-14 21:50:28 +01:00
SetPurgeTempFilesTimer ( commandlineOptions );
2018-05-22 21:49:48 +02:00
2019-02-25 12:34:32 +01:00
SetLiveControls ();
2013-02-12 21:43:14 +00:00
2019-02-28 17:51:52 +01:00
SetWorkerThread ();
2013-02-12 21:43:14 +00:00
2019-02-28 17:51:52 +01:00
StartWebServer ( commandlineOptions );
2014-06-23 11:22:54 +02:00
2014-07-25 14:09:26 +02:00
if ( Library . Utility . Utility . ParseBoolOption ( commandlineOptions , "ping-pong-keepalive" ))
{
2019-02-28 17:51:52 +01:00
PingPongThread = new System . Threading . Thread ( PingPongMethod ) { IsBackground = true };
PingPongThread . Start ();
2014-07-25 14:09:26 +02:00
}
2013-02-12 21:43:14 +00:00
ServerStartedEvent . Set ();
ApplicationExitEvent . WaitOne ();
}
2014-03-21 10:34:34 +01:00
catch ( SingleInstance . MultipleInstanceException mex )
{
2015-01-20 21:07:24 +01:00
System . Diagnostics . Trace . WriteLine ( Strings . Program . SeriousError ( mex . ToString ()));
2019-02-25 12:34:32 +01:00
if (! writeToConsole ) throw ;
Console . WriteLine ( Strings . Program . SeriousError ( mex . ToString ()));
return 100 ;
2014-03-21 10:34:34 +01:00
}
2013-02-12 21:43:14 +00:00
catch ( Exception ex )
{
2015-01-20 21:07:24 +01:00
System . Diagnostics . Trace . WriteLine ( Strings . Program . SeriousError ( ex . ToString ()));
2019-02-12 21:59:43 +01:00
if ( writeToConsole )
2017-10-11 12:49:14 +02:00
{
2015-01-20 21:07:24 +01:00
Console . WriteLine ( Strings . Program . SeriousError ( ex . ToString ()));
2017-10-11 12:49:14 +02:00
return 100 ;
}
2013-02-27 14:25:14 +00:00
else
2015-01-20 21:07:24 +01:00
throw new Exception ( Strings . Program . SeriousError ( ex . ToString ()), ex );
2013-02-12 21:43:14 +00:00
}
2013-02-27 14:25:14 +00:00
finally
{
StatusEventNotifyer . SignalNewEvent ();
2013-02-12 21:43:14 +00:00
2024-04-08 07:47:12 +02:00
if ( ShutdownModernWebserver != null )
ShutdownModernWebserver ();
2019-02-14 21:50:28 +01:00
UpdatePoller ?. Terminate ();
Scheduler ?. Terminate ( true );
WorkThread ?. Terminate ( true );
ApplicationInstance ?. Dispose ();
PurgeTempFilesTimer ?. Dispose ();
2013-02-12 21:43:14 +00:00
2016-12-01 23:15:49 +01:00
Library . UsageReporter . Reporter . ShutDown ();
2021-01-03 08:54:09 -08:00
try { PingPongThread ?. Interrupt (); }
2019-02-28 17:56:32 +01:00
catch { }
2014-07-25 14:09:26 +02:00
2019-02-14 21:50:28 +01:00
LogHandler ?. Dispose ();
2013-02-27 14:25:14 +00:00
}
2017-10-11 12:49:14 +02:00
return 0 ;
2013-02-12 21:43:14 +00:00
}
2019-02-28 17:51:52 +01:00
private static void StartWebServer ( Dictionary < string , string > commandlineOptions )
{
WebServer = new WebServer . Server ( commandlineOptions );
ServerPortChanged |= WebServer . Port != DataConnection . ApplicationSettings . LastWebserverPort ;
DataConnection . ApplicationSettings . LastWebserverPort = WebServer . Port ;
2022-12-30 10:59:29 -08:00
2024-04-12 15:32:25 +02:00
// var server = new DuplicatiWebserver();
// ShutdownModernWebserver = server.Foo();
2019-02-28 17:51:52 +01:00
}
private static void SetWorkerThread ()
{
WorkThread = new Duplicati . Library . Utility . WorkerThread < Runner . IRunnerData >(( x ) => { Runner . Run ( x , true ); },
LiveControl . State == LiveControls . LiveControlState . Paused );
Scheduler = new Scheduler ( WorkThread );
WorkThread . StartingWork += ( worker , task ) => { SignalNewEvent ( null , null ); };
WorkThread . CompletedWork += ( worker , task ) => { SignalNewEvent ( null , null ); };
WorkThread . WorkQueueChanged += ( worker ) => { SignalNewEvent ( null , null ); };
Scheduler . NewSchedule += new EventHandler ( SignalNewEvent );
WorkThread . OnError += ( worker , task , exception ) =>
{
Program . DataConnection . LogError ( task ?. BackupID , "Error in worker" , exception );
};
var lastScheduleId = LastDataUpdateID ;
Program . StatusEventNotifyer . NewEvent += ( sender , e ) =>
{
if ( lastScheduleId == LastDataUpdateID ) return ;
lastScheduleId = LastDataUpdateID ;
Program . Scheduler . Reschedule ();
};
void RegisterTaskResult ( long id , Exception ex )
{
lock ( MainLock )
{
// If the new results says it crashed, we store that instead of success
if ( Program . TaskResultCache . Count > 0 && Program . TaskResultCache . Last (). Key == id )
{
if ( ex != null && Program . TaskResultCache . Last (). Value == null )
Program . TaskResultCache . RemoveAt ( Program . TaskResultCache . Count - 1 );
else
return ;
}
Program . TaskResultCache . Add ( new KeyValuePair < long , Exception >( id , ex ));
while ( Program . TaskResultCache . Count > MAX_TASK_RESULT_CACHE_SIZE )
Program . TaskResultCache . RemoveAt ( 0 );
}
}
Program . WorkThread . CompletedWork += ( worker , task ) => { RegisterTaskResult ( task . TaskID , null ); };
Program . WorkThread . OnError += ( worker , task , exception ) => { RegisterTaskResult ( task . TaskID , exception ); };
}
2019-02-25 12:34:32 +01:00
private static void SetLiveControls ()
{
LiveControl = new LiveControls ( DataConnection . ApplicationSettings );
LiveControl . StateChanged += LiveControl_StateChanged ;
LiveControl . ThreadPriorityChanged += LiveControl_ThreadPriorityChanged ;
LiveControl . ThrottleSpeedChanged += LiveControl_ThrottleSpeedChanged ;
}
2019-02-14 21:50:28 +01:00
private static void SetPurgeTempFilesTimer ( Dictionary < string , string > commandlineOptions )
{
var lastPurge = new DateTime ( 0 );
System . Threading . TimerCallback purgeTempFilesCallback = ( x ) =>
{
try
{
2019-08-31 13:22:45 -04:00
#if DEBUG
if ( Math . Abs (( DateTime . Now - lastPurge ). TotalHours ) < 1 )
{
return ;
}
#else
2019-02-14 21:50:28 +01:00
if ( Math . Abs (( DateTime . Now - lastPurge ). TotalHours ) < 23 )
2019-08-31 13:22:45 -04:00
{
2019-02-14 21:50:28 +01:00
return ;
2019-08-31 13:22:45 -04:00
}
#endif
2019-02-14 21:50:28 +01:00
lastPurge = DateTime . Now ;
foreach ( var e in DataConnection . GetTempFiles (). Where (( f ) => f . Expires < DateTime . Now ))
{
try
{
if ( System . IO . File . Exists ( e . Path ))
System . IO . File . Delete ( e . Path );
}
catch ( Exception ex )
{
DataConnection . LogError ( null , $"Failed to delete temp file: {e.Path}" , ex );
}
DataConnection . DeleteTempFile ( e . ID );
}
Library . Utility . TempFile . RemoveOldApplicationTempFiles (( path , ex ) =>
{
DataConnection . LogError ( null , $"Failed to delete temp file: {path}" , ex );
});
if (! commandlineOptions . TryGetValue ( "log-retention" , out string pts ))
{
pts = DEFAULT_LOG_RETENTION ;
}
DataConnection . PurgeLogData ( Library . Utility . Timeparser . ParseTimeInterval ( pts , DateTime . Now , true ));
}
catch ( Exception ex )
{
DataConnection . LogError ( null , "Failed during temp file cleanup" , ex );
}
};
try
{
2019-08-31 13:22:45 -04:00
#if DEBUG
PurgeTempFilesTimer =
new System . Threading . Timer ( purgeTempFilesCallback , null , TimeSpan . FromSeconds ( 10 ), TimeSpan . FromHours ( 1 ));
#else
2019-02-14 21:50:28 +01:00
PurgeTempFilesTimer =
new System . Threading . Timer ( purgeTempFilesCallback , null , TimeSpan . FromHours ( 1 ), TimeSpan . FromDays ( 1 ));
2019-08-31 13:22:45 -04:00
#endif
2019-02-14 21:50:28 +01:00
}
catch ( ArgumentOutOfRangeException )
{
//Bugfix for older Mono, slightly more resources used to avoid large values in the period field
PurgeTempFilesTimer =
new System . Threading . Timer ( purgeTempFilesCallback , null , TimeSpan . FromHours ( 1 ), TimeSpan . FromHours ( 1 ));
}
}
2019-02-13 22:10:00 +01:00
private static void AdjustApplicationSettings ( Dictionary < string , string > commandlineOptions )
{
if ( commandlineOptions . ContainsKey ( "webservice-password" ))
{
DataConnection . ApplicationSettings . SetWebserverPassword ( commandlineOptions [ "webservice-password" ]);
}
DataConnection . ApplicationSettings . GenerateWebserverPasswordTrayIcon ();
if ( commandlineOptions . ContainsKey ( "webservice-allowed-hostnames" ))
{
DataConnection . ApplicationSettings . SetAllowedHostnames ( commandlineOptions [ "webservice-allowed-hostnames" ]);
}
}
2019-02-12 21:59:43 +01:00
private static void CreateApplicationInstance ( bool writeConsole )
{
try
{
//This will also create DATAFOLDER if it does not exist
ApplicationInstance = new SingleInstance ( DataFolder );
}
catch ( Exception ex )
{
if ( writeConsole )
{
Console . WriteLine ( Strings . Program . StartupFailure ( ex ));
Environment . Exit ( 200 );
}
throw new Exception ( Strings . Program . StartupFailure ( ex ));
}
if (! ApplicationInstance . IsFirstInstance )
{
if ( writeConsole )
{
Console . WriteLine ( Strings . Program . AnotherInstanceDetected );
Environment . Exit ( 200 );
}
throw new SingleInstance . MultipleInstanceException ( Strings . Program . AnotherInstanceDetected );
}
}
2019-02-09 16:39:40 +01:00
private static void ConfigureLogging ( Dictionary < string , string > commandlineOptions )
{
#if DEBUG
//Log various information in the logfile
if (! commandlineOptions . ContainsKey ( "log-file" ))
{
commandlineOptions [ "log-file" ] = System . IO . Path . Combine ( StartupPath , "Duplicati.debug.log" );
commandlineOptions [ "log-level" ] = Duplicati . Library . Logging . LogMessageType . Profiling . ToString ();
if ( System . IO . File . Exists ( commandlineOptions [ "log-file" ]))
{
System . IO . File . Delete ( commandlineOptions [ "log-file" ]);
}
}
#endif
// Setup the log redirect
Library . Logging . Log . StartScope ( LogHandler , null );
if ( commandlineOptions . ContainsKey ( "log-file" ))
{
var loglevel = Library . Logging . LogMessageType . Error ;
if ( commandlineOptions . ContainsKey ( "log-level" ))
Enum . TryParse ( commandlineOptions [ "log-level" ], true , out loglevel );
LogHandler . SetServerFile ( commandlineOptions [ "log-file" ], loglevel );
}
}
2019-02-07 21:41:58 +01:00
private static int ShowHelp ( bool writeConsole )
{
if ( writeConsole )
{
Console . WriteLine ( Strings . Program . HelpDisplayDialog );
foreach ( Library . Interface . ICommandLineArgument arg in SupportedCommands )
Console . WriteLine ( Strings . Program . HelpDisplayFormat ( arg . Name , arg . LongDescription ));
return 0 ;
}
throw new Exception ( "Server invoked with --help" );
}
2017-05-21 19:25:39 +02:00
public static Database . Connection GetDatabaseConnection ( Dictionary < string , string > commandlineOptions )
{
var serverDataFolder = Environment . GetEnvironmentVariable ( DATAFOLDER_ENV_NAME );
if ( commandlineOptions . ContainsKey ( "server-datafolder" ))
serverDataFolder = commandlineOptions [ "server-datafolder" ];
if ( string . IsNullOrEmpty ( serverDataFolder ))
{
#if DEBUG
//debug mode uses a lock file located in the app folder
2017-09-18 23:23:45 -06:00
DataFolder = StartupPath ;
2017-05-21 19:25:39 +02:00
#else
2017-06-27 21:35:23 +02:00
bool portableMode = commandlineOptions . ContainsKey ( "portable-mode" ) ? Library . Utility . Utility . ParseBool ( commandlineOptions [ "portable-mode" ], true ) : false ;
2017-05-21 19:25:39 +02:00
if ( portableMode )
{
//Portable mode uses a data folder in the application home dir
DataFolder = System . IO . Path . Combine ( StartupPath , "data" );
System . IO . Directory . SetCurrentDirectory ( StartupPath );
}
else
{
2017-06-27 21:35:23 +02:00
//Normal release mode uses the systems "(Local) Application Data" folder
// %LOCALAPPDATA% on Windows, ~/.config on Linux
// Special handling for Windows:
// - Older versions use %APPDATA%
// - but new versions use %LOCALAPPDATA%
//
// If we find a new version, lets use that
// otherwise use the older location
//
serverDataFolder = System . IO . Path . Combine ( System . Environment . GetFolderPath ( Environment . SpecialFolder . ApplicationData ), Library . AutoUpdater . AutoUpdateSettings . AppName );
2018-11-02 22:13:25 +01:00
if ( Platform . IsClientWindows )
2017-06-27 21:35:23 +02:00
{
var localappdata = System . IO . Path . Combine ( System . Environment . GetFolderPath ( Environment . SpecialFolder . LocalApplicationData ), Library . AutoUpdater . AutoUpdateSettings . AppName );
var prefile = System . IO . Path . Combine ( serverDataFolder , "Duplicati-server.sqlite" );
var curfile = System . IO . Path . Combine ( localappdata , "Duplicati-server.sqlite" );
// If the new file exists, we use that
// If the new file does not exist, and the old file exists we use the old
// Otherwise we use the new location
if ( System . IO . File . Exists ( curfile ) || ! System . IO . File . Exists ( prefile ))
serverDataFolder = localappdata ;
}
DataFolder = serverDataFolder ;
2017-05-21 19:25:39 +02:00
}
#endif
2017-09-18 23:23:45 -06:00
}
2017-05-21 19:25:39 +02:00
else
2018-10-27 12:17:07 +02:00
DataFolder = Util . AppendDirSeparator ( Environment . ExpandEnvironmentVariables ( serverDataFolder ). Trim ( '"' ));
2017-05-21 19:25:39 +02:00
2024-03-05 08:55:13 +01:00
var sqliteVersion = new Version ( Duplicati . Library . SQLiteHelper . SQLiteLoader . SQLiteVersion );
2017-05-21 19:25:39 +02:00
if ( sqliteVersion < new Version ( 3 , 6 , 3 ))
{
//The official Mono SQLite provider is also broken with less than 3.6.3
throw new Exception ( Strings . Program . WrongSQLiteVersion ( sqliteVersion , "3.6.3" ));
}
//Create the connection instance
var con = Library . SQLiteHelper . SQLiteLoader . LoadConnection ();
try
{
DatabasePath = System . IO . Path . Combine ( DataFolder , "Duplicati-server.sqlite" );
if (! System . IO . Directory . Exists ( System . IO . Path . GetDirectoryName ( DatabasePath )))
System . IO . Directory . CreateDirectory ( System . IO . Path . GetDirectoryName ( DatabasePath ));
2024-04-23 17:06:32 +02:00
// Attempt to open the database, removing any encryption present
Duplicati . Library . SQLiteHelper . SQLiteLoader . OpenDatabase ( con , DatabasePath , Library . SQLiteHelper . SQLiteRC4Decrypter . GetEncryptionPassword ( commandlineOptions ));
2017-05-21 19:25:39 +02:00
2022-12-30 10:59:29 -08:00
Duplicati . Library . SQLiteHelper . DatabaseUpgrader . UpgradeDatabase ( con , DatabasePath , typeof ( Duplicati . Library . RestAPI . Database . DatabaseConnectionSchemaMarker ));
2017-05-21 19:25:39 +02:00
}
catch ( Exception ex )
{
//Unwrap the reflection exceptions
if ( ex is System . Reflection . TargetInvocationException && ex . InnerException != null )
ex = ex . InnerException ;
throw new Exception ( Strings . Program . DatabaseOpenError ( ex . Message ));
}
return new Database . Connection ( con );
}
2016-03-12 00:29:51 +01:00
public static void StartOrStopUsageReporter ()
{
2018-05-22 21:49:48 +02:00
var disableUsageReporter =
2017-09-18 23:23:45 -06:00
string . Equals ( DataConnection . ApplicationSettings . UsageReporterLevel , "none" , StringComparison . OrdinalIgnoreCase )
2016-03-12 00:29:51 +01:00
||
2017-09-18 23:23:45 -06:00
string . Equals ( DataConnection . ApplicationSettings . UsageReporterLevel , "disabled" , StringComparison . OrdinalIgnoreCase );
2016-03-12 00:29:51 +01:00
Library . UsageReporter . ReportType reportLevel ;
2018-05-22 21:49:48 +02:00
if (! Enum . TryParse < Library . UsageReporter . ReportType >( DataConnection . ApplicationSettings . UsageReporterLevel , true , out reportLevel ))
2016-03-12 00:29:51 +01:00
Library . UsageReporter . Reporter . SetReportLevel ( null , disableUsageReporter );
else
Library . UsageReporter . Reporter . SetReportLevel ( reportLevel , disableUsageReporter );
}
2017-06-20 13:00:52 +02:00
public static void UpdateThrottleSpeeds ()
{
2017-06-20 14:17:52 +02:00
if ( Program . WorkThread == null )
return ;
2018-05-22 21:49:48 +02:00
2017-06-20 13:00:52 +02:00
var cur = Program . WorkThread . CurrentTask ;
if ( cur != null )
cur . UpdateThrottleSpeed ();
}
2013-02-12 21:43:14 +00:00
private static void SignalNewEvent ( object sender , EventArgs e )
{
StatusEventNotifyer . SignalNewEvent ();
2018-05-22 21:49:48 +02:00
}
2013-02-12 21:43:14 +00:00
/// <summary>
/// Handles a change in the LiveControl and updates the Runner
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private static void LiveControl_ThreadPriorityChanged ( object sender , EventArgs e )
2018-05-22 21:49:48 +02:00
{
2013-02-12 21:43:14 +00:00
StatusEventNotifyer . SignalNewEvent ();
}
/// <summary>
/// Handles a change in the LiveControl and updates the Runner
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private static void LiveControl_ThrottleSpeedChanged ( object sender , EventArgs e )
{
StatusEventNotifyer . SignalNewEvent ();
}
/// <summary>
/// This event handler updates the trayicon menu with the current state of the runner.
/// </summary>
static void LiveControl_StateChanged ( object sender , EventArgs e )
{
switch ( LiveControl . State )
{
case LiveControls . LiveControlState . Paused :
2014-05-15 12:47:16 +02:00
{
WorkThread . Pause ();
var t = WorkThread . CurrentTask ;
2019-02-25 12:34:32 +01:00
t ?. Pause ();
2014-05-15 12:47:16 +02:00
break ;
}
2013-02-12 21:43:14 +00:00
case LiveControls . LiveControlState . Running :
2014-05-15 12:47:16 +02:00
{
WorkThread . Resume ();
var t = WorkThread . CurrentTask ;
2019-02-25 12:34:32 +01:00
t ?. Resume ();
2014-05-15 12:47:16 +02:00
break ;
}
2013-02-12 21:43:14 +00:00
}
StatusEventNotifyer . SignalNewEvent ();
}
2018-06-21 18:08:52 +02:00
2015-08-24 08:04:22 +01:00
/// <summary>
/// Simple method for tracking if the server has crashed
/// </summary>
2014-07-25 14:09:26 +02:00
private static void PingPongMethod ()
{
var rd = new System . IO . StreamReader ( Console . OpenStandardInput ());
var wr = new System . IO . StreamWriter ( Console . OpenStandardOutput ());
2016-04-13 11:44:21 +02:00
string line ;
while (( line = rd . ReadLine ()) != null )
2014-07-25 14:09:26 +02:00
{
2016-04-13 11:44:21 +02:00
if ( string . Equals ( "shutdown" , line , StringComparison . OrdinalIgnoreCase ))
{
// TODO: All calls to ApplicationExitEvent and TrayIcon->Quit
// should check if we are running something
ApplicationExitEvent . Set ();
}
else
{
wr . WriteLine ( "pong" );
wr . Flush ();
}
2014-07-25 14:09:26 +02:00
}
}
2015-02-15 23:26:52 +01:00
/// <summary>
/// The default log retention
/// </summary>
2019-10-19 10:56:21 -07:00
private static readonly string DEFAULT_LOG_RETENTION = "30D" ;
2015-02-15 23:26:52 +01:00
2013-02-12 21:43:14 +00:00
/// <summary>
/// Gets a list of all supported commandline options
/// </summary>
2014-04-02 11:34:33 +02:00
public static Library . Interface . ICommandLineArgument [] SupportedCommands
2013-02-12 21:43:14 +00:00
{
get
{
2024-04-23 17:06:32 +02:00
var lst = new List < Duplicati . Library . Interface . ICommandLineArgument >( new Duplicati . Library . Interface . ICommandLineArgument [] {
2018-05-22 21:49:48 +02:00
new Duplicati . Library . Interface . CommandLineArgument ( "tempdir" , Duplicati . Library . Interface . CommandLineArgument . ArgumentType . Path , Strings . Program . TempdirShort , Strings . Program . TempdirLong , System . IO . Path . GetTempPath ()),
2013-02-12 21:43:14 +00:00
new Duplicati . Library . Interface . CommandLineArgument ( "help" , Duplicati . Library . Interface . CommandLineArgument . ArgumentType . Boolean , Strings . Program . HelpCommandDescription , Strings . Program . HelpCommandDescription ),
2018-01-28 20:03:06 +01:00
new Duplicati . Library . Interface . CommandLineArgument ( "parameters-file" , Library . Interface . CommandLineArgument . ArgumentType . Path , Strings . Program . ParametersFileOptionShort , Strings . Program . ParametersFileOptionLong2 , "" , new string [] { "parameter-file" , "parameterfile" }),
2024-04-23 17:06:32 +02:00
new Duplicati . Library . Interface . CommandLineArgument ( "unencrypted-database" , Duplicati . Library . Interface . CommandLineArgument . ArgumentType . Boolean , Strings . Program . UnencrypteddatabaseCommandDescription , Strings . Program . UnencrypteddatabaseCommandDescription , "false" , null , null , "Database encryption is no longer supported, this setting has no effect" ),
2013-02-12 21:43:14 +00:00
new Duplicati . Library . Interface . CommandLineArgument ( "portable-mode" , Duplicati . Library . Interface . CommandLineArgument . ArgumentType . Boolean , Strings . Program . PortablemodeCommandDescription , Strings . Program . PortablemodeCommandDescription ),
new Duplicati . Library . Interface . CommandLineArgument ( "log-file" , Duplicati . Library . Interface . CommandLineArgument . ArgumentType . Path , Strings . Program . LogfileCommandDescription , Strings . Program . LogfileCommandDescription ),
2014-06-27 13:23:05 +02:00
new Duplicati . Library . Interface . CommandLineArgument ( "log-level" , Duplicati . Library . Interface . CommandLineArgument . ArgumentType . Enumeration , Strings . Program . LoglevelCommandDescription , Strings . Program . LoglevelCommandDescription , "Warning" , null , Enum . GetNames ( typeof ( Duplicati . Library . Logging . LogMessageType ))),
new Duplicati . Library . Interface . CommandLineArgument ( Duplicati . Server . WebServer . Server . OPTION_WEBROOT , Duplicati . Library . Interface . CommandLineArgument . ArgumentType . Path , Strings . Program . WebserverWebrootDescription , Strings . Program . WebserverWebrootDescription , Duplicati . Server . WebServer . Server . DEFAULT_OPTION_WEBROOT ),
new Duplicati . Library . Interface . CommandLineArgument ( Duplicati . Server . WebServer . Server . OPTION_PORT , Duplicati . Library . Interface . CommandLineArgument . ArgumentType . String , Strings . Program . WebserverPortDescription , Strings . Program . WebserverPortDescription , Duplicati . Server . WebServer . Server . DEFAULT_OPTION_PORT . ToString ()),
2017-11-26 15:24:53 -08:00
new Duplicati . Library . Interface . CommandLineArgument ( Duplicati . Server . WebServer . Server . OPTION_SSLCERTIFICATEFILE , Duplicati . Library . Interface . CommandLineArgument . ArgumentType . String , Strings . Program . WebserverCertificateFileDescription , Strings . Program . WebserverCertificateFileDescription , Duplicati . Server . WebServer . Server . OPTION_SSLCERTIFICATEFILE ),
new Duplicati . Library . Interface . CommandLineArgument ( Duplicati . Server . WebServer . Server . OPTION_SSLCERTIFICATEFILEPASSWORD , Duplicati . Library . Interface . CommandLineArgument . ArgumentType . String , Strings . Program . WebserverCertificatePasswordDescription , Strings . Program . WebserverCertificatePasswordDescription , Duplicati . Server . WebServer . Server . OPTION_SSLCERTIFICATEFILEPASSWORD ),
2014-06-27 13:23:05 +02:00
new Duplicati . Library . Interface . CommandLineArgument ( Duplicati . Server . WebServer . Server . OPTION_INTERFACE , Duplicati . Library . Interface . CommandLineArgument . ArgumentType . String , Strings . Program . WebserverInterfaceDescription , Strings . Program . WebserverInterfaceDescription , Duplicati . Server . WebServer . Server . DEFAULT_OPTION_INTERFACE ),
2014-06-30 11:31:06 +02:00
new Duplicati . Library . Interface . CommandLineArgument ( "webservice-password" , Duplicati . Library . Interface . CommandLineArgument . ArgumentType . Password , Strings . Program . WebserverPasswordDescription , Strings . Program . WebserverPasswordDescription ),
2018-08-07 00:36:12 +02:00
new Duplicati . Library . Interface . CommandLineArgument ( "webservice-allowed-hostnames" , Duplicati . Library . Interface . CommandLineArgument . ArgumentType . String , Strings . Program . WebserverAllowedhostnamesDescription , Strings . Program . WebserverAllowedhostnamesDescription ),
2015-01-20 21:11:14 +01:00
new Duplicati . Library . Interface . CommandLineArgument ( "ping-pong-keepalive" , Duplicati . Library . Interface . CommandLineArgument . ArgumentType . Boolean , Strings . Program . PingpongkeepaliveShort , Strings . Program . PingpongkeepaliveLong ),
2015-02-15 23:26:52 +01:00
new Duplicati . Library . Interface . CommandLineArgument ( "log-retention" , Duplicati . Library . Interface . CommandLineArgument . ArgumentType . Timespan , Strings . Program . LogretentionShort , Strings . Program . LogretentionLong , DEFAULT_LOG_RETENTION ),
2015-08-24 08:04:22 +01:00
new Duplicati . Library . Interface . CommandLineArgument ( "server-datafolder" , Duplicati . Library . Interface . CommandLineArgument . ArgumentType . Path , Strings . Program . ServerdatafolderShort , Strings . Program . ServerdatafolderLong ( DATAFOLDER_ENV_NAME ), System . IO . Path . Combine ( System . Environment . GetFolderPath ( Environment . SpecialFolder . ApplicationData ), Library . AutoUpdater . AutoUpdateSettings . AppName )),
2018-05-22 21:49:48 +02:00
2015-08-24 08:04:22 +01:00
});
2018-11-03 09:26:04 +01:00
if (! Platform . IsClientPosix )
2024-04-23 17:06:32 +02:00
lst . Add ( new Duplicati . Library . Interface . CommandLineArgument ( "server-encryption-key" , Duplicati . Library . Interface . CommandLineArgument . ArgumentType . Password , Strings . Program . ServerencryptionkeyShort , Strings . Program . ServerencryptionkeyLong ( DB_KEY_ENV_NAME , "unencrypted-database" ), Library . AutoUpdater . AutoUpdateSettings . AppName + "_Key_42" , null , null , "Database encryption is no longer supported, this setting can only be used to decrypt the database" ));
2015-08-24 08:04:22 +01:00
return lst . ToArray ();
2013-02-12 21:43:14 +00:00
}
}
2018-01-28 20:03:06 +01:00
private static bool ReadOptionsFromFile ( string filename , ref Library . Utility . IFilter filter , List < string > cargs , Dictionary < string , string > options )
{
try
{
2018-09-22 19:51:30 -07:00
List < string > fargs = new List < string >( Library . Utility . Utility . ReadFileWithDefaultEncoding ( Environment . ExpandEnvironmentVariables ( filename )). Replace ( "\r\n" , "\n" ). Replace ( "\r" , "\n" ). Split ( new String [] { "\n" }, StringSplitOptions . RemoveEmptyEntries ). Select ( x => x . Trim ()));
2018-01-28 20:03:06 +01:00
var newsource = new List < string >();
string newtarget = null ;
string prependfilter = null ;
string appendfilter = null ;
string replacefilter = null ;
var tmpparsed = Library . Utility . FilterCollector . ExtractOptions ( fargs , ( key , value ) => {
if ( key . Equals ( "source" , StringComparison . OrdinalIgnoreCase ))
{
newsource . Add ( value );
return false ;
}
else if ( key . Equals ( "target" , StringComparison . OrdinalIgnoreCase ))
{
newtarget = value ;
return false ;
}
else if ( key . Equals ( "append-filter" , StringComparison . OrdinalIgnoreCase ))
{
appendfilter = value ;
return false ;
}
else if ( key . Equals ( "prepend-filter" , StringComparison . OrdinalIgnoreCase ))
{
prependfilter = value ;
return false ;
}
else if ( key . Equals ( "replace-filter" , StringComparison . OrdinalIgnoreCase ))
{
replacefilter = value ;
return false ;
}
return true ;
});
var opt = tmpparsed . Item1 ;
var newfilter = tmpparsed . Item2 ;
// If the user specifies parameters-file, all filters must be in the file.
// Allowing to specify some filters on the command line could result in wrong filter ordering
if (! filter . Empty && ! newfilter . Empty )
2018-03-12 14:07:11 +01:00
throw new Duplicati . Library . Interface . UserInformationException ( Strings . Program . FiltersCannotBeUsedWithFileError2 , "FiltersCannotBeUsedOnCommandLineAndInParameterFile" );
2018-01-28 20:03:06 +01:00
if (! newfilter . Empty )
filter = newfilter ;
if (! string . IsNullOrWhiteSpace ( prependfilter ))
filter = Library . Utility . FilterExpression . Combine ( Library . Utility . FilterExpression . Deserialize ( prependfilter . Split ( new string [] { System . IO . Path . PathSeparator . ToString () }, StringSplitOptions . RemoveEmptyEntries )), filter );
if (! string . IsNullOrWhiteSpace ( appendfilter ))
filter = Library . Utility . FilterExpression . Combine ( filter , Library . Utility . FilterExpression . Deserialize ( appendfilter . Split ( new string [] { System . IO . Path . PathSeparator . ToString () }, StringSplitOptions . RemoveEmptyEntries )));
if (! string . IsNullOrWhiteSpace ( replacefilter ))
filter = Library . Utility . FilterExpression . Deserialize ( replacefilter . Split ( new string [] { System . IO . Path . PathSeparator . ToString () }, StringSplitOptions . RemoveEmptyEntries ));
foreach ( KeyValuePair < String , String > keyvalue in opt )
options [ keyvalue . Key ] = keyvalue . Value ;
if (! string . IsNullOrEmpty ( newtarget ))
{
if ( cargs . Count <= 1 )
cargs . Add ( newtarget );
else
cargs [ 1 ] = newtarget ;
}
if ( cargs . Count >= 1 && cargs [ 0 ]. Equals ( "backup" , StringComparison . OrdinalIgnoreCase ))
cargs . AddRange ( newsource );
2018-03-12 14:07:11 +01:00
else if ( newsource . Count > 0 )
Library . Logging . Log . WriteVerboseMessage ( LOGTAG , "NotUsingBackupSources" , Strings . Program . SkippingSourceArgumentsOnNonBackupOperation );
2018-01-28 20:03:06 +01:00
return true ;
}
catch ( Exception e )
{
throw new Exception ( Strings . Program . FailedToParseParametersFileError ( filename , e . Message ));
}
}
2013-02-12 21:43:14 +00:00
}
}