This fixes the restore issues as all sources will now appear as if they were originally part of the source file system. The backend implementation has now been simplified so the backends only return names of entries with no path information. Most backends already do this, but the S3 backends did not, so this was fixed as well. This also clears up the interfaces, so the backend does not know about the `ISourceProviderEntry`. All mapping is done by the `BackendSourceProvider`, making it simpler to support other backends as sources.
1166 lines
53 KiB
C#
1166 lines
53 KiB
C#
// Copyright (C) 2025, 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.
|
|
|
|
using System;
|
|
using System.Linq;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Text.RegularExpressions;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Duplicati.Library.Common.IO;
|
|
using Duplicati.Library.Utility;
|
|
|
|
namespace Duplicati.CommandLine
|
|
{
|
|
public static class Commands
|
|
{
|
|
private class PeriodicOutput : IDisposable
|
|
{
|
|
public event Action<float, long, long, bool> WriteOutput;
|
|
|
|
private readonly System.Threading.ManualResetEvent m_readyEvent;
|
|
private readonly System.Threading.ManualResetEvent m_finishEvent;
|
|
private readonly ConsoleOutput m_output;
|
|
private readonly TimeSpan m_updateFrequency;
|
|
private Task m_task;
|
|
private readonly CancellationTokenSource m_cancellationTokenSource;
|
|
|
|
public PeriodicOutput(ConsoleOutput messageSink, TimeSpan updateFrequency)
|
|
{
|
|
m_output = messageSink;
|
|
m_readyEvent = new System.Threading.ManualResetEvent(false);
|
|
m_finishEvent = new System.Threading.ManualResetEvent(false);
|
|
m_updateFrequency = updateFrequency;
|
|
m_cancellationTokenSource = new CancellationTokenSource();
|
|
m_task = Task.Run(() => this.ThreadMain(m_cancellationTokenSource.Token), m_cancellationTokenSource.Token);
|
|
}
|
|
|
|
public void SetReady() { m_readyEvent.Set(); }
|
|
public void SetFinished() { m_finishEvent.Set(); }
|
|
|
|
public bool Join(TimeSpan wait)
|
|
{
|
|
if (m_task != null)
|
|
return this.m_task.Wait(wait);
|
|
|
|
return true;
|
|
}
|
|
|
|
private void ThreadMain(CancellationToken cancellationToken)
|
|
{
|
|
m_readyEvent.WaitOne();
|
|
if (m_finishEvent.WaitOne(TimeSpan.FromMilliseconds(10), true))
|
|
return;
|
|
|
|
var last_count = -1L;
|
|
var finished = false;
|
|
|
|
while (!cancellationToken.IsCancellationRequested)
|
|
{
|
|
float progress;
|
|
long filesprocessed;
|
|
long filesizeprocessed;
|
|
long filecount;
|
|
long filesize;
|
|
bool counting;
|
|
m_output.OperationProgress.UpdateOverall(out _, out progress, out filesprocessed, out filesizeprocessed, out filecount, out filesize, out counting);
|
|
|
|
var files = Math.Max(0, filecount - filesprocessed);
|
|
var size = Math.Max(0, filesize - filesizeprocessed);
|
|
|
|
if (finished)
|
|
{
|
|
files = 0;
|
|
size = 0;
|
|
}
|
|
else if (size > 0)
|
|
files = Math.Max(1, files);
|
|
|
|
if (last_count < 0 || files != last_count)
|
|
if (WriteOutput != null)
|
|
WriteOutput(progress, files, size, counting);
|
|
|
|
if (!counting)
|
|
last_count = files;
|
|
|
|
if (finished)
|
|
return;
|
|
|
|
finished = m_finishEvent.WaitOne(m_updateFrequency, true);
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (m_task != null)
|
|
{
|
|
try
|
|
{
|
|
m_finishEvent.Set();
|
|
m_readyEvent.Set();
|
|
|
|
if (m_task != null)
|
|
{
|
|
m_cancellationTokenSource.Cancel();
|
|
m_task.Wait(500);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
m_task = null;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public static int Examples(TextWriter outwriter, Action<Duplicati.Library.Main.Controller> setup, List<string> args, Dictionary<string, string> options, Library.Utility.IFilter filter)
|
|
{
|
|
using (new Duplicati.Library.Main.LocaleChange(options))
|
|
Duplicati.CommandLine.Help.PrintUsage(outwriter, "example", options);
|
|
return 0;
|
|
}
|
|
|
|
public static int Help(TextWriter outwriter, Action<Duplicati.Library.Main.Controller> setup, List<string> args, Dictionary<string, string> options, Library.Utility.IFilter filter)
|
|
{
|
|
using (new Duplicati.Library.Main.LocaleChange(options))
|
|
Duplicati.CommandLine.Help.PrintUsage(outwriter, args.Count > 1 ? args[1] : "help", options);
|
|
return 0;
|
|
}
|
|
|
|
public static int Affected(TextWriter outwriter, Action<Duplicati.Library.Main.Controller> setup, List<string> args, Dictionary<string, string> options, Library.Utility.IFilter filter)
|
|
{
|
|
var fullresult = Duplicati.Library.Utility.Utility.ParseBoolOption(options, "full-result");
|
|
var backend = args[0];
|
|
args.RemoveAt(0);
|
|
|
|
if (args.Count == 0)
|
|
{
|
|
outwriter.WriteLine("You must specify at least a remote filename");
|
|
return 200;
|
|
}
|
|
|
|
// Support for not adding the --auth-username if possible
|
|
string dbpath;
|
|
options.TryGetValue("dbpath", out dbpath);
|
|
if (string.IsNullOrEmpty(dbpath))
|
|
{
|
|
dbpath = Library.Main.CLIDatabaseLocator.GetDatabasePathForCLI(backend, new Library.Main.Options(options), false, true);
|
|
if (dbpath != null)
|
|
options["dbpath"] = dbpath;
|
|
}
|
|
|
|
// Don't ask for passphrase if we have a local db
|
|
if (!string.IsNullOrEmpty(dbpath) && System.IO.File.Exists(dbpath) && !options.ContainsKey("no-encryption") && !Duplicati.Library.Utility.Utility.ParseBoolOption(options, "no-local-db"))
|
|
{
|
|
string passphrase;
|
|
options.TryGetValue("passphrase", out passphrase);
|
|
if (string.IsNullOrEmpty(passphrase))
|
|
options["no-encryption"] = "true";
|
|
}
|
|
|
|
using (var console = new ConsoleOutput(outwriter, options))
|
|
using (var i = new Library.Main.Controller(backend, options, console))
|
|
{
|
|
setup(i);
|
|
i.ListAffected(args, res =>
|
|
{
|
|
if (res.Filesets != null && res.Filesets.Any())
|
|
{
|
|
outwriter.WriteLine("The following filesets are affected:");
|
|
foreach (var e in res.Filesets)
|
|
outwriter.WriteLine("{0}\t: {1}", e.Version, e.Time);
|
|
outwriter.WriteLine();
|
|
}
|
|
|
|
if (res.Files != null)
|
|
{
|
|
var filecount = res.Files.Count();
|
|
if (filecount == 0)
|
|
{
|
|
outwriter.WriteLine("No files are affected");
|
|
}
|
|
else
|
|
{
|
|
var c = 0;
|
|
outwriter.WriteLine("A total of {0} file(s) are affected:", filecount);
|
|
foreach (var file in res.Files)
|
|
{
|
|
c++;
|
|
outwriter.WriteLine(file.Path);
|
|
if (c > 10 && filecount > 10 && !fullresult)
|
|
{
|
|
outwriter.WriteLine(" ... and {0} more (use --{1} to see all filenames)", filecount - c, "full-result");
|
|
break;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
outwriter.WriteLine();
|
|
}
|
|
|
|
if (res.LogMessages != null)
|
|
{
|
|
var logcount = res.LogMessages.Count();
|
|
if (logcount == 0 || (logcount > 10 && !fullresult))
|
|
outwriter.WriteLine("Found {0} related log messages (use --{1} to see the data)", res.Files.Count(), "full-result");
|
|
else
|
|
{
|
|
outwriter.WriteLine("The following related log messages were found:");
|
|
foreach (var log in res.LogMessages)
|
|
if (log.Message.Length > 100 && !fullresult)
|
|
outwriter.WriteLine("{0}: {1}", log.Timestamp, log.Message.Substring(0, 96) + " ...");
|
|
else
|
|
outwriter.WriteLine("{0}: {1}", log.Timestamp, log.Message);
|
|
}
|
|
|
|
outwriter.WriteLine();
|
|
}
|
|
});
|
|
|
|
|
|
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// For bare file names with no wildcards, replace argument with
|
|
/// the equivalent of prefixing with "*/" so we search all folders.
|
|
/// </summary>
|
|
public static IEnumerable<string> PrefixArgsWithAsterisk(IEnumerable<string> argList) => argList.Select(PrefixArgWithAsterisk);
|
|
|
|
/// <summary>
|
|
/// For bare file names with no wildcards, return argument with
|
|
/// the equivalent of prefixing with "*/" so we search all folders.
|
|
/// </summary>
|
|
private static string PrefixArgWithAsterisk(string arg)
|
|
{
|
|
var containsSeparators = ContainsDirectorySeparators(arg);
|
|
var containsWildcards = ContainsWildcards(arg);
|
|
|
|
if (!containsSeparators && arg.StartsWith("@", StringComparison.Ordinal))
|
|
{
|
|
// Convert to Regexp filter and prefix with ".*/"
|
|
return $"[.*{Library.Utility.Utility.ConvertLiteralToRegExp(Util.DirectorySeparatorString + arg.Substring(1))}]";
|
|
}
|
|
else if (!containsSeparators && !containsWildcards && !arg.StartsWith("[", StringComparison.Ordinal))
|
|
{
|
|
// Prefix files with "*/"
|
|
return "*" + Util.DirectorySeparatorString + arg;
|
|
}
|
|
else
|
|
{
|
|
return arg;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// For folders, replace argument with the equivalent of suffixing
|
|
/// with "*" so we restore contents in the folder.
|
|
/// </summary>
|
|
public static IEnumerable<string> SuffixArgsWithAsterisk(IEnumerable<string> argList) => argList.Select(SuffixArgWithAsterisk);
|
|
|
|
/// <summary>
|
|
/// For folders, return argument with the equivalent of suffixing
|
|
/// with "*" so we restore contents in the folder.
|
|
/// </summary>
|
|
private static string SuffixArgWithAsterisk(string arg)
|
|
{
|
|
var containsWildcards = ContainsWildcards(arg);
|
|
var endsWithSeparator = arg.EndsWith(Util.DirectorySeparatorString, StringComparison.Ordinal);
|
|
|
|
if (endsWithSeparator && arg.StartsWith("@", StringComparison.Ordinal))
|
|
{
|
|
// Convert to Regexp filter and suffix with ".*"
|
|
return $"[{Library.Utility.Utility.ConvertLiteralToRegExp(arg.Substring(1))}.*]";
|
|
}
|
|
else if (endsWithSeparator && !containsWildcards && !arg.StartsWith("[", StringComparison.Ordinal))
|
|
{
|
|
// Suffix with "*"
|
|
return arg + "*";
|
|
}
|
|
else
|
|
{
|
|
return arg;
|
|
}
|
|
}
|
|
|
|
private static readonly char[] pathSeparatorsCharacters = new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar };
|
|
|
|
/// <summary>
|
|
/// Returns true if <paramref name="s"/> contains directory separator characters.
|
|
/// </summary>
|
|
public static bool ContainsDirectorySeparators(string s) => s.IndexOfAny(pathSeparatorsCharacters) >= 0;
|
|
|
|
private static readonly char[] wildcardCharacters = new[] { '*', '?' };
|
|
|
|
/// <summary>
|
|
/// Returns true if <paramref name="s"/> contains wildcard characters.
|
|
/// </summary>
|
|
public static bool ContainsWildcards(string s) => s.IndexOfAny(wildcardCharacters) >= 0;
|
|
|
|
public static int List(TextWriter outwriter, Action<Duplicati.Library.Main.Controller> setup, List<string> args, Dictionary<string, string> options, Library.Utility.IFilter filter)
|
|
{
|
|
filter = filter ?? new Duplicati.Library.Utility.FilterExpression();
|
|
if (Duplicati.Library.Utility.Utility.ParseBoolOption(options, "list-sets-only"))
|
|
filter = new Duplicati.Library.Utility.FilterExpression();
|
|
|
|
using (var console = new ConsoleOutput(outwriter, options))
|
|
using (var i = new Library.Main.Controller(args[0], options, console))
|
|
{
|
|
setup(i);
|
|
var backend = args[0];
|
|
args.RemoveAt(0);
|
|
|
|
if (args.Count == 1)
|
|
{
|
|
long v;
|
|
if (long.TryParse(args[0], out v))
|
|
{
|
|
if (!options.ContainsKey("version"))
|
|
{
|
|
args.RemoveAt(0);
|
|
args.Add("*");
|
|
options["version"] = v.ToString();
|
|
}
|
|
}
|
|
else if (!ContainsWildcards(args[0]) && !args[0].StartsWith("[", StringComparison.Ordinal) && !args[0].StartsWith("@", StringComparison.Ordinal))
|
|
{
|
|
try
|
|
{
|
|
var t = Library.Utility.Timeparser.ParseTimeInterval(args[0], DateTime.Now, true);
|
|
args.RemoveAt(0);
|
|
args.Add("*");
|
|
options["time"] = t.ToString();
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
}
|
|
}
|
|
|
|
args = PrefixArgsWithAsterisk(args).ToList();
|
|
|
|
// Support for not adding the --auth-username if possible
|
|
string dbpath;
|
|
options.TryGetValue("dbpath", out dbpath);
|
|
if (string.IsNullOrEmpty(dbpath))
|
|
{
|
|
dbpath = Library.Main.CLIDatabaseLocator.GetDatabasePathForCLI(backend, new Duplicati.Library.Main.Options(options), false, true);
|
|
if (dbpath != null)
|
|
options["dbpath"] = dbpath;
|
|
}
|
|
|
|
// Don't ask for passphrase if we have a local db
|
|
if (!string.IsNullOrEmpty(dbpath) && System.IO.File.Exists(dbpath) && !options.ContainsKey("no-encryption") && !Duplicati.Library.Utility.Utility.ParseBoolOption(options, "no-local-db"))
|
|
{
|
|
string passphrase;
|
|
options.TryGetValue("passphrase", out passphrase);
|
|
if (string.IsNullOrEmpty(passphrase))
|
|
{
|
|
string existing;
|
|
options.TryGetValue("disable-module", out existing);
|
|
if (string.IsNullOrWhiteSpace(existing))
|
|
options["disable-module"] = "console-password-input";
|
|
else
|
|
options["disable-module"] = string.Join(",", new string[] { existing, "console-password-input" });
|
|
}
|
|
}
|
|
|
|
|
|
bool controlFiles = Library.Utility.Utility.ParseBoolOption(options, "control-files");
|
|
options.Remove("control-files");
|
|
|
|
var res = controlFiles ? i.ListControlFiles(args, filter) : i.List(args, filter);
|
|
|
|
//If there are no files matching, and we are looking for one or more files,
|
|
// try again with all-versions set
|
|
var compareFilter = Library.Utility.JoinedFilterExpression.Join(new Library.Utility.FilterExpression(args), filter);
|
|
var isRequestForFiles =
|
|
!controlFiles && res.Filesets.Any() &&
|
|
(res.Files == null || !res.Files.Any()) &&
|
|
!compareFilter.Empty;
|
|
|
|
if (isRequestForFiles && !Library.Utility.Utility.ParseBoolOption(options, "all-versions"))
|
|
{
|
|
outwriter.WriteLine("No files matching, looking in all versions");
|
|
options["all-versions"] = "true";
|
|
options.Remove("time");
|
|
options.Remove("version");
|
|
res = i.List(args, filter);
|
|
}
|
|
|
|
if (res.Filesets.Any() && (res.Files == null || !res.Files.Any()) && compareFilter.Empty)
|
|
{
|
|
outwriter.WriteLine("Listing filesets:");
|
|
|
|
foreach (var e in res.Filesets)
|
|
{
|
|
if (e.FileCount >= 0)
|
|
outwriter.WriteLine("{0}\t: {1} ({2} files, {3})", e.Version, e.Time, e.FileCount, Library.Utility.Utility.FormatSizeString(e.FileSizes));
|
|
else
|
|
outwriter.WriteLine("{0}\t: {1}", e.Version, e.Time);
|
|
}
|
|
}
|
|
else if (isRequestForFiles)
|
|
{
|
|
outwriter.WriteLine("No files matched expression");
|
|
}
|
|
else
|
|
{
|
|
if (!res.Filesets.Any())
|
|
{
|
|
outwriter.WriteLine("No time or version matched a fileset");
|
|
}
|
|
else if (res.Files == null || !res.Files.Any())
|
|
{
|
|
outwriter.WriteLine("Found {0} filesets, but no files matched", res.Filesets.Count());
|
|
}
|
|
else if (res.Filesets.Count() == 1)
|
|
{
|
|
var f = res.Filesets.First();
|
|
outwriter.WriteLine("Listing contents {0} ({1}):", f.Version, f.Time);
|
|
foreach (var e in res.Files)
|
|
outwriter.WriteLine("{0} {1}", e.Path, e.Path.EndsWith(Util.DirectorySeparatorString, StringComparison.Ordinal) ? "" : "(" + Library.Utility.Utility.FormatSizeString(e.Sizes.First()) + ")");
|
|
}
|
|
else
|
|
{
|
|
outwriter.WriteLine("Listing files and versions:");
|
|
foreach (var e in res.Files)
|
|
{
|
|
outwriter.WriteLine(e.Path);
|
|
foreach (var nx in res.Filesets.Zip(e.Sizes, (a, b) => new { Index = a.Version, Time = a.Time, Size = b }))
|
|
outwriter.WriteLine("{0}\t: {1} {2}", nx.Index, nx.Time, nx.Size < 0 ? " - " : Library.Utility.Utility.FormatSizeString(nx.Size));
|
|
|
|
outwriter.WriteLine();
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
public static int Delete(TextWriter outwriter, Action<Duplicati.Library.Main.Controller> setup, List<string> args, Dictionary<string, string> options, Library.Utility.IFilter filter)
|
|
{
|
|
var requiredOptions = new string[] { "keep-time", "keep-versions", "version" };
|
|
|
|
if (!options.Keys.Any(x => requiredOptions.Contains(x, StringComparer.OrdinalIgnoreCase)))
|
|
{
|
|
outwriter.WriteLine(Strings.Program.DeleteCommandNeedsOptions("delete", requiredOptions));
|
|
return 200;
|
|
}
|
|
|
|
using (var console = new ConsoleOutput(outwriter, options))
|
|
using (var i = new Library.Main.Controller(args[0], options, console))
|
|
{
|
|
setup(i);
|
|
args.RemoveAt(0);
|
|
var res = i.Delete();
|
|
|
|
if (!res.DeletedSets.Any())
|
|
{
|
|
outwriter.WriteLine(Strings.Program.NoFilesetsMatching);
|
|
}
|
|
else
|
|
{
|
|
if (res.Dryrun)
|
|
outwriter.WriteLine(Strings.Program.WouldDeleteBackups);
|
|
else
|
|
outwriter.WriteLine(Strings.Program.DeletedBackups);
|
|
|
|
foreach (var f in res.DeletedSets)
|
|
outwriter.WriteLine("{0}: {1}", f.Item1, f.Item2);
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
public static int Repair(TextWriter outwriter, Action<Duplicati.Library.Main.Controller> setup, List<string> args, Dictionary<string, string> options, Library.Utility.IFilter filter)
|
|
{
|
|
if (args.Count != 1)
|
|
return PrintWrongNumberOfArguments(outwriter, args, 1);
|
|
|
|
using (var console = new ConsoleOutput(outwriter, options))
|
|
using (var i = new Duplicati.Library.Main.Controller(args[0], options, console))
|
|
{
|
|
setup(i);
|
|
i.Repair(filter);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
public static int Restore(TextWriter outwriter, Action<Duplicati.Library.Main.Controller> setup, List<string> args, Dictionary<string, string> options, Library.Utility.IFilter filter)
|
|
{
|
|
if (args.Count < 1)
|
|
return PrintWrongNumberOfArguments(outwriter, args, 1);
|
|
|
|
string backend = args[0];
|
|
args.RemoveAt(0);
|
|
|
|
bool controlFiles = Library.Utility.Utility.ParseBoolOption(options, "control-files");
|
|
options.Remove("control-files");
|
|
|
|
args = SuffixArgsWithAsterisk(PrefixArgsWithAsterisk(args)).ToList();
|
|
|
|
using (var output = new ConsoleOutput(outwriter, options))
|
|
using (var i = new Library.Main.Controller(backend, options, output))
|
|
{
|
|
output.MessageEvent(string.Format("Restore started at {0}", DateTime.Now));
|
|
setup(i);
|
|
if (controlFiles)
|
|
{
|
|
var res = i.RestoreControlFiles(args.ToArray(), filter);
|
|
output.MessageEvent("Restore control files completed:");
|
|
foreach (var s in res.Files)
|
|
outwriter.WriteLine(s);
|
|
}
|
|
else
|
|
{
|
|
using (var periodicOutput = new PeriodicOutput(output, TimeSpan.FromSeconds(5)))
|
|
{
|
|
output.PhaseChanged += (phase, previousPhase) =>
|
|
{
|
|
switch (phase)
|
|
{
|
|
case Duplicati.Library.Main.OperationPhase.Restore_PreRestoreVerify:
|
|
output.MessageEvent("Checking remote backup ...");
|
|
break;
|
|
case Duplicati.Library.Main.OperationPhase.Restore_ScanForExistingFiles:
|
|
output.MessageEvent("Checking existing target files ...");
|
|
break;
|
|
case Duplicati.Library.Main.OperationPhase.Restore_PatchWithLocalBlocks:
|
|
output.MessageEvent("Updating target files with local data ...");
|
|
break;
|
|
case Duplicati.Library.Main.OperationPhase.Restore_PostRestoreVerify:
|
|
periodicOutput.SetFinished();
|
|
periodicOutput.Join(TimeSpan.FromMilliseconds(100));
|
|
output.MessageEvent("Verifying restored files ...");
|
|
break;
|
|
case Duplicati.Library.Main.OperationPhase.Restore_ScanForLocalBlocks:
|
|
output.MessageEvent("Scanning local files for needed data ...");
|
|
break;
|
|
case Duplicati.Library.Main.OperationPhase.Restore_CreateTargetFolders:
|
|
periodicOutput.SetReady();
|
|
break;
|
|
}
|
|
};
|
|
|
|
periodicOutput.WriteOutput += (progress, files, size, counting) =>
|
|
{
|
|
output.MessageEvent(string.Format(" {0} files need to be restored ({1})", files, Library.Utility.Utility.FormatSizeString(size)));
|
|
};
|
|
|
|
var res = i.Restore(args.ToArray(), filter);
|
|
string restorePath;
|
|
options.TryGetValue("restore-path", out restorePath);
|
|
|
|
output.MessageEvent(string.Format("Restored {0} ({1}) files to {2}", res.RestoredFiles, Library.Utility.Utility.FormatSizeString(res.SizeOfRestoredFiles), string.IsNullOrEmpty(restorePath) ? "original path" : restorePath));
|
|
output.MessageEvent(string.Format("Duration of restore: {0:hh\\:mm\\:ss}", res.Duration));
|
|
|
|
if (output.FullResults)
|
|
Library.Utility.Utility.PrintSerializeObject(res, outwriter);
|
|
|
|
if (res.Warnings.Any())
|
|
return 2;
|
|
}
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
public static int Backup(TextWriter outwriter, Action<Duplicati.Library.Main.Controller> setup, List<string> args, Dictionary<string, string> options, Library.Utility.IFilter filter)
|
|
{
|
|
if (args.Count < 2)
|
|
return PrintWrongNumberOfArguments(outwriter, args, 2);
|
|
|
|
var backend = args[0];
|
|
args.RemoveAt(0);
|
|
var dirs = args.ToArray();
|
|
Library.Interface.IBackupResults result;
|
|
using (var output = new ConsoleOutput(outwriter, options))
|
|
{
|
|
using (var periodicOutput = new PeriodicOutput(output, TimeSpan.FromSeconds(5)))
|
|
{
|
|
if ((new Duplicati.Library.Main.Options(options)).DisableOnBattery && (Duplicati.Library.Utility.Power.PowerSupply.GetSource() == Duplicati.Library.Utility.Power.PowerSupply.Source.Battery))
|
|
{
|
|
output.MessageEvent("The \"disable-on-battery\" option only affects scheduled backups and is ignored by backups run manually or from the command line.");
|
|
}
|
|
|
|
output.MessageEvent(string.Format("Backup started at {0}", DateTime.Now));
|
|
|
|
output.PhaseChanged += (phase, previousPhase) =>
|
|
{
|
|
if (previousPhase == Duplicati.Library.Main.OperationPhase.Backup_PostBackupTest)
|
|
output.MessageEvent("Remote backup verification completed");
|
|
|
|
switch (phase)
|
|
{
|
|
case Duplicati.Library.Main.OperationPhase.Backup_ProcessingFiles:
|
|
output.MessageEvent("Scanning source files ...");
|
|
periodicOutput.SetReady();
|
|
break;
|
|
case Duplicati.Library.Main.OperationPhase.Backup_Finalize:
|
|
periodicOutput.SetFinished();
|
|
break;
|
|
case Duplicati.Library.Main.OperationPhase.Backup_PreBackupVerify:
|
|
output.MessageEvent("Checking remote backup ...");
|
|
break;
|
|
case Duplicati.Library.Main.OperationPhase.Backup_PostBackupVerify:
|
|
output.MessageEvent("Checking remote backup ...");
|
|
break;
|
|
case Duplicati.Library.Main.OperationPhase.Backup_PostBackupTest:
|
|
output.MessageEvent("Verifying remote backup ...");
|
|
break;
|
|
case Duplicati.Library.Main.OperationPhase.Backup_Compact:
|
|
output.MessageEvent("Compacting remote backup ...");
|
|
break;
|
|
}
|
|
};
|
|
|
|
periodicOutput.WriteOutput += (progress, files, size, counting) =>
|
|
{
|
|
output.MessageEvent(string.Format(" {0} files need to be examined ({1}){2}", files, Library.Utility.Utility.FormatSizeString(size), counting ? " (still counting)" : ""));
|
|
};
|
|
|
|
using (var i = new Library.Main.Controller(backend, options, output))
|
|
{
|
|
setup(i);
|
|
result = i.Backup(dirs, filter);
|
|
}
|
|
}
|
|
|
|
if (output.FullResults)
|
|
{
|
|
Library.Utility.Utility.PrintSerializeObject(result, outwriter);
|
|
outwriter.WriteLine();
|
|
}
|
|
|
|
var parsedStats = result.BackendStatistics as Duplicati.Library.Interface.IParsedBackendStatistics;
|
|
output.MessageEvent(string.Format(" Duration of backup: {0:c}", result.Duration));
|
|
if (parsedStats != null)
|
|
{
|
|
if (parsedStats.KnownFileCount > 0)
|
|
{
|
|
output.MessageEvent(string.Format(" Remote files: {0}", parsedStats.KnownFileCount));
|
|
output.MessageEvent(string.Format(" Remote size: {0}", Library.Utility.Utility.FormatSizeString(parsedStats.KnownFileSize)));
|
|
}
|
|
|
|
if (parsedStats.TotalQuotaSpace >= 0)
|
|
{
|
|
output.MessageEvent(string.Format(" Total remote quota: {0}", Library.Utility.Utility.FormatSizeString(parsedStats.TotalQuotaSpace)));
|
|
}
|
|
|
|
if (parsedStats.FreeQuotaSpace >= 0)
|
|
{
|
|
output.MessageEvent(string.Format(" Available remote quota: {0}", Library.Utility.Utility.FormatSizeString(parsedStats.FreeQuotaSpace)));
|
|
}
|
|
}
|
|
|
|
output.MessageEvent(string.Format(" Files added: {0}", result.AddedFiles));
|
|
output.MessageEvent(string.Format(" Files deleted: {0}", result.DeletedFiles));
|
|
output.MessageEvent(string.Format(" Files changed: {0}", result.ModifiedFiles));
|
|
|
|
output.MessageEvent(string.Format(" Data uploaded: {0}", Library.Utility.Utility.FormatSizeString(result.BackendStatistics.BytesUploaded)));
|
|
output.MessageEvent(string.Format(" Data downloaded: {0}", Library.Utility.Utility.FormatSizeString(result.BackendStatistics.BytesDownloaded)));
|
|
|
|
if (result.ExaminedFiles == 0 && (filter != null && !filter.Empty))
|
|
output.MessageEvent("No files were processed. If this was not intentional you may want to use the \"test-filters\" command");
|
|
|
|
output.MessageEvent("Backup completed successfully!");
|
|
|
|
//Interrupted = 50
|
|
if (result.PartialBackup)
|
|
return 50;
|
|
|
|
//Completed with errors = 3
|
|
if (result.ParsedResult == Library.Interface.ParsedResultType.Error)
|
|
return 3;
|
|
|
|
//Completed with warnings = 2
|
|
if (result.ParsedResult == Library.Interface.ParsedResultType.Warning)
|
|
return 2;
|
|
|
|
//Success, but no upload = 1
|
|
if (result.BackendStatistics.BytesUploaded == 0)
|
|
return 1;
|
|
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
public static int Compact(TextWriter outwriter, Action<Duplicati.Library.Main.Controller> setup, List<string> args, Dictionary<string, string> options, Library.Utility.IFilter filter)
|
|
{
|
|
if (args.Count != 1)
|
|
return PrintWrongNumberOfArguments(outwriter, args, 1);
|
|
|
|
using (var console = new ConsoleOutput(outwriter, options))
|
|
using (var i = new Library.Main.Controller(args[0], options, console))
|
|
{
|
|
setup(i);
|
|
i.Compact();
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
public static int Test(TextWriter outwriter, Action<Duplicati.Library.Main.Controller> setup, List<string> args, Dictionary<string, string> options, Library.Utility.IFilter filter)
|
|
{
|
|
var fullResults = Duplicati.Library.Utility.Utility.ParseBoolOption(options, "full-result");
|
|
if (args.Count != 1 && args.Count != 2)
|
|
return PrintWrongNumberOfArguments(outwriter, args, 1);
|
|
|
|
var tests = 1L;
|
|
if (args.Count == 2)
|
|
{
|
|
if (new string[] { "all", "everything" }.Contains(args[1], StringComparer.OrdinalIgnoreCase))
|
|
tests = long.MaxValue;
|
|
else
|
|
tests = Convert.ToInt64(args[1]);
|
|
}
|
|
|
|
Library.Interface.ITestResults result;
|
|
using (var console = new ConsoleOutput(outwriter, options))
|
|
using (var i = new Library.Main.Controller(args[0], options, console))
|
|
{
|
|
setup(i);
|
|
result = i.Test(tests);
|
|
}
|
|
|
|
var totalFiles = result.Verifications.Count();
|
|
if (totalFiles == 0)
|
|
{
|
|
outwriter.WriteLine("No files examined, is the remote destination is empty?");
|
|
return 100;
|
|
}
|
|
else
|
|
{
|
|
var filtered = from n in result.Verifications where n.Value.Any() select n;
|
|
if (!filtered.Any())
|
|
{
|
|
outwriter.WriteLine("Examined {0} files and found no errors", totalFiles);
|
|
return 0;
|
|
}
|
|
else
|
|
{
|
|
foreach (var n in result.Verifications)
|
|
{
|
|
var changecount = n.Value.Count();
|
|
if (changecount == 0)
|
|
{
|
|
if (fullResults)
|
|
outwriter.WriteLine("{0}: No errors", n.Key);
|
|
}
|
|
else
|
|
{
|
|
outwriter.WriteLine("{0}: {1} errors", n.Key, changecount);
|
|
var count = 0;
|
|
foreach (var c in n.Value)
|
|
{
|
|
count++;
|
|
outwriter.WriteLine("\t{0}: {1}", c.Key, c.Value);
|
|
if (!fullResults && count == 10 && changecount > 10)
|
|
{
|
|
outwriter.WriteLine("\t... and {0} more", changecount - count);
|
|
break;
|
|
}
|
|
}
|
|
|
|
outwriter.WriteLine();
|
|
}
|
|
}
|
|
|
|
return 3;
|
|
}
|
|
}
|
|
}
|
|
|
|
private static int PrintWrongNumberOfArguments(TextWriter outwriter, List<string> args, int expected)
|
|
{
|
|
outwriter.WriteLine(Strings.Program.WrongNumberOfCommandsError_v2(args.Count, expected, args.Select(n => "\"" + n + "\"").ToArray()));
|
|
return 200;
|
|
}
|
|
|
|
public static int PrintInvalidCommand(TextWriter outwriter, string command)
|
|
{
|
|
outwriter.WriteLine(Strings.Program.InvalidCommandError(command));
|
|
return 200;
|
|
}
|
|
|
|
public static int CreateBugReport(TextWriter outwriter, Action<Duplicati.Library.Main.Controller> setup, List<string> args, Dictionary<string, string> options, Library.Utility.IFilter filter)
|
|
{
|
|
// Support for not adding the --auth-username if possible
|
|
string dbpath = null;
|
|
options.TryGetValue("dbpath", out dbpath);
|
|
if (string.IsNullOrEmpty(dbpath))
|
|
{
|
|
if (args.Count > 0)
|
|
dbpath = Library.Main.CLIDatabaseLocator.GetDatabasePathForCLI(args[0], new Duplicati.Library.Main.Options(options), false, true);
|
|
|
|
if (dbpath == null)
|
|
{
|
|
outwriter.WriteLine("No local database found, please add --{0}", "dbpath");
|
|
return 100;
|
|
}
|
|
else
|
|
options["dbpath"] = dbpath;
|
|
|
|
}
|
|
|
|
if (args.Count == 0)
|
|
args = new List<string>(new string[] { "file://unused", "report" });
|
|
else if (args.Count == 1)
|
|
args.Add("report");
|
|
|
|
using (var console = new ConsoleOutput(outwriter, options))
|
|
using (var i = new Library.Main.Controller(args[0], options, console))
|
|
{
|
|
setup(i);
|
|
i.CreateLogDatabase(args[1]);
|
|
}
|
|
|
|
outwriter.WriteLine("Completed!");
|
|
outwriter.WriteLine();
|
|
outwriter.WriteLine("Please examine the log table of the database to see that no filenames are accidentially left over.");
|
|
outwriter.WriteLine("If you are concerned that your filenames may contain sensitive information,");
|
|
outwriter.WriteLine(" do not attach the database to an issue!!!");
|
|
outwriter.WriteLine();
|
|
|
|
return 0;
|
|
}
|
|
|
|
public static int ListChanges(TextWriter outwriter, Action<Duplicati.Library.Main.Controller> setup, List<string> args, Dictionary<string, string> options, Library.Utility.IFilter filter)
|
|
{
|
|
var fullresult = Duplicati.Library.Utility.Utility.ParseBoolOption(options, "full-result");
|
|
|
|
if (args.Count < 1)
|
|
return PrintWrongNumberOfArguments(outwriter, args, 1);
|
|
|
|
// Support for not adding the --auth-username if possible
|
|
string dbpath;
|
|
options.TryGetValue("dbpath", out dbpath);
|
|
if (string.IsNullOrEmpty(dbpath))
|
|
{
|
|
dbpath = Library.Main.CLIDatabaseLocator.GetDatabasePathForCLI(args[0], new Duplicati.Library.Main.Options(options), false, true);
|
|
if (dbpath != null)
|
|
options["dbpath"] = dbpath;
|
|
}
|
|
|
|
// Don't ask for passphrase if we have a local db
|
|
if (!string.IsNullOrEmpty(dbpath) && System.IO.File.Exists(dbpath) && !options.ContainsKey("no-encryption") && !Duplicati.Library.Utility.Utility.ParseBoolOption(options, "no-local-db"))
|
|
{
|
|
string passphrase;
|
|
options.TryGetValue("passphrase", out passphrase);
|
|
if (string.IsNullOrEmpty(passphrase))
|
|
options["no-encryption"] = "true";
|
|
}
|
|
|
|
Action<Duplicati.Library.Interface.IListChangesResults, IEnumerable<Tuple<Library.Interface.ListChangesChangeType, Library.Interface.ListChangesElementType, string>>> handler =
|
|
(result, items) =>
|
|
{
|
|
outwriter.WriteLine("Listing changes");
|
|
outwriter.WriteLine(" {0}: {1}", result.BaseVersionIndex, result.BaseVersionTimestamp);
|
|
outwriter.WriteLine(" {0}: {1}", result.CompareVersionIndex, result.CompareVersionTimestamp);
|
|
outwriter.WriteLine();
|
|
|
|
outwriter.WriteLine("Size of backup {0}: {1}", result.BaseVersionIndex, Library.Utility.Utility.FormatSizeString(result.PreviousSize));
|
|
|
|
if (items != null)
|
|
{
|
|
outwriter.WriteLine();
|
|
|
|
var added = result.ChangeDetails.Where(x => x.Item1 == Library.Interface.ListChangesChangeType.Added);
|
|
var deleted = result.ChangeDetails.Where(x => x.Item1 == Library.Interface.ListChangesChangeType.Deleted);
|
|
var modified = result.ChangeDetails.Where(x => x.Item1 == Library.Interface.ListChangesChangeType.Modified);
|
|
|
|
var count = added.Count();
|
|
if (count > 0)
|
|
{
|
|
var c = 0;
|
|
outwriter.WriteLine(" {0} added entries:", count);
|
|
foreach (var n in added)
|
|
{
|
|
c++;
|
|
outwriter.WriteLine(" + {0}", n.Item3);
|
|
if (c > 10 && count > 10 && !fullresult)
|
|
{
|
|
outwriter.WriteLine(" ... and {0} more", count - c);
|
|
break;
|
|
}
|
|
}
|
|
outwriter.WriteLine();
|
|
}
|
|
count = modified.Count();
|
|
if (count > 0)
|
|
{
|
|
var c = 0;
|
|
outwriter.WriteLine(" {0} modified entries:", count);
|
|
foreach (var n in modified)
|
|
{
|
|
c++;
|
|
outwriter.WriteLine(" ~ {0}", n.Item3);
|
|
if (c > 10 && count > 10 && !fullresult)
|
|
{
|
|
outwriter.WriteLine(" ... and {0} more", count - c);
|
|
break;
|
|
}
|
|
}
|
|
outwriter.WriteLine();
|
|
}
|
|
count = deleted.Count();
|
|
if (count > 0)
|
|
{
|
|
var c = 0;
|
|
outwriter.WriteLine("{0} deleted entries:", count);
|
|
foreach (var n in deleted)
|
|
{
|
|
c++;
|
|
outwriter.WriteLine(" - {0}", n.Item3);
|
|
if (c > 10 && count > 10 && !fullresult)
|
|
{
|
|
outwriter.WriteLine(" ... and {0} more", count - c);
|
|
break;
|
|
}
|
|
}
|
|
outwriter.WriteLine();
|
|
}
|
|
|
|
outwriter.WriteLine();
|
|
}
|
|
|
|
|
|
if (result.AddedFolders > 0)
|
|
outwriter.WriteLine(" Added folders: {0}", result.AddedFolders);
|
|
if (result.AddedSymlinks > 0)
|
|
outwriter.WriteLine(" Added symlinks: {0}", result.AddedSymlinks);
|
|
if (result.AddedFiles > 0)
|
|
outwriter.WriteLine(" Added files: {0}", result.AddedFiles);
|
|
if (result.DeletedFolders > 0)
|
|
outwriter.WriteLine(" Deleted folders: {0}", result.DeletedFolders);
|
|
if (result.DeletedSymlinks > 0)
|
|
outwriter.WriteLine(" Deleted symlinks: {0}", result.DeletedSymlinks);
|
|
if (result.DeletedFiles > 0)
|
|
outwriter.WriteLine(" Deleted files: {0}", result.DeletedFiles);
|
|
if (result.ModifiedFolders > 0)
|
|
outwriter.WriteLine(" Modified folders: {0}", result.ModifiedFolders);
|
|
if (result.ModifiedSymlinks > 0)
|
|
outwriter.WriteLine(" Modified symlinka: {0}", result.ModifiedSymlinks);
|
|
if (result.ModifiedFiles > 0)
|
|
outwriter.WriteLine(" Modified files: {0}", result.ModifiedFiles);
|
|
|
|
if (result.AddedFolders + result.AddedSymlinks + result.AddedFolders +
|
|
result.ModifiedFolders + result.ModifiedSymlinks + result.ModifiedFiles +
|
|
result.DeletedFolders + result.DeletedSymlinks + result.DeletedFiles == 0)
|
|
outwriter.WriteLine(" No changes found");
|
|
|
|
outwriter.WriteLine("Size of backup {0}: {1}", result.CompareVersionIndex, Library.Utility.Utility.FormatSizeString(result.CurrentSize));
|
|
};
|
|
|
|
using (var console = new ConsoleOutput(outwriter, options))
|
|
using (var i = new Library.Main.Controller(args[0], options, console))
|
|
{
|
|
setup(i);
|
|
if (args.Count == 2)
|
|
i.ListChanges(null, args[1], null, filter, handler);
|
|
else
|
|
i.ListChanges(args.Count > 1 ? args[1] : null, args.Count > 2 ? args[2] : null, null, filter, handler);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
public static int TestFilters(TextWriter outwriter, Action<Duplicati.Library.Main.Controller> setup, List<string> args, Dictionary<string, string> options, Library.Utility.IFilter filter)
|
|
{
|
|
if (args == null || args.Count < 1)
|
|
{
|
|
outwriter.WriteLine("No source paths given");
|
|
return 200;
|
|
}
|
|
|
|
using (var console = new ConsoleOutput(outwriter, options))
|
|
using (var i = new Library.Main.Controller("dummy://", options, console))
|
|
{
|
|
setup(i);
|
|
var result = i.TestFilter(args.ToArray(), filter);
|
|
|
|
outwriter.WriteLine("Matched {0} files ({1})", result.FileCount, Duplicati.Library.Utility.Utility.FormatSizeString(result.FileSize));
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
public static int SystemInfo(TextWriter outwriter, Action<Duplicati.Library.Main.Controller> setup, List<string> args, Dictionary<string, string> options, Library.Utility.IFilter filter)
|
|
{
|
|
if (args != null && args.Count != 0)
|
|
{
|
|
outwriter.WriteLine("Command takes no arguments");
|
|
return 200;
|
|
}
|
|
|
|
using (var console = new ConsoleOutput(outwriter, options))
|
|
using (var i = new Library.Main.Controller("dummy://", options, console))
|
|
{
|
|
setup(i);
|
|
foreach (var line in i.SystemInfo().Lines)
|
|
outwriter.WriteLine(line);
|
|
}
|
|
|
|
outwriter.WriteLine("Know locales: {0}", string.Join(", ", Library.Localization.LocalizationService.AllLocales));
|
|
outwriter.WriteLine("Translated locales: {0}", string.Join(", ", Library.Localization.LocalizationService.SupportedCultures));
|
|
|
|
return 0;
|
|
}
|
|
|
|
public static int PurgeFiles(TextWriter outwriter, Action<Duplicati.Library.Main.Controller> setup, List<string> args, Dictionary<string, string> options, Library.Utility.IFilter filter)
|
|
{
|
|
if (args.Count < 1)
|
|
return PrintWrongNumberOfArguments(outwriter, args, 1);
|
|
|
|
var backend = args[0];
|
|
var paths = args.Skip(1).ToArray();
|
|
|
|
if (paths.Length > 0)
|
|
{
|
|
if (filter == null || filter.Empty)
|
|
filter = new Library.Utility.FilterExpression(paths);
|
|
else
|
|
{
|
|
outwriter.WriteLine("You cannot combine filters and paths on the commandline");
|
|
return 200;
|
|
}
|
|
}
|
|
else if (filter == null || filter.Empty)
|
|
{
|
|
outwriter.WriteLine("You must provide either filename filters, or a list of paths to remove");
|
|
return 200;
|
|
}
|
|
|
|
|
|
using (var console = new ConsoleOutput(outwriter, options))
|
|
using (var i = new Library.Main.Controller(backend, options, console))
|
|
{
|
|
setup(i);
|
|
i.PurgeFiles(filter);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
public static int ListBrokenFiles(TextWriter outwriter, Action<Duplicati.Library.Main.Controller> setup, List<string> args, Dictionary<string, string> options, Library.Utility.IFilter filter)
|
|
{
|
|
if (args.Count != 1)
|
|
return PrintWrongNumberOfArguments(outwriter, args, 1);
|
|
|
|
var previd = -1L;
|
|
var outputcount = 0L;
|
|
var fullresult = Duplicati.Library.Utility.Utility.ParseBoolOption(options, "full-result");
|
|
|
|
using (var con = new ConsoleOutput(outwriter, options))
|
|
using (var i = new Library.Main.Controller(args[0], options, con))
|
|
{
|
|
setup(i);
|
|
i.ListBrokenFiles(filter, (id, time, count, path, size) =>
|
|
{
|
|
if (previd != id)
|
|
{
|
|
previd = id;
|
|
outputcount = 0;
|
|
con.MessageEvent(string.Format("{0}\t: {1}\t({2} match(es))", id, time.ToLocalTime(), count));
|
|
}
|
|
|
|
con.MessageEvent(string.Format("\t{0} ({1})", path, Library.Utility.Utility.FormatSizeString(size)));
|
|
outputcount++;
|
|
if (outputcount >= 5 && !fullresult && count != outputcount)
|
|
{
|
|
con.MessageEvent(string.Format("\t ... and {0} more, (use --{1} to list all)", count - outputcount, "full-result"));
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
|
|
});
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
public static int PurgeBrokenFiles(TextWriter outwriter, Action<Duplicati.Library.Main.Controller> setup, List<string> args, Dictionary<string, string> options, Library.Utility.IFilter filter)
|
|
{
|
|
if (args.Count != 1)
|
|
return PrintWrongNumberOfArguments(outwriter, args, 1);
|
|
|
|
using (var console = new ConsoleOutput(outwriter, options))
|
|
using (var i = new Library.Main.Controller(args[0], options, console))
|
|
{
|
|
setup(i);
|
|
i.PurgeBrokenFiles(filter);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
public static int SendMail(TextWriter outwriter, Action<Duplicati.Library.Main.Controller> setup, List<string> args, Dictionary<string, string> options, Library.Utility.IFilter filter)
|
|
{
|
|
if (args != null && args.Count != 0)
|
|
{
|
|
outwriter.WriteLine("Command takes no arguments");
|
|
return 200;
|
|
}
|
|
|
|
using (var console = new ConsoleOutput(outwriter, options))
|
|
using (var i = new Library.Main.Controller("dummy://", options, console))
|
|
{
|
|
setup(i);
|
|
foreach (var l in i.SendMail().Lines)
|
|
outwriter.WriteLine(l);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
public static int Vacuum(
|
|
TextWriter outwriter,
|
|
Action<Duplicati.Library.Main.Controller> setup,
|
|
List<string> args, Dictionary<string, string> options,
|
|
Library.Utility.IFilter filter)
|
|
{
|
|
if (args.Count != 1)
|
|
return PrintWrongNumberOfArguments(outwriter, args, 1);
|
|
|
|
using (var console = new ConsoleOutput(outwriter, options))
|
|
using (var controller = new Library.Main.Controller(args[0], options, console))
|
|
{
|
|
setup(controller);
|
|
controller.Vacuum();
|
|
}
|
|
return 0;
|
|
}
|
|
}
|
|
}
|
|
|