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))
{