From 8018545cff7f3d65c7bf5f2d232aace71fc2bb28 Mon Sep 17 00:00:00 2001 From: Kenneth Skovhede Date: Fri, 21 Aug 2015 21:59:12 +0200 Subject: [PATCH] Added a simple restore tool for recovering data from a broken backup. This fixes #1439. --- Duplicati.sln | 8 + .../CommandLine/Duplicati.CommandLine.csproj | 4 + Duplicati/CommandLine/Program.cs | 49 +- .../CommandLine/RecoveryTool/Download.cs | 150 ++++++ .../Duplicati.CommandLine.RecoveryTool.csproj | 134 +++++ .../CommandLine/RecoveryTool/Duplicati.snk | Bin 0 -> 596 bytes Duplicati/CommandLine/RecoveryTool/Help.cs | 41 ++ Duplicati/CommandLine/RecoveryTool/Index.cs | 242 +++++++++ Duplicati/CommandLine/RecoveryTool/List.cs | 123 +++++ Duplicati/CommandLine/RecoveryTool/Program.cs | 76 +++ .../RecoveryTool/Properties/AssemblyInfo.cs | 43 ++ Duplicati/CommandLine/RecoveryTool/Restore.cs | 476 ++++++++++++++++++ Duplicati/CommandLine/RecoveryTool/help.txt | 74 +++ .../Duplicati.GUI.TrayIcon.csproj | 5 + .../Utility/Duplicati.Library.Utility.csproj | 1 + Duplicati/Server/Duplicati.Server.csproj | 4 + 16 files changed, 1383 insertions(+), 47 deletions(-) create mode 100644 Duplicati/CommandLine/RecoveryTool/Download.cs create mode 100644 Duplicati/CommandLine/RecoveryTool/Duplicati.CommandLine.RecoveryTool.csproj create mode 100644 Duplicati/CommandLine/RecoveryTool/Duplicati.snk create mode 100644 Duplicati/CommandLine/RecoveryTool/Help.cs create mode 100644 Duplicati/CommandLine/RecoveryTool/Index.cs create mode 100644 Duplicati/CommandLine/RecoveryTool/List.cs create mode 100644 Duplicati/CommandLine/RecoveryTool/Program.cs create mode 100644 Duplicati/CommandLine/RecoveryTool/Properties/AssemblyInfo.cs create mode 100644 Duplicati/CommandLine/RecoveryTool/Restore.cs create mode 100644 Duplicati/CommandLine/RecoveryTool/help.txt diff --git a/Duplicati.sln b/Duplicati.sln index e98b4f68d..3222ee63e 100644 --- a/Duplicati.sln +++ b/Duplicati.sln @@ -69,6 +69,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.Backend.O EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.Backend.HubiC", "Duplicati\Library\Backend\HubiC\Duplicati.Library.Backend.HubiC.csproj", "{D60AD540-0E7D-40CE-83AE-D26E01FFE9B8}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.CommandLine.RecoveryTool", "Duplicati\CommandLine\RecoveryTool\Duplicati.CommandLine.RecoveryTool.csproj", "{4A010589-76E6-4F05-A5C4-4598D5DF11F8}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -130,6 +132,12 @@ Global {33FD1D24-C28F-4C71-933F-98F1586EA76C}.Release|Any CPU.Build.0 = Release|Any CPU {33FD1D24-C28F-4C71-933F-98F1586EA76C}.Test|Any CPU.ActiveCfg = Debug|Any CPU {33FD1D24-C28F-4C71-933F-98F1586EA76C}.Test|Any CPU.Build.0 = Debug|Any CPU + {4A010589-76E6-4F05-A5C4-4598D5DF11F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4A010589-76E6-4F05-A5C4-4598D5DF11F8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4A010589-76E6-4F05-A5C4-4598D5DF11F8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4A010589-76E6-4F05-A5C4-4598D5DF11F8}.Release|Any CPU.Build.0 = Release|Any CPU + {4A010589-76E6-4F05-A5C4-4598D5DF11F8}.Test|Any CPU.ActiveCfg = Debug|Any CPU + {4A010589-76E6-4F05-A5C4-4598D5DF11F8}.Test|Any CPU.Build.0 = Debug|Any CPU {4D012CB1-4B92-47F4-89B7-BF80A73A2E99}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {4D012CB1-4B92-47F4-89B7-BF80A73A2E99}.Debug|Any CPU.Build.0 = Debug|Any CPU {4D012CB1-4B92-47F4-89B7-BF80A73A2E99}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/Duplicati/CommandLine/Duplicati.CommandLine.csproj b/Duplicati/CommandLine/Duplicati.CommandLine.csproj index 6434b8ae3..2a7347425 100644 --- a/Duplicati/CommandLine/Duplicati.CommandLine.csproj +++ b/Duplicati/CommandLine/Duplicati.CommandLine.csproj @@ -161,6 +161,10 @@ {D60AD540-0E7D-40CE-83AE-D26E01FFE9B8} Duplicati.Library.Backend.HubiC + + {4A010589-76E6-4F05-A5C4-4598D5DF11F8} + Duplicati.CommandLine.RecoveryTool + diff --git a/Duplicati/CommandLine/Program.cs b/Duplicati/CommandLine/Program.cs index 1832c890b..ecf9376cd 100644 --- a/Duplicati/CommandLine/Program.cs +++ b/Duplicati/CommandLine/Program.cs @@ -27,51 +27,6 @@ namespace Duplicati.CommandLine { public class Program { - private class FilterCollector - { - private List m_filters = new List(); - private Library.Utility.IFilter Filter - { - get - { - if (m_filters.Count == 0) - return new Library.Utility.FilterExpression(); - else if (m_filters.Count == 1) - return m_filters[0]; - else - return m_filters.Aggregate((a,b) => Library.Utility.JoinedFilterExpression.Join(a, b)); - } - } - - private Dictionary DoExtractOptions(List args, Func callbackHandler = null) - { - return Library.Utility.CommandLineParser.ExtractOptions(args, (key, value) => { - if (key.Equals("include", StringComparison.InvariantCultureIgnoreCase)) - { - m_filters.Add(new Library.Utility.FilterExpression(value, true)); - return false; - } - else if (key.Equals("exclude", StringComparison.InvariantCultureIgnoreCase)) - { - m_filters.Add(new Library.Utility.FilterExpression(value, false)); - return false; - } - - if (callbackHandler != null) - return callbackHandler(key, value); - - return true; - }); - } - - public static Tuple, Library.Utility.IFilter> ExtractOptions(List args, Func callbackHandler = null) - { - var fc = new FilterCollector(); - var opts = fc.DoExtractOptions(args, callbackHandler); - return new Tuple, Library.Utility.IFilter>(opts, fc.Filter); - } - } - /// /// The main entry point for the application. /// @@ -89,7 +44,7 @@ namespace Duplicati.CommandLine { List cargs = new List(args); - var tmpparsed = FilterCollector.ExtractOptions(cargs); + var tmpparsed = Library.Utility.FilterCollector.ExtractOptions(cargs); var options = tmpparsed.Item1; var filter = tmpparsed.Item2; @@ -300,7 +255,7 @@ namespace Duplicati.CommandLine var newsource = new List(); string newtarget = null; - var tmpparsed = FilterCollector.ExtractOptions(fargs, (key, value) => { + var tmpparsed = Library.Utility.FilterCollector.ExtractOptions(fargs, (key, value) => { if (key.Equals("source", StringComparison.InvariantCultureIgnoreCase)) { newsource.Add(value); diff --git a/Duplicati/CommandLine/RecoveryTool/Download.cs b/Duplicati/CommandLine/RecoveryTool/Download.cs new file mode 100644 index 000000000..e15635224 --- /dev/null +++ b/Duplicati/CommandLine/RecoveryTool/Download.cs @@ -0,0 +1,150 @@ +// Copyright (C) 2015, 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.IO; +using System.Collections.Generic; + +namespace Duplicati.CommandLine.RecoveryTool +{ + public static class Download + { + public static int Run(List args, Dictionary options, Library.Utility.IFilter filter) + { + if (args.Count != 3) + { + Console.WriteLine("Invalid argument count ({0} expected 3): {1}{2}", args.Count, Environment.NewLine, string.Join(Environment.NewLine, args)); + return 100; + } + + using(var backend = Library.DynamicLoader.BackendLoader.GetBackend(args[1], options)) + { + if (backend == null) + { + Console.WriteLine("Backend not found: {0}{1}Backends supported: {2}", args[1], Environment.NewLine, string.Join(", ", Library.DynamicLoader.BackendLoader.Keys)); + return 100; + } + + var targetfolder = Path.GetFullPath(args[2]); + + if (!Directory.Exists(args[2])) + { + Console.WriteLine("Creating target folder: {0}", targetfolder); + Directory.CreateDirectory(targetfolder); + } + + Console.WriteLine("Listing files on backend: {0} ...", backend.ProtocolKey); + + var lst = backend.List(); + + Console.WriteLine("Found {0} files", lst.Count); + + var i = 0; + var downloaded = 0; + var errors = 0; + var needspass = 0; + string passphrase; + options.TryGetValue("passphrase", out passphrase); + + foreach(var file in lst) + { + try + { + Console.Write("{0}: {1}", i, file.Name); + var p = Duplicati.Library.Main.Volumes.VolumeBase.ParseFilename(file); + if (p == null) + { + Console.WriteLine(" - Not a Duplicati file, ignoring"); + continue; + } + + var local = Path.Combine(targetfolder, file.Name); + if (p.EncryptionModule != null) + { + if (string.IsNullOrWhiteSpace(passphrase)) + { + needspass++; + Console.WriteLine(" - No passphrase supplied, skipping"); + continue; + } + + local = local.Substring(0, local.Length - p.EncryptionModule.Length - 1); + } + + if (p.FileType != Duplicati.Library.Main.RemoteVolumeType.Blocks && p.FileType != Duplicati.Library.Main.RemoteVolumeType.Files) + { + Console.WriteLine(" - Filetype {0}, skipping", p.FileType); + continue; + } + + if (File.Exists(local)) + { + Console.WriteLine(" - Already exists, skipping"); + continue; + } + + Console.Write(" - downloading ({0})...", Library.Utility.Utility.FormatSizeString(file.Size)); + + using(var tf = new Library.Utility.TempFile()) + { + backend.Get(file.Name, tf); + + if (p.EncryptionModule != null) + { + Console.Write(" - decrypting ..."); + using(var m = Library.DynamicLoader.EncryptionLoader.GetModule(p.EncryptionModule, passphrase, options)) + using(var tf2 = new Library.Utility.TempFile()) + { + m.Decrypt(tf, tf2); + File.Copy(tf2, local); + } + } + else + File.Copy(tf, local); + } + + Console.WriteLine(" done!"); + + } + catch(Exception ex) + { + Console.WriteLine(" error: {0}", ex.ToString()); + errors++; + } + + i++; + } + + if (needspass > 0 && downloaded == 0) + { + Console.WriteLine("No files downloaded, try adding --passphrase to decrypt files"); + return 100; + } + + Console.WriteLine("Download complete, of {0} remote files, {1} were downloaded with {2} errors", lst.Count, downloaded, errors); + if (needspass > 0) + Console.WriteLine("Additonally {0} remote files were skipped because of encryption, supply --passphrase to download those"); + + if (errors > 0) + return 200; + else + return 0; + + } + } + } +} + diff --git a/Duplicati/CommandLine/RecoveryTool/Duplicati.CommandLine.RecoveryTool.csproj b/Duplicati/CommandLine/RecoveryTool/Duplicati.CommandLine.RecoveryTool.csproj new file mode 100644 index 000000000..218a7e11d --- /dev/null +++ b/Duplicati/CommandLine/RecoveryTool/Duplicati.CommandLine.RecoveryTool.csproj @@ -0,0 +1,134 @@ + + + + Debug + AnyCPU + 8.0.30703 + 2.0 + {4A010589-76E6-4F05-A5C4-4598D5DF11F8} + Exe + Duplicati.CommandLine.RecoveryTool + Duplicati.CommandLine.RecoveryTool + true + Duplicati.snk + v4.5 + + + true + full + false + bin\Debug + DEBUG; + prompt + 4 + false + + + + + + index /Users/kenneth/testdata/recovery --targetpath=/Users/kenneth/testdata/recovery-restored + + + full + true + bin\Release + prompt + 4 + true + + + + + + + + + + + + + + + + + {7E119745-1F62-43F0-936C-F312A1912C0B} + Duplicati.Library.AutoUpdater + + + {8E4CECFB-0413-4B00-AB93-78D1C3902BD5} + Duplicati.Library.Backend.AzureBlob + + + {1BFAE226-8364-4086-825C-BB83F6F3EE4C} + Duplicati.Library.Backend.CloudFiles + + + {FC9B7611-836F-4127-8B44-A7C31F506807} + Duplicati.Library.Backend.File + + + {F61679A9-E5DE-468A-B5A4-05F92D0143D2} + Duplicati.Library.Backend.FTP + + + {5489181D-950C-44AF-873C-45EB0A3B6BD2} + Duplicati.Library.Backend.GoogleServices + + + {D60AD540-0E7D-40CE-83AE-D26E01FFE9B8} + Duplicati.Library.Backend.HubiC + + + {CCD76347-7DC7-4B42-B7E1-E500E624CAC3} + Duplicati.Library.Backend.OneDrive + + + {D9E4E686-423C-48EC-A392-404E7C00860C} + Duplicati.Library.Backend.OpenStack + + + {C03F6DFD-805A-4BE0-9338-64870ADDB4A2} + Duplicati.Library.Backend.S3 + + + {FF2BF37C-E502-4C98-BEA0-701671DDFA08} + Duplicati.Library.Backend.SSHv2 + + + {C0270709-2A40-43B5-8CF1-69581B9FA2A1} + Duplicati.Library.Backend.TahoeLAFS + + + {BAE27510-8B5D-44B2-B33E-372A98908041} + Duplicati.Library.Backend.WEBDAV + + + {19ECCE09-B5EB-406C-8C57-BAC66997D469} + Duplicati.Library.Compression + + + {0CA86ECF-5BEC-4909-B4F6-110A03B30B92} + Duplicati.Library.DynamicLoader + + + {94484FDB-2EFA-4CF0-9BE6-A561157B4F87} + Duplicati.Library.Encryption + + + {C5899F45-B0FF-483C-9D38-24A9FCAAB237} + Duplicati.Library.Interface + + + {DE3E5D4C-51AB-4E5E-BEE8-E636CEBFBA65} + Duplicati.Library.Utility + + + {10D2D1B7-C664-41D8-9B3A-00040C3D421B} + Duplicati.Library.Main + + + + + + \ No newline at end of file diff --git a/Duplicati/CommandLine/RecoveryTool/Duplicati.snk b/Duplicati/CommandLine/RecoveryTool/Duplicati.snk new file mode 100644 index 0000000000000000000000000000000000000000..e0c1e2dd86119728adaeefc5f14f2e597dabceeb GIT binary patch literal 596 zcmZQ)VqjoUVPFUfb~IvOVPIfnU}U&>ZqBFG;TL!xbC@f=sW??FHp%RuD04u?d|@dM zp47GKm!Hl%dFaei`<5*qzAgXqbzUyVi_n}?OMO*3eH|OE?krr| zFEl@9)L*-L?Q8KJPtTajTTV9l{O!|eX~_>-AD#yZ=6m07=(;Lz?67b_fmBH7>6XPy zU(fsaYt6=H^BbXYGcsePe{{ZI&XLwz9=grGu+&OTZN+`>n#aL@H_qHKNL{x|{OX?< z|2?cWgj<|a*&Wn5qtR68+v$gad`lNSIGSM4$`K*nE_8`M(8%hLo?+%X^GU{qHtmys zKah1))%qg*H$8)~{Blp<;-4#gv3~y3w(VO5op^Nq_Qo%mz;QX`zXzj&$<=3DJ8QK~doHe9_tNXtnN&4~oeV$j z^o2{LFP>08&A&p7*^IG!Mnm`8x8Lq$er`8lWi+;b>yXC%XWDAv$p-_y)7-T(lU$#a z#IstS7OnePcICwJ+w3(l+=qQ0gsr@P>rWc{j)4B;9b4MXBe*rpp6LBh{}W&Ed`G$N IQKi@%02MtM!T args, Dictionary options, Library.Utility.IFilter filter) + { + if (args.Count >= 1 && !string.Equals("help", args[0])) + { + Console.WriteLine("Unsupported command: {0}", args[0]); + Console.WriteLine(); + return 100; + } + + Console.Write(new System.IO.StreamReader(System.Reflection.Assembly.GetExecutingAssembly().GetManifestResourceStream(System.Reflection.Assembly.GetExecutingAssembly().GetName().Name + "." + RESOURCE_NAME)).ReadToEnd()); + + return 0; + } + } +} + diff --git a/Duplicati/CommandLine/RecoveryTool/Index.cs b/Duplicati/CommandLine/RecoveryTool/Index.cs new file mode 100644 index 000000000..6b3eb1540 --- /dev/null +++ b/Duplicati/CommandLine/RecoveryTool/Index.cs @@ -0,0 +1,242 @@ +// Copyright (C) 2015, 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.IO; +using System.Collections.Generic; +using System.Linq; + +namespace Duplicati.CommandLine.RecoveryTool +{ + public static class Index + { + public static int Run(List args, Dictionary options, Library.Utility.IFilter filter) + { + if (args.Count != 2) + { + Console.WriteLine("Invalid argument count ({0} expected 2): {1}{2}", args.Count, Environment.NewLine, string.Join(Environment.NewLine, args)); + return 100; + } + + var folder = Path.GetFullPath(args[1]); + + if (!Directory.Exists(folder)) + { + Console.WriteLine("Folder not found: {0}", folder); + return 100; + } + + Directory.SetCurrentDirectory(folder); + + string ixfile; + options.TryGetValue("indexfile", out ixfile); + if (string.IsNullOrWhiteSpace(ixfile)) + ixfile = "index.txt"; + + ixfile = Path.GetFullPath(ixfile); + if (!File.Exists(ixfile)) + { + using(File.Create(ixfile)) + { + } + } + else + { + Console.WriteLine("Sorting existing index file"); + SortFile(ixfile, ixfile); + } + + var filecount = Directory.EnumerateFiles(folder).Count(); + + Console.WriteLine("Processing {0} files", filecount); + + var i = 0; + var errors = 0; + var totalblocks = 0L; + var files = 0; + foreach(var file in Directory.EnumerateFiles(folder)) + { + Console.Write("{0}: {1}", i, file); + + try + { + var p = Duplicati.Library.Main.Volumes.VolumeBase.ParseFilename(file); + if (p == null) + { + Console.WriteLine(" - Not a Duplicati file, ignoring"); + continue; + } + + if (p.FileType != Duplicati.Library.Main.RemoteVolumeType.Blocks) + { + Console.WriteLine(" - Filetype {0}, skipping", p.FileType); + continue; + } + + if (!string.IsNullOrWhiteSpace(p.EncryptionModule)) + { + Console.WriteLine(" - Encrypted, skipping"); + continue; + } + + var filekey = Path.GetFileName(file); + + var blocks = 0; + using(var cp = Library.DynamicLoader.CompressionLoader.GetModule(p.CompressionModule, file, options)) + using(var tf = new Library.Utility.TempFile()) + { + using(var sw = new StreamWriter(tf)) + foreach(var f in cp.ListFiles(null)) + { + sw.WriteLine("{0}, {1}", Library.Utility.Utility.Base64UrlToBase64Plain(f), filekey); + blocks++; + } + + files++; + totalblocks += blocks; + + Console.Write(" {0} hashes found, sorting ...", blocks); + + SortFile(tf, tf); + + Console.WriteLine(" done!"); + + Console.Write("Merging {0} hashes ...", totalblocks); + + MergeFiles(ixfile, tf, ixfile); + + Console.WriteLine(" done!"); + } + } + catch(Exception ex) + { + Console.WriteLine(" error: {0}", ex.ToString()); + errors++; + } + + i++; + } + + Console.WriteLine("Processed {0} files and found {1} hashes", files, totalblocks); + if (errors > 0) + Console.WriteLine("Experienced {0} errors", errors); + + return 0; + } + + public static void SortFile(string filein, string fileout) + { + try + { + // If the file can fit into memory, this is MUCH faster + using(var tfout = new Library.Utility.TempFile()) + { + var data = File.ReadAllLines(filein); + Array.Sort(data, StringComparer.Ordinal); + File.WriteAllLines(tfout, data); + + File.Copy(tfout, fileout, true); + return; + } + } + catch + { + } + + using(var tfin = new Library.Utility.TempFile()) + using(var tfout = new Library.Utility.TempFile()) + { + long swaps; + + File.Copy(filein, tfin, true); + + do + { + swaps = 0L; + + using(var sw = new System.IO.StreamWriter(tfout)) + using(var sr = new System.IO.StreamReader(tfin)) + { + var c1 = sr.ReadLine(); + var c2 = sr.ReadLine(); + + while (c1 != null || c2 != null) + { + if (c1 != null && c1.StartsWith("a")) + Console.Write(""); + var cmp = StringComparer.Ordinal.Compare(c1, c2); + + if (c2 == null || (c1 != null && cmp < 0)) + { + sw.WriteLine(c1); + + c1 = c2; + c2 = sr.ReadLine(); + } + else + { + if (cmp != 0) + sw.WriteLine(c2); + c2 = sr.ReadLine(); + swaps++; + } + } + } + + File.Copy(tfout, tfin, true); + + } while(swaps > 0); + + File.Copy(tfout, fileout, true); + } + } + + private static void MergeFiles(string file1, string file2, string fileout) + { + using(var tf = new Library.Utility.TempFile()) + { + using(var sw = new System.IO.StreamWriter(tf)) + using(var sr1 = new System.IO.StreamReader(file1)) + using(var sr2 = new System.IO.StreamReader(file2)) + { + var c1 = sr1.ReadLine(); + var c2 = sr2.ReadLine(); + + while(c1 != null || c2 != null) + { + var cmp = StringComparer.Ordinal.Compare(c1, c2); + + if (c2 == null || (c1 != null && cmp < 0)) + { + sw.WriteLine(c1); + c1 = sr1.ReadLine(); + } + else + { + if (cmp != 0) + sw.WriteLine(c2); + + c2 = sr2.ReadLine(); + } + } + } + + File.Copy(tf, fileout, true); + } + } + } +} + diff --git a/Duplicati/CommandLine/RecoveryTool/List.cs b/Duplicati/CommandLine/RecoveryTool/List.cs new file mode 100644 index 000000000..e6bd67b80 --- /dev/null +++ b/Duplicati/CommandLine/RecoveryTool/List.cs @@ -0,0 +1,123 @@ +// Copyright (C) 2015, 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.Linq; +using System.Collections.Generic; +using System.IO; + +namespace Duplicati.CommandLine.RecoveryTool +{ + public static class List + { + public static int Run(List args, Dictionary options, Library.Utility.IFilter filter) + { + if (args.Count != 2 && args.Count != 3) + { + Console.WriteLine("Invalid argument count ({0} expected 2 or 3): {1}{2}", args.Count, Environment.NewLine, string.Join(Environment.NewLine, args)); + return 100; + } + + var folder = Path.GetFullPath(args[1]); + + if (!Directory.Exists(folder)) + { + Console.WriteLine("Folder not found: {0}", folder); + return 100; + } + + Directory.SetCurrentDirectory(folder); + + if (args.Count == 2) + { + var times = ParseListFiles(folder); + foreach(var v in times.Zip(Enumerable.Range(0, times.Length), (a,b) => new KeyValuePair(b, a.Key))) + Console.WriteLine("{0}: {1}", v.Key, v.Value.ToLocalTime()); + } + else + { + var file = SelectListFile(args[2], args[1]); + + var p = Library.Main.Volumes.VolumeBase.ParseFilename(file); + + Library.Main.Volumes.VolumeReaderBase.UpdateOptionsFromManifest(p.CompressionModule, file, new Duplicati.Library.Main.Options(options)); + + foreach(var f in EnumerateFilesInDList(file, filter, options)) + Console.WriteLine("{0} ({1})", f.Path, Library.Utility.Utility.FormatSizeString(f.Size)); + } + + return 0; + } + + public static IEnumerable EnumerateFilesInDList(string file, Duplicati.Library.Utility.IFilter filter, Dictionary options) + { + var p = Library.Main.Volumes.VolumeBase.ParseFilename(file); + using(var cm = Library.DynamicLoader.CompressionLoader.GetModule(p.CompressionModule, file, options)) + using(var filesetreader = new Library.Main.Volumes.FilesetVolumeReader(cm, new Duplicati.Library.Main.Options(options))) + foreach(var f in filesetreader.Files) + { + if (f.Type != Duplicati.Library.Main.FilelistEntryType.File) + continue; + + bool result; + Library.Utility.IFilter evfilter; + bool match = filter.Matches(f.Path, out result, out evfilter); + if (!match || (match && result)) + yield return f; + } + } + + public static string SelectListFile(string time, string folder) + { + if (File.Exists(Path.Combine(folder, time))) + return Path.Combine(folder, time); + + int index; + if (int.TryParse(time, out index)) + { + var items = ParseListFiles(folder); + if (index < 0 || index >= items.Length) + throw new Exception(string.Format("Valid range for version is 0 to {1}", items.Length - 1)); + + return items[index].Value; + } + + var parsedtime = Library.Utility.Timeparser.ParseTimeInterval(time, DateTime.Now, true); + var el = ( + from n in ParseListFiles(folder) + let diff = Math.Abs((n.Key - parsedtime).TotalSeconds) + orderby diff + select n).First(); + + Console.WriteLine("Selected time {0}", el.Key); + + return el.Value; + + } + + public static KeyValuePair[] ParseListFiles(string folder) + { + return ( + from v in Directory.EnumerateFiles(folder) + let p = Library.Main.Volumes.VolumeBase.ParseFilename(v) + where p != null && p.FileType == Duplicati.Library.Main.RemoteVolumeType.Files + orderby p.Time descending + select new KeyValuePair(p.Time, v)).ToArray(); + + } + } +} + diff --git a/Duplicati/CommandLine/RecoveryTool/Program.cs b/Duplicati/CommandLine/RecoveryTool/Program.cs new file mode 100644 index 000000000..b90c08906 --- /dev/null +++ b/Duplicati/CommandLine/RecoveryTool/Program.cs @@ -0,0 +1,76 @@ +// Copyright (C) 2015, 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.Linq; +using System.Collections.Generic; + +namespace Duplicati.CommandLine.RecoveryTool +{ + static class Program + { + /// + /// The main entry point for the application. + /// + [STAThread] + public static int Main(string[] args) + { + return Duplicati.Library.AutoUpdater.UpdaterManager.RunFromMostRecent(typeof(Program).GetMethod("RealMain"), args); + } + + private delegate int CommandRunner(List args, Dictionary options, Library.Utility.IFilter filter); + + public static int RealMain(string[] _args) + { + try + { + var args = new List(_args); + var tmpparsed = Library.Utility.FilterCollector.ExtractOptions(args); + var options = tmpparsed.Item1; + var filter = tmpparsed.Item2; + + if (!options.ContainsKey("auth_password") && !string.IsNullOrEmpty(System.Environment.GetEnvironmentVariable("AUTH_PASSWORD"))) + options["auth_password"] = System.Environment.GetEnvironmentVariable("AUTH_PASSWORD"); + + if (!options.ContainsKey("auth_username") && !string.IsNullOrEmpty(System.Environment.GetEnvironmentVariable("AUTH_USERNAME"))) + options["auth_username"] = System.Environment.GetEnvironmentVariable("AUTH_USERNAME"); + + if (options.ContainsKey("tempdir") && !string.IsNullOrEmpty(options["tempdir"])) + Library.Utility.TempFolder.SystemTempPath = options["tempdir"]; + + var actions = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + actions["download"] = Download.Run; + actions["index"] = Index.Run; + actions["list"] = List.Run; + actions["restore"] = Restore.Run; + actions["help"] = Help.Run; + + CommandRunner command; + + actions.TryGetValue(args.FirstOrDefault(), out command); + + command = command ?? actions["help"]; + + return command(args, options, filter); + } + catch(Exception ex) + { + Console.WriteLine("Program crashed: {0}{1}", Environment.NewLine, ex.ToString()); + return 200; + } + } + } +} diff --git a/Duplicati/CommandLine/RecoveryTool/Properties/AssemblyInfo.cs b/Duplicati/CommandLine/RecoveryTool/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..54b281749 --- /dev/null +++ b/Duplicati/CommandLine/RecoveryTool/Properties/AssemblyInfo.cs @@ -0,0 +1,43 @@ +// Copyright (C) 2015, 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.Reflection; +using System.Runtime.CompilerServices; + +// Information about this assembly is defined by the following attributes. +// Change them to the values specific to your project. + +[assembly: AssemblyTitle("Duplicati.CommandLine.RecoveryTool")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("")] +[assembly: AssemblyCopyright("kenneth")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}". +// The form "{Major}.{Minor}.*" will automatically update the build and revision, +// and "{Major}.{Minor}.{Build}.*" will update just the revision. + +[assembly: AssemblyVersion("1.0.*")] + +// The following attributes are used to specify the signing key for the assembly, +// if desired. See the Mono documentation for more information about signing. + +//[assembly: AssemblyDelaySign(false)] +//[assembly: AssemblyKeyFile("")] + diff --git a/Duplicati/CommandLine/RecoveryTool/Restore.cs b/Duplicati/CommandLine/RecoveryTool/Restore.cs new file mode 100644 index 000000000..6f7d8b328 --- /dev/null +++ b/Duplicati/CommandLine/RecoveryTool/Restore.cs @@ -0,0 +1,476 @@ +// Copyright (C) 2015, 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.IO; +using System.Linq; +using System.Collections.Generic; + +namespace Duplicati.CommandLine.RecoveryTool +{ + public static class Restore + { + public static int Run(List args, Dictionary options, Library.Utility.IFilter filter) + { + if (args.Count != 2 && args.Count != 3) + { + Console.WriteLine("Invalid argument count ({0} expected 2 or 3): {1}{2}", args.Count, Environment.NewLine, string.Join(Environment.NewLine, args)); + return 100; + } + + var folder = Path.GetFullPath(args[1]); + + if (!Directory.Exists(folder)) + { + Console.WriteLine("Folder not found: {0}", folder); + return 100; + } + + Directory.SetCurrentDirectory(folder); + + string targetpath; + options.TryGetValue("targetpath", out targetpath); + + string ixfile; + options.TryGetValue("indexfile", out ixfile); + if (string.IsNullOrWhiteSpace(ixfile)) + ixfile = "index.txt"; + + ixfile = Path.GetFullPath(ixfile); + if (!File.Exists(ixfile)) + { + Console.WriteLine("Index file not found, perhaps you need to run the index command?"); + return 100; + } + + Console.Write("Sorting index file ..."); + Index.SortFile(ixfile, ixfile); + Console.WriteLine(" done!"); + + string filelist; + if (args.Count == 2) + { + var time = List.ParseListFiles(folder).First(); + filelist = time.Value; + + Console.WriteLine("Using set 0 with timestamp {0}", time.Key.ToLocalTime()); + } + else + { + filelist = List.SelectListFile(args[2], args[1]); + } + + Library.Main.Volumes.VolumeReaderBase.UpdateOptionsFromManifest(Path.GetExtension(filelist).Trim('.'), filelist, new Duplicati.Library.Main.Options(options)); + + string blocksize_str; + options.TryGetValue("blocksize", out blocksize_str); + string blockhash_str; + options.TryGetValue("block-hash-algorithm", out blockhash_str); + string filehash_str; + options.TryGetValue("block-hash-algorithm", out filehash_str); + + long blocksize = string.IsNullOrWhiteSpace(blocksize_str) ? 0 : Library.Utility.Sizeparser.ParseSize(blocksize_str); + + if (blocksize <= 0) + { + Console.WriteLine("Invalid blocksize: {0}, try setting --blocksize manually"); + return 100; + } + + var blockhasher = string.IsNullOrWhiteSpace(blockhash_str) ? null : System.Security.Cryptography.HashAlgorithm.Create(blockhash_str); + var filehasher = string.IsNullOrWhiteSpace(filehash_str) ? null : System.Security.Cryptography.HashAlgorithm.Create(filehash_str); + + if (blockhasher == null) + throw new Exception(string.Format("Block hash algorithm not valid: {0}", blockhash_str)); + if (filehasher == null) + throw new Exception(string.Format("File hash algorithm not valid: {0}", filehash_str)); + + var hashesprblock = blocksize / (blockhasher.HashSize / 8); + + using(var mru = new CompressedFileMRUCache(options)) + { + Console.WriteLine("Building lookup table for file hashes"); + var lookup = new HashLookupHelper(ixfile, mru, (int)blocksize, blockhasher.HashSize / 8); + + var filecount = 0L; + string largestprefix = null; + string[] largestprefixparts = null; + + if (!string.IsNullOrWhiteSpace(targetpath)) + Console.WriteLine("Computing restore path"); + + foreach(var f in List.EnumerateFilesInDList(filelist, filter, options)) + { + if (largestprefix == null) + { + largestprefix = f.Path; + largestprefixparts = largestprefix.Split(new char[] { Path.DirectorySeparatorChar }); + } + else if (largestprefix.Length > 1) + { + var parts = f.Path.Split(new char[] { Path.DirectorySeparatorChar }); + + var ni = 0; + for(; ni < Math.Min(parts.Length, largestprefixparts.Length); ni++) + if (!Library.Utility.Utility.ClientFilenameStringComparer.Equals(parts[ni], largestprefixparts[ni])) + break; + + if (ni != largestprefixparts.Length) + { + if (ni == 0) + { + largestprefixparts = new string[0]; + largestprefix = string.Empty; + } + else + { + Array.Resize(ref largestprefixparts, ni - 1); + largestprefix = string.Join(Path.DirectorySeparatorChar.ToString(), largestprefixparts); + } + } + } + filecount++; + } + + Console.WriteLine("Restoring {0} files to {1}", filecount, string.IsNullOrWhiteSpace(targetpath) ? "original position" : targetpath); + + if (Library.Utility.Utility.IsClientLinux || largestprefix.Length > 0) + largestprefix = Library.Utility.Utility.AppendDirSeparator(largestprefix); + + if (!string.IsNullOrEmpty(largestprefix)) + Console.WriteLine("Removing common prefix {0} from files", largestprefix); + + var i = 0L; + var errors = 0L; + foreach(var f in List.EnumerateFilesInDList(filelist, filter, options)) + { + try + { + var targetfile = MapToRestorePath(f.Path, largestprefix, targetpath); + if (!Directory.Exists(Path.GetDirectoryName(targetfile))) + Directory.CreateDirectory(Path.GetDirectoryName(targetfile)); + + Console.Write("{0}: {1} ({2})", i, targetfile, Library.Utility.Utility.FormatSizeString(f.Size)); + + using(var tf = new Library.Utility.TempFile()) + { + using(var sw = File.OpenWrite(tf)) + { + if (f.BlocklistHashes == null) + { + lookup.WriteHash(sw, f.Hash); + } + else + { + var blhi = 0L; + foreach(var blh in f.BlocklistHashes) + { + Console.Write(" {0}", blhi); + var blockhashoffset = blhi * hashesprblock * blocksize; + + try + { + var bi = 0; + foreach(var h in lookup.ReadBlocklistHashes(blh)) + { + try + { + sw.Position = blockhashoffset + (bi * blocksize); + lookup.WriteHash(sw, h); + } + catch(Exception ex) + { + Console.WriteLine("Failed to read hash: {0}{1}{2}", h, Environment.NewLine, ex.ToString()); + } + + bi++; + } + } + catch (Exception ex) + { + Console.WriteLine("Failed to read Blocklist hash: {0}{1}{2}", blh, Environment.NewLine, ex.ToString()); + } + + blhi++; + } + } + } + + string fh; + using(var fs = File.OpenRead(tf)) + fh = Convert.ToBase64String(filehasher.ComputeHash(fs)); + + if (fh == f.Hash) + { + Console.WriteLine(" done!"); + File.Copy(tf, targetfile, true); + } + else + { + Console.Write(" - Restored file hash mismatch"); + if (File.Exists(targetfile)) + Console.WriteLine(" - not overwriting existing file: {0}", targetfile); + else + Console.WriteLine(" - restoring file in damaged condition"); + + + } + } + + } + catch (Exception ex) + { + Console.WriteLine(" error: {0}", ex.ToString()); + errors++; + } + i++; + } + } + + return 0; + } + + private static string MapToRestorePath(string path, string prefixpath, string restorepath) + { + if (string.IsNullOrWhiteSpace(restorepath) || string.IsNullOrWhiteSpace(prefixpath)) + return path; + + return Path.Combine(restorepath, path.Substring(prefixpath.Length)); + } + + + + private class HashLookupHelper : IDisposable + { + private const int LOOKUP_TABLE_SIZE = 2048; + + private CompressedFileMRUCache m_cache; + private int m_hashsize; + private int m_blocksize; + private Stream m_indexfile; + private List m_lookup = new List(); + private List m_offsets = new List(); + private byte[] m_linebuf = new byte[128]; + private byte[] m_newline = Environment.NewLine.Select(x => (byte)x).ToArray(); + + public HashLookupHelper(string indexfile, CompressedFileMRUCache cache, int blocksize, int hashsize) + { + m_cache = cache; + m_blocksize = blocksize; + m_hashsize = hashsize; + m_indexfile = File.OpenRead(indexfile); + + // Build index .... + var hashes = 0L; + string prev = null; + m_indexfile.Position = 0; + foreach(var n in AllFileLines()) + { + if (n.Key != prev) + { + hashes++; + prev = n.Key; + } + } + + Console.WriteLine("Index file has {0} hashes in total", hashes); + + var lookuptablesize = Math.Max(1, Math.Min(LOOKUP_TABLE_SIZE, hashes) - 1); + var lookupincrements = hashes / lookuptablesize; + + Console.WriteLine("Building lookup table with {0} entries, giving increments of {1}", lookuptablesize, lookupincrements); + + prev = null; + var prevoff = 0L; + var hc = 0L; + m_indexfile.Position = 0; + foreach(var n in AllFileLines()) + { + if (n.Key != prev) + { + if ((hc % lookupincrements) == 0) + { + m_lookup.Add(n.Key); + m_offsets.Add(prevoff); + } + + hc++; + prev = n.Key; + prevoff = m_indexfile.Position; + } + } + } + + private string ReadNextLine() + { + var p = m_indexfile.Position; + var max = m_indexfile.Read(m_linebuf, 0, m_linebuf.Length); + if (max == 0) + return null; + + var lfi = 0; + for(int i = 0; i < max; i++) + if (m_linebuf[i] == m_newline[lfi]) + { + if (lfi == m_newline.Length - 1) + { + m_indexfile.Position = p + i + 1; + return System.Text.Encoding.Default.GetString(m_linebuf, 0, i - m_newline.Length + 1); + + } + else + { + lfi++; + } + } + else + { + lfi = 0; + } + + throw new Exception(string.Format("Unexpected long line starting at offset {0}, read {1} bytes without a newline", p, m_linebuf.Length)); + } + + private IEnumerable> AllFileLines() + { + while(true) + { + var str = ReadNextLine(); + if (str == null) + break; + + if (str.Length == 0) + continue; + + var ix = str.IndexOf(", "); + if (ix < 0) + Console.WriteLine("Failed to parse line starting at offset {0} in index file, string: {1}", m_indexfile.Position - str.Length - m_newline.Length, str); + yield return new KeyValuePair(str.Substring(0, ix), str.Substring(ix + 2)); + } + } + + public IEnumerable ReadBlocklistHashes(string hash) + { + var bytes = ReadHash(hash); + for(var i = 0; i < bytes.Length; i += m_hashsize) + yield return Convert.ToBase64String(bytes, i, m_hashsize); + } + + public byte[] ReadHash(string hash) + { + var ix = m_lookup.BinarySearch(hash, StringComparer.Ordinal); + if (ix < 0) + { + ix = ~ix; + if (ix != 0) + ix--; + } + + m_indexfile.Position = m_offsets[ix]; + + foreach(var v in AllFileLines()) + { + if (v.Key == hash) + using(var fs = m_cache.ReadBlock(v.Value, hash)) + { + var buf = new byte[m_blocksize]; + var l = fs.Read(buf, 0, buf.Length); + Array.Resize(ref buf, l); + + return buf; + } + + + if (StringComparer.Ordinal.Compare(hash, v.Key) < 0) + break; + } + + throw new Exception(string.Format("Unable to locate block with hash: {0}", hash)); + } + + public void WriteHash(Stream sw, string hash) + { + var h = ReadHash(hash); + sw.Write(h, 0, h.Length); + } + + + public void Dispose() + { + if (m_indexfile != null) + m_indexfile.Dispose(); + m_indexfile = null; + } + } + + private class CompressedFileMRUCache : IDisposable + { + private Dictionary m_lookup = new Dictionary(); + private List m_mru = new List(); + private Dictionary m_options; + + private const int MAX_OPEN_ARCHIVES = 20; + + public CompressedFileMRUCache(Dictionary options) + { + m_options = options; + } + + public Stream ReadBlock(string filename, string hash) + { + Library.Interface.ICompression cf; + if (!m_lookup.TryGetValue(filename, out cf)) + { + cf = Library.DynamicLoader.CompressionLoader.GetModule(Path.GetExtension(filename).Trim('.'), filename, m_options); + if (cf == null) + throw new Exception(string.Format("Unable to decompress {0}, no such compression module {1}", filename, Path.GetExtension(filename).Trim('.'))); + m_lookup[filename] = cf; + } + + if (m_mru.Count > 0 && m_mru[0] != filename) + { + m_mru.Remove(filename); + m_mru.Insert(0, filename); + } + + while (m_mru.Count > MAX_OPEN_ARCHIVES) + { + var f = m_mru.Last(); + m_lookup[f].Dispose(); + m_lookup.Remove(f); + m_mru.Remove(f); + } + + return cf.OpenRead(Library.Utility.Utility.Base64PlainToBase64Url(hash)); + } + + #region IDisposable implementation + + public void Dispose() + { + foreach(var v in m_lookup.Values) + try { v.Dispose(); } + catch { } + + m_lookup = null; + m_mru = null; + } + + #endregion + } + } +} + diff --git a/Duplicati/CommandLine/RecoveryTool/help.txt b/Duplicati/CommandLine/RecoveryTool/help.txt new file mode 100644 index 000000000..a96cc100b --- /dev/null +++ b/Duplicati/CommandLine/RecoveryTool/help.txt @@ -0,0 +1,74 @@ +Duplicati Recovery Tool +======================= + +This tool performs a recovery of as much data as possible in small steps that must be performed in order. +We recommend that you use Duplicati.CommandLine.exe to do the restore, and rely only on this tool if all else fails. + + +The steps to perform are: +------------------------- + +1: Download + Download files from the remote store and keep them unencrypted on a location available in the local filesystem. + +2: Index + Builds an index file to figure out what data is contained inside the files downloaded + +3: Restore + Restores the files to a destination you choose + + +Optionally you can also run: +---------------------------- + +4: List + Shows what files are available and tests filters + + + +Command descriptions: +===================== + + +Download +-------- + +Usage: +Duplicati.RecoveryTool.exe download [options] + +Downloads all files matching the Duplicati filenames from to , and decrypts them in the process. +The remote url must be one supported by Duplicati. +Use `Duplicati.CommandLine.exe help backends` to see backends and options. + + + +Index +----- +Duplicati.RecoveryTool.exe index [options] + +Examines all files found in localfolder and produces an index.txt file, which is a list of all block hashes found in the files. +The index file can be rather large. It defaults to being stored in but can be specified with --indexfile. +Some files are created in the system temporary folder, use --tempdir to set an alternative temporary folder location. + + + +Restore +------- +Duplicati.RecoveryTool.exe index [version] [options] + +Restores all files to their respective destinations. Use --targetpath to choose another folder where the files are restored into. +Use the filters, --exclude, to perform a partial restore. +Version can be either a number, a filename or a date. If omitted the most recent backup is used. + + + +List +---- +Duplicati.RecoveryTool.exe list [version] [options] + +Lists contents of backups. +Version can be either a number, a filename or a date. +If [version] is omitted a list of backup versions are shown, if [version] is supplied, files from that version are listed. +Use the filters, --exclude, to show a subset of files. + + diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/Duplicati.GUI.TrayIcon.csproj b/Duplicati/GUI/Duplicati.GUI.TrayIcon/Duplicati.GUI.TrayIcon.csproj index ca386af17..6b7cc666a 100644 --- a/Duplicati/GUI/Duplicati.GUI.TrayIcon/Duplicati.GUI.TrayIcon.csproj +++ b/Duplicati/GUI/Duplicati.GUI.TrayIcon/Duplicati.GUI.TrayIcon.csproj @@ -46,6 +46,7 @@ false + --webservice-webroot=/Users/kenneth/Udvikler/my-new-angular-app bin\Release @@ -276,6 +277,10 @@ {D60AD540-0E7D-40CE-83AE-D26E01FFE9B8} Duplicati.Library.Backend.HubiC + + {4A010589-76E6-4F05-A5C4-4598D5DF11F8} + Duplicati.CommandLine.RecoveryTool + diff --git a/Duplicati/Library/Utility/Duplicati.Library.Utility.csproj b/Duplicati/Library/Utility/Duplicati.Library.Utility.csproj index 2408d8baf..1b134d619 100644 --- a/Duplicati/Library/Utility/Duplicati.Library.Utility.csproj +++ b/Duplicati/Library/Utility/Duplicati.Library.Utility.csproj @@ -68,6 +68,7 @@ + diff --git a/Duplicati/Server/Duplicati.Server.csproj b/Duplicati/Server/Duplicati.Server.csproj index eb4301658..fb4dae71b 100644 --- a/Duplicati/Server/Duplicati.Server.csproj +++ b/Duplicati/Server/Duplicati.Server.csproj @@ -247,6 +247,10 @@ {D60AD540-0E7D-40CE-83AE-D26E01FFE9B8} Duplicati.Library.Backend.HubiC + + {4A010589-76E6-4F05-A5C4-4598D5DF11F8} + Duplicati.CommandLine.RecoveryTool +