diff --git a/Duplicati.sln b/Duplicati.sln index 40bb8081a..3ffa84026 100644 --- a/Duplicati.sln +++ b/Duplicati.sln @@ -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 diff --git a/Duplicati/CommandLine/Commands.cs b/Duplicati/CommandLine/Commands.cs index 25aa20776..8406f9678 100644 --- a/Duplicati/CommandLine/Commands.cs +++ b/Duplicati/CommandLine/Commands.cs @@ -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 args, Dictionary options, Library.Utility.IFilter filter) + public static int Examples(TextWriter outwriter, Action setup, List args, Dictionary 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 args, Dictionary options, Library.Utility.IFilter filter) + public static int Help(TextWriter outwriter, Action setup, List args, Dictionary 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 args, Dictionary options, Library.Utility.IFilter filter) + public static int Affected(TextWriter outwriter, Action setup, List args, Dictionary 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 args, Dictionary options, Library.Utility.IFilter filter) + public static int List(TextWriter outwriter, Action setup, List args, Dictionary 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 args, Dictionary options, Library.Utility.IFilter filter) + public static int Delete(TextWriter outwriter, Action setup, List args, Dictionary 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 args, Dictionary options, Library.Utility.IFilter filter) + public static int Repair(TextWriter outwriter, Action setup, List args, Dictionary 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 args, Dictionary options, Library.Utility.IFilter filter) + public static int Restore(TextWriter outwriter, Action setup, List args, Dictionary 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 args, Dictionary options, Library.Utility.IFilter filter) + public static int Backup(TextWriter outwriter, Action setup, List args, Dictionary 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 args, Dictionary options, Library.Utility.IFilter filter) + public static int Compact(TextWriter outwriter, Action setup, List args, Dictionary 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 args, Dictionary options, Library.Utility.IFilter filter) + public static int Test(TextWriter outwriter, Action setup, List args, Dictionary 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 args, int expected) + private static int PrintWrongNumberOfArguments(TextWriter outwriter, List 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 args) + public static int PrintInvalidCommand(TextWriter outwriter, string command, List args) { - Console.WriteLine(Strings.Program.InvalidCommandError(command)); + outwriter.WriteLine(Strings.Program.InvalidCommandError(command)); return 200; } - public static int CreateBugReport(List args, Dictionary options, Library.Utility.IFilter filter) + public static int CreateBugReport(TextWriter outwriter, Action setup, List args, Dictionary 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(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 args, Dictionary options, Library.Utility.IFilter filter) + public static int ListChanges(TextWriter outwriter, Action setup, List args, Dictionary 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>> 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 args, Dictionary options, Library.Utility.IFilter filter) + public static int TestFilters(TextWriter outwriter, Action setup, List args, Dictionary 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 args, Dictionary options, Library.Utility.IFilter filter) + public static int SystemInfo(TextWriter outwriter, Action setup, List args, Dictionary 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 args, Dictionary options, Library.Utility.IFilter filter) + public static int PurgeFiles(TextWriter outwriter, Action setup, List args, Dictionary 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 args, Dictionary options, Library.Utility.IFilter filter) + public static int ListBrokenFiles(TextWriter outwriter, Action setup, List args, Dictionary 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 args, Dictionary options, Library.Utility.IFilter filter) + public static int PurgeBrokenFiles(TextWriter outwriter, Action setup, List args, Dictionary 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 args, Dictionary options, Library.Utility.IFilter filter) + public static int SendMail(TextWriter outwriter, Action setup, List args, Dictionary 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; diff --git a/Duplicati/CommandLine/ConsoleOutput.cs b/Duplicati/CommandLine/ConsoleOutput.cs index dfece48c7..dc04e806f 100644 --- a/Duplicati/CommandLine/ConsoleOutput.cs +++ b/Duplicati/CommandLine/ConsoleOutput.cs @@ -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 options) + public ConsoleOutput(TextWriter output, Dictionary 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 } diff --git a/Duplicati/CommandLine/Help.cs b/Duplicati/CommandLine/Help.cs index e14576559..5712861ac 100644 --- a/Duplicati/CommandLine/Help.cs +++ b/Duplicati/CommandLine/Help.cs @@ -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 options) + public static void PrintUsage(TextWriter outwriter, string topic, IDictionary 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()); + PrintUsage(outwriter, "help", new Dictionary()); 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()); + outwriter.WriteLine("Topic not found: {0}", topic); + outwriter.WriteLine(); + PrintUsage(outwriter, "help", new Dictionary()); } } } @@ -361,14 +365,15 @@ namespace Duplicati.CommandLine } - private static void PrintFormatted(IEnumerable lines) + private static void PrintFormatted(TextWriter outwriter, IEnumerable 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> foundArgs = new List>(); foundArgs.Add(new Library.Main.Options(new Dictionary()).SupportedCommands); - foundArgs.Add(Program.SupportedCommands); + foundArgs.Add(Program.SupportedOptions); foreach (Duplicati.Library.Interface.IBackend backend in Library.DynamicLoader.BackendLoader.Backends) if (backend.SupportedCommands != null) diff --git a/Duplicati/CommandLine/Program.cs b/Duplicati/CommandLine/Program.cs index 71a85c95f..28bf6a1ac 100644 --- a/Duplicati/CommandLine/Program.cs +++ b/Duplicati/CommandLine/Program.cs @@ -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; + /// /// The main entry point for the application. /// @@ -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 cargs = new List(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, List, Dictionary, Library.Utility.IFilter, int>> CommandMap + { + get + { + var knownCommands = new Dictionary, List, Dictionary, 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 SupportedCommands { get { return CommandMap.Keys; } } + + public static int RunCommandLine(TextWriter outwriter, TextWriter errwriter, Action setup, string[] args) + { + + bool verboseErrors = false; + bool verbose = false; + try + { + List cargs = new List(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, Dictionary, 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 SupportedCommands + public static IList SupportedOptions { get { @@ -278,7 +302,7 @@ namespace Duplicati.CommandLine } } - private static bool ReadOptionsFromFile(string filename, ref Library.Utility.IFilter filter, List cargs, Dictionary options) + private static bool ReadOptionsFromFile(TextWriter outwriter, string filename, ref Library.Utility.IFilter filter, List cargs, Dictionary 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; } } diff --git a/Duplicati/Library/Main/Controller.cs b/Duplicati/Library/Main/Controller.cs index 2fc166eb7..63554d2e9 100644 --- a/Duplicati/Library/Main/Controller.cs +++ b/Duplicati/Library/Main/Controller.cs @@ -388,19 +388,19 @@ namespace Duplicati.Library.Main }); } - public Duplicati.Library.Interface.IListChangesResults ListChanges(string baseVersion, string targetVersion, IEnumerable filterstrings = null, Library.Utility.IFilter filter = null) + public Duplicati.Library.Interface.IListChangesResults ListChanges(string baseVersion, string targetVersion, IEnumerable filterstrings = null, Library.Utility.IFilter filter = null, Action>> 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 args) + public Duplicati.Library.Interface.IListAffectedResults ListAffected(List args, Action callback = null) { return RunAction(new ListAffectedResults(), (result) => { - new Operation.ListAffected(m_options, result).Run(args); + new Operation.ListAffected(m_options, result).Run(args, callback); }); } diff --git a/Duplicati/Library/Main/Operation/ListAffected.cs b/Duplicati/Library/Main/Operation/ListAffected.cs index b2ae3ab6e..b5a491269 100644 --- a/Duplicati/Library/Main/Operation/ListAffected.cs +++ b/Duplicati/Library/Main/Operation/ListAffected.cs @@ -32,17 +32,31 @@ namespace Duplicati.Library.Main.Operation m_result = result; } - public void Run(List args) + public void Run(List args, Action 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); + } } } } diff --git a/Duplicati/Library/Main/Operation/ListChangesHandler.cs b/Duplicati/Library/Main/Operation/ListChangesHandler.cs index ffb193863..1d4280bbb 100644 --- a/Duplicati/Library/Main/Operation/ListChangesHandler.cs +++ b/Duplicati/Library/Main/Operation/ListChangesHandler.cs @@ -55,7 +55,7 @@ namespace Duplicati.Library.Main.Operation return res; } - public void Run(string baseVersion, string compareVersion, IEnumerable filterstrings = null, Library.Utility.IFilter compositefilter = null) + public void Run(string baseVersion, string compareVersion, IEnumerable filterstrings = null, Library.Utility.IFilter compositefilter = null, Action>> 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; } } diff --git a/Duplicati/Library/Main/Options.cs b/Duplicati/Library/Main/Options.cs index 790f63148..92ee1c1af 100644 --- a/Duplicati/Library/Main/Options.cs +++ b/Duplicati/Library/Main/Options.cs @@ -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 /// public bool Verbose { get { return GetBool("verbose"); } } - + + /// + /// A value indicating if the returned value should not be truncated + /// + public bool FullResult { get { return GetBool("full-result"); } } + /// /// A value indicating restored files overwrite existing ones /// diff --git a/Duplicati/Library/Main/Strings.cs b/Duplicati/Library/Main/Strings.cs index fa862b64a..fe71080df 100644 --- a/Duplicati/Library/Main/Strings.cs +++ b/Duplicati/Library/Main/Strings.cs @@ -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"); } } diff --git a/Duplicati/Server/Duplicati.Server.Serialization/Enums.cs b/Duplicati/Server/Duplicati.Server.Serialization/Enums.cs index 98d79834d..b939fe375 100755 --- a/Duplicati/Server/Duplicati.Server.Serialization/Enums.cs +++ b/Duplicati/Server/Duplicati.Server.Serialization/Enums.cs @@ -49,7 +49,8 @@ namespace Duplicati.Server.Serialization Compact, CreateReport, ListRemote, - Delete + Delete, + CustomRunner } public enum SuggestedStatusIcon diff --git a/Duplicati/Server/Duplicati.Server.csproj b/Duplicati/Server/Duplicati.Server.csproj index a3c748c43..77746bc38 100644 --- a/Duplicati/Server/Duplicati.Server.csproj +++ b/Duplicati/Server/Duplicati.Server.csproj @@ -122,6 +122,7 @@ + diff --git a/Duplicati/Server/Runner.cs b/Duplicati/Server/Runner.cs index 1226de5f1..463c02d0a 100644 --- a/Duplicati/Server/Runner.cs +++ b/Duplicati/Server/Runner.cs @@ -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 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(); + + 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; } diff --git a/Duplicati/Server/Serializable/ServerStatus.cs b/Duplicati/Server/Serializable/ServerStatus.cs index 3ea0dfc17..1804dc87f 100644 --- a/Duplicati/Server/Serializable/ServerStatus.cs +++ b/Duplicati/Server/Serializable/ServerStatus.cs @@ -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(t.TaskID, t.Backup.ID); + return new Tuple(t.TaskID, t.Backup == null ? null : t.Backup.ID); } } diff --git a/Duplicati/Server/WebServer/RESTMethods/Backup.cs b/Duplicati/Server/WebServer/RESTMethods/Backup.cs index f8c547e16..014d7f971 100644 --- a/Duplicati/Server/WebServer/RESTMethods/Backup.cs +++ b/Duplicati/Server/WebServer/RESTMethods/Backup.cs @@ -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; diff --git a/Duplicati/Server/WebServer/RESTMethods/CommandLine.cs b/Duplicati/Server/WebServer/RESTMethods/CommandLine.cs new file mode 100644 index 000000000..5f73b1da2 --- /dev/null +++ b/Duplicati/Server/WebServer/RESTMethods/CommandLine.cs @@ -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 m_activeItems = new Dictionary(); + 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(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(); + 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 + }); + } + } + } +} diff --git a/Duplicati/Server/webroot/ngax/index.html b/Duplicati/Server/webroot/ngax/index.html index a9d72374d..3054952fd 100755 --- a/Duplicati/Server/webroot/ngax/index.html +++ b/Duplicati/Server/webroot/ngax/index.html @@ -92,6 +92,7 @@ + diff --git a/Duplicati/Server/webroot/ngax/scripts/app.js b/Duplicati/Server/webroot/ngax/scripts/app.js index c30dabbe5..6f6a05cb0 100644 --- a/Duplicati/Server/webroot/ngax/scripts/app.js +++ b/Duplicati/Server/webroot/ngax/scripts/app.js @@ -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' diff --git a/Duplicati/Server/webroot/ngax/scripts/controllers/CommandlineController.js b/Duplicati/Server/webroot/ngax/scripts/controllers/CommandlineController.js new file mode 100644 index 000000000..c9df0ea1d --- /dev/null +++ b/Duplicati/Server/webroot/ngax/scripts/controllers/CommandlineController.js @@ -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 })); + } + ); + } +}); \ No newline at end of file diff --git a/Duplicati/Server/webroot/ngax/scripts/controllers/HomeController.js b/Duplicati/Server/webroot/ngax/scripts/controllers/HomeController.js index ca51d272a..ee52e45a3 100644 --- a/Duplicati/Server/webroot/ngax/scripts/controllers/HomeController.js +++ b/Duplicati/Server/webroot/ngax/scripts/controllers/HomeController.js @@ -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'); }; diff --git a/Duplicati/Server/webroot/ngax/scripts/controllers/LogController.js b/Duplicati/Server/webroot/ngax/scripts/controllers/LogController.js index 4cd49ad8f..168e7c5b4 100644 --- a/Duplicati/Server/webroot/ngax/scripts/controllers/LogController.js +++ b/Duplicati/Server/webroot/ngax/scripts/controllers/LogController.js @@ -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); } ); diff --git a/Duplicati/Server/webroot/ngax/templates/commandline.html b/Duplicati/Server/webroot/ngax/templates/commandline.html new file mode 100644 index 000000000..4c6f7ee51 --- /dev/null +++ b/Duplicati/Server/webroot/ngax/templates/commandline.html @@ -0,0 +1,70 @@ +
+
+ + +
+ +
+ + +
{{CommandHelp[Command]}}
+
+ + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + Edit as text + +
+ +
+ +
+ +
+
+ +
+

Running commandline entry

+
+ {{'Server is currently paused,' | translate}} resume now +
+
+ Connecting to task .... +
+
+ Waiting for task to start .... +
+
+ Running .... stop now +
+
+ Finished! +
+ +
+
{{line}}
+
+ +
+ +
diff --git a/Duplicati/Server/webroot/ngax/templates/home.html b/Duplicati/Server/webroot/ngax/templates/home.html index 4b1121e17..2875e76e9 100644 --- a/Duplicati/Server/webroot/ngax/templates/home.html +++ b/Duplicati/Server/webroot/ngax/templates/home.html @@ -24,9 +24,10 @@
Advanced:
-

Manage database ...

+

Database ...

Verify files

Compact now

+

Commandline ...

Reporting:
diff --git a/Duplicati/UnitTest/SVNCheckoutsTest.cs b/Duplicati/UnitTest/SVNCheckoutsTest.cs index da718ab5c..74107abf4 100644 --- a/Duplicati/UnitTest/SVNCheckoutsTest.cs +++ b/Duplicati/UnitTest/SVNCheckoutsTest.cs @@ -237,7 +237,7 @@ namespace Duplicati.UnitTest } IList 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 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(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(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); } }