#region Disclaimer / License
// Copyright (C) 2011, Kenneth Skovhede
// http://www.hexad.dk, opensource@hexad.dk
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
//
#endregion
using System;
using System.Collections.Generic;
using System.Text;
using Duplicati.Library.Utility;
namespace Duplicati.Library.Main
{
public class Controller : IDisposable
{
///
/// The backend url
///
private string m_backend;
///
/// The parsed type-safe version of the commandline options
///
private Options m_options;
///
/// The destination for all output messages during execution
///
private IMessageSink m_messageSink;
///
/// A flag indicating if logging has been set, used to dispose the logging
///
private bool m_hasSetLogging = false;
///
/// The current executing task
///
private ITaskControl m_currentTask = null;
///
/// The thread running the current task
///
private System.Threading.Thread m_currentTaskThread = null;
///
/// This gets called whenever execution of an operation is started or stopped; it currently handles the AllowSleep option
///
/// Flag indicating execution state
private void OperationRunning(bool isRunning)
{
if (m_options != null && !m_options.AllowSleep && !Duplicati.Library.Utility.Utility.IsClientLinux)
try
{
Win32.SetThreadExecutionState(Win32.EXECUTION_STATE.ES_CONTINUOUS | (isRunning ? Win32.EXECUTION_STATE.ES_SYSTEM_REQUIRED : 0));
}
catch { } //TODO: Report this somehow
}
///
/// Constructs a new interface for performing backup and restore operations
///
/// The url for the backend to use
/// All required options
public Controller(string backend, Dictionary options, IMessageSink messageSink)
{
m_backend = backend;
m_options = new Options(options);
m_messageSink = messageSink;
}
public Duplicati.Library.Interface.IBackupResults Backup(string[] inputsources, IFilter filter = null)
{
return RunAction(new BackupResults(), ref inputsources, (result) => {
if (inputsources == null || inputsources.Length == 0)
throw new Exception(Strings.Controller.NoSourceFoldersError);
var sources = new List(inputsources);
//Make sure they all have the same format and exist
for(int i = 0; i < sources.Count; i++)
{
try
{
sources[i] = System.IO.Path.GetFullPath(sources[i]);
}
catch (Exception ex)
{
throw new ArgumentException(string.Format(Strings.Controller.InvalidPathError, sources[i], ex.Message), ex);
}
var fi = new System.IO.FileInfo(sources[i]);
var di = new System.IO.DirectoryInfo(sources[i]);
if (!(fi.Exists || di.Exists) && !m_options.AllowMissingSource)
throw new System.IO.IOException(String.Format(Strings.Controller.SourceIsMissingError, sources[i]));
if (!fi.Exists)
sources[i] = Library.Utility.Utility.AppendDirSeparator(sources[i]);
}
//Sanity check for duplicate folders and multiple inclusions of the same folder
for(int i = 0; i < sources.Count - 1; i++)
{
for(int j = i + 1; j < sources.Count; j++)
if (sources[i].Equals(sources[j], Library.Utility.Utility.IsFSCaseSensitive ? StringComparison.CurrentCulture : StringComparison.CurrentCultureIgnoreCase))
{
result.AddVerboseMessage("Removing duplicate source: {0}", sources[j]);
sources.RemoveAt(j);
j--;
}
else if (sources[i].StartsWith(sources[j], Library.Utility.Utility.IsFSCaseSensitive ? StringComparison.CurrentCulture : StringComparison.CurrentCultureIgnoreCase))
{
result.AddVerboseMessage("Removing source \"{0}\" because it is a subfolder of \"{1}\"", sources[i], sources[j]);
filter = Library.Utility.JoinedFilterExpression.Join(new FilterExpression(sources[i]), filter);
sources.RemoveAt(i);
i--;
break;
}
}
using(var h = new Operation.BackupHandler(m_backend, m_options, result))
h.Run(sources.ToArray(), filter);
});
}
public Library.Interface.IRestoreResults Restore(string[] paths, Library.Utility.IFilter filter = null)
{
return RunAction(new RestoreResults(), ref paths, (result) => {
new Operation.RestoreHandler(m_backend, m_options, result).Run(paths, filter);
});
}
public Duplicati.Library.Interface.IRestoreControlFilesResults RestoreControlFiles(IEnumerable files = null, Library.Utility.IFilter filter = null)
{
return RunAction(new RestoreControlFilesResults(), (result) => {
new Operation.RestoreControlFilesHandler(m_backend, m_options, result).Run(files, filter);
});
}
public Duplicati.Library.Interface.IDeleteResults Delete()
{
return RunAction(new DeleteResults(), (result) => {
new Operation.DeleteHandler(m_backend, m_options, result).Run();
});
}
public Duplicati.Library.Interface.IRepairResults Repair()
{
return RunAction(new RepairResults(), (result) => {
new Operation.RepairHandler(m_backend, m_options, result).Run();
});
}
public Duplicati.Library.Interface.IListResults List(Library.Utility.IFilter filter = null)
{
return List((IEnumerable)null, filter);
}
public Duplicati.Library.Interface.IListResults List (string filterstring, Library.Utility.IFilter filter = null)
{
return List(filterstring == null ? null : new string[] { filterstring }, null);
}
public Duplicati.Library.Interface.IListResults List(IEnumerable filterstrings, Library.Utility.IFilter filter = null)
{
return RunAction(new ListResults(), (result) => {
new Operation.ListFilesHandler(m_backend, m_options, result).Run(filterstrings, filter);
});
}
public Duplicati.Library.Interface.IListResults ListControlFiles(IEnumerable filterstrings = null, Library.Utility.IFilter filter = null)
{
return RunAction(new ListResults(), (result) => {
new Operation.ListControlFilesHandler(m_backend, m_options, result).Run(filterstrings, filter);
});
}
public Duplicati.Library.Interface.ICompactResults Compact()
{
return RunAction(new CompactResults(), (result) => {
new Operation.CompactHandler(m_backend, m_options, result).Run();
});
}
public Duplicati.Library.Interface.IRecreateDatabaseResults RecreateDatabase(string targetpath)
{
var t = new string[] { string.IsNullOrEmpty(targetpath) ? m_options.Dbpath : targetpath };
return RunAction(new RecreateDatabaseResults(), ref t, (result) => {
using(var h = new Operation.RecreateDatabaseHandler(m_backend, m_options, result))
h.Run(t[0]);
});
}
public Duplicati.Library.Interface.ICreateLogDatabaseResults CreateLogDatabase(string targetpath)
{
var t = new string[] { targetpath };
return RunAction(new CreateLogDatabaseResults(), ref t, (result) => {
result.TargetPath = t[0];
new Operation.CreateBugReportHandler(t[0], m_options, result).Run();
});
}
public Duplicati.Library.Interface.IListChangesResults ListChanges(string baseVersion, string targetVersion, IEnumerable filterstrings = null, Library.Utility.IFilter filter = null)
{
var t = new string[] { baseVersion, targetVersion };
return RunAction(new ListChangesResults(), ref t, (result) => {
new Operation.ListChangesHandler(m_backend, m_options, result).Run(t[0], t[1], filterstrings, filter);
});
}
public Duplicati.Library.Interface.ITestResults Test(long samples = 1)
{
return RunAction(new TestResults(), (result) => {
new Operation.TestHandler(m_backend, m_options, result).Run(samples);
});
}
public Library.Interface.ITestFilterResults TestFilter(string[] paths, Library.Utility.IFilter filter = null)
{
m_options.RawOptions["verbose"] = "true";
m_options.RawOptions["dry-run"] = "true";
m_options.RawOptions["dbpath"] = "INVALID!";
return RunAction(new TestFilterResults(), ref paths, (result) => {
new Operation.TestFilterHandler(m_options, result).Run(paths, filter);
});
}
private T RunAction(T result, Action method)
where T : ISetCommonOptions, ITaskControl
{
var tmp = new string[0];
return RunAction(result, ref tmp, method);
}
private T RunAction(T result, ref string[] paths, Action method)
where T : ISetCommonOptions, ITaskControl
{
try
{
m_currentTask = result;
m_currentTaskThread = System.Threading.Thread.CurrentThread;
using(new Logging.Timer(string.Format("Running {0} took", result.MainOperation)))
{
SetupCommonOptions(result, ref paths);
OperationRunning(true);
method(result);
result.EndTime = DateTime.UtcNow;
result.SetDatabase(null);
OnOperationComplete(result);
return result;
}
}
catch (Exception ex)
{
Logging.Log.WriteMessage("Terminated with error: " + ex.Message, Duplicati.Library.Logging.LogMessageType.Error, ex);
OnOperationComplete(ex);
try { (result as BasicResults).OperationProgressUpdater.UpdatePhase(OperationPhase.Error); }
catch { }
throw;
}
finally
{
m_currentTask = null;
m_currentTaskThread = null;
}
}
private void OnOperationComplete(object result)
{
if (m_options != null && m_options.LoadedModules != null)
{
foreach (KeyValuePair mx in m_options.LoadedModules)
if (mx.Key && mx.Value is Duplicati.Library.Interface.IGenericCallbackModule)
try { ((Duplicati.Library.Interface.IGenericCallbackModule)mx.Value).OnFinish(result); }
catch (Exception ex) { Logging.Log.WriteMessage(string.Format("OnFinish callback {0} failed: {1}", mx.Key, ex.Message), Duplicati.Library.Logging.LogMessageType.Warning, ex); }
foreach (KeyValuePair mx in m_options.LoadedModules)
if (mx.Key && mx.Value is IDisposable)
try { ((IDisposable)mx.Value).Dispose(); }
catch (Exception ex) { Logging.Log.WriteMessage(string.Format("Dispose for {0} failed: {1}", mx.Key, ex.Message), Duplicati.Library.Logging.LogMessageType.Warning, ex); }
m_options.LoadedModules.Clear();
OperationRunning(false);
}
if (m_hasSetLogging && Logging.Log.CurrentLog is Logging.StreamLog)
{
Logging.StreamLog sl = (Logging.StreamLog)Logging.Log.CurrentLog;
Logging.Log.CurrentLog = null;
sl.Dispose();
m_hasSetLogging = false;
}
}
private void SetupCommonOptions(ISetCommonOptions result, ref string[] paths)
{
m_options.MainAction = result.MainOperation;
result.MessageSink = m_messageSink;
switch (m_options.MainAction)
{
case OperationMode.Backup:
break;
default:
//It only makes sense to enable auto-creation if we are writing files.
if (!m_options.RawOptions.ContainsKey("disable-autocreate-folder"))
m_options.RawOptions["disable-autocreate-folder"] = "true";
break;
}
//Load all generic modules
m_options.LoadedModules.Clear();
foreach (Library.Interface.IGenericModule m in DynamicLoader.GenericLoader.Modules)
m_options.LoadedModules.Add(new KeyValuePair(Array.IndexOf(m_options.DisableModules, m.Key.ToLower()) < 0 && (m.LoadAsDefault || Array.IndexOf(m_options.EnableModules, m.Key.ToLower()) >= 0), m));
foreach (KeyValuePair mx in m_options.LoadedModules)
if (mx.Key)
{
mx.Value.Configure(m_options.RawOptions);
if (mx.Value is Library.Interface.IGenericCallbackModule)
((Library.Interface.IGenericCallbackModule)mx.Value).OnStart(result.MainOperation.ToString(), ref m_backend, ref paths);
}
OperationRunning(true);
Library.Logging.Log.LogLevel = m_options.Loglevel;
if (!string.IsNullOrEmpty(m_options.Logfile))
{
m_hasSetLogging = true;
var path = System.IO.Path.GetDirectoryName(System.IO.Path.GetFullPath(m_options.Logfile));
if (!System.IO.Directory.Exists(path))
System.IO.Directory.CreateDirectory(path);
Library.Logging.Log.CurrentLog = new Library.Logging.StreamLog(m_options.Logfile);
}
result.VerboseErrors = m_options.DebugOutput;
result.VerboseOutput = m_options.Verbose;
if (m_options.HasTempDir)
Library.Utility.TempFolder.SystemTempPath = m_options.TempDir;
if (!string.IsNullOrEmpty(m_options.ThreadPriority))
System.Threading.Thread.CurrentThread.Priority = Library.Utility.Utility.ParsePriority(m_options.ThreadPriority);
if (string.IsNullOrEmpty(m_options.Dbpath))
m_options.Dbpath = DatabaseLocator.GetDatabasePath(m_backend, m_options);
ValidateOptions(result);
Library.Logging.Log.WriteMessage(string.Format(Strings.Controller.StartingOperationMessage, m_options.MainAction), Logging.LogMessageType.Information);
}
///
/// This function will examine all options passed on the commandline, and test for unsupported or deprecated values.
/// Any errors will be logged into the statistics module.
///
/// The commandline options given
/// The backend url
/// The statistics into which warnings are written
private void ValidateOptions(ILogWriter log)
{
//No point in going through with this if we can't report
if (log == null)
return;
//Keep a list of all supplied options
Dictionary ropts = new Dictionary(m_options.RawOptions);
//Keep a list of all supported options
Dictionary supportedOptions = new Dictionary();
//There are a few internal options that are not accessible from outside, and thus not listed
foreach (string s in Options.InternalOptions)
supportedOptions[s] = null;
//Figure out what module options are supported in the current setup
List moduleOptions = new List();
Dictionary disabledModuleOptions = new Dictionary();
foreach (KeyValuePair m in m_options.LoadedModules)
if (m.Value.SupportedCommands != null)
if (m.Key)
moduleOptions.AddRange(m.Value.SupportedCommands);
else
{
foreach (Library.Interface.ICommandLineArgument c in m.Value.SupportedCommands)
{
disabledModuleOptions[c.Name] = m.Value.DisplayName + " (" + m.Value.Key + ")";
if (c.Aliases != null)
foreach (string s in c.Aliases)
disabledModuleOptions[s] = disabledModuleOptions[c.Name];
}
}
// Throw url-encoded options into the mix
//TODO: This can hide values if both commandline and url-parameters supply the same key
var ext = new Library.Utility.Uri(m_backend).QueryParameters;
foreach(var k in ext.AllKeys)
ropts[k] = ext[k];
//Now run through all supported options, and look for deprecated options
foreach (IList l in new IList[] {
m_options.SupportedCommands,
DynamicLoader.BackendLoader.GetSupportedCommands(m_backend),
m_options.NoEncryption ? null : DynamicLoader.EncryptionLoader.GetSupportedCommands(m_options.EncryptionModule),
moduleOptions,
DynamicLoader.CompressionLoader.GetSupportedCommands(m_options.CompressionModule) })
{
if (l != null)
foreach (Library.Interface.ICommandLineArgument a in l)
{
if (supportedOptions.ContainsKey(a.Name) && Array.IndexOf(Options.KnownDuplicates, a.Name.ToLower()) < 0)
log.AddWarning(string.Format(Strings.Controller.DuplicateOptionNameWarning, a.Name), null);
supportedOptions[a.Name] = a;
if (a.Aliases != null)
foreach (string s in a.Aliases)
{
if (supportedOptions.ContainsKey(s) && Array.IndexOf(Options.KnownDuplicates, s.ToLower()) < 0)
log.AddWarning(string.Format(Strings.Controller.DuplicateOptionNameWarning, s), null);
supportedOptions[s] = a;
}
if (a.Deprecated)
{
List aliases = new List();
aliases.Add(a.Name);
if (a.Aliases != null)
aliases.AddRange(a.Aliases);
foreach (string s in aliases)
if (ropts.ContainsKey(s))
{
string optname = a.Name;
if (a.Name != s)
optname += " (" + s + ")";
log.AddWarning(string.Format(Strings.Controller.DeprecatedOptionUsedWarning, optname, a.DeprecationMessage), null);
}
}
}
}
//Now look for options that were supplied but not supported
foreach (string s in ropts.Keys)
if (!supportedOptions.ContainsKey(s))
if (disabledModuleOptions.ContainsKey(s))
log.AddWarning(string.Format(Strings.Controller.UnsupportedOptionDisabledModuleWarning, s, disabledModuleOptions[s]), null);
else
log.AddWarning(string.Format(Strings.Controller.UnsupportedOptionWarning, s), null);
//Look at the value supplied for each argument and see if is valid according to its type
foreach (string s in ropts.Keys)
{
Library.Interface.ICommandLineArgument arg;
if (supportedOptions.TryGetValue(s, out arg) && arg != null)
{
string validationMessage = ValidateOptionValue(arg, s, ropts[s]);
if (validationMessage != null)
log.AddWarning(validationMessage, null);
}
}
//TODO: Based on the action, see if all options are relevant
}
///
/// Checks if the value passed to an option is actually valid.
///
/// The argument being validated
/// The name of the option to validate
/// The value to check
/// Null if no errors are found, an error message otherwise
public static string ValidateOptionValue(Library.Interface.ICommandLineArgument arg, string optionname, string value)
{
if (arg.Type == Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Enumeration)
{
bool found = false;
foreach (string v in arg.ValidValues ?? new string[0])
if (string.Equals(v, value, StringComparison.CurrentCultureIgnoreCase))
{
found = true;
break;
}
if (!found)
return string.Format(Strings.Controller.UnsupportedEnumerationValue, optionname, value, string.Join(", ", arg.ValidValues ?? new string[0]));
}
else if (arg.Type == Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Boolean)
{
if (!string.IsNullOrEmpty(value) && Library.Utility.Utility.ParseBool(value, true) != Library.Utility.Utility.ParseBool(value, false))
return string.Format(Strings.Controller.UnsupportedBooleanValue, optionname, value);
}
else if (arg.Type == Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Integer)
{
long l;
if (!long.TryParse(value, out l))
return string.Format(Strings.Controller.UnsupportedIntegerValue, optionname, value);
}
else if (arg.Type == Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Path)
{
foreach (string p in value.Split(System.IO.Path.DirectorySeparatorChar))
if (p.IndexOfAny(System.IO.Path.GetInvalidPathChars()) >= 0)
return string.Format(Strings.Controller.UnsupportedPathValue, optionname, p);
}
else if (arg.Type == Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Size)
{
try
{
Library.Utility.Sizeparser.ParseSize(value);
}
catch
{
return string.Format(Strings.Controller.UnsupportedSizeValue, optionname, value);
}
}
else if (arg.Type == Duplicati.Library.Interface.CommandLineArgument.ArgumentType.Timespan)
{
try
{
Library.Utility.Timeparser.ParseTimeSpan(value);
}
catch
{
return string.Format(Strings.Controller.UnsupportedTimeValue, optionname, value);
}
}
return null;
}
public void Pause()
{
var ct = m_currentTask;
if (ct != null)
ct.Pause();
}
public void Resume()
{
var ct = m_currentTask;
if (ct != null)
ct.Resume();
}
public void Stop()
{
var ct = m_currentTask;
if (ct != null)
ct.Stop();
}
public void Abort()
{
var ct = m_currentTask;
if (ct != null)
ct.Abort();
var t = m_currentTaskThread;
if (t != null)
t.Abort();
}
#region IDisposable Members
public void Dispose()
{
}
#endregion
}
}