Merge branch 'feature/commandline_ui'

This commit is contained in:
Kenneth Skovhede
2017-04-05 09:29:01 +02:00
24 changed files with 1149 additions and 401 deletions
+2 -2
View File
@@ -296,10 +296,10 @@ Global
$2.ResourceNamePolicy = FileFormatDefault
$0.TextStylePolicy = $3
$3.FileWidth = 120
$3.inheritsSet = null
$3.inheritsSet = VisualStudio
$3.inheritsScope = text/plain
$3.EolMarker = Windows
$3.scope = text/microsoft-resx
$3.scope = text/plain
$0.TextStylePolicy = $4
$4.FileWidth = 120
$4.TabsToSpaces = True
+336 -245
View File
@@ -18,6 +18,7 @@
using System;
using System.Linq;
using System.Collections.Generic;
using System.IO;
namespace Duplicati.CommandLine
{
@@ -124,30 +125,31 @@ namespace Duplicati.CommandLine
}
}
public static int Examples(List<string> args, Dictionary<string, string> options, Library.Utility.IFilter filter)
public static int Examples(TextWriter outwriter, Action<Duplicati.Library.Main.Controller> setup, List<string> args, Dictionary<string, string> options, Library.Utility.IFilter filter)
{
Duplicati.CommandLine.Help.PrintUsage("example", options);
Duplicati.CommandLine.Help.PrintUsage(outwriter, "example", options);
return 0;
}
public static int Help(List<string> args, Dictionary<string, string> options, Library.Utility.IFilter filter)
public static int Help(TextWriter outwriter, Action<Duplicati.Library.Main.Controller> setup, List<string> args, Dictionary<string, string> options, Library.Utility.IFilter filter)
{
if (args.Count < 1)
Duplicati.CommandLine.Help.PrintUsage("help", options);
Duplicati.CommandLine.Help.PrintUsage(outwriter, "help", options);
else
Duplicati.CommandLine.Help.PrintUsage(args[0], options);
Duplicati.CommandLine.Help.PrintUsage(outwriter, args[0], options);
return 0;
}
public static int Affected(List<string> args, Dictionary<string, string> options, Library.Utility.IFilter filter)
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, "verbose") || Duplicati.Library.Utility.Utility.ParseBoolOption(options, "full-result");
var backend = args[0];
args.RemoveAt(0);
if (args.Count == 0)
{
Console.WriteLine("You must specify at least a remote filename");
outwriter.WriteLine("You must specify at least a remote filename");
return 200;
}
@@ -170,61 +172,79 @@ namespace Duplicati.CommandLine
options["no-encryption"] = "true";
}
using(var i = new Library.Main.Controller(backend, options, new ConsoleOutput(options)))
using(var i = new Library.Main.Controller(backend, options, new ConsoleOutput(outwriter, options)))
{
var res = i.ListAffected(args);
setup(i);
i.ListAffected(args, res =>
{
if (res.Filesets != null && res.Filesets.Count() != 0)
{
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.Filesets != null && res.Filesets.Count() != 0)
{
Console.WriteLine("The following filesets are affected:");
foreach(var e in res.Filesets)
Console.WriteLine("{0}\t: {1}", e.Version, e.Time);
Console.WriteLine();
}
if (res.RemoteVolumes != null && res.RemoteVolumes.Count() != 0)
{
}
var filecount = res.Files.Count();
if (filecount == 0 || (filecount > 10 && !Duplicati.Library.Utility.Utility.ParseBoolOption(options, "verbose")))
Console.WriteLine("A total of {0} file(s) are affected (use --{1} to see filenames)", res.Files.Count(), "verbose");
else
{
Console.WriteLine("The following files are affected:");
foreach(var file in res.Files)
Console.WriteLine(file.Path);
}
Console.WriteLine();
var logcount = res.LogMessages.Count();
if (logcount == 0 || (logcount > 10 && !Duplicati.Library.Utility.Utility.ParseBoolOption(options, "verbose")))
Console.WriteLine("Found {0} related log messages (use --{1} to see the data)", res.Files.Count(), "verbose");
else
{
Console.WriteLine("The following related log messages were found:");
foreach(var log in res.LogMessages)
if (log.Message.Length > 100 && !Duplicati.Library.Utility.Utility.ParseBoolOption(options, "verbose"))
Console.WriteLine("{0}: {1}", log.Timestamp, log.Message.Substring(0, 96) + " ...");
if (res.Files != null)
{
var filecount = res.Files.Count();
if (filecount == 0)
{
outwriter.WriteLine("No files are affected");
}
else
Console.WriteLine("{0}: {1}", log.Timestamp, log.Message);
}
{
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();
}
});
Console.WriteLine();
return 0;
}
}
public static int List(List<string> args, Dictionary<string, string> options, Library.Utility.IFilter filter)
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 i = new Library.Main.Controller(args[0], options, new ConsoleOutput(options)))
using(var i = new Library.Main.Controller(args[0], options, new ConsoleOutput(outwriter, options)))
{
setup(i);
var backend = args[0];
args.RemoveAt(0);
@@ -302,7 +322,7 @@ namespace Duplicati.CommandLine
if (isRequestForFiles && !Library.Utility.Utility.ParseBoolOption(options, "all-versions"))
{
Console.WriteLine("No files matching, looking in all versions");
outwriter.WriteLine("No files matching, looking in all versions");
options["all-versions"] = "true";
options.Remove("time");
options.Remove("version");
@@ -311,47 +331,47 @@ namespace Duplicati.CommandLine
if (res.Filesets.Count() != 0 && (res.Files == null || res.Files.Count() == 0) && compareFilter.Empty)
{
Console.WriteLine("Listing filesets:");
outwriter.WriteLine("Listing filesets:");
foreach(var e in res.Filesets)
{
if (e.FileCount >= 0)
Console.WriteLine("{0}\t: {1} ({2} files, {3})", e.Version, e.Time, e.FileCount, Library.Utility.Utility.FormatSizeString(e.FileSizes));
outwriter.WriteLine("{0}\t: {1} ({2} files, {3})", e.Version, e.Time, e.FileCount, Library.Utility.Utility.FormatSizeString(e.FileSizes));
else
Console.WriteLine("{0}\t: {1}", e.Version, e.Time);
outwriter.WriteLine("{0}\t: {1}", e.Version, e.Time);
}
}
else if (isRequestForFiles)
{
Console.WriteLine("No files matched expression");
outwriter.WriteLine("No files matched expression");
}
else
{
if (res.Filesets.Count() == 0)
{
Console.WriteLine("No time or version matched a fileset");
outwriter.WriteLine("No time or version matched a fileset");
}
else if (res.Files == null || res.Files.Count() == 0)
{
Console.WriteLine("Found {0} filesets, but no files matched", res.Filesets.Count());
outwriter.WriteLine("Found {0} filesets, but no files matched", res.Filesets.Count());
}
else if (res.Filesets.Count() == 1)
{
var f = res.Filesets.First();
Console.WriteLine("Listing contents {0} ({1}):", f.Version, f.Time);
outwriter.WriteLine("Listing contents {0} ({1}):", f.Version, f.Time);
foreach(var e in res.Files)
Console.WriteLine("{0} {1}", e.Path, e.Path.EndsWith(System.IO.Path.DirectorySeparatorChar.ToString()) ? "" : "(" + Library.Utility.Utility.FormatSizeString(e.Sizes.First()) + ")");
outwriter.WriteLine("{0} {1}", e.Path, e.Path.EndsWith(System.IO.Path.DirectorySeparatorChar.ToString()) ? "" : "(" + Library.Utility.Utility.FormatSizeString(e.Sizes.First()) + ")");
}
else
{
Console.WriteLine("Listing files and versions:");
outwriter.WriteLine("Listing files and versions:");
foreach(var e in res.Files)
{
Console.WriteLine(e.Path);
outwriter.WriteLine(e.Path);
foreach(var nx in res.Filesets.Zip(e.Sizes, (a, b) => new { Index = a.Version, Time = a.Time, Size = b } ))
Console.WriteLine("{0}\t: {1} {2}", nx.Index, nx.Time, nx.Size < 0 ? " - " : Library.Utility.Utility.FormatSizeString(nx.Size));
outwriter.WriteLine("{0}\t: {1} {2}", nx.Index, nx.Time, nx.Size < 0 ? " - " : Library.Utility.Utility.FormatSizeString(nx.Size));
Console.WriteLine();
outwriter.WriteLine();
}
}
@@ -361,34 +381,35 @@ namespace Duplicati.CommandLine
return 0;
}
public static int Delete(List<string> args, Dictionary<string, string> options, Library.Utility.IFilter filter)
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.Where(x => requiredOptions.Contains(x, StringComparer.InvariantCultureIgnoreCase)).Any())
{
Console.WriteLine(Strings.Program.DeleteCommandNeedsOptions("delete", requiredOptions));
outwriter.WriteLine(Strings.Program.DeleteCommandNeedsOptions("delete", requiredOptions));
return 200;
}
using(var i = new Library.Main.Controller(args[0], options, new ConsoleOutput(options)))
using(var i = new Library.Main.Controller(args[0], options, new ConsoleOutput(outwriter, options)))
{
setup(i);
args.RemoveAt(0);
var res = i.Delete();
if (res.DeletedSets.Count() == 0)
{
Console.WriteLine(Strings.Program.NoFilesetsMatching);
outwriter.WriteLine(Strings.Program.NoFilesetsMatching);
}
else
{
if (res.Dryrun)
Console.WriteLine(Strings.Program.WouldDeleteBackups);
outwriter.WriteLine(Strings.Program.WouldDeleteBackups);
else
Console.WriteLine(Strings.Program.DeletedBackups);
outwriter.WriteLine(Strings.Program.DeletedBackups);
foreach(var f in res.DeletedSets)
Console.WriteLine(string.Format("{0}: {1}", f.Item1, f.Item2));
outwriter.WriteLine(string.Format("{0}: {1}", f.Item1, f.Item2));
}
}
@@ -396,21 +417,24 @@ namespace Duplicati.CommandLine
}
public static int Repair(List<string> args, Dictionary<string, string> options, Library.Utility.IFilter filter)
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(args, 1);
return PrintWrongNumberOfArguments(outwriter, args, 1);
using(var i = new Duplicati.Library.Main.Controller(args[0], options, new ConsoleOutput(options)))
using (var i = new Duplicati.Library.Main.Controller(args[0], options, new ConsoleOutput(outwriter, options)))
{
setup(i);
i.Repair(filter);
}
return 0;
}
public static int Restore(List<string> args, Dictionary<string, string> options, Library.Utility.IFilter filter)
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(args, 1);
return PrintWrongNumberOfArguments(outwriter, args, 1);
string backend = args[0];
args.RemoveAt(0);
@@ -428,29 +452,32 @@ namespace Duplicati.CommandLine
if (args[ix].IndexOfAny(new char[] { '*', '?' }) < 0 && !args[ix].StartsWith("[") && args[ix].EndsWith(System.IO.Path.DirectorySeparatorChar.ToString()))
args[ix] += "*";
var output = new ConsoleOutput(options);
var output = new ConsoleOutput(outwriter, options);
output.MessageEvent(string.Format("Restore started at {0}", DateTime.Now));
using(var i = new Library.Main.Controller(backend, options, output))
using (var i = new Library.Main.Controller(backend, options, output))
{
setup(i);
if (controlFiles)
{
var res = i.RestoreControlFiles(args.ToArray(), filter);
output.MessageEvent("Restore control files completed:");
foreach(var s in res.Files)
Console.WriteLine(s);
foreach (var s in res.Files)
outwriter.WriteLine(s);
}
else
{
using(var periodicOutput = new PeriodicOutput(output, TimeSpan.FromSeconds(5)))
using (var periodicOutput = new PeriodicOutput(output, TimeSpan.FromSeconds(5)))
{
output.PhaseChanged += (phase, previousPhase) => {
output.PhaseChanged += (phase, previousPhase) =>
{
if (phase == Duplicati.Library.Main.OperationPhase.Restore_PreRestoreVerify)
output.MessageEvent("Checking remote backup ...");
else if (phase == Duplicati.Library.Main.OperationPhase.Restore_ScanForExistingFiles)
output.MessageEvent("Checking existing target files ...");
/*else if (phase == Duplicati.Library.Main.OperationPhase.Restore_DownloadingRemoteFiles)
output.MessageEvent("Downloading remote files ..."); */
else if (phase == Duplicati.Library.Main.OperationPhase.Restore_PatchWithLocalBlocks)
/*else if (phase == Duplicati.Library.Main.OperationPhase.Restore_DownloadingRemoteFiles)
output.MessageEvent("Downloading remote files ..."); */
else if (phase == Duplicati.Library.Main.OperationPhase.Restore_PatchWithLocalBlocks)
output.MessageEvent("Updating target files with local data ...");
else if (phase == Duplicati.Library.Main.OperationPhase.Restore_PostRestoreVerify)
{
@@ -463,18 +490,19 @@ namespace Duplicati.CommandLine
else if (phase == Duplicati.Library.Main.OperationPhase.Restore_CreateTargetFolders)
periodicOutput.SetReady();
};
periodicOutput.WriteOutput += (progress, files, size, counting) => {
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.FilesRestored, 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 (res.FilesRestored > 0 && !Library.Main.Utility.SuppressDonationMessages)
{
output.MessageEvent("***********************************************");
@@ -484,27 +512,28 @@ namespace Duplicati.CommandLine
output.MessageEvent("Bitcoin: 1L74qa1n5SFKwwyHhECTHBJgcf6WT2rJKf");
output.MessageEvent("***********************************************");
}
if (output.VerboseOutput)
Library.Utility.Utility.PrintSerializeObject(res, Console.Out);
Library.Utility.Utility.PrintSerializeObject(res, outwriter);
if (res.Warnings.Count() > 0)
return 2;
}
}
}
return 0;
}
public static int Backup(List<string> args, Dictionary<string, string> options, Library.Utility.IFilter filter)
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(args, 2);
return PrintWrongNumberOfArguments(outwriter, args, 2);
var backend = args[0];
args.RemoveAt(0);
var dirs = args.ToArray();
var output = new ConsoleOutput(options);
var output = new ConsoleOutput(outwriter, options);
Library.Interface.IBackupResults result;
@@ -537,14 +566,17 @@ namespace Duplicati.CommandLine
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))
using (var i = new Library.Main.Controller(backend, options, output))
{
setup(i);
result = i.Backup(dirs, filter);
}
}
if (output.VerboseOutput)
{
Library.Utility.Utility.PrintSerializeObject(result, Console.Out);
Library.Utility.Utility.PrintSerializeObject(result, outwriter);
}
else
{
@@ -588,21 +620,25 @@ namespace Duplicati.CommandLine
return 0;
}
public static int Compact(List<string> args, Dictionary<string, string> options, Library.Utility.IFilter filter)
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(args, 1);
using(var i = new Library.Main.Controller(args[0], options, new ConsoleOutput(options)))
return PrintWrongNumberOfArguments(outwriter, args, 1);
using (var i = new Library.Main.Controller(args[0], options, new ConsoleOutput(outwriter, options)))
{
setup(i);
i.Compact();
}
return 0;
}
public static int Test(List<string> args, Dictionary<string, string> options, Library.Utility.IFilter filter)
public static int Test(TextWriter outwriter, Action<Duplicati.Library.Main.Controller> setup, List<string> args, Dictionary<string, string> options, Library.Utility.IFilter filter)
{
var verbose = Library.Utility.Utility.ParseBoolOption(options, "verbose") || Duplicati.Library.Utility.Utility.ParseBoolOption(options, "full-result");
if (args.Count != 1 && args.Count != 2)
return PrintWrongNumberOfArguments(args, 1);
return PrintWrongNumberOfArguments(outwriter, args, 1);
var tests = 1L;
if (args.Count == 2)
@@ -614,13 +650,16 @@ namespace Duplicati.CommandLine
}
Library.Interface.ITestResults result;
using(var i = new Library.Main.Controller(args[0], options, new ConsoleOutput(options)))
using (var i = new Library.Main.Controller(args[0], options, new ConsoleOutput(outwriter, options)))
{
setup(i);
result = i.Test(tests);
}
var totalFiles = result.Verifications.Count();
if (totalFiles == 0)
{
Console.WriteLine("No files examined, is the remote destination is empty?");
outwriter.WriteLine("No files examined, is the remote destination is empty?");
return 100;
}
else
@@ -628,33 +667,36 @@ namespace Duplicati.CommandLine
var filtered = from n in result.Verifications where n.Value.Count() != 0 select n;
if (filtered.Count() == 0)
{
Console.WriteLine("Examined {0} files and found no errors", totalFiles);
outwriter.WriteLine("Examined {0} files and found no errors", totalFiles);
return 0;
}
else
{
if (Library.Utility.Utility.ParseBoolOption(options, "verbose"))
foreach(var n in result.Verifications)
{
foreach(var n in result.Verifications)
var changecount = n.Value.Count();
if (changecount == 0)
{
var changecount = n.Value.Count();
if (changecount == 0)
if (verbose)
Console.WriteLine("{0}: No errors", n.Key);
else
{
Console.WriteLine("{0}: {1} errors", n.Key, changecount);
foreach(var c in n.Value)
Console.WriteLine("{0}: {1}", c.Key, c.Value);
Console.WriteLine();
}
}
}
else
{
Console.WriteLine("Examined {0} files and found errors in the following files:", totalFiles);
foreach(var n in filtered)
Console.WriteLine(n.Key);
Console.WriteLine();
else
{
Console.WriteLine("{0}: {1} errors", n.Key, changecount);
var count = 0;
foreach (var c in n.Value)
{
count++;
Console.WriteLine("\t{0}: {1}", c.Key, c.Value);
if (!verbose && count == 10 && changecount > 10)
{
Console.WriteLine("\t... and {0} more", changecount - count);
break;
}
}
Console.WriteLine();
}
}
return 3;
@@ -662,19 +704,19 @@ namespace Duplicati.CommandLine
}
}
private static int PrintWrongNumberOfArguments(List<string> args, int expected)
private static int PrintWrongNumberOfArguments(TextWriter outwriter, List<string> args, int expected)
{
Console.WriteLine(Strings.Program.WrongNumberOfCommandsError_v2(args.Count, expected, args.Select(n => "\"" + n + "\"").ToArray()));
outwriter.WriteLine(Strings.Program.WrongNumberOfCommandsError_v2(args.Count, expected, args.Select(n => "\"" + n + "\"").ToArray()));
return 200;
}
public static int PrintInvalidCommand(string command, List<string> args)
public static int PrintInvalidCommand(TextWriter outwriter, string command, List<string> args)
{
Console.WriteLine(Strings.Program.InvalidCommandError(command));
outwriter.WriteLine(Strings.Program.InvalidCommandError(command));
return 200;
}
public static int CreateBugReport(List<string> args, Dictionary<string, string> options, Library.Utility.IFilter filter)
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;
@@ -686,7 +728,7 @@ namespace Duplicati.CommandLine
if (dbpath == null)
{
Console.WriteLine("No local database found, please add --{0}", "dbpath");
outwriter.WriteLine("No local database found, please add --{0}", "dbpath");
return 100;
}
else
@@ -698,24 +740,29 @@ namespace Duplicati.CommandLine
args = new List<string>(new string[] { "file://unused", "report" });
else if (args.Count == 1)
args.Add("report");
using(var i = new Library.Main.Controller(args[0], options, new ConsoleOutput(options)))
i.CreateLogDatabase(args[1]);
Console.WriteLine("Completed!");
Console.WriteLine();
Console.WriteLine("Please examine the log table of the database to see that no filenames are accidentially left over.");
Console.WriteLine("If you are concerned that your filenames may contain sensitive information,");
Console.WriteLine(" do not attach the database to an issue!!!");
Console.WriteLine();
using (var i = new Library.Main.Controller(args[0], options, new ConsoleOutput(outwriter, options)))
{
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(List<string> args, Dictionary<string, string> options, Library.Utility.IFilter filter)
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, "verbose") || Duplicati.Library.Utility.Utility.ParseBoolOption(options, "full-result");
if (args.Count < 1)
return PrintWrongNumberOfArguments(args, 1);
return PrintWrongNumberOfArguments(outwriter, args, 1);
// Support for not adding the --auth-username if possible
string dbpath;
@@ -735,130 +782,166 @@ namespace Duplicati.CommandLine
if (string.IsNullOrEmpty(passphrase))
options["no-encryption"] = "true";
}
Library.Interface.IListChangesResults result;
using(var i = new Library.Main.Controller(args[0], options, new ConsoleOutput(options)))
if (args.Count == 2)
result = i.ListChanges(null, args[1], null, filter);
else
result = i.ListChanges(args.Count > 1 ? args[1] : null, args.Count > 2 ? args[2] : null, null, filter);
Console.WriteLine("Listing changes");
Console.WriteLine(" {0}: {1}", result.BaseVersionIndex, result.BaseVersionTimestamp);
Console.WriteLine(" {0}: {1}", result.CompareVersionIndex, result.CompareVersionTimestamp);
Console.WriteLine();
Console.WriteLine("Size of backup {0}: {1}", result.BaseVersionIndex, Library.Utility.Utility.FormatSizeString(result.PreviousSize));
if (result.ChangeDetails != null)
{
Console.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)
{
Console.WriteLine(" {0} added entries:", count);
foreach(var n in added)
Console.WriteLine(" + {0}", n.Item3);
Console.WriteLine();
}
count = modified.Count();
if (count > 0)
{
Console.WriteLine(" {0} modified entries:", count);
foreach(var n in modified)
Console.WriteLine(" ~ {0}", n.Item3);
Console.WriteLine();
}
count = deleted.Count();
if (count > 0)
{
Console.WriteLine("{0} deleted entries:", count);
foreach(var n in deleted)
Console.WriteLine(" - {0}", n.Item3);
Console.WriteLine();
}
Console.WriteLine();
}
else
{
if (result.AddedFolders > 0)
Console.WriteLine(" Added folders: {0}", result.AddedFolders);
if (result.AddedSymlinks > 0)
Console.WriteLine(" Added symlinks: {0}", result.AddedSymlinks);
if (result.AddedFiles > 0)
Console.WriteLine(" Added files: {0}", result.AddedFiles);
if (result.DeletedFolders > 0)
Console.WriteLine(" Deleted folders: {0}", result.DeletedFolders);
if (result.DeletedSymlinks > 0)
Console.WriteLine(" Deleted symlinks: {0}", result.DeletedSymlinks);
if (result.DeletedFiles > 0)
Console.WriteLine(" Deleted files: {0}", result.DeletedFiles);
if (result.ModifiedFolders > 0)
Console.WriteLine(" Modified folders: {0}", result.ModifiedFolders);
if (result.ModifiedSymlinks > 0)
Console.WriteLine(" Modified symlinka: {0}", result.ModifiedSymlinks);
if (result.ModifiedFiles > 0)
Console.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)
Console.WriteLine(" No changes found");
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 i = new Library.Main.Controller(args[0], options, new ConsoleOutput(outwriter, options)))
{
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);
}
Console.WriteLine("Size of backup {0}: {1}", result.CompareVersionIndex, Library.Utility.Utility.FormatSizeString(result.CurrentSize));
return 0;
}
public static int TestFilters(List<string> args, Dictionary<string, string> options, Library.Utility.IFilter filter)
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)
{
Console.WriteLine("No source paths given");
outwriter.WriteLine("No source paths given");
return 200;
}
options["verbose"] = "true";
using(var i = new Library.Main.Controller("dummy://", options, new ConsoleOutput(options)))
using(var i = new Library.Main.Controller("dummy://", options, new ConsoleOutput(outwriter, options)))
{
setup(i);
var result = i.TestFilter(args.ToArray(), filter);
Console.WriteLine("Matched {0} files ({1})", result.FileCount, Duplicati.Library.Utility.Utility.FormatSizeString(result.FileSize));
outwriter.WriteLine("Matched {0} files ({1})", result.FileCount, Duplicati.Library.Utility.Utility.FormatSizeString(result.FileSize));
}
return 0;
}
public static int SystemInfo(List<string> args, Dictionary<string, string> options, Library.Utility.IFilter filter)
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)
{
Console.WriteLine("Command takes no arguments");
outwriter.WriteLine("Command takes no arguments");
return 200;
}
using(var i = new Library.Main.Controller("dummy://", options, new ConsoleOutput(options)))
foreach(var line in i.SystemInfo().Lines)
Console.WriteLine(line);
using (var i = new Library.Main.Controller("dummy://", options, new ConsoleOutput(outwriter, options)))
{
setup(i);
foreach (var line in i.SystemInfo().Lines)
outwriter.WriteLine(line);
}
Console.WriteLine("Know locales: {0}", string.Join(", ", Library.Localization.LocalizationService.AllLocales));
Console.WriteLine("Translated locales: {0}", string.Join(", ", Library.Localization.LocalizationService.SupportedCultures));
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(List<string> args, Dictionary<string, string> options, Library.Utility.IFilter filter)
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(args, 1);
return PrintWrongNumberOfArguments(outwriter, args, 1);
var backend = args[0];
var paths = args.Skip(1).ToArray();
@@ -869,35 +952,40 @@ namespace Duplicati.CommandLine
filter = new Library.Utility.FilterExpression(paths);
else
{
Console.WriteLine("You cannot combine filters and paths on the commandline");
outwriter.WriteLine("You cannot combine filters and paths on the commandline");
return 200;
}
}
else if (filter == null || filter.Empty)
{
Console.WriteLine("You must provide either filename filters, or a list of paths to remove");
outwriter.WriteLine("You must provide either filename filters, or a list of paths to remove");
return 200;
}
using (var i = new Library.Main.Controller(args[0], options, new ConsoleOutput(options)))
using (var i = new Library.Main.Controller(args[0], options, new ConsoleOutput(outwriter, options)))
{
setup(i);
i.PurgeFiles(filter);
}
return 0;
}
public static int ListBrokenFiles(List<string> args, Dictionary<string, string> options, Library.Utility.IFilter filter)
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(args, 1);
return PrintWrongNumberOfArguments(outwriter, args, 1);
var con = new ConsoleOutput(options);
var con = new ConsoleOutput(outwriter, options);
var previd = -1L;
var outputcount = 0L;
var verbose = Duplicati.Library.Utility.Utility.ParseBoolOption(options, "verbose");
var verbose = Duplicati.Library.Utility.Utility.ParseBoolOption(options, "verbose") || Duplicati.Library.Utility.Utility.ParseBoolOption(options, "full-result");
using (var i = new Library.Main.Controller(args[0], options, con))
i.ListBrokenFiles(filter, (id, time, count, path, size) =>
{
setup(i);
i.ListBrokenFiles(filter, (id, time, count, path, size) =>
{
if (previd != id)
{
@@ -910,42 +998,45 @@ namespace Duplicati.CommandLine
outputcount++;
if (outputcount >= 5 && !verbose && count != outputcount)
{
con.MessageEvent(string.Format("\t ... and {0} more, (use --{1} to list all)", count - outputcount, "verbose"));
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(List<string> args, Dictionary<string, string> options, Library.Utility.IFilter filter)
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(args, 1);
return PrintWrongNumberOfArguments(outwriter, args, 1);
using (var i = new Library.Main.Controller(args[0], options, new ConsoleOutput(options)))
using (var i = new Library.Main.Controller(args[0], options, new ConsoleOutput(outwriter, options)))
{
setup(i);
var res = i.PurgeBrokenFiles(filter);
}
return 0;
}
public static int SendMail(List<string> args, Dictionary<string, string> options, Library.Utility.IFilter filter)
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)
{
Console.WriteLine("Command takes no arguments");
outwriter.WriteLine("Command takes no arguments");
return 200;
}
using (var i = new Library.Main.Controller("dummy://", options, new ConsoleOutput(options)))
using (var i = new Library.Main.Controller("dummy://", options, new ConsoleOutput(outwriter, options)))
{
setup(i);
foreach (var l in i.SendMail().Lines)
Console.WriteLine(l);
outwriter.WriteLine(l);
}
return 0;
+15 -12
View File
@@ -17,6 +17,7 @@
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
using System;
using System.Collections.Generic;
using System.IO;
using Duplicati.Library.Main;
namespace Duplicati.CommandLine
@@ -28,9 +29,11 @@ namespace Duplicati.CommandLine
public bool QuietConsole { get; private set; }
public bool VerboseOutput { get; private set; }
public bool VerboseErrors { get; private set; }
public TextWriter Output { get; private set; }
public ConsoleOutput(Dictionary<string, string> options)
public ConsoleOutput(TextWriter output, Dictionary<string, string> options)
{
this.Output = output;
this.QuietConsole = Library.Utility.Utility.ParseBoolOption(options, "quiet-console");
this.VerboseOutput = Library.Utility.Utility.ParseBoolOption(options, "verbose");
this.VerboseErrors = Library.Utility.Utility.ParseBoolOption(options, "debug-output");
@@ -70,15 +73,15 @@ namespace Duplicati.CommandLine
if (type == BackendEventType.Started)
{
if (action == BackendActionType.Put)
Console.WriteLine(" Uploading file ({0}) ...", Library.Utility.Utility.FormatSizeString(size));
Output.WriteLine(" Uploading file ({0}) ...", Library.Utility.Utility.FormatSizeString(size));
else if (action == BackendActionType.Get)
Console.WriteLine(" Downloading file ({0}) ...", size < 0 ? "unknown" : Library.Utility.Utility.FormatSizeString(size));
Output.WriteLine(" Downloading file ({0}) ...", size < 0 ? "unknown" : Library.Utility.Utility.FormatSizeString(size));
else if (action == BackendActionType.List)
Console.WriteLine(" Listing remote folder ...");
Output.WriteLine(" Listing remote folder ...");
else if (action == BackendActionType.CreateFolder)
Console.WriteLine(" Creating remote folder ...");
Output.WriteLine(" Creating remote folder ...");
else if (action == BackendActionType.Delete)
Console.WriteLine(" Deleting file {0}{1} ...", path, size < 0 ? "" : (" (" + Library.Utility.Utility.FormatSizeString(size) + ")"));
Output.WriteLine(" Deleting file {0}{1} ...", path, size < 0 ? "" : (" (" + Library.Utility.Utility.FormatSizeString(size) + ")"));
}
}
@@ -86,38 +89,38 @@ namespace Duplicati.CommandLine
{
if (VerboseOutput)
lock(m_lock)
Console.WriteLine(message, args);
Output.WriteLine(message, args);
}
public void MessageEvent(string message)
{
if (!QuietConsole)
lock(m_lock)
Console.WriteLine(message);
Output.WriteLine(message);
}
public void RetryEvent(string message, Exception ex)
{
if (!QuietConsole)
lock(m_lock)
Console.WriteLine(ex == null ? message : string.Format("{0} => {1}", message, VerboseErrors ? ex.ToString() : ex.Message));
Output.WriteLine(ex == null ? message : string.Format("{0} => {1}", message, VerboseErrors ? ex.ToString() : ex.Message));
}
public void WarningEvent(string message, Exception ex)
{
if (!QuietConsole)
lock(m_lock)
Console.WriteLine(ex == null ? message : string.Format("{0} => {1}", message, VerboseErrors ? ex.ToString() : ex.Message));
Output.WriteLine(ex == null ? message : string.Format("{0} => {1}", message, VerboseErrors ? ex.ToString() : ex.Message));
}
public void ErrorEvent(string message, Exception ex)
{
if (!QuietConsole)
lock(m_lock)
Console.WriteLine(ex == null ? message : string.Format("{0} => {1}", message, VerboseErrors ? ex.ToString() : ex.Message));
Output.WriteLine(ex == null ? message : string.Format("{0} => {1}", message, VerboseErrors ? ex.ToString() : ex.Message));
}
public void DryrunEvent(string message)
{
if (!QuietConsole)
lock(m_lock)
Console.WriteLine(string.Format("[Dryrun]: {0}", message));
Output.WriteLine(string.Format("[Dryrun]: {0}", message));
}
#endregion
}
+24 -19
View File
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
@@ -57,13 +58,16 @@ namespace Duplicati.CommandLine
}
}
public static void PrintUsage(string topic, IDictionary<string, string> options)
public static void PrintUsage(TextWriter outwriter, string topic, IDictionary<string, string> options)
{
try
{
//Force translation off
System.Threading.Thread.CurrentThread.CurrentCulture = System.Globalization.CultureInfo.InvariantCulture;
System.Threading.Thread.CurrentThread.CurrentUICulture = System.Globalization.CultureInfo.InvariantCulture;
//Force translation off if this is from the commandline
if (Program.FROM_COMMANDLINE)
{
System.Threading.Thread.CurrentThread.CurrentCulture = System.Globalization.CultureInfo.InvariantCulture;
System.Threading.Thread.CurrentThread.CurrentUICulture = System.Globalization.CultureInfo.InvariantCulture;
}
}
catch
{
@@ -109,7 +113,7 @@ namespace Duplicati.CommandLine
foreach (Library.Interface.ICommandLineArgument arg in opts.SupportedCommands)
sorted.Add(arg.Name, arg);
foreach (Library.Interface.ICommandLineArgument arg in Program.SupportedCommands)
foreach (Library.Interface.ICommandLineArgument arg in Program.SupportedOptions)
sorted[arg.Name] = arg;
foreach (Library.Interface.ICommandLineArgument arg in sorted.Values)
@@ -125,7 +129,7 @@ namespace Duplicati.CommandLine
Library.Interface.CommandLineArgument.PrintArgument(lines, arg, " ");
foreach (Library.Interface.ICommandLineArgument arg in Program.SupportedCommands)
foreach (Library.Interface.ICommandLineArgument arg in Program.SupportedOptions)
Library.Interface.CommandLineArgument.PrintArgument(lines, arg, " ");
lines.Add("");
@@ -204,11 +208,11 @@ namespace Duplicati.CommandLine
tp = tp.Replace("%MODULEOPTIONS%", PrintArgsSimple(args));
else
{
Console.WriteLine("Topic not found: {0}", topic);
Console.WriteLine();
outwriter.WriteLine("Topic not found: {0}", topic);
outwriter.WriteLine();
//Prevent recursive lookups
if (topic != "help")
PrintUsage("help", new Dictionary<string, string>());
PrintUsage(outwriter, "help", new Dictionary<string, string>());
return;
}
}
@@ -216,7 +220,7 @@ namespace Duplicati.CommandLine
if (NAMEDOPTION_REGEX.IsMatch(tp))
tp = NAMEDOPTION_REGEX.Replace(tp, new Matcher().MathEvaluator);
PrintFormatted(tp.Split(new string[] { Environment.NewLine }, StringSplitOptions.None));
PrintFormatted(outwriter, tp.Split(new string[] { Environment.NewLine }, StringSplitOptions.None));
}
else
{
@@ -259,13 +263,13 @@ namespace Duplicati.CommandLine
if (lines.Count != 0)
{
PrintFormatted(lines);
PrintFormatted(outwriter, lines);
}
else
{
Console.WriteLine("Topic not found: {0}", topic);
Console.WriteLine();
PrintUsage("help", new Dictionary<string, string>());
outwriter.WriteLine("Topic not found: {0}", topic);
outwriter.WriteLine();
PrintUsage(outwriter, "help", new Dictionary<string, string>());
}
}
}
@@ -361,14 +365,15 @@ namespace Duplicati.CommandLine
}
private static void PrintFormatted(IEnumerable<string> lines)
private static void PrintFormatted(TextWriter outwriter, IEnumerable<string> lines)
{
int windowWidth = 80;
try
{
// This can go wrong if we have no attached console
windowWidth = Math.Max(12, Console.WindowWidth == 0 ? 80 : Console.WindowWidth);
if (outwriter == Console.Out)
windowWidth = Math.Max(12, Console.WindowWidth == 0 ? 80 : Console.WindowWidth);
}
catch { }
@@ -376,7 +381,7 @@ namespace Duplicati.CommandLine
{
if (string.IsNullOrEmpty(s) || s.Trim().Length == 0)
{
Console.WriteLine();
outwriter.WriteLine();
continue;
}
@@ -402,7 +407,7 @@ namespace Duplicati.CommandLine
len = ix;
}
Console.WriteLine(leadingSpaces + c.Substring(0, len).Trim());
outwriter.WriteLine(leadingSpaces + c.Substring(0, len).Trim());
c = c.Remove(0, len);
if (extraIndent)
{
@@ -421,7 +426,7 @@ namespace Duplicati.CommandLine
{
List<IList<Library.Interface.ICommandLineArgument>> foundArgs = new List<IList<Library.Interface.ICommandLineArgument>>();
foundArgs.Add(new Library.Main.Options(new Dictionary<string, string>()).SupportedCommands);
foundArgs.Add(Program.SupportedCommands);
foundArgs.Add(Program.SupportedOptions);
foreach (Duplicati.Library.Interface.IBackend backend in Library.DynamicLoader.BackendLoader.Backends)
if (backend.SupportedCommands != null)
+98 -74
View File
@@ -22,11 +22,14 @@ using System.Collections.Generic;
using System.Text;
using System.Linq;
using Duplicati.Library.Localization.Short;
using System.IO;
namespace Duplicati.CommandLine
{
public class Program
{
public static bool FROM_COMMANDLINE = false;
/// <summary>
/// The main entry point for the application.
/// </summary>
@@ -40,20 +43,9 @@ namespace Duplicati.CommandLine
public static int RealMain(string[] args)
{
Library.UsageReporter.Reporter.Initialize();
bool verboseErrors = false;
bool verbose = false;
FROM_COMMANDLINE = true;
try
{
List<string> cargs = new List<string>(args);
var tmpparsed = Library.Utility.FilterCollector.ExtractOptions(cargs);
var options = tmpparsed.Item1;
var filter = tmpparsed.Item2;
verboseErrors = Library.Utility.Utility.ParseBoolOption(options, "debug-output");
verbose = Library.Utility.Utility.ParseBoolOption(options, "verbose");
//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
if (Library.Utility.Utility.IsClientWindows)
@@ -68,17 +60,82 @@ namespace Duplicati.CommandLine
);
}
return RunCommandLine(Console.Out, Console.Error, c => { }, args);
}
finally
{
Library.UsageReporter.Reporter.ShutDown();
}
}
private static Dictionary<string, Func<TextWriter, Action<Library.Main.Controller>, List<string>, Dictionary<string, string>, Library.Utility.IFilter, int>> CommandMap
{
get
{
var knownCommands = new Dictionary<string, Func<TextWriter, Action<Library.Main.Controller>, List<string>, Dictionary<string, string>, Library.Utility.IFilter, int>>(StringComparer.InvariantCultureIgnoreCase);
knownCommands["help"] = Commands.Help;
knownCommands["example"] = Commands.Examples;
knownCommands["examples"] = Commands.Examples;
knownCommands["find"] = Commands.List;
knownCommands["list"] = Commands.List;
knownCommands["delete"] = Commands.Delete;
knownCommands["backup"] = Commands.Backup;
knownCommands["restore"] = Commands.Restore;
knownCommands["repair"] = Commands.Repair;
knownCommands["purge"] = Commands.PurgeFiles;
knownCommands["list-broken-files"] = Commands.ListBrokenFiles;
knownCommands["purge-broken-files"] = Commands.PurgeBrokenFiles;
knownCommands["compact"] = Commands.Compact;
knownCommands["create-report"] = Commands.CreateBugReport;
knownCommands["compare"] = Commands.ListChanges;
knownCommands["test"] = Commands.Test;
knownCommands["verify"] = Commands.Test;
knownCommands["test-filters"] = Commands.TestFilters;
knownCommands["test-filter"] = Commands.TestFilters;
knownCommands["affected"] = Commands.Affected;
knownCommands["system-info"] = Commands.SystemInfo;
knownCommands["systeminfo"] = Commands.SystemInfo;
knownCommands["send-mail"] = Commands.SendMail;
return knownCommands;
}
}
public static IEnumerable<string> SupportedCommands { get { return CommandMap.Keys; } }
public static int RunCommandLine(TextWriter outwriter, TextWriter errwriter, Action<Library.Main.Controller> setup, string[] args)
{
bool verboseErrors = false;
bool verbose = false;
try
{
List<string> cargs = new List<string>(args);
var tmpparsed = Library.Utility.FilterCollector.ExtractOptions(cargs);
var options = tmpparsed.Item1;
var filter = tmpparsed.Item2;
verboseErrors = Library.Utility.Utility.ParseBoolOption(options, "debug-output");
verbose = Library.Utility.Utility.ParseBoolOption(options, "verbose");
if (cargs.Count == 1 && string.Equals(cargs[0], "changelog", StringComparison.InvariantCultureIgnoreCase))
{
var path = System.IO.Path.Combine(System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location), "changelog.txt");
Console.WriteLine(System.IO.File.ReadAllText(path));
outwriter.WriteLine(System.IO.File.ReadAllText(path));
return 0;
}
foreach (string internaloption in Library.Main.Options.InternalOptions)
if (options.ContainsKey(internaloption))
{
Console.WriteLine(Strings.Program.InternalOptionUsedError(internaloption));
outwriter.WriteLine(Strings.Program.InternalOptionUsedError(internaloption));
return 200;
}
@@ -103,7 +160,7 @@ namespace Duplicati.CommandLine
options.Remove("parameterfile");
}
if (!ReadOptionsFromFile(filename, ref filter, cargs, options))
if (!ReadOptionsFromFile(outwriter, filename, ref filter, cargs, options))
return 100;
}
@@ -135,56 +192,27 @@ namespace Duplicati.CommandLine
options["auth-username"] = System.Environment.GetEnvironmentVariable("AUTH_USERNAME");
}
var knownCommands = new Dictionary<string, Func<List<string>, Dictionary<string, string>, Library.Utility.IFilter, int>>(StringComparer.InvariantCultureIgnoreCase);
knownCommands["help"] = Commands.Help;
knownCommands["example"] = Commands.Examples;
knownCommands["examples"] = Commands.Examples;
knownCommands["find"] = Commands.List;
knownCommands["list"] = Commands.List;
knownCommands["delete"] = Commands.Delete;
knownCommands["backup"] = Commands.Backup;
knownCommands["restore"] = Commands.Restore;
knownCommands["repair"] = Commands.Repair;
knownCommands["purge"] = Commands.PurgeFiles;
knownCommands["list-broken-files"] = Commands.ListBrokenFiles;
knownCommands["purge-broken-files"] = Commands.PurgeBrokenFiles;
knownCommands["compact"] = Commands.Compact;
knownCommands["create-report"] = Commands.CreateBugReport;
knownCommands["compare"] = Commands.ListChanges;
knownCommands["test"] = Commands.Test;
knownCommands["verify"] = Commands.Test;
knownCommands["test-filters"] = Commands.TestFilters;
knownCommands["test-filter"] = Commands.TestFilters;
knownCommands["affected"] = Commands.Affected;
knownCommands["system-info"] = Commands.SystemInfo;
knownCommands["systeminfo"] = Commands.SystemInfo;
knownCommands["send-mail"] = Commands.SendMail;
var knownCommands = CommandMap;
if (!isHelp && verbose)
{
Console.WriteLine("Input command: {0}", command);
outwriter.WriteLine("Input command: {0}", command);
Console.WriteLine("Input arguments: ");
outwriter.WriteLine("Input arguments: ");
foreach (var a in cargs)
Console.WriteLine("\t{0}", a);
Console.WriteLine();
outwriter.WriteLine("\t{0}", a);
outwriter.WriteLine();
Console.WriteLine("Input options: ");
outwriter.WriteLine("Input options: ");
foreach (var n in options)
Console.WriteLine("{0}: {1}", n.Key, n.Value);
Console.WriteLine();
outwriter.WriteLine("{0}: {1}", n.Key, n.Value);
outwriter.WriteLine();
}
Duplicati.Library.Utility.TempFile.RemoveOldApplicationTempFiles((path, ex) =>
{
if (verbose)
Console.WriteLine(string.Format("Failed to delete temp file: {0}", path));
outwriter.WriteLine(string.Format("Failed to delete temp file: {0}", path));
});
var autoupdate = Library.Utility.Utility.ParseBoolOption(options, "auto-update");
@@ -192,9 +220,9 @@ namespace Duplicati.CommandLine
if (knownCommands.ContainsKey(command))
{
var res = knownCommands[command](cargs, options, filter);
var res = knownCommands[command](outwriter, setup, cargs, options, filter);
if (autoupdate)
if (autoupdate && FROM_COMMANDLINE)
{
var update = Library.AutoUpdater.UpdaterManager.LastUpdateCheckVersion;
if (update == null)
@@ -202,7 +230,7 @@ namespace Duplicati.CommandLine
if (update != null && update.Version != Library.AutoUpdater.UpdaterManager.SelfVersion.Version)
{
Console.WriteLine("Found update \"{0}\", downloading ...", update.Displayname);
outwriter.WriteLine("Found update \"{0}\", downloading ...", update.Displayname);
long lastpg = 0;
Library.AutoUpdater.UpdaterManager.DownloadAndUnpackUpdate(update, f =>
{
@@ -210,10 +238,10 @@ namespace Duplicati.CommandLine
if (Math.Abs(npg - lastpg) >= 5 || (npg == 100 && lastpg != 100))
{
lastpg = npg;
Console.WriteLine("Downloading {0}% ...", npg);
outwriter.WriteLine("Downloading {0}% ...", npg);
}
});
Console.WriteLine("Update \"{0}\" ({1}) installed, using on next launch", update.Displayname, update.Version);
outwriter.WriteLine("Update \"{0}\" ({1}) installed, using on next launch", update.Displayname, update.Version);
}
}
@@ -221,7 +249,7 @@ namespace Duplicati.CommandLine
}
else
{
Commands.PrintInvalidCommand(command, cargs);
Commands.PrintInvalidCommand(outwriter, command, cargs);
return 200;
}
}
@@ -234,36 +262,32 @@ namespace Duplicati.CommandLine
if (ex is Duplicati.Library.Interface.UserInformationException && !verboseErrors)
{
Console.Error.WriteLine();
Console.WriteLine(ex.Message);
errwriter.WriteLine();
errwriter.WriteLine(ex.Message);
}
else if (!(ex is Library.Interface.CancelException))
{
Console.Error.WriteLine();
Console.Error.WriteLine(ex.ToString());
errwriter.WriteLine();
errwriter.WriteLine(ex.ToString());
}
else
{
Console.Error.WriteLine(Strings.Program.UnhandledException(ex.ToString()));
errwriter.WriteLine(Strings.Program.UnhandledException(ex.ToString()));
while (ex.InnerException != null)
{
ex = ex.InnerException;
Console.Error.WriteLine();
Console.Error.WriteLine(Strings.Program.UnhandledInnerException(ex.ToString()));
errwriter.WriteLine();
errwriter.WriteLine(Strings.Program.UnhandledInnerException(ex.ToString()));
}
}
//Error = 100
return 100;
}
finally
{
Library.UsageReporter.Reporter.ShutDown();
}
}
public static IList<Library.Interface.ICommandLineArgument> SupportedCommands
public static IList<Library.Interface.ICommandLineArgument> SupportedOptions
{
get
{
@@ -278,7 +302,7 @@ namespace Duplicati.CommandLine
}
}
private static bool ReadOptionsFromFile(string filename, ref Library.Utility.IFilter filter, List<string> cargs, Dictionary<string, string> options)
private static bool ReadOptionsFromFile(TextWriter outwriter, string filename, ref Library.Utility.IFilter filter, List<string> cargs, Dictionary<string, string> options)
{
try
{
@@ -353,13 +377,13 @@ namespace Duplicati.CommandLine
if (cargs.Count >= 1 && cargs[0].Equals("backup", StringComparison.InvariantCultureIgnoreCase))
cargs.AddRange(newsource);
else if (newsource.Count > 0 && Library.Utility.Utility.ParseBoolOption(options, "verbose"))
Console.WriteLine(Strings.Program.SkippingSourceArgumentsOnNonBackupOperation);
outwriter.WriteLine(Strings.Program.SkippingSourceArgumentsOnNonBackupOperation);
return true;
}
catch (Exception e)
{
Console.WriteLine(Strings.Program.FailedToParseParametersFileError(filename, e.Message));
outwriter.WriteLine(Strings.Program.FailedToParseParametersFileError(filename, e.Message));
return false;
}
}
+4 -4
View File
@@ -388,19 +388,19 @@ 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);
});
}
@@ -32,17 +32,31 @@ namespace Duplicati.Library.Main.Operation
m_result = result;
}
public void Run(List<string> args)
public void Run(List<string> args, Action<Duplicati.Library.Interface.IListAffectedResults> callback = null)
{
using(var db = new Database.LocalListAffectedDatabase(m_options.Dbpath))
{
m_result.SetDatabase(db);
m_result.SetResult(
db.GetFilesets(args).OrderByDescending(x => x.Time).ToArray(),
db.GetFiles(args).ToArray(),
db.GetLogLines(args).ToArray(),
db.GetVolumes(args).ToArray()
);
if (callback == null)
{
m_result.SetResult(
db.GetFilesets(args).OrderByDescending(x => x.Time).ToArray(),
db.GetFiles(args).ToArray(),
db.GetLogLines(args).ToArray(),
db.GetVolumes(args).ToArray()
);
}
else
{
m_result.SetResult(
db.GetFilesets(args).OrderByDescending(x => x.Time),
db.GetFiles(args),
db.GetLogLines(args),
db.GetVolumes(args)
);
callback(m_result);
}
}
}
}
@@ -55,7 +55,7 @@ namespace Duplicati.Library.Main.Operation
return res;
}
public void Run(string baseVersion, string compareVersion, IEnumerable<string> filterstrings = null, Library.Utility.IFilter compositefilter = null)
public void Run(string baseVersion, string compareVersion, IEnumerable<string> filterstrings = null, Library.Utility.IFilter compositefilter = null, Action<IListChangesResults, IEnumerable<Tuple<Library.Interface.ListChangesChangeType, Library.Interface.ListChangesElementType, string>>> callback = null)
{
var filter = Library.Utility.JoinedFilterExpression.Join(new Library.Utility.FilterExpression(filterstrings), compositefilter);
@@ -149,19 +149,23 @@ namespace Duplicati.Library.Main.Operation
var changes = storageKeeper.CreateChangeCountReport();
var sizes = storageKeeper.CreateChangeSizeReport();
var lst = (m_options.Verbose || m_options.FullResult || callback != null) ?
(from n in storageKeeper.CreateChangedFileReport()
select n) : null;
m_result.SetResult(
baseVersionTime, baseVersionIndex, compareVersionTime, compareVersionIndex,
changes.AddedFolders, changes.AddedSymlinks, changes.AddedFiles,
changes.DeletedFolders, changes.DeletedSymlinks, changes.DeletedFiles,
changes.ModifiedFolders, changes.ModifiedSymlinks, changes.ModifiedFiles,
sizes.AddedSize, sizes.DeletedSize, sizes.PreviousSize, sizes.CurrentSize,
m_options.Verbose ?
(from n in storageKeeper.CreateChangedFileReport()
select n).ToArray() : null
(lst == null || callback != null) ? null : lst.ToArray()
);
if (callback != null)
callback(m_result, lst);
return;
}
}
+7 -1
View File
@@ -469,6 +469,7 @@ namespace Duplicati.Library.Main
new CommandLineArgument("compression-extension-file", CommandLineArgument.ArgumentType.Path, Strings.Options.CompressionextensionfileShort, Strings.Options.CompressionextensionfileLong(DEFAULT_COMPRESSED_EXTENSION_FILE), DEFAULT_COMPRESSED_EXTENSION_FILE),
new CommandLineArgument("verbose", CommandLineArgument.ArgumentType.Boolean, Strings.Options.VerboseShort, Strings.Options.VerboseLong, "false"),
new CommandLineArgument("full-result", CommandLineArgument.ArgumentType.Boolean, Strings.Options.FullresultShort, Strings.Options.FullresultLong, "false"),
new CommandLineArgument("overwrite", CommandLineArgument.ArgumentType.Boolean, Strings.Options.OverwriteShort, Strings.Options.OverwriteLong, "false"),
@@ -1279,7 +1280,12 @@ namespace Duplicati.Library.Main
/// A value indicating if the output should be verbose
/// </summary>
public bool Verbose { get { return GetBool("verbose"); } }
/// <summary>
/// A value indicating if the returned value should not be truncated
/// </summary>
public bool FullResult { get { return GetBool("full-result"); } }
/// <summary>
/// A value indicating restored files overwrite existing ones
/// </summary>
+2
View File
@@ -179,6 +179,8 @@ namespace Duplicati.Library.Main.Strings
public static string OverwriteLong { get { return LC.L(@"Use this option to overwrite target files when restoring, if this option is not set the files will be restored with a timestamp and a number appended."); } }
public static string VerboseShort { get { return LC.L(@"Output more progress information"); } }
public static string VerboseLong { get { return LC.L(@"Use this option to increase the amount of output generated when running an option. Generally this option will produce a line for each file processed."); } }
public static string FullresultShort { get { return LC.L(@"Output full results"); } }
public static string FullresultLong { get { return LC.L(@"Use this option to increase the amount of output generated as the result of the operation, including all filenames."); } }
public static string UploadverificationfileShort { get { return LC.L(@"Determine if verification files are uploaded"); } }
public static string UploadverificationfileLong { get { return LC.L(@"Use this option to upload a verification file after changing the remote storage. The file is not encrypted and contains the size and SHA256 hashes of all the remote files and can be used to verify the integrity of the files."); } }
public static string BackendtestsamplesShort { get { return LC.L(@"The number of samples to test after a backup"); } }
@@ -49,7 +49,8 @@ namespace Duplicati.Server.Serialization
Compact,
CreateReport,
ListRemote,
Delete
Delete,
CustomRunner
}
public enum SuggestedStatusIcon
+1
View File
@@ -122,6 +122,7 @@
<Compile Include="WebServer\RESTMethods\Licenses.cs" />
<Compile Include="WebServer\CaptchaUtil.cs" />
<Compile Include="WebServer\RESTMethods\Captcha.cs" />
<Compile Include="WebServer\RESTMethods\CommandLine.cs" />
</ItemGroup>
<ItemGroup>
<None Include="app.config" />
+96 -18
View File
@@ -33,6 +33,7 @@ namespace Duplicati.Server
void Abort();
void Pause();
void Resume();
void SetController(Duplicati.Library.Main.Controller controller);
}
private class RunnerData : IRunnerData
@@ -48,7 +49,12 @@ namespace Duplicati.Server
public long TaskID { get { return m_taskID; } }
internal Duplicati.Library.Main.Controller Controller { get; set; }
public void SetController(Duplicati.Library.Main.Controller controller)
{
Controller = controller;
}
public void Stop()
{
var c = Controller;
@@ -84,6 +90,26 @@ namespace Duplicati.Server
m_taskID = System.Threading.Interlocked.Increment(ref RunnerTaskID);
}
}
private class CustomRunnerTask : RunnerData
{
public readonly Action Run;
public CustomRunnerTask(Action runner)
: base()
{
if (runner == null)
throw new ArgumentNullException("runner");
Run = runner;
Operation = DuplicatiOperation.CustomRunner;
Backup = new Database.Backup();
}
}
public static IRunnerData CreateCustomTask(Action runner)
{
return new CustomRunnerTask(runner);
}
public static IRunnerData CreateTask(Duplicati.Server.Serialization.DuplicatiOperation operation, Duplicati.Server.Serialization.Interface.IBackup backup, IDictionary<string, string> extraOptions = null, string[] filterStrings = null)
{
@@ -339,9 +365,61 @@ namespace Duplicati.Server
return cmd.ToString();
}
public static string[] GetCommandLineParts(IRunnerData data)
{
var backup = data.Backup;
var options = ApplyOptions(backup, data.Operation, GetCommonOptions(backup, data.Operation));
if (data.ExtraOptions != null)
foreach (var k in data.ExtraOptions)
options[k.Key] = k.Value;
var cf = Program.DataConnection.Filters;
var bf = backup.Filters;
var sources =
(from n in backup.Sources
let p = SpecialFolders.ExpandEnvironmentVariables(n)
where !string.IsNullOrWhiteSpace(p)
select p).ToArray();
var parts = new List<string>();
parts.Add(backup.TargetURL);
parts.AddRange(sources);
foreach (var opt in options)
parts.Add(string.Format("--{0}={1}", opt.Key, opt.Value));
if (cf != null)
foreach (var f in cf)
parts.Add(string.Format("--{0}={1}", f.Include ? "include" : "exclude", f.Expression));
if (bf != null)
foreach (var f in bf)
parts.Add(string.Format("--{0}={1}", f.Include ? "include" : "exclude", f.Expression));
return parts.ToArray();
}
public static Duplicati.Library.Interface.IBasicResults Run(IRunnerData data, bool fromQueue)
{
if (data is CustomRunnerTask)
{
try
{
((CustomRunnerTask)data).Run();
}
catch(Exception ex)
{
Program.DataConnection.LogError(string.Empty, "Failed while executing custom task", ex);
}
return null;
}
var backup = data.Backup;
Duplicati.Library.Utility.TempFolder tempfolder = null;
@@ -350,14 +428,14 @@ namespace Duplicati.Server
try
{
var options = ApplyOptions(backup, data.Operation, GetCommonOptions(backup, data.Operation));
var sink = new MessageSink(data.TaskID, backup.ID);
if (fromQueue)
{
Program.GenerateProgressState = () => sink.Copy();
Program.StatusEventNotifyer.SignalNewEvent();
}
var options = ApplyOptions(backup, data.Operation, GetCommonOptions(backup, data.Operation));
if (data.ExtraOptions != null)
foreach(var k in data.ExtraOptions)
options[k.Key] = k.Value;
@@ -498,25 +576,25 @@ namespace Duplicati.Server
return r;
}
case DuplicatiOperation.Delete:
{
if (Library.Utility.Utility.ParseBoolOption(data.ExtraOptions, "delete-remote-files"))
controller.DeleteAllRemoteFiles();
if (Library.Utility.Utility.ParseBoolOption(data.ExtraOptions, "delete-local-db"))
case DuplicatiOperation.Delete:
{
string dbpath;
options.TryGetValue("db-path", out dbpath);
if (Library.Utility.Utility.ParseBoolOption(data.ExtraOptions, "delete-remote-files"))
controller.DeleteAllRemoteFiles();
if (!string.IsNullOrWhiteSpace(dbpath) && System.IO.File.Exists(dbpath))
System.IO.File.Delete(dbpath);
if (Library.Utility.Utility.ParseBoolOption(data.ExtraOptions, "delete-local-db"))
{
string dbpath;
options.TryGetValue("db-path", out dbpath);
if (!string.IsNullOrWhiteSpace(dbpath) && System.IO.File.Exists(dbpath))
System.IO.File.Delete(dbpath);
}
Program.DataConnection.DeleteBackup(backup);
Program.Scheduler.Reschedule();
return null;
}
Program.DataConnection.DeleteBackup(backup);
Program.Scheduler.Reschedule();
return null;
}
default:
default:
//TODO: Log this
return null;
}
@@ -65,10 +65,10 @@ namespace Duplicati.Server.Serializable
get
{
var t = Program.WorkThread.CurrentTask;
if (t == null || t.Backup == null)
if (t == null)
return null;
else
return new Tuple<long, string>(t.TaskID, t.Backup.ID);
return new Tuple<long, string>(t.TaskID, t.Backup == null ? null : t.Backup.ID);
}
}
@@ -119,10 +119,21 @@ namespace Duplicati.Server.WebServer.RESTMethods
private void Export(IBackup backup, RequestInfo info)
{
var cmdline = Library.Utility.Utility.ParseBool(info.Request.QueryString["cmdline"].Value, false);
var argsonly = Library.Utility.Utility.ParseBool(info.Request.QueryString["argsonly"].Value, false);
if (cmdline)
{
info.OutputOK(new { Command = Runner.GetCommandLine(Runner.CreateTask(DuplicatiOperation.Backup, backup)) });
}
else if (argsonly)
{
var parts = Runner.GetCommandLineParts(Runner.CreateTask(DuplicatiOperation.Backup, backup));
info.OutputOK(new {
Backend = parts.First(),
Arguments = parts.Skip(1).Where(x => !x.StartsWith("--", StringComparison.Ordinal)),
Options = parts.Skip(1).Where(x => x.StartsWith("--", StringComparison.Ordinal))
});
}
else
{
var passphrase = info.Request.QueryString["passphrase"].Value;
@@ -0,0 +1,249 @@
// Copyright (C) 2017, The Duplicati Team
// http://www.duplicati.com, info@duplicati.com
//
// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace Duplicati.Server.WebServer.RESTMethods
{
public class CommandLine : IRESTMethodGET, IRESTMethodPOST
{
private class LogWriter : System.IO.TextWriter
{
private readonly ActiveRun m_target;
private readonly StringBuilder m_sb = new StringBuilder();
private int m_newlinechars = 0;
public LogWriter(ActiveRun target)
{
m_target = target;
}
public override Encoding Encoding { get { return System.Text.Encoding.UTF8; } }
public override void Write(char value)
{
lock(m_target.Lock)
{
m_sb.Append(value);
if (NewLine[m_newlinechars] == value)
{
m_newlinechars++;
if (m_newlinechars == NewLine.Length)
WriteLine(string.Empty);
}
else
m_newlinechars = 0;
}
}
public override void WriteLine(string value)
{
value = value ?? string.Empty;
lock(m_target.Lock)
{
m_target.LastAccess = DateTime.Now;
if (m_sb.Length != 0)
{
m_target.Log.Add(m_sb.ToString() + value);
m_sb.Length = 0;
m_newlinechars = 0;
}
else
{
m_target.Log.Add(value);
}
}
}
}
private class ActiveRun
{
public readonly string ID = Guid.NewGuid().ToString();
public DateTime LastAccess = DateTime.Now;
public Library.Utility.FileBackedStringList Log = new Library.Utility.FileBackedStringList();
public Runner.IRunnerData Task;
public LogWriter Writer;
public readonly object Lock = new object();
public bool Finished = false;
public bool Started = false;
public System.Threading.Thread Thread;
}
private readonly Dictionary<string, ActiveRun> m_activeItems = new Dictionary<string, ActiveRun>();
private System.Threading.Tasks.Task m_cleanupTask;
public void POST(string key, RequestInfo info)
{
if (string.IsNullOrWhiteSpace(key))
{
string[] args;
using (var sr = new StreamReader(info.Request.Body, System.Text.Encoding.UTF8, true))
args = Newtonsoft.Json.JsonConvert.DeserializeObject<string[]>(sr.ReadToEnd());
var k = new ActiveRun();
k.Writer = new LogWriter(k);
m_activeItems[k.ID] = k;
StartCleanupTask();
k.Task = Runner.CreateCustomTask(() =>
{
try
{
k.Thread = System.Threading.Thread.CurrentThread;
k.Started = true;
var code = Duplicati.CommandLine.Program.RunCommandLine(k.Writer, k.Writer, c => k.Task.SetController(c), args);
k.Writer.WriteLine(string.Format("Return code: {0}", code));
}
catch (Exception ex)
{
var rx = ex;
if (rx is System.Reflection.TargetInvocationException)
rx = rx.InnerException;
if (rx is Library.Interface.UserInformationException)
k.Log.Add(rx.Message);
else
k.Log.Add(rx.ToString());
throw rx;
}
finally
{
k.Finished = true;
k.Thread = null;
}
});
Program.WorkThread.AddTask(k.Task);
info.OutputOK(new
{
ID = k.ID
});
}
else
{
if (!key.EndsWith("/abort", StringComparison.InvariantCultureIgnoreCase))
{
info.ReportClientError("Only abort commands are allowed");
return;
}
key = key.Substring(0, key.Length - "/abort".Length);
if (string.IsNullOrWhiteSpace(key))
{
info.ReportClientError("No task key found");
return;
}
ActiveRun t;
if (!m_activeItems.TryGetValue(key, out t))
{
info.OutputError(code: System.Net.HttpStatusCode.NotFound);
return;
}
var tr = t.Thread;
if (tr != null)
tr.Abort();
info.OutputOK();
}
}
private void StartCleanupTask()
{
if (m_cleanupTask == null || m_cleanupTask.IsCompleted || m_cleanupTask.IsFaulted || m_cleanupTask.IsCanceled)
m_cleanupTask = RunCleanupAsync();
}
private async System.Threading.Tasks.Task RunCleanupAsync()
{
while (m_activeItems.Count > 0)
{
var oldest = m_activeItems.Values.OrderBy(x => x.LastAccess).FirstOrDefault();
if (oldest != null && DateTime.Now - oldest.LastAccess > TimeSpan.FromMinutes(5))
{
m_activeItems.Remove(oldest.ID);
oldest.Log.Dispose();
// Fix all expired, or stop running
continue;
}
await System.Threading.Tasks.Task.Delay(TimeSpan.FromSeconds(60));
}
}
public void GET(string key, RequestInfo info)
{
if (string.IsNullOrWhiteSpace(key))
{
info.OutputOK(
Duplicati.CommandLine.Program.SupportedCommands
);
}
else
{
ActiveRun t;
if (!m_activeItems.TryGetValue(key, out t))
{
info.OutputError(code: System.Net.HttpStatusCode.NotFound);
return;
}
int pagesize;
int offset;
int.TryParse(info.Request.QueryString["pagesize"].Value, out pagesize);
int.TryParse(info.Request.QueryString["offset"].Value, out offset);
pagesize = Math.Max(10, Math.Min(500, pagesize));
offset = Math.Max(0, offset);
var items = new List<string>();
long count;
bool started;
bool finished;
lock(t.Lock)
{
t.LastAccess = DateTime.Now;
count = t.Log.Count;
offset = Math.Min((int)count, offset);
items.AddRange(t.Log.Skip(offset).Take(pagesize));
finished = t.Finished;
started = t.Started;
}
info.OutputOK(new
{
Pagesize = pagesize,
Offset = offset,
Count = count,
Items = items,
Finished = finished,
Started = started
});
}
}
}
}
+1
View File
@@ -92,6 +92,7 @@
<script type="text/javascript" src="scripts/controllers/RestoreWizardController.js"></script>
<script type="text/javascript" src="scripts/controllers/AddWizardController.js"></script>
<script type="text/javascript" src="scripts/controllers/PauseController.js"></script>
<script type="text/javascript" src="scripts/controllers/CommandlineController.js"></script>
<script type="text/javascript" src="scripts/filters/timeremaining.js"></script>
<script type="text/javascript" src="scripts/filters/highlight.js"></script>
@@ -74,6 +74,15 @@ backupApp.config(['$routeProvider',
when('/localdb/:backupid', {
templateUrl: 'templates/localdatabase.html'
}).
when('/commandline', {
templateUrl: 'templates/commandline.html'
}).
when('/commandline/:backupid', {
templateUrl: 'templates/commandline.html'
}).
when('/commandline/view/:viewid', {
templateUrl: 'templates/commandline.html'
}).
otherwise({
templateUrl: 'templates/home.html'
//redirectTo: '/home'
@@ -0,0 +1,174 @@
backupApp.controller('CommandlineController', function($scope, $routeParams, $location, $timeout, SystemInfo, ServerStatus, AppUtils, AppService, DialogService, gettextCatalog) {
$scope.SystemInfo = SystemInfo.watch($scope);
$scope.ServerStatus = ServerStatus;
$scope.serverstate = ServerStatus.watch($scope);
$scope.ExtendedOptions = [];
$scope.Arguments = [];
$scope.Running = false;
$scope.Mode = 'submit';
$scope.ViewLines = [];
$scope.Started = null;
$scope.Finished = false;
$scope.RawFinished = false;
var viewid = null;
var view_offset = 0;
var view_refreshing = false;
var view_timer = null;
var scope = $scope;
function reloadOptionsList()
{
var opts = AppUtils.parseOptionStrings($scope.ExtendedOptions || []);
var encmodule = opts['encryption-module'] || opts['--encryption-module'] || '';
var compmodule = opts['compression-module'] || opts['--compression-module'] || 'zip';
var backmodule = $scope.TargetURL || '';
var ix = backmodule.indexOf(':');
if (ix > 0)
backmodule = backmodule.substr(0, ix);
$scope.ExtendedOptionList = AppUtils.buildOptionList($scope.SystemInfo, encmodule, compmodule, backmodule);
};
$scope.$watchCollection("ExtendedOptions", reloadOptionsList);
$scope.$watch("TargetURL", reloadOptionsList);
$scope.$on('systeminfochanged', reloadOptionsList);
reloadOptionsList();
var scope = $scope;
$scope.HideEditUri = function() {
scope.EditUriState = false;
};
$scope.run = function() {
var opts = AppUtils.parseOptionStrings($scope.ExtendedOptions || []);
var combined = angular.copy($scope.Arguments || []);
if (($scope.TargetURL || '').trim().length != 0)
combined.unshift($scope.TargetURL);
combined.unshift($scope.Command);
for(n in opts) {
if (opts[n] == null)
combined.push(n);
else
combined.push(n + '=' + opts[n]);
}
options = {
headers: {'Content-Type': 'application/json; charset=utf-8'},
responseType: 'text'
};
AppService.post('/commandline', combined, options).then(
function(resp) {
$location.path('/commandline/view/' + resp.data.ID);
},
function(resp) {
DialogService.dialog(gettextCatalog.getString('Error'), gettextCatalog.getString('Failed to connect: {{message}}', { message: message }));
}
);
};
$scope.Abort = function()
{
AppService.post('/commandline/' + viewid + '/abort' ).then(function(resp) {
}, function() {
AppUtils.connectionError.apply(AppUtils, arguments);
});
};
function FetchOutputLines()
{
if (view_refreshing)
return;
view_refreshing = true;
if (view_timer != null) {
$timeout.cancel(view_timer);
view_timer = null;
}
AppService.get('/commandline/' + viewid + '?pagesize=100&offset=' + view_offset).then(function(resp) {
$scope.ViewLines.push.apply($scope.ViewLines, resp.data.Items);
view_offset = view_offset + resp.data.Items.length;
while ($scope.ViewLines.length > 2000)
$scope.ViewLines.shift();
$scope.Started = resp.data.Started;
$scope.Finished = resp.data.Finished && view_offset == resp.data.Count;
$scope.RawFinished = resp.data.Finished;
view_refreshing = false;
var wait_time = 2000;
// Fetch more as we are not empty
if (resp.data.Items.length != 0)
wait_time = 100;
// All done, slowly keep the data alive
else if (resp.data.Finished)
wait_time = 10000;
view_timer = $timeout(FetchOutputLines, wait_time);
}, function(resp) {
$scope.Started = true;
if (resp.status == 404) {
$scope.ViewLines.push('Connection lost, data has expired ...');
$scope.Finished = true;
} else {
$scope.ViewLines.push('Connection error, retry in 2 sec ...');
view_refreshing = false;
view_timer = $timeout(FetchOutputLines, 2000);
}
});
};
AppService.get('/commandline').then(function(resp) {
var cmds = [];
for (var i = resp.data.length - 1; i >= 0; i--)
cmds.push(resp.data[i]);
$scope.SupportedCommands = cmds;
$scope.Command = 'help';
}, function() {
AppUtils.connectionError.apply(AppUtils, arguments);
$location.path('/');
});
if ($routeParams.viewid != null) {
$scope.Mode = 'view';
viewid = $routeParams.viewid;
view_offset = 0;
FetchOutputLines();
}
if ($routeParams.backupid != null) {
AppService.get('/backup/' + $routeParams.backupid + '/export?argsonly=true').then(
function(resp) {
$scope.TargetURL = resp.data.Backend;
$scope.Arguments = resp.data.Arguments;
$scope.ExtendedOptions = resp.data.Options;
$scope.Command = 'backup';
},
function(resp) {
var message = resp.statusText;
if (resp.data != null && resp.data.Message != null)
message = resp.data.Message;
DialogService.dialog(gettextCatalog.getString('Error'), gettextCatalog.getString('Failed to connect: {{message}}', { message: message }));
}
);
}
});
@@ -49,6 +49,10 @@ backupApp.controller('HomeController', function ($scope, $location, ServerStatus
$location.path('/log/' + id);
};
$scope.doCommandLine = function(id, name) {
$location.path('/commandline/' + id);
};
$scope.doCreateBugReport = function(id, name) {
AppService.post('/backup/' + id + '/createreport');
};
@@ -1,4 +1,4 @@
backupApp.controller('LogController', function($scope, $routeParams, SystemInfo, ServerStatus, AppService, DialogService, BackupList, gettextCatalog) {
backupApp.controller('LogController', function($scope, $routeParams, $timeout, SystemInfo, ServerStatus, AppService, DialogService, BackupList, gettextCatalog) {
$scope.state = ServerStatus.watch($scope);
$scope.BackupID = $routeParams.backupid;
$scope.SystemInfo = SystemInfo.watch($scope);
@@ -13,7 +13,7 @@ backupApp.controller('LogController', function($scope, $routeParams, SystemInfo,
}
if (liveRefreshTimer != null) {
clearTimeout(liveRefreshTimer);
$timeout.cancel(liveRefreshTimer);
liveRefreshTimer = null;
}
@@ -39,11 +39,11 @@ backupApp.controller('LogController', function($scope, $routeParams, SystemInfo,
updateLivePoll();
else
if ($scope.Page == 'live' && $scope.LiveLogLevel != '')
liveRefreshTimer = setTimeout(updateLivePoll, 3000);
liveRefreshTimer = $timeout(updateLivePoll, 3000);
}, function(resp) {
if ($scope.Page == 'live' && $scope.LiveLogLevel != '')
liveRefreshTimer = setTimeout(updateLivePoll, 3000);
liveRefreshTimer = $timeout(updateLivePoll, 3000);
}
);
@@ -0,0 +1,70 @@
<div class="commandline form" ng-controller="CommandlineController">
<form class="styled" ng-hide="Mode == 'view'">
<backup-edit-uri uri="TargetURL" hide="HideEditUri" ng-show="EditUriState"></backup-edit-uri>
<div ng-hide="EditUriState">
<div class="input mixed">
<label for="command">Command</label>
<select name="command" id="command" ng-model="Command" ng-options="item for item in SupportedCommands">
</select>
<div style="float: left">{{CommandHelp[Command]}}</div>
</div>
<div class="input textarea linklabel">
<label for="target"><a href class="target" ng-click="EditUriState = true">Target URL &gt;</a></label>
<textarea name="target" id="target" ng-model="TargetURL" placeholder="Enter a url, or click the 'Target URL &gt;' link"></textarea>
</div>
<div class="input textarea">
<label for="arguments">Commandline arguments</label>
<textarea id="arguments" ng-model="Arguments" string-array-as-text placeholder="Enter one argument per line without quotes, e.g. *.txt"></textarea>
</div>
<div class="input textarea" ng-show="ShowAdvancedTextArea">
<label for="backupOptions">Advanced options</label>
<div>
<a href ng-click="ShowAdvancedTextArea = false" class="advanced-toggle"><i class="fa fa-check"></i> Edit as text</a>
<textarea id="backupOptions" ng-model="ExtendedOptions" string-array-as-text placeholder="Enter one option per line in command-line format, eg. --dblock-size=100MB"></textarea>
</div>
</div>
<div class="input" ng-hide="ShowAdvancedTextArea">
<label for="backupOptions">Advanced options</label>
<a href ng-click="ShowAdvancedTextArea = true" class="advanced-toggle"><i class="fa"></i> Edit as text</a>
<advanced-options-editor ng-option-list="ExtendedOptionList" ng-model="ExtendedOptions"></advanced-options-editor>
</div>
<div class="buttons">
<input class="submit run" type="submit" id="run" ng-click="run()" value="Run &quot;{{Command}}&quot; command now" />
</div>
</div>
</form>
<div ng-show="Mode == 'view'">
<h2 translate>Running commandline entry</h2>
<div ng-show="serverstate.programState == 'Paused' &amp;&amp; Started != true">
{{'Server is currently paused,' | translate}} <a ng-click="ServerStatus.resume()" translate>resume now</a>
</div>
<div ng-show="serverstate.programState != 'Paused' &amp;&amp; Started == null" translate="">
Connecting to task ....
</div>
<div ng-show="serverstate.programState != 'Paused' &amp;&amp; Started == false" translate="">
Waiting for task to start ....
</div>
<div ng-show="Started == true &amp;&amp; Finished == false">
<span translate>Running ....</span> <a href ng-click="Abort()" ng-hide="RawFinished">stop now</a>
</div>
<div ng-show="Started == true &amp;&amp; Finished == true" translate>
Finished!
</div>
<div ng-show="Started == true" class="prewrapped-text" style="overflow-x: auto;">
<div class="fixed-width-font" style="white-space: pre" ng-repeat="line in ViewLines track by $index">{{line}}</div>
</div>
</div>
</div>
@@ -24,9 +24,10 @@
<dt translate>Advanced:</dt>
<dd>
<p ng-click="doLocalDb(item.Backup.ID)" translate>Manage database ...</p>
<p ng-click="doLocalDb(item.Backup.ID)" translate>Database ...</p>
<p ng-click="doVerifyRemote(item.Backup.ID)" translate>Verify files</p>
<p ng-click="doCompact(item.Backup.ID)" translate>Compact now</p>
<p ng-click="doCommandLine(item.Backup.ID)" translate>Commandline ...</p>
</dd>
<dt translate>Reporting:</dt>
+5 -5
View File
@@ -237,7 +237,7 @@ namespace Duplicati.UnitTest
}
IList<DateTime> entries;
using (var i = new Duplicati.Library.Main.Controller(target, options, new CommandLine.ConsoleOutput(options)))
using (var i = new Duplicati.Library.Main.Controller(target, options, new CommandLine.ConsoleOutput(Console.Out, options)))
entries = (from n in i.List().Filesets select n.Time.ToLocalTime()).ToList();
if (entries.Count != folders.Length)
@@ -269,7 +269,7 @@ namespace Duplicati.UnitTest
using (new Timer("Extract list of files from" + folders[i]))
{
List<string> sourcefiles;
using (var inst = new Library.Main.Controller(target, options, new CommandLine.ConsoleOutput(options)))
using (var inst = new Library.Main.Controller(target, options, new CommandLine.ConsoleOutput(Console.Out, options)))
sourcefiles = (from n in inst.List("*").Files select n.Path).ToList();
//Remove all folders from list
@@ -454,7 +454,7 @@ namespace Duplicati.UnitTest
{
BasicSetupHelper.ProgressWriteLine("Backing up the copy: " + sourcename);
using (new Timer("Backup of " + sourcename))
using(var i = new Duplicati.Library.Main.Controller(target, options, new CommandLine.ConsoleOutput(options)))
using(var i = new Duplicati.Library.Main.Controller(target, options, new CommandLine.ConsoleOutput(Console.Out, options)))
Log.WriteMessage(i.Backup(source.Split(System.IO.Path.PathSeparator)).ToString(), LogMessageType.Information);
}
@@ -463,7 +463,7 @@ namespace Duplicati.UnitTest
var tops = new Dictionary<string, string>(options);
tops["restore-path"] = tempfolder;
using (new Timer("Restore of " + source))
using(var i = new Duplicati.Library.Main.Controller(target, tops, new CommandLine.ConsoleOutput(options)))
using(var i = new Duplicati.Library.Main.Controller(target, tops, new CommandLine.ConsoleOutput(Console.Out, options)))
Log.WriteMessage(i.Restore(null).ToString(), LogMessageType.Information);
}
@@ -472,7 +472,7 @@ namespace Duplicati.UnitTest
var tops = new Dictionary<string, string>(options);
tops["restore-path"] = tempfolder;
using (new Timer("Partial restore of " + source))
using(var i = new Duplicati.Library.Main.Controller(target, tops, new CommandLine.ConsoleOutput(options)))
using(var i = new Duplicati.Library.Main.Controller(target, tops, new CommandLine.ConsoleOutput(Console.Out, options)))
Log.WriteMessage(i.Restore(files).ToString(), LogMessageType.Information);
}
}