From fecde4f992aee3471b1b2ab6bf7dbdbd370247d0 Mon Sep 17 00:00:00 2001 From: Kenneth Hsu Date: Mon, 7 Jan 2019 21:38:39 -0800 Subject: [PATCH] Add command-line program to import backup configurations. This concerns issue #2693. --- Duplicati.sln | 6 + .../ConfigurationImporter.csproj | 56 ++++++++++ Duplicati/ConfigurationImporter/Program.cs | 105 ++++++++++++++++++ .../Properties/AssemblyInfo.cs | 42 +++++++ Duplicati/Server/Database/Connection.cs | 10 +- .../Serializable/ImportExportStructure.cs | 5 +- .../Server/WebServer/RESTMethods/Backups.cs | 6 +- 7 files changed, 218 insertions(+), 12 deletions(-) create mode 100644 Duplicati/ConfigurationImporter/ConfigurationImporter.csproj create mode 100644 Duplicati/ConfigurationImporter/Program.cs create mode 100644 Duplicati/ConfigurationImporter/Properties/AssemblyInfo.cs diff --git a/Duplicati.sln b/Duplicati.sln index 1d58a62fb..11feb4e99 100644 --- a/Duplicati.sln +++ b/Duplicati.sln @@ -101,6 +101,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.Backend.R EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.Common", "Duplicati\Library\Common\Duplicati.Library.Common.csproj", "{D63E53E4-A458-4C2F-914D-92F715F58ACF}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConfigurationImporter", "Duplicati\ConfigurationImporter\ConfigurationImporter.csproj", "{864B98B9-1B56-40D9-9733-9002E5B9B9D5}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -303,6 +305,10 @@ Global {D63E53E4-A458-4C2F-914D-92F715F58ACF}.Debug|Any CPU.Build.0 = Debug|Any CPU {D63E53E4-A458-4C2F-914D-92F715F58ACF}.Release|Any CPU.ActiveCfg = Release|Any CPU {D63E53E4-A458-4C2F-914D-92F715F58ACF}.Release|Any CPU.Build.0 = Release|Any CPU + {864B98B9-1B56-40D9-9733-9002E5B9B9D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {864B98B9-1B56-40D9-9733-9002E5B9B9D5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {864B98B9-1B56-40D9-9733-9002E5B9B9D5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {864B98B9-1B56-40D9-9733-9002E5B9B9D5}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Duplicati/ConfigurationImporter/ConfigurationImporter.csproj b/Duplicati/ConfigurationImporter/ConfigurationImporter.csproj new file mode 100644 index 000000000..d5b7c7ee6 --- /dev/null +++ b/Duplicati/ConfigurationImporter/ConfigurationImporter.csproj @@ -0,0 +1,56 @@ + + + + Debug + AnyCPU + {864B98B9-1B56-40D9-9733-9002E5B9B9D5} + Exe + ConfigurationImporter + ConfigurationImporter + v4.7 + + + true + full + false + bin\Debug + DEBUG; + prompt + 4 + true + + + true + bin\Release + prompt + 4 + true + + + + + + + + + + + + {19E661D2-C5DA-4F35-B3EE-7586E5734B5F} + Duplicati.Server + + + {DE3E5D4C-51AB-4E5E-BEE8-E636CEBFBA65} + Duplicati.Library.Utility + + + {33FD1D24-C28F-4C71-933F-98F1586EA76C} + Duplicati.Server.Serialization + + + {2C838169-B187-4B09-8768-1C24C2521C8D} + Duplicati.Library.SQLiteHelper + + + + \ No newline at end of file diff --git a/Duplicati/ConfigurationImporter/Program.cs b/Duplicati/ConfigurationImporter/Program.cs new file mode 100644 index 000000000..479d4fdcc --- /dev/null +++ b/Duplicati/ConfigurationImporter/Program.cs @@ -0,0 +1,105 @@ +// Copyright (C) 2019, 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 Duplicati.Library.SQLiteHelper; +using Duplicati.Server; +using Duplicati.Server.Serializable; +using Duplicati.Server.WebServer.RESTMethods; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Duplicati.CommandLine.ConfigurationImporter +{ + public class ConfigurationImporter + { + private static readonly string usageString = $"Usage: {nameof(ConfigurationImporter)}.exe --import-metadata=(true | false) []..."; + + public static void Main(string[] args) + { + if (args.Length < 2) + { + throw new ArgumentException($"Incorrect number of input arguments. {ConfigurationImporter.usageString}"); + } + + string configurationFile = args[0]; + Dictionary importOptions = Duplicati.Library.Utility.CommandLineParser.ExtractOptions(new List { args[1] }); + if (!importOptions.TryGetValue("import-metadata", out string importMetadataString)) + { + throw new ArgumentException($"Invalid import-metadata argument. {ConfigurationImporter.usageString}"); + } + bool importMetadata = Duplicati.Library.Utility.Utility.ParseBool(importMetadataString, false); + + // This removes the ID and DBPath from the backup configuration. + ImportExportStructure importedStructure = Backups.LoadConfiguration(configurationFile, importMetadata, () => ConfigurationImporter.ReadPassword($"Password for {configurationFile}: ")); + + Dictionary advancedOptions = Duplicati.Library.Utility.CommandLineParser.ExtractOptions(new List(args.Skip(2))); + + // This will create the Duplicati-server.sqlite database file if it doesn't exist. + Duplicati.Server.Database.Connection connection = Program.GetDatabaseConnection(advancedOptions); + + if (connection.Backups.Any(x => x.Name.Equals(importedStructure.Backup.Name, StringComparison.OrdinalIgnoreCase))) + { + throw new InvalidOperationException($"A backup with the name {importedStructure.Backup.Name} already exists."); + } + + string error = connection.ValidateBackup(importedStructure.Backup, importedStructure.Schedule); + if (!string.IsNullOrWhiteSpace(error)) + { + throw new InvalidOperationException(error); + } + + // This creates a new ID and DBPath. + connection.AddOrUpdateBackupAndSchedule(importedStructure.Backup, importedStructure.Schedule); + + // This creates the local database file at the location specified by the DBPath. + SQLiteLoader.LoadConnection(importedStructure.Backup.DBPath); + + Console.WriteLine($"Imported \"{importedStructure.Backup.Name}\" with ID {importedStructure.Backup.ID} and local database at {importedStructure.Backup.DBPath}."); + } + + private static string ReadPassword(string prompt) + { + Console.Write(prompt); + string password = ""; + while (true) + { + ConsoleKeyInfo keyInfo = Console.ReadKey(true); + if (keyInfo.Key == ConsoleKey.Enter) + { + break; + } + if (keyInfo.Key == ConsoleKey.Backspace) + { + if (password.Length > 0) + { + password = password.Substring(0, password.Length - 1); + Console.Write("\b \b"); + } + } + else if (keyInfo.KeyChar != '\u0000') // Only accept if the key maps to a Unicode character (e.g., ignore F1 or Home). + { + + password += keyInfo.KeyChar; + Console.Write("*"); + } + } + + Console.WriteLine(); + return password; + } + } +} diff --git a/Duplicati/ConfigurationImporter/Properties/AssemblyInfo.cs b/Duplicati/ConfigurationImporter/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..573882cd0 --- /dev/null +++ b/Duplicati/ConfigurationImporter/Properties/AssemblyInfo.cs @@ -0,0 +1,42 @@ +// Copyright (C) 2019, 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("ConfigurationImporter")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("")] +[assembly: AssemblyCopyright("")] +[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/Server/Database/Connection.cs b/Duplicati/Server/Database/Connection.cs index 806c4cb6d..7ebd35f97 100644 --- a/Duplicati/Server/Database/Connection.cs +++ b/Duplicati/Server/Database/Connection.cs @@ -372,12 +372,12 @@ namespace Duplicati.Server.Database } } - internal void AddOrUpdateBackupAndSchedule(IBackup item, ISchedule schedule) + public void AddOrUpdateBackupAndSchedule(IBackup item, ISchedule schedule) { AddOrUpdateBackup(item, true, schedule); } - internal string ValidateBackup(IBackup item, ISchedule schedule) + public string ValidateBackup(IBackup item, ISchedule schedule) { if (string.IsNullOrWhiteSpace(item.Name)) return "Missing a name"; @@ -506,7 +506,7 @@ namespace Duplicati.Server.Database var folder = Program.DataFolder; if (!System.IO.Directory.Exists(folder)) System.IO.Directory.CreateDirectory(folder); - + for(var i = 0; i < 100; i++) { var guess = System.IO.Path.Combine(folder, System.IO.Path.ChangeExtension(Duplicati.Library.Main.DatabaseLocator.GenerateRandomName(), ".sqlite")); @@ -520,7 +520,7 @@ namespace Duplicati.Server.Database if (item.DBPath == null) throw new Exception("Unable to generate a unique database file name"); } - + using(var tr = m_connection.BeginTransaction()) { OverwriteAndUpdateDb( @@ -669,7 +669,7 @@ namespace Duplicati.Server.Database tr.Commit(); } } - + System.Threading.Interlocked.Increment(ref Program.LastDataUpdateID); Program.StatusEventNotifyer.SignalNewEvent(); } diff --git a/Duplicati/Server/Serializable/ImportExportStructure.cs b/Duplicati/Server/Serializable/ImportExportStructure.cs index bd5d186da..cd1ba632e 100644 --- a/Duplicati/Server/Serializable/ImportExportStructure.cs +++ b/Duplicati/Server/Serializable/ImportExportStructure.cs @@ -15,19 +15,16 @@ // 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; namespace Duplicati.Server.Serializable { - internal class ImportExportStructure + public class ImportExportStructure { public string CreatedByVersion { get; set; } public Duplicati.Server.Database.Schedule Schedule { get; set; } public Duplicati.Server.Database.Backup Backup { get; set; } public Dictionary DisplayNames { get; set; } } - - } diff --git a/Duplicati/Server/WebServer/RESTMethods/Backups.cs b/Duplicati/Server/WebServer/RESTMethods/Backups.cs index ff73120bd..cbef4d1b5 100644 --- a/Duplicati/Server/WebServer/RESTMethods/Backups.cs +++ b/Duplicati/Server/WebServer/RESTMethods/Backups.cs @@ -71,7 +71,7 @@ namespace Duplicati.Server.WebServer.RESTMethods if (file == null) throw new Exception("No file uploaded"); - Serializable.ImportExportStructure ipx = Backups.LoadConfiguration(file.Filename, import_metadata, input["passphrase"].Value); + Serializable.ImportExportStructure ipx = Backups.LoadConfiguration(file.Filename, import_metadata, () => input["passphrase"].Value); if (direct) { lock (Program.DataConnection.m_lock) @@ -121,7 +121,7 @@ namespace Duplicati.Server.WebServer.RESTMethods } } - internal static Serializable.ImportExportStructure LoadConfiguration(string filename, bool importMetadata, string passphrase = null) + public static Serializable.ImportExportStructure LoadConfiguration(string filename, bool importMetadata, Func getPassword) { Serializable.ImportExportStructure ipx; @@ -133,7 +133,7 @@ namespace Duplicati.Server.WebServer.RESTMethods fs.Position = 0; if (buf[0] == 'A' && buf[1] == 'E' && buf[2] == 'S') { - using (var m = new Duplicati.Library.Encryption.AESEncryption(passphrase, new Dictionary())) + using (var m = new Duplicati.Library.Encryption.AESEncryption(getPassword(), new Dictionary())) { using (var m2 = m.Decrypt(fs)) {