Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/duplicati/duplicati.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'Duplicati/Library/Main/Controller.cs')
-rw-r--r--Duplicati/Library/Main/Controller.cs387
1 files changed, 316 insertions, 71 deletions
diff --git a/Duplicati/Library/Main/Controller.cs b/Duplicati/Library/Main/Controller.cs
index 6310103bd..8bdf91a92 100644
--- a/Duplicati/Library/Main/Controller.cs
+++ b/Duplicati/Library/Main/Controller.cs
@@ -43,9 +43,14 @@ namespace Duplicati.Library.Main
private IMessageSink m_messageSink;
/// <summary>
- /// A flag indicating if logging has been set, used to dispose the logging
+ /// The stream log, if any
/// </summary>
- private bool m_hasSetLogging = false;
+ private Logging.StreamLog m_logfile = null;
+
+ /// <summary>
+ /// The logging filescope
+ /// </summary>
+ private IDisposable m_logfilescope = null;
/// <summary>
/// The current executing task
@@ -168,49 +173,127 @@ namespace Duplicati.Library.Main
m_messageSink = messageSink;
}
+ /// <summary>
+ /// Appends another message sink to the controller
+ /// </summary>
+ /// <param name="sink">The sink to use.</param>
+ public void AppendSink(IMessageSink sink)
+ {
+ if (m_messageSink is MultiMessageSink)
+ ((MultiMessageSink)m_messageSink).Append(sink);
+ else
+ m_messageSink = new MultiMessageSink(m_messageSink, sink);
+ }
+
public Duplicati.Library.Interface.IBackupResults Backup(string[] inputsources, IFilter filter = null)
{
Library.UsageReporter.Reporter.Report("USE_BACKEND", new Library.Utility.Uri(m_backend).Scheme);
Library.UsageReporter.Reporter.Report("USE_COMPRESSION", m_options.CompressionModule);
Library.UsageReporter.Reporter.Report("USE_ENCRYPTION", m_options.EncryptionModule);
-
+
return RunAction(new BackupResults(), ref inputsources, ref filter, (result) => {
if (inputsources == null || inputsources.Length == 0)
- throw new Exception(Strings.Controller.NoSourceFoldersError);
+ throw new Duplicati.Library.Interface.UserInformationException(Strings.Controller.NoSourceFoldersError);
- var sources = new List<string>(inputsources);
+ var sources = new List<string>(inputsources.Length);
+
+ System.IO.DriveInfo[] drives = null;
//Make sure they all have the same format and exist
- for(int i = 0; i < sources.Count; i++)
+ for (int i = 0; i < inputsources.Length; i++)
{
- try
+ List<string> expandedSources = new List<string>();
+
+ if (Library.Utility.Utility.IsClientWindows && (inputsources[i].StartsWith("*:") || inputsources[i].StartsWith("?:")))
{
- sources[i] = System.IO.Path.GetFullPath(sources[i]);
+ // *: drive paths are only supported on Windows clients
+ // Lazily load the drive info
+ drives = drives ?? System.IO.DriveInfo.GetDrives();
+
+ // Replace the drive letter with each available drive
+ string sourcePath = inputsources[i].Substring(1);
+ foreach (System.IO.DriveInfo drive in drives)
+ {
+ string expandedSource = drive.Name[0] + sourcePath;
+ result.AddVerboseMessage(@"Adding source path ""{0}"" due to wildcard source path ""{1}""", expandedSource, inputsources[i]);
+ expandedSources.Add(expandedSource);
+ }
}
- catch (Exception ex)
+ else if (Library.Utility.Utility.IsClientWindows && inputsources[i].StartsWith(@"\\?\Volume{", StringComparison.OrdinalIgnoreCase))
{
- throw new ArgumentException(Strings.Controller.InvalidPathError(sources[i], ex.Message), ex);
+ // In order to specify a drive by it's volume name, adopt the volume guid path syntax:
+ // \\?\Volume{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}
+ // The volume guid can be found using the 'mountvol' commandline tool.
+ // However, instead of using this path with Windows APIs directory, it is adapted here to a standard path.
+ Guid volumeGuid;
+ if (Guid.TryParse(inputsources[i].Substring(@"\\?\Volume{".Length, @"XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX".Length), out volumeGuid))
+ {
+ string driveLetter = Library.Utility.Utility.GetDriveLetterFromVolumeGuid(volumeGuid);
+ if (!string.IsNullOrEmpty(driveLetter))
+ {
+ string expandedSource = driveLetter + inputsources[i].Substring(@"\\?\Volume{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}".Length);
+ result.AddVerboseMessage(@"Adding source path ""{0}"" in place of volume guid source path ""{1}""", expandedSource, inputsources[i]);
+ expandedSources.Add(expandedSource);
+ }
+ else
+ {
+ // If we aren't allow to have missing sources, throw an exception indicating we couldn't find a drive where this volume is mounted
+ if (!m_options.AllowMissingSource)
+ throw new Duplicati.Library.Interface.UserInformationException(Strings.Controller.SourceVolumeNameNotFoundError(inputsources[i], volumeGuid));
+ }
+ }
+ else
+ {
+ // If we aren't allow to have missing sources, throw an exception indicating we couldn't find this volume
+ if (!m_options.AllowMissingSource)
+ throw new Duplicati.Library.Interface.UserInformationException(Strings.Controller.SourceVolumeNameInvalidError(inputsources[i]));
+ }
+ }
+ else
+ {
+ expandedSources.Add(inputsources[i]);
}
- 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(Strings.Controller.SourceIsMissingError(sources[i]));
+ bool foundAnyPaths = false;
+ foreach (string expandedSource in expandedSources)
+ {
+ string source;
+ try
+ {
+ source = System.IO.Path.GetFullPath(expandedSource);
+ }
+ catch (Exception ex)
+ {
+ // Note that we use the original source (with the *) in the error
+ throw new Duplicati.Library.Interface.UserInformationException(Strings.Controller.InvalidPathError(expandedSource, ex.Message), ex);
+ }
- if (!fi.Exists)
- sources[i] = Library.Utility.Utility.AppendDirSeparator(sources[i]);
+ var fi = new System.IO.FileInfo(source);
+ var di = new System.IO.DirectoryInfo(source);
+ if (fi.Exists || di.Exists)
+ {
+ foundAnyPaths = true;
+
+ if (!fi.Exists)
+ source = Library.Utility.Utility.AppendDirSeparator(source);
+
+ sources.Add(source);
+ }
+ }
+
+ // If no paths were found, and we aren't allowed to have missing sources, throw an error
+ if (!foundAnyPaths && !m_options.AllowMissingSource)
+ throw new System.IO.IOException(Strings.Controller.SourceIsMissingError(inputsources[i]));
}
//Sanity check for duplicate files/folders
- var pathDuplicates = sources.GroupBy(x => x, Library.Utility.Utility.ClientFilenameStringComparer)
- .Where(g => g.Count() > 1).Select(y => y.Key).ToList();
+ ISet<string> pathDuplicates;
+ sources = Library.Utility.Utility.GetUniqueItems(sources, Library.Utility.Utility.ClientFilenameStringComparer, out pathDuplicates).OrderBy(a => a).ToList();
foreach (var pathDuplicate in pathDuplicates)
result.AddVerboseMessage(string.Format("Removing duplicate source: {0}", pathDuplicate));
- sources = sources.Distinct(Library.Utility.Utility.ClientFilenameStringComparer).OrderBy(a => a).ToList();
-
//Sanity check for multiple inclusions of the same folder
for (int i = 0; i < sources.Count; i++)
for (int j = 0; j < sources.Count; j++)
@@ -305,6 +388,52 @@ namespace Duplicati.Library.Main
});
}
+ public Duplicati.Library.Interface.IListRemoteResults ListRemote()
+ {
+ return RunAction(new ListRemoteResults(), (result) =>
+ {
+ using (var tf = System.IO.File.Exists(m_options.Dbpath) ? null : new Library.Utility.TempFile())
+ using (var db = new Database.LocalDatabase(((string)tf) ?? m_options.Dbpath, "list-remote", true))
+ using (var bk = new BackendManager(m_backend, m_options, result.BackendWriter, null))
+ result.SetResult(bk.List());
+ });
+ }
+
+ public Duplicati.Library.Interface.IListRemoteResults DeleteAllRemoteFiles()
+ {
+ return RunAction(new ListRemoteResults(), (result) =>
+ {
+ result.OperationProgressUpdater.UpdatePhase(OperationPhase.Delete_Listing);
+ using (var tf = System.IO.File.Exists(m_options.Dbpath) ? null : new Library.Utility.TempFile())
+ using (var db = new Database.LocalDatabase(((string)tf) ?? m_options.Dbpath, "list-remote", true))
+ using (var bk = new BackendManager(m_backend, m_options, result.BackendWriter, null))
+ {
+ // Only delete files that match the expected pattern and prefix
+ var list = bk.List()
+ .Select(x => Volumes.VolumeBase.ParseFilename(x))
+ .Where(x => x != null)
+ .Where(x => x.Prefix == m_options.Prefix)
+ .ToList();
+
+ result.OperationProgressUpdater.UpdatePhase(OperationPhase.Delete_Deleting);
+ result.OperationProgressUpdater.UpdateProgress(0);
+ for (var i = 0; i < list.Count; i++)
+ {
+ try
+ {
+ bk.Delete(list[i].File.Name, list[i].File.Size, true);
+ }
+ catch (Exception ex)
+ {
+ result.AddWarning(string.Format("Failed to delete remote file: {0}", list[i].File.Name), ex);
+ }
+ result.OperationProgressUpdater.UpdateProgress((float)i / list.Count);
+ }
+ result.OperationProgressUpdater.UpdateProgress(1);
+ }
+ });
+ }
+
public Duplicati.Library.Interface.ICompactResults Compact()
{
return RunAction(new CompactResults(), (result) => {
@@ -343,24 +472,27 @@ namespace Duplicati.Library.Main
});
}
- public Duplicati.Library.Interface.IListChangesResults ListChanges(string baseVersion, string targetVersion, IEnumerable<string> filterstrings = null, Library.Utility.IFilter filter = null)
+ public Duplicati.Library.Interface.IListChangesResults ListChanges(string baseVersion, string targetVersion, IEnumerable<string> filterstrings = null, Library.Utility.IFilter filter = null, Action<Duplicati.Library.Interface.IListChangesResults, IEnumerable<Tuple<Library.Interface.ListChangesChangeType, Library.Interface.ListChangesElementType, string>>> callback = null)
{
var t = new string[] { baseVersion, targetVersion };
return RunAction(new ListChangesResults(), ref t, ref filter, (result) => {
- new Operation.ListChangesHandler(m_backend, m_options, result).Run(t[0], t[1], filterstrings, filter);
+ new Operation.ListChangesHandler(m_backend, m_options, result).Run(t[0], t[1], filterstrings, filter, callback);
});
}
- public Duplicati.Library.Interface.IListAffectedResults ListAffected(List<string> args)
+ public Duplicati.Library.Interface.IListAffectedResults ListAffected(List<string> args, Action<Duplicati.Library.Interface.IListAffectedResults> callback = null)
{
return RunAction(new ListAffectedResults(), (result) => {
- new Operation.ListAffected(m_options, result).Run(args);
+ new Operation.ListAffected(m_options, result).Run(args, callback);
});
}
public Duplicati.Library.Interface.ITestResults Test(long samples = 1)
{
+ if (!m_options.RawOptions.ContainsKey("full-remote-verification"))
+ m_options.RawOptions["full-remote-verification"] = "true";
+
return RunAction(new TestResults(), (result) => {
new Operation.TestHandler(m_backend, m_options, result).Run(samples);
});
@@ -384,8 +516,67 @@ namespace Duplicati.Library.Main
});
}
+ public Library.Interface.IPurgeFilesResults PurgeFiles(Library.Utility.IFilter filter)
+ {
+ return RunAction(new PurgeFilesResults(), result =>
+ {
+ new Operation.PurgeFilesHandler(m_backend, m_options, result).Run(filter);
+ });
+ }
+
+ public Library.Interface.IListBrokenFilesResults ListBrokenFiles(Library.Utility.IFilter filter, Func<long, DateTime, long, string, long, bool> callbackhandler = null)
+ {
+ return RunAction(new ListBrokenFilesResults(), result =>
+ {
+ new Operation.ListBrokenFilesHandler(m_backend, m_options, result).Run(filter, callbackhandler);
+ });
+ }
+
+ public Library.Interface.IPurgeBrokenFilesResults PurgeBrokenFiles(Library.Utility.IFilter filter)
+ {
+ return RunAction(new PurgeBrokenFilesResults(), result =>
+ {
+ new Operation.PurgeBrokenFilesHandler(m_backend, m_options, result).Run(filter);
+ });
+ }
+
+ public Library.Interface.ISendMailResults SendMail()
+ {
+ m_options.RawOptions["send-mail-level"] = "all";
+ m_options.RawOptions["send-mail-any-operation"] = "true";
+ string targetmail;
+ m_options.RawOptions.TryGetValue("send-mail-to", out targetmail);
+ if (string.IsNullOrWhiteSpace(targetmail))
+ throw new Exception(string.Format("No email specified, please use --{0}", "send-mail-to"));
+
+ if (m_options.Loglevel == Logging.LogMessageType.Error)
+ m_options.RawOptions["log-level"] = Logging.LogMessageType.Warning.ToString();
+
+ m_options.RawOptions["disable-module"] = string.Join(
+ ",",
+ DynamicLoader.GenericLoader.Modules
+ .Where(m =>
+ !(m is Library.Interface.IConnectionModule) && m.GetType().FullName != "Duplicati.Library.Modules.Builtin.SendMail"
+ )
+ .Select(x => x.Key)
+ );
+
+ return RunAction(new SendMailResults(), result =>
+ {
+ result.Lines = new string[0];
+ System.Threading.Thread.Sleep(5);
+ });
+ }
+
+ public Library.Interface.IVacuumResults Vacuum()
+ {
+ return RunAction(new VacuumResult(), result => {
+ new Operation.VacuumHandler(m_options, result).Run();
+ });
+ }
+
private T RunAction<T>(T result, Action<T> method)
- where T : ISetCommonOptions, ITaskControl
+ where T : ISetCommonOptions, ITaskControl, Logging.ILog
{
var tmp = new string[0];
IFilter tempfilter = null;
@@ -393,57 +584,63 @@ namespace Duplicati.Library.Main
}
private T RunAction<T>(T result, ref string[] paths, Action<T> method)
- where T : ISetCommonOptions, ITaskControl
+ where T : ISetCommonOptions, ITaskControl, Logging.ILog
{
IFilter tempfilter = null;
return RunAction<T>(result, ref paths, ref tempfilter, method);
}
private T RunAction<T>(T result, ref IFilter filter, Action<T> method)
- where T : ISetCommonOptions, ITaskControl
+ where T : ISetCommonOptions, ITaskControl, Logging.ILog
{
var tmp = new string[0];
return RunAction<T>(result, ref tmp, ref filter, method);
}
private T RunAction<T>(T result, ref string[] paths, ref IFilter filter, Action<T> method)
- where T : ISetCommonOptions, ITaskControl
+ where T : ISetCommonOptions, ITaskControl, Logging.ILog
{
- try
+ using (Logging.Log.StartScope(result))
{
- m_currentTask = result;
- m_currentTaskThread = System.Threading.Thread.CurrentThread;
- using(new Logging.Timer(string.Format("Running {0}", result.MainOperation)))
+ try
{
+ m_currentTask = result;
+ m_currentTaskThread = System.Threading.Thread.CurrentThread;
SetupCommonOptions(result, ref paths, ref filter);
- method(result);
+ result.WriteLogMessageDirect(Strings.Controller.StartingOperationMessage(m_options.MainAction), Logging.LogMessageType.Information, null);
- result.EndTime = DateTime.UtcNow;
+ using (new Logging.Timer(string.Format("Running {0}", result.MainOperation)))
+ method(result);
+
+ if (result.EndTime.Ticks == 0)
+ result.EndTime = DateTime.UtcNow;
result.SetDatabase(null);
OnOperationComplete(result);
- Library.Logging.Log.WriteMessage(Strings.Controller.CompletedOperationMessage(m_options.MainAction), Logging.LogMessageType.Information);
+ result.WriteLogMessageDirect(Strings.Controller.CompletedOperationMessage(m_options.MainAction), Logging.LogMessageType.Information, null);
return result;
}
- }
- catch (Exception ex)
- {
- OnOperationComplete(ex);
+ catch (Exception ex)
+ {
+ result.EndTime = DateTime.UtcNow;
- try { (result as BasicResults).OperationProgressUpdater.UpdatePhase(OperationPhase.Error); }
- catch { }
+ try { (result as BasicResults).OperationProgressUpdater.UpdatePhase(OperationPhase.Error); }
+ catch { }
- Library.Logging.Log.WriteMessage(Strings.Controller.FailedOperationMessage(m_options.MainAction, ex.Message), Logging.LogMessageType.Error, ex);
+ OnOperationComplete(ex);
- throw;
- }
- finally
- {
- m_currentTask = null;
- m_currentTaskThread = null;
+ result.WriteLogMessageDirect(Strings.Controller.FailedOperationMessage(m_options.MainAction, ex.Message), Logging.LogMessageType.Error, ex);
+
+ throw;
+ }
+ finally
+ {
+ m_currentTask = null;
+ m_currentTaskThread = null;
+ }
}
}
@@ -516,13 +713,18 @@ namespace Duplicati.Library.Main
}
}
- if (m_hasSetLogging && Logging.Log.CurrentLog is Logging.StreamLog)
+ if (m_logfilescope != null)
+ {
+ m_logfilescope.Dispose();
+ m_logfilescope = null;
+ }
+
+ if (m_logfile != null)
{
- Logging.StreamLog sl = (Logging.StreamLog)Logging.Log.CurrentLog;
- Logging.Log.CurrentLog = null;
- sl.Dispose();
- m_hasSetLogging = false;
+ m_logfile.Dispose();
+ m_logfile = null;
}
+
}
private void SetupCommonOptions(ISetCommonOptions result, ref string[] paths, ref IFilter filter)
@@ -548,15 +750,17 @@ namespace Duplicati.Library.Main
foreach (Library.Interface.IGenericModule m in DynamicLoader.GenericLoader.Modules)
m_options.LoadedModules.Add(new KeyValuePair<bool, Library.Interface.IGenericModule>(Array.IndexOf<string>(m_options.DisableModules, m.Key.ToLower()) < 0 && (m.LoadAsDefault || Array.IndexOf<string>(m_options.EnableModules, m.Key.ToLower()) >= 0), m));
+ // Make the filter read-n-write able in the generic modules
+ var pristinefilter = string.Join(System.IO.Path.PathSeparator.ToString(), FilterExpression.Serialize(filter));
+ m_options.RawOptions["filter"] = pristinefilter;
+
+ // Store the URL connection options separately, as these should only be visible to modules implementing IConnectionModule
var conopts = new Dictionary<string, string>(m_options.RawOptions);
var qp = new Library.Utility.Uri(m_backend).QueryParameters;
- foreach(var k in qp.Keys)
+ foreach (var k in qp.Keys)
conopts[(string)k] = qp[(string)k];
- // Make the filter read-n-write able in the generic modules
- var pristinefilter = conopts["filter"] = string.Join(System.IO.Path.PathSeparator.ToString(), FilterExpression.Serialize(filter));
-
- foreach (KeyValuePair<bool, Library.Interface.IGenericModule> mx in m_options.LoadedModules)
+ foreach (var mx in m_options.LoadedModules)
if (mx.Key)
{
if (mx.Value is Library.Interface.IConnectionModule)
@@ -564,13 +768,29 @@ namespace Duplicati.Library.Main
else
mx.Value.Configure(m_options.RawOptions);
+ if (mx.Value is Library.Interface.IGenericSourceModule)
+ {
+ var sourcemodule = (Library.Interface.IGenericSourceModule)mx.Value;
+
+ if (sourcemodule.ContainFilesForBackup(paths))
+ {
+ var sourceoptions = sourcemodule.ParseSourcePaths(ref paths, ref pristinefilter, m_options.RawOptions);
+
+ foreach (var sourceoption in sourceoptions)
+ m_options.RawOptions[sourceoption.Key] = sourceoption.Value;
+ }
+ }
+
if (mx.Value is Library.Interface.IGenericCallbackModule)
((Library.Interface.IGenericCallbackModule)mx.Value).OnStart(result.MainOperation.ToString(), ref m_backend, ref paths);
}
- // If the filters were changed, read them back in
- if (pristinefilter != conopts["filter"])
- filter = FilterExpression.Deserialize(conopts["filter"].Split(new string[] {System.IO.Path.PathSeparator.ToString()}, StringSplitOptions.RemoveEmptyEntries));
+ // If the filters were changed by a module, read them back in
+ if (pristinefilter != m_options.RawOptions["filter"])
+ {
+ filter = FilterExpression.Deserialize(m_options.RawOptions["filter"].Split(new string[] { System.IO.Path.PathSeparator.ToString() }, StringSplitOptions.RemoveEmptyEntries));
+ }
+ m_options.RawOptions.Remove("filter"); // "--filter" is not a supported command line option
OperationRunning(true);
@@ -579,11 +799,11 @@ namespace Duplicati.Library.Main
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);
+
+ m_logfilescope = Logging.Log.StartScope(m_logfile = new Library.Logging.StreamLog(m_options.Logfile));
}
result.VerboseErrors = m_options.DebugOutput;
@@ -634,21 +854,36 @@ namespace Duplicati.Library.Main
m_options.Dbpath = DatabaseLocator.GetDatabasePath(m_backend, m_options);
ValidateOptions(result);
-
- Library.Logging.Log.WriteMessage(Strings.Controller.StartingOperationMessage(m_options.MainAction), Logging.LogMessageType.Information);
}
/// <summary>
/// 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.
/// </summary>
- /// <param name="options">The commandline options given</param>
- /// <param name="backend">The backend url</param>
- /// <param name="stats">The statistics into which warnings are written</param>
+ /// <param name="log">The log instance</param>
private void ValidateOptions(ILogWriter log)
{
if (m_options.KeepTime.Ticks > 0 && m_options.KeepVersions > 0)
- throw new Exception(string.Format("Setting both --{0} and --{1} is not permitted", "keep-versions", "keep-time"));
+ throw new Interface.UserInformationException(string.Format("Setting both --{0} and --{1} is not permitted", "keep-versions", "keep-time"));
+
+ if (!string.IsNullOrWhiteSpace(m_options.Prefix) && m_options.Prefix.Contains("-"))
+ throw new Interface.UserInformationException("The prefix cannot contain hyphens (-)");
+
+ //Check validity of retention-policy option value
+ try
+ {
+ foreach (var configEntry in m_options.RetentionPolicy)
+ {
+ if (configEntry.Value >= configEntry.Key)
+ {
+ throw new Interface.UserInformationException("A time frame cannot be smaller than its interval");
+ }
+ }
+ }
+ catch (Exception e) // simply reading the option value might also result in an exception due to incorrect formatting
+ {
+ throw new Interface.UserInformationException(string.Format("An error occoured while processing the value of --{0}", "retention-policy"), e);
+ }
//No point in going through with this if we can't report
if (log == null)
@@ -673,7 +908,6 @@ namespace Duplicati.Library.Main
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 + ")";
@@ -682,7 +916,6 @@ namespace Duplicati.Library.Main
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
@@ -879,6 +1112,18 @@ namespace Duplicati.Library.Main
t.Abort();
}
+ public long MaxUploadSpeed
+ {
+ get { return m_options.MaxUploadPrSecond; }
+ set { m_options.MaxUploadPrSecond = value; }
+ }
+
+ public long MaxDownloadSpeed
+ {
+ get { return m_options.MaxDownloadPrSecond; }
+ set { m_options.MaxDownloadPrSecond = value; }
+ }
+
#region IDisposable Members
public void Dispose()