Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/duplicati/duplicati.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKenneth Skovhede <kenneth@hexad.dk>2017-01-07 01:01:54 +0300
committerKenneth Skovhede <kenneth@hexad.dk>2017-01-07 01:01:54 +0300
commit9a849a2b58943c53eeba995c7b78d39bfda95dac (patch)
tree9a40f26c0a75f4e999b217bf4b6b5eb0325a0d4d
parent9c0b463765bb64d4bf31f5efb96305924fc8b01c (diff)
Improved support for cross-os restores.
This fixes #1302 This fixes #1983
-rw-r--r--Duplicati/Library/Main/Database/LocalBackupDatabase.cs14
-rw-r--r--Duplicati/Library/Main/Database/LocalListDatabase.cs27
-rw-r--r--Duplicati/Library/Main/Database/LocalRestoreDatabase.cs85
-rw-r--r--Duplicati/Library/Main/Operation/BackupHandler.cs4
-rw-r--r--Duplicati/Library/Main/Operation/RestoreHandler.cs5
-rw-r--r--Duplicati/Library/Main/Options.cs15
-rw-r--r--Duplicati/Library/Main/Strings.cs2
-rw-r--r--Duplicati/Library/Utility/Utility.cs27
-rw-r--r--Duplicati/Server/WebServer/RESTMethods/Backup.cs6
-rw-r--r--Duplicati/Server/webroot/ngax/scripts/controllers/RestoreController.js30
-rw-r--r--Duplicati/Server/webroot/ngax/scripts/directives/restoreFilePicker.js14
11 files changed, 181 insertions, 48 deletions
diff --git a/Duplicati/Library/Main/Database/LocalBackupDatabase.cs b/Duplicati/Library/Main/Database/LocalBackupDatabase.cs
index a3c760e2f..3ce409eed 100644
--- a/Duplicati/Library/Main/Database/LocalBackupDatabase.cs
+++ b/Duplicati/Library/Main/Database/LocalBackupDatabase.cs
@@ -721,5 +721,19 @@ namespace Duplicati.Library.Main.Database
throw new Exception(string.Format("Failed to link filesetid {0} to volumeid {1}", filesetid, volumeid));
}
}
+
+ public string GetFirstPath()
+ {
+ using (var cmd = m_connection.CreateCommand())
+ {
+ cmd.CommandText = string.Format(@"SELECT ""Path"" FROM ""File"" ORDER BY LENGTH(""Path"") DESC LIMIT 1");
+ var v0 = cmd.ExecuteScalar();
+ if (v0 == null || v0 == DBNull.Value)
+ return null;
+
+ return v0.ToString();
+ }
+ }
+
}
}
diff --git a/Duplicati/Library/Main/Database/LocalListDatabase.cs b/Duplicati/Library/Main/Database/LocalListDatabase.cs
index d634eb6ec..89f3a8c86 100644
--- a/Duplicati/Library/Main/Database/LocalListDatabase.cs
+++ b/Duplicati/Library/Main/Database/LocalListDatabase.cs
@@ -147,6 +147,8 @@ namespace Duplicati.Library.Main.Database
string maxpath = "";
if (v0 != null)
maxpath = v0.ToString();
+
+ var dirsep = Duplicati.Library.Utility.Utility.GuessDirSeparator(maxpath);
cmd.CommandText = string.Format(@"SELECT COUNT(*) FROM ""{0}""", tmpnames.Tablename);
var filecount = cmd.ExecuteScalarInt64(0);
@@ -159,7 +161,7 @@ namespace Duplicati.Library.Main.Database
while (filecount != foundfiles && maxpath.Length > 0)
{
- var mp = Library.Utility.Utility.AppendDirSeparator(maxpath);
+ var mp = Duplicati.Library.Utility.Utility.AppendDirSeparator(maxpath, dirsep);
cmd.SetParameterValue(0, mp.Length);
cmd.SetParameterValue(1, mp);
foundfiles = cmd.ExecuteScalarInt64(0);
@@ -167,7 +169,8 @@ namespace Duplicati.Library.Main.Database
if (filecount != foundfiles)
{
var oldlen = maxpath.Length;
- maxpath = Library.Snapshots.SnapshotUtility.SystemIO.PathGetDirectoryName(maxpath);
+ var lix = maxpath.LastIndexOf(dirsep, maxpath.Length - 2, StringComparison.Ordinal);
+ maxpath = maxpath.Substring(0, lix + 1);
if (string.IsNullOrWhiteSpace(maxpath) || maxpath.Length == oldlen)
maxpath = "";
}
@@ -182,7 +185,7 @@ namespace Duplicati.Library.Main.Database
return
new IFileversion[] {
- new FileversionFixed() { Path = maxpath == "" ? "" : Library.Utility.Utility.AppendDirSeparator(maxpath) }
+ new FileversionFixed() { Path = maxpath == "" ? "" : Duplicati.Library.Utility.Utility.AppendDirSeparator(maxpath, dirsep) }
};
}
@@ -191,7 +194,7 @@ namespace Duplicati.Library.Main.Database
private IEnumerable<string> SelectFolderEntries(System.Data.IDbCommand cmd, string prefix, string table)
{
if (!string.IsNullOrEmpty(prefix))
- prefix = Duplicati.Library.Utility.Utility.AppendDirSeparator(prefix);
+ prefix = Duplicati.Library.Utility.Utility.AppendDirSeparator(prefix, Duplicati.Library.Utility.Utility.GuessDirSeparator(prefix));
var ppl = prefix.Length;
using(var rd = cmd.ExecuteReader(string.Format(@"SELECT DISTINCT ""Path"" FROM ""{0}"" ", table)))
@@ -200,9 +203,11 @@ namespace Duplicati.Library.Main.Database
var s = rd.GetString(0);
if (!s.StartsWith(prefix))
continue;
-
+
+ var dirsep = Duplicati.Library.Utility.Utility.GuessDirSeparator(s);
+
s = s.Substring(ppl);
- var ix = s.IndexOf(System.IO.Path.DirectorySeparatorChar);
+ var ix = s.IndexOf(dirsep, StringComparison.Ordinal);
if (ix > 0 && ix != s.Length - 1)
s = s.Substring(0, ix + 1);
yield return prefix + s;
@@ -221,11 +226,13 @@ namespace Duplicati.Library.Main.Database
throw new ArgumentException("Filter for list-folder-contents must be a path prefix with no wildcards", "filter");
else
pathprefix = ((Library.Utility.FilterExpression)filter).GetSimpleList().First();
+
+ var dirsep = Duplicati.Library.Utility.Utility.GuessDirSeparator(pathprefix);
+
+ if (pathprefix.Length > 0 || dirsep == "/")
+ pathprefix = Duplicati.Library.Utility.Utility.AppendDirSeparator(pathprefix, dirsep);
- if (pathprefix.Length > 0 || Duplicati.Library.Utility.Utility.IsClientLinux)
- pathprefix = Duplicati.Library.Utility.Utility.AppendDirSeparator(pathprefix);
-
- using(var tmpnames = new FilteredFilenameTable(m_connection, new Library.Utility.FilterExpression(pathprefix + "*", true), null))
+ using(var tmpnames = new FilteredFilenameTable(m_connection, new Library.Utility.FilterExpression(new string[] { pathprefix + "*" }, true), null))
using(var cmd = m_connection.CreateCommand())
{
//First we trim the filelist to exclude filenames not found in any of the filesets
diff --git a/Duplicati/Library/Main/Database/LocalRestoreDatabase.cs b/Duplicati/Library/Main/Database/LocalRestoreDatabase.cs
index bc4220ac7..db8efdd12 100644
--- a/Duplicati/Library/Main/Database/LocalRestoreDatabase.cs
+++ b/Duplicati/Library/Main/Database/LocalRestoreDatabase.cs
@@ -201,7 +201,7 @@ namespace Duplicati.Library.Main.Database
cmd.ExecuteNonQuery(string.Format(@"DROP TABLE IF EXISTS ""{0}"" ", m_tempfiletable));
cmd.ExecuteNonQuery(string.Format(@"DROP TABLE IF EXISTS ""{0}"" ", m_tempblocktable));
- cmd.ExecuteNonQuery(string.Format(@"CREATE TEMPORARY TABLE ""{0}"" (""ID"" INTEGER PRIMARY KEY, ""Path"" TEXT NOT NULL, ""BlocksetID"" INTEGER NOT NULL, ""MetadataID"" INTEGER NOT NULL, ""Targetpath"" TEXT NULL, ""DataVerified"" BOOLEAN NOT NULL) ", m_tempfiletable));
+ cmd.ExecuteNonQuery(string.Format(@"CREATE TEMPORARY TABLE ""{0}"" (""ID"" INTEGER PRIMARY KEY, ""Path"" TEXT NOT NULL, ""BlocksetID"" INTEGER NOT NULL, ""MetadataID"" INTEGER NOT NULL, ""TargetPath"" TEXT NULL, ""DataVerified"" BOOLEAN NOT NULL) ", m_tempfiletable));
cmd.ExecuteNonQuery(string.Format(@"CREATE TEMPORARY TABLE ""{0}"" (""ID"" INTEGER PRIMARY KEY, ""FileID"" INTEGER NOT NULL, ""Index"" INTEGER NOT NULL, ""Hash"" TEXT NOT NULL, ""Size"" INTEGER NOT NULL, ""Restored"" BOOLEAN NOT NULL, ""Metadata"" BOOLEAN NOT NULL)", m_tempblocktable));
cmd.ExecuteNonQuery(string.Format(@"CREATE INDEX ""{0}_Index"" ON ""{0}"" (""TargetPath"")", m_tempfiletable));
cmd.ExecuteNonQuery(string.Format(@"CREATE INDEX ""{0}_HashSizeIndex"" ON ""{0}"" (""Hash"", ""Size"")", m_tempblocktable));
@@ -315,6 +315,19 @@ namespace Duplicati.Library.Main.Database
return new Tuple<long, long>(0, 0);
}
+ public string GetFirstPath()
+ {
+ using (var cmd = m_connection.CreateCommand())
+ {
+ cmd.CommandText = string.Format(@"SELECT ""Path"" FROM ""{0}"" ORDER BY LENGTH(""Path"") DESC LIMIT 1", m_tempfiletable);
+ var v0 = cmd.ExecuteScalar();
+ if (v0 == null || v0 == DBNull.Value)
+ return null;
+
+ return v0.ToString();
+ }
+ }
+
public string GetLargestPrefix()
{
using (var cmd = m_connection.CreateCommand())
@@ -322,9 +335,11 @@ namespace Duplicati.Library.Main.Database
cmd.CommandText = string.Format(@"SELECT ""Path"" FROM ""{0}"" ORDER BY LENGTH(""Path"") DESC LIMIT 1", m_tempfiletable);
var v0 = cmd.ExecuteScalar();
string maxpath = "";
- if (v0 != null)
+ if (v0 != null && v0 != DBNull.Value)
maxpath = v0.ToString();
+ var dirsep = Duplicati.Library.Utility.Utility.GuessDirSeparator(maxpath);
+
cmd.CommandText = string.Format(@"SELECT COUNT(*) FROM ""{0}""", m_tempfiletable);
var filecount = cmd.ExecuteScalarInt64(-1);
long foundfiles = -1;
@@ -336,7 +351,7 @@ namespace Duplicati.Library.Main.Database
while (filecount != foundfiles && maxpath.Length > 0)
{
- var mp = Library.Utility.Utility.AppendDirSeparator(maxpath);
+ var mp = Library.Utility.Utility.AppendDirSeparator(maxpath, dirsep);
cmd.SetParameterValue(0, mp.Length);
cmd.SetParameterValue(1, mp);
foundfiles = cmd.ExecuteScalarInt64(-1);
@@ -344,19 +359,22 @@ namespace Duplicati.Library.Main.Database
if (filecount != foundfiles)
{
var oldlen = maxpath.Length;
- maxpath = Duplicati.Library.Snapshots.SnapshotUtility.SystemIO.PathGetDirectoryName(maxpath);
+
+ var lix = maxpath.LastIndexOf(dirsep, maxpath.Length - 2, StringComparison.Ordinal);
+ maxpath = maxpath.Substring(0, lix + 1);
if (string.IsNullOrWhiteSpace(maxpath) || maxpath.Length == oldlen)
maxpath = "";
}
}
- return maxpath == "" ? "" : Library.Utility.Utility.AppendDirSeparator(maxpath);
+ return maxpath == "" ? "" : Library.Utility.Utility.AppendDirSeparator(maxpath, dirsep);
}
-
}
public void SetTargetPaths(string largest_prefix, string destination)
{
+ var dirsep = Duplicati.Library.Utility.Utility.GuessDirSeparator(string.IsNullOrWhiteSpace(largest_prefix) ? GetFirstPath() : largest_prefix);
+
using(var cmd = m_connection.CreateCommand())
{
if (string.IsNullOrEmpty(destination))
@@ -364,33 +382,56 @@ namespace Duplicati.Library.Main.Database
//The string fixing here is meant to provide some non-random
// defaults when restoring cross OS, e.g. backup on Linux, restore on Windows
//This is mostly meaningless, and the user really should use --restore-path
-
- if (Library.Utility.Utility.IsClientLinux)
+
+ if (Library.Utility.Utility.IsClientLinux && dirsep == "\\")
+ {
// For Win -> Linux, we remove the colon from the drive letter, and use the drive letter as root folder
- cmd.ExecuteNonQuery(string.Format(@"UPDATE ""{0}"" SET ""Targetpath"" = CASE WHEN SUBSTR(""Path"", 2, 1) == "":"" THEN ""/"" || SUBSTR(""Path"", 1, 1) || SUBSTR(""Path"", 3) ELSE ""Path"" END", m_tempfiletable));
- else
+ cmd.ExecuteNonQuery(string.Format(@"UPDATE ""{0}"" SET ""Targetpath"" = CASE WHEN SUBSTR(""Path"", 2, 1) == "":"" THEN ""\\"" || SUBSTR(""Path"", 1, 1) || SUBSTR(""Path"", 3) ELSE ""Path"" END", m_tempfiletable));
+ cmd.ExecuteNonQuery(string.Format(@"UPDATE ""{0}"" SET ""Targetpath"" = CASE WHEN SUBSTR(""Path"", 1, 2) == ""\\"" THEN ""\\"" || SUBSTR(""Path"", 2) ELSE ""Path"" END", m_tempfiletable));
+
+ }
+ else if (Library.Utility.Utility.IsClientWindows && dirsep == "/")
+ {
// For Linux -> Win, we use the temporary folder's drive as the root path
- cmd.ExecuteNonQuery(string.Format(@"UPDATE ""{0}"" SET ""Targetpath"" = CASE WHEN SUBSTR(""Path"", 1, 1) == ""/"" THEN ? || SUBSTR(""Path"", 2) ELSE ""Path"" END", m_tempfiletable), Library.Utility.Utility.AppendDirSeparator(System.IO.Path.GetPathRoot(Library.Utility.TempFolder.SystemTempPath)));
-
+ cmd.ExecuteNonQuery(string.Format(@"UPDATE ""{0}"" SET ""Targetpath"" = CASE WHEN SUBSTR(""Path"", 1, 1) == ""/"" THEN ? || SUBSTR(""Path"", 2) ELSE ""Path"" END", m_tempfiletable), Library.Utility.Utility.AppendDirSeparator(System.IO.Path.GetPathRoot(Library.Utility.TempFolder.SystemTempPath)).Replace("\\", "/"));
+ }
}
else
{
if (string.IsNullOrEmpty(largest_prefix))
{
- //Special case, restoring to new folder, but files are from different drives
- // So we use the format <restore path> / <drive letter> / <source path>
- // To avoid generating paths with a colon
- cmd.ExecuteNonQuery(string.Format(@"UPDATE ""{0}"" SET ""Targetpath"" = ? || CASE WHEN SUBSTR(""Path"", 2, 1) == "":"" THEN SUBSTR(""Path"", 1, 1) || SUBSTR(""Path"", 3) ELSE ""Path"" END", m_tempfiletable), destination);
+ //Special case, restoring to new folder, but files are from different drives (no shared root on Windows)
+
+ // We use the format <restore path> / <drive letter> / <source path>
+ cmd.ExecuteNonQuery(string.Format(@"UPDATE ""{0}"" SET ""TargetPath"" = CASE WHEN SUBSTR(""Path"", 2, 1) == "":"" THEN SUBSTR(""Path"", 1, 1) || SUBSTR(""Path"", 3) ELSE ""Path"" END", m_tempfiletable));
+
+ // For UNC paths, we use \\server\folder -> <restore path> / <servername> / <source path>
+ cmd.ExecuteNonQuery(string.Format(@"UPDATE ""{0}"" SET ""TargetPath"" = CASE WHEN SUBSTR(""Path"", 1, 2) == ""\\"" THEN SUBSTR(""Path"", 2) ELSE ""Path"" END", m_tempfiletable));
}
else
{
- largest_prefix = Library.Utility.Utility.AppendDirSeparator(largest_prefix);
- cmd.CommandText = string.Format(@"UPDATE ""{0}"" SET ""Targetpath"" = ? || SUBSTR(""Path"", ?)", m_tempfiletable);
- cmd.AddParameter(destination);
- cmd.AddParameter(largest_prefix.Length + 1);
- cmd.ExecuteNonQuery();
+ largest_prefix = Library.Utility.Utility.AppendDirSeparator(largest_prefix, dirsep);
+ cmd.ExecuteNonQuery(string.Format(@"UPDATE ""{0}"" SET ""TargetPath"" = SUBSTR(""Path"", ?)", m_tempfiletable), largest_prefix.Length + 1);
}
- }
+ }
+
+ // Cross-os path remapping support
+ if (Library.Utility.Utility.IsClientLinux && dirsep == "\\")
+ // For Win paths on Linux
+ cmd.ExecuteNonQuery(string.Format(@"UPDATE ""{0}"" SET ""TargetPath"" = REPLACE(""TargetPath"", ""\"", ""/"")", m_tempfiletable));
+ else if (Library.Utility.Utility.IsClientWindows && dirsep == "/")
+ // For Linux paths on Windows
+ cmd.ExecuteNonQuery(string.Format(@"UPDATE ""{0}"" SET ""TargetPath"" = REPLACE(REPLACE(""TargetPath"", ""\"", ""_""), ""/"", ""\"")", m_tempfiletable));
+
+ if (!string.IsNullOrEmpty(destination))
+ {
+ // Paths are now relative with target-os naming system
+ // so we prefix them with the target path
+
+ cmd.ExecuteNonQuery(string.Format(@"UPDATE ""{0}"" SET ""TargetPath"" = ? || ""TargetPath"" ", m_tempfiletable), Library.Utility.Utility.AppendDirSeparator(destination));
+ }
+
+
}
}
diff --git a/Duplicati/Library/Main/Operation/BackupHandler.cs b/Duplicati/Library/Main/Operation/BackupHandler.cs
index 821c39dfb..e9f4dea91 100644
--- a/Duplicati/Library/Main/Operation/BackupHandler.cs
+++ b/Duplicati/Library/Main/Operation/BackupHandler.cs
@@ -680,6 +680,10 @@ namespace Duplicati.Library.Main.Operation
Utility.UpdateOptionsFromDb(m_database, m_options);
Utility.VerifyParameters(m_database, m_options);
+ var probe_path = m_database.GetFirstPath();
+ if (probe_path != null && Duplicati.Library.Utility.Utility.GuessDirSeparator(probe_path) != System.IO.Path.DirectorySeparatorChar.ToString())
+ throw new Exception(string.Format("The backup contains files that belong to another operating system. Proceeding with a backup would cause the database to contain paths from two different operation systems, which is not supported. To proceed without loosing remote data, delete all filesets and make sure the --{0} option is set, then run the backup again to re-use the existing data on the remote store.", "no-auto-compact"));
+
if (m_database.PartiallyRecreated)
throw new Exception("The database was only partially recreated. This database may be incomplete and the repair process is not allowed to alter remote files as that could result in data loss.");
diff --git a/Duplicati/Library/Main/Operation/RestoreHandler.cs b/Duplicati/Library/Main/Operation/RestoreHandler.cs
index a780bb056..0ea04d3e3 100644
--- a/Duplicati/Library/Main/Operation/RestoreHandler.cs
+++ b/Duplicati/Library/Main/Operation/RestoreHandler.cs
@@ -776,7 +776,10 @@ namespace Duplicati.Library.Main.Operation
if (!string.IsNullOrEmpty(options.Restorepath))
{
// Find the largest common prefix
- string largest_prefix = database.GetLargestPrefix();
+ var largest_prefix = options.DontCompressRestorePaths ? "" : database.GetLargestPrefix();
+ if (options.DontCompressRestorePaths)
+ largest_prefix = "";
+
result.AddVerboseMessage("Mapping restore path prefix to \"{0}\" to \"{1}\"", largest_prefix, Library.Utility.Utility.AppendDirSeparator(options.Restorepath));
// Set the target paths, special care with C:\ and /
diff --git a/Duplicati/Library/Main/Options.cs b/Duplicati/Library/Main/Options.cs
index 8ce734b18..3c38dd5ba 100644
--- a/Duplicati/Library/Main/Options.cs
+++ b/Duplicati/Library/Main/Options.cs
@@ -369,7 +369,8 @@ namespace Duplicati.Library.Main
"allow-passphrase-change",
"no-local-db",
"no-local-blocks",
- "full-block-verification"
+ "full-block-verification",
+ "dont-compress-restore-paths"
};
}
}
@@ -499,7 +500,8 @@ namespace Duplicati.Library.Main
new CommandLineArgument("patch-with-local-blocks", CommandLineArgument.ArgumentType.Boolean, Strings.Options.PatchwithlocalblocksShort, Strings.Options.PatchwithlocalblocksLong, "false"),
new CommandLineArgument("no-local-db", CommandLineArgument.ArgumentType.Boolean, Strings.Options.NolocaldbShort, Strings.Options.NolocaldbLong, "false"),
-
+ new CommandLineArgument("dont-compress-restore-paths", CommandLineArgument.ArgumentType.Boolean, Strings.Options.DontcompressrestorepathsShort, Strings.Options.DontcompressrestorepathsLong, "false"),
+
new CommandLineArgument("keep-versions", CommandLineArgument.ArgumentType.Integer, Strings.Options.KeepversionsShort, Strings.Options.KeepversionsLong, DEFAULT_KEEP_VERSIONS.ToString()),
new CommandLineArgument("keep-time", CommandLineArgument.ArgumentType.Timespan, Strings.Options.KeeptimeShort, Strings.Options.KeeptimeLong),
new CommandLineArgument("upload-verification-file", CommandLineArgument.ArgumentType.Boolean, Strings.Options.UploadverificationfileShort, Strings.Options.UploadverificationfileLong, "false"),
@@ -1707,6 +1709,15 @@ namespace Duplicati.Library.Main
}
/// <summary>
+ /// Gets a flag indicating if the local database should not be used
+ /// </summary>
+ /// <value><c>true</c> if no local db is used; otherwise, <c>false</c>.</value>
+ public bool DontCompressRestorePaths
+ {
+ get { return Library.Utility.Utility.ParseBoolOption(m_options, "dont-compress-restore-paths"); }
+ }
+
+ /// <summary>
/// Gets a flag indicating if block hashes are checked before being applied
/// </summary>
/// <value><c>true</c> if block hashes are checked; otherwise, <c>false</c>.</value>
diff --git a/Duplicati/Library/Main/Strings.cs b/Duplicati/Library/Main/Strings.cs
index c8ef270af..9c4f813e8 100644
--- a/Duplicati/Library/Main/Strings.cs
+++ b/Duplicati/Library/Main/Strings.cs
@@ -218,6 +218,8 @@ namespace Duplicati.Library.Main.Strings
public static string DisablesyntheticfilelistShort { get { return LC.L(@"Disables synethic filelist"); } }
public static string CheckfiletimeonlyLong { get { return LC.L(@"This flag instructs Duplicati to not look at metadata or filesize when deciding to scan a file for changes. Use this option if you have a large number of files and notice that the scanning takes a long time with unmodified files."); } }
public static string CheckfiletimeonlyShort { get { return LC.L(@"Checks only file lastmodified"); } }
+ public static string DontcompressrestorepathsShort { get { return LC.L(@"Disables path compresion on restore"); } }
+ public static string DontcompressrestorepathsLong { get { return LC.L(@"When restore a subset of a backup into a new folder, the shortest possible path is used to avoid generating deep paths with empty folders. Use this flag to skip this compression, such that the entire original folder structure is preserved, including upper level empty folders."); } }
}
internal static class Common
diff --git a/Duplicati/Library/Utility/Utility.cs b/Duplicati/Library/Utility/Utility.cs
index 139387dcd..6cae5dc05 100644
--- a/Duplicati/Library/Utility/Utility.cs
+++ b/Duplicati/Library/Utility/Utility.cs
@@ -399,13 +399,38 @@ namespace Duplicati.Library.Utility
/// <returns>The path with the directory separator appended</returns>
public static string AppendDirSeparator(string path)
{
- if (!path.EndsWith(DirectorySeparatorString))
+ if (!path.EndsWith(DirectorySeparatorString, StringComparison.Ordinal))
return path += DirectorySeparatorString;
else
return path;
}
/// <summary>
+ /// Appends the appropriate directory separator to paths, depending on OS.
+ /// Does not append the separator if the path already ends with it.
+ /// </summary>
+ /// <param name="path">The path to append to</param>
+ /// <param name="separator">The directory separator to use</param>
+ /// <returns>The path with the directory separator appended</returns>
+ public static string AppendDirSeparator(string path, string separator)
+ {
+ if (!path.EndsWith(separator, StringComparison.Ordinal))
+ return path += separator;
+ else
+ return path;
+ }
+
+ /// <summary>
+ /// Guesses the directory separator from the path
+ /// </summary>
+ /// <param name="path">The path to guess the separator from</param>
+ /// <returns>The guessed directory separator</returns>
+ public static string GuessDirSeparator(string path)
+ {
+ return string.IsNullOrWhiteSpace(path) || path.StartsWith("/", StringComparison.Ordinal) ? "/" : "\\";
+ }
+
+ /// <summary>
/// Some streams can return a number that is less than the requested number of bytes.
/// This is usually due to fragmentation, and is solved by issuing a new read.
/// This function wraps that functionality.
diff --git a/Duplicati/Server/WebServer/RESTMethods/Backup.cs b/Duplicati/Server/WebServer/RESTMethods/Backup.cs
index cbd421f9f..643a786c9 100644
--- a/Duplicati/Server/WebServer/RESTMethods/Backup.cs
+++ b/Duplicati/Server/WebServer/RESTMethods/Backup.cs
@@ -41,7 +41,7 @@ namespace Duplicati.Server.WebServer.RESTMethods
private void SearchFiles(IBackup backup, string filterstring, RequestInfo info)
{
- var filter = filterstring.Split(new string[] { System.IO.Path.PathSeparator.ToString() }, StringSplitOptions.RemoveEmptyEntries);
+ var filter = filterstring;
var timestring = info.Request.QueryString["time"].Value;
var allversion = Duplicati.Library.Utility.Utility.ParseBool(info.Request.QueryString["all-versions"].Value, false);
@@ -57,7 +57,7 @@ namespace Duplicati.Server.WebServer.RESTMethods
if (!allversion)
time = Duplicati.Library.Utility.Timeparser.ParseTimeInterval(timestring, DateTime.Now);
- var r = Runner.Run(Runner.CreateListTask(backup, filter, prefixonly, allversion, foldercontents, time), false) as Duplicati.Library.Interface.IListResults;
+ var r = Runner.Run(Runner.CreateListTask(backup, new string[] { filter }, prefixonly, allversion, foldercontents, time), false) as Duplicati.Library.Interface.IListResults;
var result = new Dictionary<string, object>();
@@ -382,7 +382,7 @@ namespace Duplicati.Server.WebServer.RESTMethods
case "isdbusedelsewhere":
IsDBUsedElseWhere(bk, info);
return;
- case "isactive":
+ case "isactive":
IsActive(bk, info);
return;
default:
diff --git a/Duplicati/Server/webroot/ngax/scripts/controllers/RestoreController.js b/Duplicati/Server/webroot/ngax/scripts/controllers/RestoreController.js
index 00956fd57..72ad84bbb 100644
--- a/Duplicati/Server/webroot/ngax/scripts/controllers/RestoreController.js
+++ b/Duplicati/Server/webroot/ngax/scripts/controllers/RestoreController.js
@@ -13,6 +13,7 @@ backupApp.controller('RestoreController', function ($rootScope, $scope, $routePa
var filesetsRepaired = {};
var filesetStamps = {};
var inProgress = {};
+ var dirsep = $scope.SystemInfo.DirectorySeparator || '/';
$scope.filesetStamps = filesetStamps;
$scope.treedata = {};
@@ -62,6 +63,7 @@ backupApp.controller('RestoreController', function ($rootScope, $scope, $routePa
$scope.fetchBackupTimes = function() {
$scope.connecting = true;
+ $scope.ConnectionProgress = gettextCatalog.getString('Getting file versions ...');
var qp = '';
if ($scope.IsBackupTemporary)
@@ -70,6 +72,7 @@ backupApp.controller('RestoreController', function ($rootScope, $scope, $routePa
AppService.get('/backup/' + $scope.BackupID + '/filesets' + qp).then(
function(resp) {
$scope.connecting = false;
+ $scope.ConnectionProgress = '';
$scope.Filesets = resp.data;
$scope.parseBackupTimesData();
$scope.fetchPathInformation();
@@ -164,6 +167,9 @@ backupApp.controller('RestoreController', function ($rootScope, $scope, $routePa
filesetsBuilt[version] = resp.data.Files;
$scope.Paths = filesetsBuilt[version];
+
+ dirsep = resp.data.Files[0].Path[0] == '/' ? '/' : '\\';
+
}, handleError);
}
} else {
@@ -217,7 +223,6 @@ backupApp.controller('RestoreController', function ($rootScope, $scope, $routePa
function(resp) {
$scope.Searching = false;
var searchNodes = [];
- var dirsep = $scope.SystemInfo.DirectorySeparator || '/';
function compareablePath(path) {
return $scope.SystemInfo.CaseSensitiveFilesystem ? path : path.toLowerCase();
@@ -289,10 +294,29 @@ backupApp.controller('RestoreController', function ($rootScope, $scope, $routePa
};
$scope.onStartRestore = function() {
+ if ($scope.RestoreLocation == 'custom' && ($scope.RestorePath || '').trim().length == 0)
+ {
+ DialogService.alert(gettextCatalog.getString('You have chosen to restore to a new location, but not entered one'));
+ return;
+ }
+
+ if ($scope.RestoreLocation != 'custom' && dirsep != $scope.SystemInfo.DirectorySeparator)
+ {
+ DialogService.confirm(gettextCatalog.getString('This backup was created on another operating system. Restoring files without specifying a destination folder can cause files to be restored in unexpected places. Are you sure you want to continue without choosing a destination folder?'), function(ix) {
+ if (ix == 1)
+ $scope.onStartRestoreProcess();
+ });
+ }
+ else
+ {
+ $scope.onStartRestoreProcess();
+ }
+ }
+
+ $scope.onStartRestoreProcess = function() {
+
var version = $scope.RestoreVersion + '';
var stamp = filesetStamps[version];
- var dirsep = $scope.SystemInfo.DirectorySeparator || '/';
- var pathSep = $scope.SystemInfo.PathSeparator || ':';
function handleError(resp) {
var message = resp.statusText;
diff --git a/Duplicati/Server/webroot/ngax/scripts/directives/restoreFilePicker.js b/Duplicati/Server/webroot/ngax/scripts/directives/restoreFilePicker.js
index 56b60063a..ef142f6bc 100644
--- a/Duplicati/Server/webroot/ngax/scripts/directives/restoreFilePicker.js
+++ b/Duplicati/Server/webroot/ngax/scripts/directives/restoreFilePicker.js
@@ -17,6 +17,8 @@ backupApp.directive('restoreFilePicker', function() {
controller: function($scope, $timeout, SystemInfo, AppService, AppUtils) {
var scope = $scope;
+ var dirsep = '/';
+
$scope.systeminfo = SystemInfo.watch($scope);
$scope.treedata = [];
@@ -35,7 +37,6 @@ backupApp.directive('restoreFilePicker', function() {
AppService.get('/backup/' + $scope.ngBackupId + '/files/' + encodeURIComponent(node.id) + '?prefix-only=false&folder-contents=true&time=' + encodeURIComponent($scope.ngTimestamp) + '&filter=' + encodeURIComponent(node.id)).then(function(data) {
var children = []
- var dirsep = scope.systeminfo.DirectorySeparator || '/';
for(var n in data.data.Files)
{
@@ -95,7 +96,6 @@ backupApp.directive('restoreFilePicker', function() {
};
function findParent(node) {
- var dirsep = scope.systeminfo.DirectorySeparator || '/';
var e = [];
e.push.apply(e, $scope.treedata.children);
var p = compareablePath(node.id);
@@ -152,7 +152,6 @@ backupApp.directive('restoreFilePicker', function() {
};
function buildPartialMap() {
- var dirsep = scope.systeminfo.DirectorySeparator || '/';
var map = {};
for (var i = $scope.ngSelected.length - 1; i >= 0; i--) {
var is_dir = $scope.ngSelected[i].substr($scope.ngSelected[i].length - 1, 1) == dirsep;
@@ -180,7 +179,6 @@ backupApp.directive('restoreFilePicker', function() {
};
$scope.toggleCheck = function(node) {
- var dirsep = scope.systeminfo.DirectorySeparator || '/';
if (node.include != '+') {
var p = findParent(node) || node;
@@ -281,7 +279,6 @@ backupApp.directive('restoreFilePicker', function() {
var buildnodes = function(items, parentpath) {
var res = [];
- var dirsep = scope.systeminfo.DirectorySeparator || '/';
parentpath = parentpath || '';
@@ -312,7 +309,12 @@ backupApp.directive('restoreFilePicker', function() {
var updateRoots = function()
{
- var roots = buildnodes($scope.ngSources) ;
+ if ($scope.ngSources == null || $scope.ngSources.length == 0)
+ dirsep = scope.systeminfo.DirectorySeparator || '/';
+ else
+ dirsep = $scope.ngSources[0].Path[0] == '/' ? '/' : '\\';
+
+ var roots = buildnodes($scope.ngSources);
$scope.treedata = $scope.treedata || {};