diff --git a/.travis.yml b/.travis.yml index 4412311a5..225209504 100644 --- a/.travis.yml +++ b/.travis.yml @@ -57,19 +57,19 @@ jobs: directories: - $BUILD_DIR script: - - ${ROOT_DIR}/pipeline/jobs/unittest_job.sh --testcategories SVNDataLong,SVNData,RecoveryTool + - ${ROOT_DIR}/pipeline/jobs/unittest_job.sh --testcategories SVNDataLong,SVNData,RecoveryTool,ProblematicPath,SymLink - stage: tests cache: directories: - $BUILD_DIR script: - - ${ROOT_DIR}/pipeline/jobs/unittest_job.sh --testcategories Border + - travis_wait ${ROOT_DIR}/pipeline/jobs/unittest_job.sh --testcategories Border - stage: tests cache: directories: - $BUILD_DIR script: - - ${ROOT_DIR}/pipeline/jobs/unittest_job.sh --testcategories Filter,Targeted,Purge,Serialization,WebApi,Utility,UriUtility,IO,ImportExport,Disruption,RestoreHandler,RepairHandler,DeleteHandler + - ${ROOT_DIR}/pipeline/jobs/unittest_job.sh --testcategories Filter,Targeted,Purge,Serialization,WebApi,Utility,UriUtility,IO,ImportExport,Disruption,RestoreHandler,RepairHandler,DeleteHandler,Controller - stage: tests cache: diff --git a/BuildTools/AutoUpdateBuilder/app.config b/BuildTools/AutoUpdateBuilder/app.config index 2a0024f75..d7c60fd53 100644 --- a/BuildTools/AutoUpdateBuilder/app.config +++ b/BuildTools/AutoUpdateBuilder/app.config @@ -1,3 +1,3 @@ - + diff --git a/BuildTools/DependencyFinder/app.config b/BuildTools/DependencyFinder/app.config index 2a0024f75..d7c60fd53 100644 --- a/BuildTools/DependencyFinder/app.config +++ b/BuildTools/DependencyFinder/app.config @@ -1,3 +1,3 @@ - + diff --git a/BuildTools/GnupgSigningTool/app.config b/BuildTools/GnupgSigningTool/app.config index 2a0024f75..d7c60fd53 100644 --- a/BuildTools/GnupgSigningTool/app.config +++ b/BuildTools/GnupgSigningTool/app.config @@ -1,3 +1,3 @@ - + diff --git a/Duplicati.sln b/Duplicati.sln index 1c82a31a7..0344487ae 100644 --- a/Duplicati.sln +++ b/Duplicati.sln @@ -103,7 +103,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.Library.Encryptio EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.CommandLine.AutoUpdater", "Duplicati\CommandLine\AutoUpdater\Duplicati.CommandLine.AutoUpdater.csproj", "{4D1B198B-F773-44F2-872B-2B283053323C}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.Backend.Tardigrade", "Duplicati\Library\Backend\Tardigrade\Duplicati.Library.Backend.Tardigrade.csproj", "{AE035E01-C917-4F13-A35E-78F21C1A2F17}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.Library.Backend.Tardigrade", "Duplicati\Library\Backend\Tardigrade\Duplicati.Library.Backend.Tardigrade.csproj", "{AE035E01-C917-4F13-A35E-78F21C1A2F17}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Duplicati.Library.Backend.TencentCOS", "Duplicati\Library\Backend\TencentCOS\Duplicati.Library.Backend.TencentCOS.csproj", "{545DD6D4-9476-42D6-B51C-A28E000C489E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.Library.Backend.Telegram", "Duplicati\Library\Backend\Telegram\Duplicati.Library.Backend.Telegram.csproj", "{B6BB37DE-1DCA-401B-B92F-202F341916EA}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -315,6 +319,14 @@ Global {AE035E01-C917-4F13-A35E-78F21C1A2F17}.Debug|Any CPU.Build.0 = Debug|Any CPU {AE035E01-C917-4F13-A35E-78F21C1A2F17}.Release|Any CPU.ActiveCfg = Release|Any CPU {AE035E01-C917-4F13-A35E-78F21C1A2F17}.Release|Any CPU.Build.0 = Release|Any CPU + {545DD6D4-9476-42D6-B51C-A28E000C489E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {545DD6D4-9476-42D6-B51C-A28E000C489E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {545DD6D4-9476-42D6-B51C-A28E000C489E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {545DD6D4-9476-42D6-B51C-A28E000C489E}.Release|Any CPU.Build.0 = Release|Any CPU + {B6BB37DE-1DCA-401B-B92F-202F341916EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B6BB37DE-1DCA-401B-B92F-202F341916EA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B6BB37DE-1DCA-401B-B92F-202F341916EA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B6BB37DE-1DCA-401B-B92F-202F341916EA}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Duplicati/CommandLine/BackendTester/Duplicati.CommandLine.BackendTester.csproj b/Duplicati/CommandLine/BackendTester/Duplicati.CommandLine.BackendTester.csproj index 1aa442fb0..cc2164e3c 100644 --- a/Duplicati/CommandLine/BackendTester/Duplicati.CommandLine.BackendTester.csproj +++ b/Duplicati/CommandLine/BackendTester/Duplicati.CommandLine.BackendTester.csproj @@ -39,6 +39,10 @@ {ae035e01-c917-4f13-a35e-78f21c1a2f17} Duplicati.Library.Backend.Tardigrade + + + + diff --git a/Duplicati/CommandLine/BackendTool/Duplicati.CommandLine.BackendTool.csproj b/Duplicati/CommandLine/BackendTool/Duplicati.CommandLine.BackendTool.csproj index 7a8955787..8c3675620 100644 --- a/Duplicati/CommandLine/BackendTool/Duplicati.CommandLine.BackendTool.csproj +++ b/Duplicati/CommandLine/BackendTool/Duplicati.CommandLine.BackendTool.csproj @@ -46,6 +46,12 @@ + + + + + + diff --git a/Duplicati/CommandLine/Commands.cs b/Duplicati/CommandLine/Commands.cs index 0e60ffc71..74523762c 100644 --- a/Duplicati/CommandLine/Commands.cs +++ b/Duplicati/CommandLine/Commands.cs @@ -19,7 +19,9 @@ using System; using System.Linq; using System.Collections.Generic; using System.IO; +using System.Text.RegularExpressions; using Duplicati.Library.Common.IO; +using Duplicati.Library.Utility; namespace Duplicati.CommandLine { @@ -234,14 +236,82 @@ namespace Duplicati.CommandLine } } - public static List PrefixArgsWithAsterisk(IList argList) + /// + /// For bare file names with no wildcards, replace argument with + /// the equivalent of prefixing with "*/" so we search all folders. + /// + public static IEnumerable PrefixArgsWithAsterisk(IEnumerable argList) => argList.Select(PrefixArgWithAsterisk); + + /// + /// For bare file names with no wildcards, return argument with + /// the equivalent of prefixing with "*/" so we search all folders. + /// + private static string PrefixArgWithAsterisk(string arg) { - // Prefix all filenames with "*/" so we search all folders - var specialArgChars = new[] { '*', '?', Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }; - return argList.Select(arg => arg.IndexOfAny(specialArgChars) < 0 - && !arg.StartsWith("[", StringComparison.Ordinal) ? "*" + Util.DirectorySeparatorString + arg : arg).ToList(); + var containsSeparators = ContainsDirectorySeparators(arg); + var containsWildcards = ContainsWildcards(arg); + + if (!containsSeparators && arg.StartsWith("@", StringComparison.Ordinal)) + { + // Convert to Regexp filter and prefix with ".*/" + return $"[.*{Utility.ConvertLiteralToRegExp(Util.DirectorySeparatorString + arg.Substring(1))}]"; + } + else if (!containsSeparators && !containsWildcards && !arg.StartsWith("[", StringComparison.Ordinal)) + { + // Prefix files with "*/" + return "*" + Util.DirectorySeparatorString + arg; + } + else + { + return arg; + } } + /// + /// For folders, replace argument with the equivalent of suffixing + /// with "*" so we restore contents in the folder. + /// + public static IEnumerable SuffixArgsWithAsterisk(IEnumerable argList) => argList.Select(SuffixArgWithAsterisk); + + /// + /// For folders, return argument with the equivalent of suffixing + /// with "*" so we restore contents in the folder. + /// + private static string SuffixArgWithAsterisk(string arg) + { + var containsWildcards = ContainsWildcards(arg); + var endsWithSeparator = arg.EndsWith(Util.DirectorySeparatorString, StringComparison.Ordinal); + + if (endsWithSeparator && arg.StartsWith("@", StringComparison.Ordinal)) + { + // Convert to Regexp filter and suffix with ".*" + return $"[{Utility.ConvertLiteralToRegExp(arg.Substring(1))}.*]"; + } + else if (endsWithSeparator && !containsWildcards && !arg.StartsWith("[", StringComparison.Ordinal)) + { + // Suffix with "*" + return arg + "*"; + } + else + { + return arg; + } + } + + private static readonly char[] pathSeparatorsCharacters = new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }; + + /// + /// Returns true if contains directory separator characters. + /// + public static bool ContainsDirectorySeparators(string s) => s.IndexOfAny(pathSeparatorsCharacters) >= 0; + + private static readonly char[] wildcardCharacters = new[] { '*', '?' }; + + /// + /// Returns true if contains wildcard characters. + /// + public static bool ContainsWildcards(string s) => s.IndexOfAny(wildcardCharacters) >= 0; + public static int List(TextWriter outwriter, Action setup, List args, Dictionary options, Library.Utility.IFilter filter) { filter = filter ?? new Duplicati.Library.Utility.FilterExpression(); @@ -267,7 +337,7 @@ namespace Duplicati.CommandLine options["version"] = v.ToString(); } } - else if (args[0].IndexOfAny(new char[] { '*', '?' }) < 0 && !args[0].StartsWith("[", StringComparison.Ordinal)) + else if (!ContainsWildcards(args[0]) && !args[0].StartsWith("[", StringComparison.Ordinal) && !args[0].StartsWith("@", StringComparison.Ordinal)) { try { @@ -282,7 +352,7 @@ namespace Duplicati.CommandLine } } - args = PrefixArgsWithAsterisk(args); + args = PrefixArgsWithAsterisk(args).ToList(); // Support for not adding the --auth-username if possible string dbpath; @@ -448,12 +518,7 @@ namespace Duplicati.CommandLine bool controlFiles = Library.Utility.Utility.ParseBoolOption(options, "control-files"); options.Remove("control-files"); - args = PrefixArgsWithAsterisk(args); - - // suffix all folders with "*" so we restore all contents in the folder - for (var ix = 0; ix < args.Count; ix++) - if (args[ix].IndexOfAny(new char[] { '*', '?' }) < 0 && !args[ix].StartsWith("[", StringComparison.Ordinal) && args[ix].EndsWith(Util.DirectorySeparatorString, StringComparison.Ordinal)) - args[ix] += "*"; + args = SuffixArgsWithAsterisk(PrefixArgsWithAsterisk(args)).ToList(); using(var output = new ConsoleOutput(outwriter, options)) using (var i = new Library.Main.Controller(backend, options, output)) diff --git a/Duplicati/CommandLine/ConfigurationImporter/app.config b/Duplicati/CommandLine/ConfigurationImporter/app.config index 5ab9ab477..d32715a66 100644 --- a/Duplicati/CommandLine/ConfigurationImporter/app.config +++ b/Duplicati/CommandLine/ConfigurationImporter/app.config @@ -1,9 +1,10 @@  - + + diff --git a/Duplicati/CommandLine/Duplicati.CommandLine.csproj b/Duplicati/CommandLine/Duplicati.CommandLine.csproj index d5c1927c6..ac9279d38 100644 --- a/Duplicati/CommandLine/Duplicati.CommandLine.csproj +++ b/Duplicati/CommandLine/Duplicati.CommandLine.csproj @@ -36,6 +36,8 @@ + + diff --git a/Duplicati/CommandLine/RecoveryTool/FileIndex.cs b/Duplicati/CommandLine/RecoveryTool/FileIndex.cs index afb45e42d..89fbef75b 100644 --- a/Duplicati/CommandLine/RecoveryTool/FileIndex.cs +++ b/Duplicati/CommandLine/RecoveryTool/FileIndex.cs @@ -74,7 +74,7 @@ namespace Duplicati.CommandLine.RecoveryTool try { - var p = Duplicati.Library.Main.Volumes.VolumeBase.ParseFilename(file); + var p = Duplicati.Library.Main.Volumes.VolumeBase.ParseFilename(Path.GetFileName(file)); if (p == null) { Console.WriteLine(" - Not a Duplicati file, ignoring"); diff --git a/Duplicati/CommandLine/RecoveryTool/Index.cs b/Duplicati/CommandLine/RecoveryTool/Index.cs index c1835084a..0f47f61f7 100644 --- a/Duplicati/CommandLine/RecoveryTool/Index.cs +++ b/Duplicati/CommandLine/RecoveryTool/Index.cs @@ -64,7 +64,7 @@ namespace Duplicati.CommandLine.RecoveryTool try { - var p = Library.Main.Volumes.VolumeBase.ParseFilename(file); + var p = Library.Main.Volumes.VolumeBase.ParseFilename(Path.GetFileName(file)); if (p == null) { Console.WriteLine(" - Not a Duplicati file, ignoring"); diff --git a/Duplicati/CommandLine/RecoveryTool/List.cs b/Duplicati/CommandLine/RecoveryTool/List.cs index 346e5e765..5fe20eb50 100644 --- a/Duplicati/CommandLine/RecoveryTool/List.cs +++ b/Duplicati/CommandLine/RecoveryTool/List.cs @@ -51,7 +51,7 @@ namespace Duplicati.CommandLine.RecoveryTool { var file = SelectListFile(args[2], folder); - var p = Library.Main.Volumes.VolumeBase.ParseFilename(file); + var p = Library.Main.Volumes.VolumeBase.ParseFilename(Path.GetFileName(file)); Library.Main.Volumes.VolumeReaderBase.UpdateOptionsFromManifest(p.CompressionModule, file, new Duplicati.Library.Main.Options(options)); @@ -64,7 +64,7 @@ namespace Duplicati.CommandLine.RecoveryTool public static IEnumerable EnumerateFilesInDList(string file, Duplicati.Library.Utility.IFilter filter, Dictionary options) { - var p = Library.Main.Volumes.VolumeBase.ParseFilename(file); + var p = Library.Main.Volumes.VolumeBase.ParseFilename(Path.GetFileName(file)); using (var fs = new System.IO.FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read)) using (var cm = Library.DynamicLoader.CompressionLoader.GetModule(p.CompressionModule, fs, Library.Interface.ArchiveMode.Read, options)) using (var filesetreader = new Library.Main.Volumes.FilesetVolumeReader(cm, new Duplicati.Library.Main.Options(options))) @@ -112,7 +112,7 @@ namespace Duplicati.CommandLine.RecoveryTool { return ( from v in Directory.EnumerateFiles(folder) - let p = Library.Main.Volumes.VolumeBase.ParseFilename(v) + let p = Library.Main.Volumes.VolumeBase.ParseFilename(Path.GetFileName(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/Restore.cs b/Duplicati/CommandLine/RecoveryTool/Restore.cs index 31e861a40..bd18f59a2 100644 --- a/Duplicati/CommandLine/RecoveryTool/Restore.cs +++ b/Duplicati/CommandLine/RecoveryTool/Restore.cs @@ -27,6 +27,8 @@ namespace Duplicati.CommandLine.RecoveryTool { public static class Restore { + private static readonly ISystemIO systemIO = SystemIO.IO_OS; + public static int Run(List args, Dictionary options, Library.Utility.IFilter filter) { if (args.Count != 2 && args.Count != 3) @@ -160,8 +162,8 @@ namespace Duplicati.CommandLine.RecoveryTool try { var targetfile = MapToRestorePath(f.Path, largestprefix, targetpath); - if (!Directory.Exists(Path.GetDirectoryName(targetfile))) - Directory.CreateDirectory(Path.GetDirectoryName(targetfile)); + if (!systemIO.DirectoryExists(systemIO.PathGetDirectoryName(targetfile))) + systemIO.DirectoryCreate(systemIO.PathGetDirectoryName(targetfile)); Console.Write("{0}: {1} ({2})", i, targetfile, Library.Utility.Utility.FormatSizeString(f.Size)); @@ -216,12 +218,12 @@ namespace Duplicati.CommandLine.RecoveryTool if (fh == f.Hash) { Console.WriteLine(" done!"); - File.Copy(tf, targetfile, true); + systemIO.FileCopy(tf, targetfile, true); } else { Console.Write(" - Restored file hash mismatch"); - if (File.Exists(targetfile)) + if (systemIO.FileExists(targetfile)) Console.WriteLine(" - not overwriting existing file: {0}", targetfile); else Console.WriteLine(" - restoring file in damaged condition"); @@ -256,10 +258,10 @@ namespace Duplicati.CommandLine.RecoveryTool if (path.Substring(1, 1) == ":") prefixpath = path.Substring(0, 1) + path.Substring(2); - return Path.Combine(restorepath, prefixpath); + return systemIO.PathCombine(restorepath, prefixpath); } - return Path.Combine(restorepath, path.Substring(prefixpath.Length)); + return systemIO.PathCombine(restorepath, path.Substring(prefixpath.Length)); } diff --git a/Duplicati/CommandLine/RecoveryTool/app.config b/Duplicati/CommandLine/RecoveryTool/app.config index dd1b7be4b..85bf1086d 100644 --- a/Duplicati/CommandLine/RecoveryTool/app.config +++ b/Duplicati/CommandLine/RecoveryTool/app.config @@ -1,7 +1,7 @@  - + diff --git a/Duplicati/CommandLine/packages.config b/Duplicati/CommandLine/packages.config deleted file mode 100644 index bc2727bd9..000000000 --- a/Duplicati/CommandLine/packages.config +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/Duplicati/Library/Backend/AzureBlob/app.config b/Duplicati/Library/Backend/AzureBlob/app.config index acdba516e..39df99756 100644 --- a/Duplicati/Library/Backend/AzureBlob/app.config +++ b/Duplicati/Library/Backend/AzureBlob/app.config @@ -1,6 +1,6 @@  - + diff --git a/Duplicati/Library/Backend/Mega/app.config b/Duplicati/Library/Backend/Mega/app.config index 5616b953a..d720ad020 100644 --- a/Duplicati/Library/Backend/Mega/app.config +++ b/Duplicati/Library/Backend/Mega/app.config @@ -1,7 +1,7 @@  - + diff --git a/Duplicati/Library/Backend/OpenStack/OpenStackStorage.cs b/Duplicati/Library/Backend/OpenStack/OpenStackStorage.cs index f1e5eae34..306aa9087 100644 --- a/Duplicati/Library/Backend/OpenStack/OpenStackStorage.cs +++ b/Duplicati/Library/Backend/OpenStack/OpenStackStorage.cs @@ -614,13 +614,13 @@ namespace Duplicati.Library.Backend.OpenStack authuris.AppendLine(string.Format("{0}: {1}", s.Key, s.Value)); return new List(new [] { - new CommandLineArgument(DOMAINNAME_OPTION, CommandLineArgument.ArgumentType.String, Strings.OpenStack.DomainnameOptionShort, Strings.OpenStack.UsernameOptionLong), + new CommandLineArgument(DOMAINNAME_OPTION, CommandLineArgument.ArgumentType.String, Strings.OpenStack.DomainnameOptionShort, Strings.OpenStack.DomainnameOptionLong), new CommandLineArgument(USERNAME_OPTION, CommandLineArgument.ArgumentType.String, Strings.OpenStack.UsernameOptionShort, Strings.OpenStack.UsernameOptionLong), new CommandLineArgument(PASSWORD_OPTION, CommandLineArgument.ArgumentType.Password, Strings.OpenStack.PasswordOptionShort, Strings.OpenStack.PasswordOptionLong(TENANTNAME_OPTION)), new CommandLineArgument(TENANTNAME_OPTION, CommandLineArgument.ArgumentType.String, Strings.OpenStack.TenantnameOptionShort, Strings.OpenStack.TenantnameOptionLong), new CommandLineArgument(APIKEY_OPTION, CommandLineArgument.ArgumentType.Password, Strings.OpenStack.ApikeyOptionShort, Strings.OpenStack.ApikeyOptionLong), new CommandLineArgument(AUTHURI_OPTION, CommandLineArgument.ArgumentType.String, Strings.OpenStack.AuthuriOptionShort, Strings.OpenStack.AuthuriOptionLong(authuris.ToString())), - new CommandLineArgument(VERSION_OPTION, CommandLineArgument.ArgumentType.String, Strings.OpenStack.VersionOptionShort, Strings.OpenStack.AuthuriOptionLong(authuris.ToString())), + new CommandLineArgument(VERSION_OPTION, CommandLineArgument.ArgumentType.String, Strings.OpenStack.VersionOptionShort, Strings.OpenStack.VersionOptionLong), new CommandLineArgument(REGION_OPTION, CommandLineArgument.ArgumentType.String, Strings.OpenStack.RegionOptionShort, Strings.OpenStack.RegionOptionLong), }); } diff --git a/Duplicati/Library/Backend/Rclone/Rclone.cs b/Duplicati/Library/Backend/Rclone/Rclone.cs index 5b73d312a..a4bd84fca 100644 --- a/Duplicati/Library/Backend/Rclone/Rclone.cs +++ b/Duplicati/Library/Backend/Rclone/Rclone.cs @@ -249,7 +249,7 @@ namespace Duplicati.Library.Backend { try { - RcloneCommandExecuter(rclone_executable, String.Format("copyto {2}:{3}/{4} {0}:{1}", local_repo, filename, remote_repo, remote_path, remotename), CancellationToken.None).Await(); + RcloneCommandExecuter(rclone_executable, String.Format("copyto {2}:{3} {0}:{1}", local_repo, filename, remote_repo, Path.Combine(this.remote_path, remotename)), CancellationToken.None).Await(); } catch (FolderMissingException ex) { throw new FileMissingException(ex); @@ -262,7 +262,7 @@ namespace Duplicati.Library.Backend // Will give a "directory not found" error if the file does not exist, need to change that to a missing file exception try { - RcloneCommandExecuter(rclone_executable, String.Format("delete {0}:{1}/{2}", remote_repo, remote_path, remotename), CancellationToken.None).Await(); + RcloneCommandExecuter(rclone_executable, String.Format("delete {0}:{1}", remote_repo, Path.Combine(remote_path, remotename)), CancellationToken.None).Await(); } catch (FolderMissingException ex) { throw new FileMissingException(ex); diff --git a/Duplicati/Library/Backend/S3/S3Backend.cs b/Duplicati/Library/Backend/S3/S3Backend.cs index 9515dfcec..b8dcb24c4 100644 --- a/Duplicati/Library/Backend/S3/S3Backend.cs +++ b/Duplicati/Library/Backend/S3/S3Backend.cs @@ -51,6 +51,7 @@ namespace Duplicati.Library.Backend { "dinCloud - Chicago", "d3-ord.dincloud.com" }, { "dinCloud - Los Angeles", "d3-lax.dincloud.com" }, { "IBM COS (S3) Public US", "s3-api.us-geo.objectstorage.softlayer.net" }, + { "Storadera", "eu-east-1.s3.storadera.com" }, { "Wasabi Hot Storage", "s3.wasabisys.com" }, { "Wasabi Hot Storage (US West)", "s3.us-west-1.wasabisys.com" }, { "Wasabi Hot Storage (EU Central)", "s3.eu-central-1.wasabisys.com" }, @@ -65,6 +66,7 @@ namespace Duplicati.Library.Backend { "Europe (EU, London)", "eu-west-2" }, { "Europe (EU, Paris)", "eu-west-3" }, { "Europe (EU, Stockholm)", "eu-north-1" }, + { "Europe (EU, Milan)", "eu-south-1" }, { "US East (Northern Virginia)", "us-east-1" }, { "US East (Ohio)", "us-east-2" }, { "US West (Northern California)", "us-west-1" }, @@ -90,6 +92,7 @@ namespace Duplicati.Library.Backend { "eu-west-2", "s3.eu-west-2.amazonaws.com" }, { "eu-west-3", "s3.eu-west-3.amazonaws.com" }, { "eu-north-1", "s3.eu-north-1.amazonaws.com" }, + { "eu-south-1", "s3.eu-south-1.amazonaws.com" }, { "eu-central-1", "s3.eu-central-1.amazonaws.com" }, { "us-east-1", "s3.amazonaws.com" }, { "us-east-2", "s3.us-east-2.amazonaws.com" }, @@ -105,7 +108,7 @@ namespace Duplicati.Library.Backend { "sa-east-1", "s3.sa-east-1.amazonaws.com" }, { "cn-north-1", "s3.cn-north-1.amazonaws.com.cn" }, { "cn-northwest-1", "s3.cn-northwest-1.amazonaws.com.cn" }, - { "me-south-1", "s3.me-south-1.amazonaws.com.cn" }, + { "me-south-1", "s3.me-south-1.amazonaws.com" }, }; public static readonly Dictionary KNOWN_S3_STORAGE_CLASSES; @@ -353,26 +356,7 @@ namespace Duplicati.Library.Backend public void Get(string remotename, System.IO.Stream output) { - try - { - Connection.GetFileStream(m_bucket, GetFullKey(remotename), output); - } - catch - { - //This is a fix for the S3 backend prior to beta 3, where the filenames had a slash prefixed - try - { - if (!remotename.StartsWith("/", StringComparison.Ordinal)) - Connection.GetFileStream(m_bucket, GetFullKey("/" + remotename), output); - return; - } - catch - { - } - - //Throw original error - throw; - } + Connection.GetFileStream(m_bucket, GetFullKey(remotename), output); } public void Delete(string remotename) diff --git a/Duplicati/Library/Backend/Sia/Sia.cs b/Duplicati/Library/Backend/Sia/Sia.cs index 97aed2e7f..a9c0f1030 100644 --- a/Duplicati/Library/Backend/Sia/Sia.cs +++ b/Duplicati/Library/Backend/Sia/Sia.cs @@ -83,8 +83,13 @@ namespace Duplicati.Library.Backend.Sia private string getResponseBodyOnError(string context, System.Net.WebException wex) { + HttpWebResponse response = wex.Response as HttpWebResponse; + if (response is null) + { + return $"{context} failed with error: {wex.Message}"; + } + string body = ""; - System.Net.HttpWebResponse response = (System.Net.HttpWebResponse)wex.Response; using (System.IO.Stream data = response.GetResponseStream()) using (var reader = new System.IO.StreamReader(data)) { diff --git a/Duplicati/Library/Backend/Tardigrade/Duplicati.Library.Backend.Tardigrade.csproj b/Duplicati/Library/Backend/Tardigrade/Duplicati.Library.Backend.Tardigrade.csproj index ff1d9ee40..273ffbd96 100644 --- a/Duplicati/Library/Backend/Tardigrade/Duplicati.Library.Backend.Tardigrade.csproj +++ b/Duplicati/Library/Backend/Tardigrade/Duplicati.Library.Backend.Tardigrade.csproj @@ -18,7 +18,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/Duplicati/Library/Backend/Tardigrade/TardigradeBackend.cs b/Duplicati/Library/Backend/Tardigrade/TardigradeBackend.cs index d6a3c09f5..0bf808105 100644 --- a/Duplicati/Library/Backend/Tardigrade/TardigradeBackend.cs +++ b/Duplicati/Library/Backend/Tardigrade/TardigradeBackend.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Linq.Expressions; +using System.Runtime.InteropServices; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -23,6 +24,7 @@ namespace Duplicati.Library.Backend.Tardigrade private const string TARDIGRADE_BUCKET = "tardigrade-bucket"; private const string TARDIGRADE_FOLDER = "tardigrade-folder"; private const string PROTOCOL_KEY = "tardigrade"; + private const string TARDIGRADE_PARTNER_ID = "duplicati"; private readonly string _satellite; private readonly string _api_key; @@ -36,7 +38,9 @@ namespace Duplicati.Library.Backend.Tardigrade public static readonly Dictionary KNOWN_TARDIGRADE_SATELLITES = new Dictionary(StringComparer.OrdinalIgnoreCase){ { "US Central 1", "us-central-1.tardigrade.io:7777" }, { "Asia East 1", "asia-east-1.tardigrade.io:7777" }, + { "Saltlake", "saltlake.tardigrade.io:7777" }, { "Europe West 1", "europe-west-1.tardigrade.io:7777" }, + { "Europe North 1", "europe-north-1.tardigrade.io:7777" }, }; public static readonly Dictionary KNOWN_AUTHENTICATION_METHODS = new Dictionary(StringComparer.OrdinalIgnoreCase){ @@ -44,6 +48,30 @@ namespace Duplicati.Library.Backend.Tardigrade { "Access grant", "Access grant" }, }; + [DllImport("kernel32.dll")] + protected static extern IntPtr LoadLibrary(string filename); + + private static bool _libraryLoaded = false; + private static void InitStorjLibrary() + { + if (_libraryLoaded) + return; + + if (Duplicati.Library.Common.Platform.IsClientWindows) //We need to init only on Windows to distinguish between x64 and x86 + { + if (System.Environment.Is64BitProcess) + { + var res = LoadLibrary("win-x64/storj_uplink.dll"); + } + else + { + var res = LoadLibrary("win-x86/storj_uplink.dll"); + } + } + Access.SetTempDirectory(Library.Utility.TempFolder.SystemTempPath); + _libraryLoaded = true; + } + // ReSharper disable once UnusedMember.Global // This constructor is needed by the BackendLoader. public Tardigrade() @@ -54,15 +82,14 @@ namespace Duplicati.Library.Backend.Tardigrade // This constructor is needed by the BackendLoader. public Tardigrade(string url, Dictionary options) { + InitStorjLibrary(); + var auth_method = options[TARDIGRADE_AUTH_METHOD]; - - Access.SetTempDirectory(System.IO.Path.GetTempPath()); - if (auth_method == "Access grant") { //Create an access from the access grant var shared_access = options[TARDIGRADE_SHARED_ACCESS]; - _access = new Access(shared_access); + _access = new Access(shared_access, new Config() { UserAgent = TARDIGRADE_PARTNER_ID }); } else { @@ -78,7 +105,7 @@ namespace Duplicati.Library.Backend.Tardigrade _secret = options[TARDIGRADE_SECRET]; } - _access = new Access(_satellite, _api_key, _secret); + _access = new Access(_satellite, _api_key, _secret, new Config() { UserAgent = TARDIGRADE_PARTNER_ID }); } _bucketService = new BucketService(_access); @@ -241,7 +268,10 @@ namespace Duplicati.Library.Backend.Tardigrade foreach (var obj in objects.Items) { TardigradeFile file = new TardigradeFile(obj); - file.Name = file.Name.Replace(GetBasePath(), ""); + if (prefix != "") + { + file.Name = file.Name.Replace(prefix, ""); + } files.Add(file); } diff --git a/Duplicati/Library/Backend/Tardigrade/libstorj_uplink.dylib b/Duplicati/Library/Backend/Tardigrade/libstorj_uplink.dylib new file mode 100644 index 000000000..4f0190dea Binary files /dev/null and b/Duplicati/Library/Backend/Tardigrade/libstorj_uplink.dylib differ diff --git a/Duplicati/Library/Backend/Tardigrade/libstorj_uplink.so b/Duplicati/Library/Backend/Tardigrade/libstorj_uplink.so new file mode 100644 index 000000000..7c307a134 Binary files /dev/null and b/Duplicati/Library/Backend/Tardigrade/libstorj_uplink.so differ diff --git a/Duplicati/Library/Backend/Tardigrade/win-x64/storj_uplink.dll b/Duplicati/Library/Backend/Tardigrade/win-x64/storj_uplink.dll new file mode 100644 index 000000000..29863308c Binary files /dev/null and b/Duplicati/Library/Backend/Tardigrade/win-x64/storj_uplink.dll differ diff --git a/Duplicati/Library/Backend/Tardigrade/win-x86/storj_uplink.dll b/Duplicati/Library/Backend/Tardigrade/win-x86/storj_uplink.dll new file mode 100644 index 000000000..6f1cc9e95 Binary files /dev/null and b/Duplicati/Library/Backend/Tardigrade/win-x86/storj_uplink.dll differ diff --git a/Duplicati/Library/Backend/Telegram/ChannelFileInfo.cs b/Duplicati/Library/Backend/Telegram/ChannelFileInfo.cs new file mode 100644 index 000000000..2233a9a93 --- /dev/null +++ b/Duplicati/Library/Backend/Telegram/ChannelFileInfo.cs @@ -0,0 +1,86 @@ +using System; +using Duplicati.Library.Common.IO; +using TeleSharp.TL; + +namespace Duplicati.Library.Backend +{ + public class ChannelFileInfo : IEquatable + { + public int MessageId { get; } + public long MediaDocAccessHash { get; } + public long DocumentId { get; } + public int Version { get; } + public long Size { get; } + public string Name { get; } + public DateTime Date { get; } + + public ChannelFileInfo() + { } + + public ChannelFileInfo(int messageId, long mediaDocAccessHash, long documentId, int version, long size, string name, DateTime date) + { + MessageId = messageId; + MediaDocAccessHash = mediaDocAccessHash; + DocumentId = documentId; + Version = version; + Size = size; + Name = name; + Date = date; + } + + public FileEntry ToFileEntry() + { + return new FileEntry(Name, Size) + { + LastModification = Date, + LastAccess = Date, + IsFolder = false + }; + } + + public TLInputDocumentFileLocation ToFileLocation() + { + return new TLInputDocumentFileLocation + { + Id = DocumentId, + Version = Version, + AccessHash = MediaDocAccessHash + }; + } + + public bool Equals(ChannelFileInfo other) + { + if (ReferenceEquals(null, other)) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return MessageId == other.MessageId; + } + + public override bool Equals(object obj) + { + return ReferenceEquals(this, obj) || obj is ChannelFileInfo other && Equals(other); + } + + public override int GetHashCode() + { + return MessageId; + } + + public static bool operator ==(ChannelFileInfo left, ChannelFileInfo right) + { + return Equals(left, right); + } + + public static bool operator !=(ChannelFileInfo left, ChannelFileInfo right) + { + return !Equals(left, right); + } + } +} \ No newline at end of file diff --git a/Duplicati/Library/Backend/Telegram/Duplicati.Library.Backend.Telegram.csproj b/Duplicati/Library/Backend/Telegram/Duplicati.Library.Backend.Telegram.csproj new file mode 100644 index 000000000..8a2812271 --- /dev/null +++ b/Duplicati/Library/Backend/Telegram/Duplicati.Library.Backend.Telegram.csproj @@ -0,0 +1,23 @@ + + + net5.0 + Library + Duplicati.Library.Backend + Duplicati.snk + false + false + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Duplicati/Library/Backend/Telegram/Duplicati.snk b/Duplicati/Library/Backend/Telegram/Duplicati.snk new file mode 100644 index 000000000..e0c1e2dd8 Binary files /dev/null and b/Duplicati/Library/Backend/Telegram/Duplicati.snk differ diff --git a/Duplicati/Library/Backend/Telegram/EncryptedFileSessionStore.cs b/Duplicati/Library/Backend/Telegram/EncryptedFileSessionStore.cs new file mode 100644 index 000000000..5f0016874 --- /dev/null +++ b/Duplicati/Library/Backend/Telegram/EncryptedFileSessionStore.cs @@ -0,0 +1,159 @@ +using System; +using System.Collections.Concurrent; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using Duplicati.Library.AutoUpdater; +using TLSharp.Core; + +namespace Duplicati.Library.Backend +{ + public class EncryptedFileSessionStore : ISessionStore + { + private static readonly object m_lockObj = new object(); + private static readonly uint[] m_lookup32 = CreateLookup32(); + + private readonly string m_teleDataPath; + private readonly string m_password; + private readonly SHA256 m_sha = SHA256.Create(); + private static readonly ConcurrentDictionary m_userIdLastSessionMap = new ConcurrentDictionary(); + + public EncryptedFileSessionStore(string password) + { + if (string.IsNullOrWhiteSpace(password)) + { + throw new ArgumentNullException(nameof(password)); + } + + var appData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + var appName = AutoUpdateSettings.AppName; + m_teleDataPath = Path.Combine(appData, appName, nameof(Telegram)); + m_password = password; + + Directory.CreateDirectory(m_teleDataPath); + } + + + public void Save(Session session) + { + if (session.AuthKey == null) + { + return; + } + + var sessionId = session.SessionUserId; + var filePath = GetSessionFilePath(sessionId); + var sessionBytes = session.ToBytes(); + + if (m_userIdLastSessionMap.TryGetValue(sessionId, out var sessionCache)) + { + if (sessionCache.SequenceEqual(sessionBytes)) + { + return; + } + } + + WriteToEncryptedStorage(sessionBytes, m_password, filePath, sessionId); + } + + public Session Load(string userId) + { + if (m_userIdLastSessionMap.TryGetValue(userId, out var cachedBytes)) + { + var cachedSession = Session.FromBytes(cachedBytes, this, userId); + return cachedSession; + } + + var filePath = GetSessionFilePath(userId); + var sessionBytes = ReadFromEncryptedStorage(m_password, filePath); + + if (sessionBytes == null) + { + return null; + } + + var session = Session.FromBytes(sessionBytes, this, userId); + return session; + } + + private string GetSessionFilePath(string userId) + { + userId = userId.TrimStart('+'); + var sha = GetShortSha(userId); + var sessionFilePath = Path.Combine(m_teleDataPath, $"t_{sha}.dat"); + + return sessionFilePath; + } + + private static void WriteToEncryptedStorage(byte[] bytesToWrite, string pass, string path, string sessionId) + { + lock (m_lockObj) + { + using (var sessionMs = new MemoryStream(bytesToWrite)) + using (var file = File.Open(path, FileMode.Create, FileAccess.Write)) + { + SharpAESCrypt.SharpAESCrypt.Encrypt(pass, sessionMs, file); + } + + m_userIdLastSessionMap[sessionId] = bytesToWrite; + } + } + + private byte[] ReadFromEncryptedStorage(string pass, string path) + { + var fileInfo = new FileInfo(path); + if (fileInfo.Exists == false || fileInfo.Length == 0) + { + return null; + } + + lock (m_lockObj) + { + using (var sessionMs = new MemoryStream()) + using (var file = File.Open(path, FileMode.Open, FileAccess.Read)) + { + SharpAESCrypt.SharpAESCrypt.Decrypt(pass, file, sessionMs); + return sessionMs.ToArray(); + } + } + } + + private string GetShortSha(string input) + { + var inputBytes = Encoding.UTF8.GetBytes(input); + + var longShaBytes = m_sha.ComputeHash(inputBytes); + var longSha = ByteArrayToHexViaLookup32(longShaBytes); + var result = longSha.Substring(0, 16); + + return result; + } + + private static uint[] CreateLookup32() + { + var result = new uint[256]; + for (var i = 0; i < 256; i++) + { + var s = i.ToString("X2"); + result[i] = s[0] + ((uint)s[1] << 16); + } + + return result; + } + + private static string ByteArrayToHexViaLookup32(byte[] bytes) + { + var lookup32 = m_lookup32; + var result = new char[bytes.Length * 2]; + for (var i = 0; i < bytes.Length; i++) + { + var val = lookup32[bytes[i]]; + result[2 * i] = (char)val; + result[2 * i + 1] = (char)(val >> 16); + } + + return new string(result); + } + } +} \ No newline at end of file diff --git a/Duplicati/Library/Backend/Telegram/Extensions/TelegramClientExtensions.cs b/Duplicati/Library/Backend/Telegram/Extensions/TelegramClientExtensions.cs new file mode 100644 index 000000000..d99e16339 --- /dev/null +++ b/Duplicati/Library/Backend/Telegram/Extensions/TelegramClientExtensions.cs @@ -0,0 +1,131 @@ +using System; +using System.Net; +using System.Net.Sockets; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using TLSharp.Core; +using TLSharp.Core.Network; + +namespace Duplicati.Library.Backend.Extensions +{ + public static class TelegramClientExtensions + { + private static readonly BindingFlags _bindingFlags = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Default; + + public static async Task WrapperConnectAsync(this TelegramClient client, CancellationToken cancelToken = default(CancellationToken)) + { + if (client == null) + { + throw new ArgumentNullException(nameof(client)); + } + + var oldTransportField = client.GetTcpTransport(); + var tcpClient = oldTransportField.GetTcpClient(); + var handler = client.GetTcpClientConnectionHandler(); + + var remoteEndpoint = (IPEndPoint)tcpClient.Client.RemoteEndPoint; + var newTransport = new TcpTransport(remoteEndpoint.Address.ToString(), remoteEndpoint.Port, handler); + + client.GetTcpTransportFieldInfo().SetValue(client, newTransport); + + cancelToken.ThrowIfCancellationRequested(); + + await client.ConnectAsync(false, cancelToken); + + cancelToken.ThrowIfCancellationRequested(); + + oldTransportField.Dispose(); + } + + public static bool IsReallyConnected(this TelegramClient client) + { + if (client.IsConnected == false) + { + return false; + } + + var sender = GetMtProtoSender(client); + + if (sender == null) + { + return false; + } + + var tcpTransport = client.GetTcpTransport(); + + if (tcpTransport == null || tcpTransport.IsConnected == false) + { + return false; + } + + var tcpClient = tcpTransport.GetTcpClient(); + + if (tcpClient == null || tcpClient.Connected == false) + { + return false; + } + + return true; + } + + private static TcpClientConnectionHandler GetTcpClientConnectionHandler(this TelegramClient client) + { + if (client == null) + { + throw new ArgumentNullException(nameof(client)); + } + + var handlerFieldInfo = typeof(TelegramClient).GetField("handler", _bindingFlags); + var handler = (TcpClientConnectionHandler)handlerFieldInfo.GetValue(client); + + return handler; + } + + private static TcpClient GetTcpClient(this TcpTransport tcpTransport) + { + if (tcpTransport == null) + { + throw new ArgumentNullException(nameof(tcpTransport)); + } + + var tcpClientFieldInfo = typeof(TcpTransport).GetField("tcpClient", _bindingFlags); + var tcpClient = (TcpClient)tcpClientFieldInfo.GetValue(tcpTransport); + + return tcpClient; + } + + private static FieldInfo GetTcpTransportFieldInfo(this TelegramClient client) + { + var transportFieldInfo = typeof(TelegramClient).GetField("transport", _bindingFlags); + + return transportFieldInfo; + } + + private static TcpTransport GetTcpTransport(this TelegramClient client) + { + if (client == null) + { + throw new ArgumentNullException(nameof(client)); + } + + var transportFieldInfo = client.GetTcpTransportFieldInfo(); + var oldTransportField = (TcpTransport)transportFieldInfo.GetValue(client); + + return oldTransportField; + } + + private static MtProtoSender GetMtProtoSender(this TelegramClient client) + { + if (client == null) + { + throw new ArgumentNullException(nameof(client)); + } + + var senderFieldInfo = typeof(TelegramClient).GetField("sender", _bindingFlags); + var sender = (MtProtoSender)senderFieldInfo.GetValue(client); + + return sender; + } + } +} \ No newline at end of file diff --git a/Duplicati/Library/Backend/Telegram/Properties/AssemblyInfo.cs b/Duplicati/Library/Backend/Telegram/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..d33f05968 --- /dev/null +++ b/Duplicati/Library/Backend/Telegram/Properties/AssemblyInfo.cs @@ -0,0 +1,53 @@ +#region Disclaimer / License +// 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +// +#endregion +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Telegram")] +[assembly: AssemblyDescription("A Telegram backend for Duplicati")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Duplicati Team")] +[assembly: AssemblyProduct("Duplicati.Backend.Telegram")] +[assembly: AssemblyCopyright("LGPL, Copyright © Duplicati Team 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("afa68988-4d82-490e-8044-acbc7d52ef4f")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Revision and Build Numbers +// by using the '*' as shown below: +[assembly: AssemblyVersion("2.0.0.7")] +[assembly: AssemblyFileVersion("2.0.0.7")] diff --git a/Duplicati/Library/Backend/Telegram/StreamReadHelper.cs b/Duplicati/Library/Backend/Telegram/StreamReadHelper.cs new file mode 100644 index 000000000..d436bdb27 --- /dev/null +++ b/Duplicati/Library/Backend/Telegram/StreamReadHelper.cs @@ -0,0 +1,48 @@ +using System.IO; +using Duplicati.Library.Utility; + +namespace Duplicati.Library.Backend +{ + /// + /// Private helper class to fix a bug with the StreamReader + /// + internal class StreamReadHelper : OverrideableStream + { + /// + /// Once the stream has returned 0 as the read count it is disposed, + /// and subsequent read requests will throw an ObjectDisposedException + /// + private bool m_empty; + + /// + /// Basic initialization, just pass the stream to the super class + /// + /// + public StreamReadHelper(Stream stream) : base(stream) + { } + + /// + /// Override the read function to make sure that we only return less than the requested amount of data if the stream is exhausted + /// + /// The buffer to place data in + /// The offset into the buffer to start at + /// The number of bytes to read + /// The number of bytes read + public override int Read(byte[] buffer, int offset, int count) + { + var readCount = 0; + int a; + + while (!m_empty && count > 0) + { + a = base.Read(buffer, offset, count); + readCount += a; + count -= a; + offset += a; + m_empty = a == 0; + } + + return readCount; + } + } +} \ No newline at end of file diff --git a/Duplicati/Library/Backend/Telegram/Strings.cs b/Duplicati/Library/Backend/Telegram/Strings.cs new file mode 100644 index 000000000..16a7c79a3 --- /dev/null +++ b/Duplicati/Library/Backend/Telegram/Strings.cs @@ -0,0 +1,63 @@ +using Duplicati.Library.Localization.Short; + +namespace Duplicati.Library.Backend +{ + internal static class Strings + { + #region Constants + + public const string API_ID_KEY = "api-id"; + public const string API_HASH_KEY = "api-hash"; + public const string PHONE_NUMBER_KEY = "phone-number"; + public const string AUTH_CODE_KEY = "auth-code"; + public const string AUTH_PASSWORD = "auth-password"; + public const string CHANNEL_NAME = "channel-name"; + + #endregion + + #region General + + public static string DisplayName { get; } = "Telegram"; + public static string Description => LC.L("This backend can read and write data to a Telegram backend"); + + #endregion + + #region Errors + + public static string WrongAuthCodeError => LC.L("The auth code is incorrect"); + public static string NoAuthCodeError => LC.L("The auth code is missing"); + public static string NoPasswordError => LC.L("The password is missing"); + public static string NoChannelNameError => LC.L("The channel name is missing"); + public static string NoApiIdError => LC.L("The API ID is missing"); + public static string NoApiHashError => LC.L("The API hash is missing"); + public static string NoPhoneNumberError => LC.L("The phone number is missing"); + + #endregion + + + #region Descriptions + + public static string ApiIdShort => LC.L("The API ID"); + public static string ApiHashShort => LC.L("The API hash"); + public static string ApiIdLong => LC.L("The API ID retrieved from https://my.telegram.org/"); + public static string ApiHashLong => LC.L("The API hash retrieved from https://my.telegram.org/"); + public static string PhoneNumberShort => LC.L("Your phone number"); + public static string PhoneNumberLong => LC.L("The phone number you registered with"); + public static string AuthCodeShort => LC.L("The code you should have received (if you did)"); + public static string AuthCodeLong => LC.L("The auth code that you received. Input only if you did receive it"); + public static string PasswordShort => LC.L("2FA password (if enabled)"); + public static string PasswordLong => LC.L("The 2 step verification password. Input only if you have set it up"); + public static string ChannelName => LC.L("The channel name of the backup"); + + #endregion + + #region Formats + + public const string TELEGRAM_FLOOD = "It's required to wait {0} seconds before continuing"; + public const string STARTING_EXECUTING = "Starting executing action {0}"; + public const string DONE_EXECUTING = "Done executing action {0}"; + public const string USER_INFO_EXC = "Exception thrown that should be shown on UI"; + + #endregion + } +} \ No newline at end of file diff --git a/Duplicati/Library/Backend/Telegram/TelegramBackend.cs b/Duplicati/Library/Backend/Telegram/TelegramBackend.cs new file mode 100644 index 000000000..ed6cb25ce --- /dev/null +++ b/Duplicati/Library/Backend/Telegram/TelegramBackend.cs @@ -0,0 +1,512 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using Duplicati.Library.Backend.Extensions; +using Duplicati.Library.Interface; +using Duplicati.Library.Logging; +using TeleSharp.TL; +using TeleSharp.TL.Channels; +using TeleSharp.TL.Messages; +using TLSharp.Core; +using TLSharp.Core.Exceptions; +using TLSharp.Core.Network.Exceptions; +using TLSharp.Core.Utils; +using TLRequestDeleteMessages = TeleSharp.TL.Channels.TLRequestDeleteMessages; + +namespace Duplicati.Library.Backend +{ + public class Telegram : IStreamingBackend + { + private readonly TelegramClient m_telegramClient; + private readonly EncryptedFileSessionStore m_encSessionStore; + + private static readonly object m_lockObj = new object(); + + private readonly int m_apiId; + private readonly string m_apiHash; + private readonly string m_authCode; + private readonly string m_password; + private readonly string m_channelName; + private readonly string m_phoneNumber; + private TLChannel m_channelCache; + + private static string m_phoneCodeHash; + private static readonly string m_logTag = Log.LogTagFromType(typeof(Telegram)); + private const int BYTES_IN_MEBIBYTE = 1048576; + + public Telegram() + { } + + public Telegram(string url, Dictionary options) + { + if (options.TryGetValue(Strings.API_ID_KEY, out var apiId)) + { + m_apiId = int.Parse(apiId); + } + + if (options.TryGetValue(Strings.API_HASH_KEY, out var apiHash)) + { + m_apiHash = apiHash.Trim(); + } + + if (options.TryGetValue(Strings.PHONE_NUMBER_KEY, out var phoneNumber)) + { + m_phoneNumber = phoneNumber.Trim(); + } + + if (options.TryGetValue(Strings.AUTH_CODE_KEY, out var authCode)) + { + m_authCode = authCode.Trim(); + } + + if (options.TryGetValue(Strings.AUTH_PASSWORD, out var password)) + { + m_password = password.Trim(); + } + + if (options.TryGetValue(Strings.CHANNEL_NAME, out var channelName)) + { + m_channelName = channelName.Trim(); + } + + if (m_apiId == 0) + { + throw new UserInformationException(Strings.NoApiIdError, nameof(Strings.NoApiIdError)); + } + + if (string.IsNullOrEmpty(m_apiHash)) + { + throw new UserInformationException(Strings.NoApiHashError, nameof(Strings.NoApiHashError)); + } + + if (string.IsNullOrEmpty(m_phoneNumber)) + { + throw new UserInformationException(Strings.NoPhoneNumberError, nameof(Strings.NoPhoneNumberError)); + } + + if (string.IsNullOrEmpty(m_channelName)) + { + throw new UserInformationException(Strings.NoChannelNameError, nameof(Strings.NoChannelNameError)); + } + + m_encSessionStore = new EncryptedFileSessionStore($"{m_apiHash}_{m_apiId}"); + m_telegramClient = new TelegramClient(m_apiId, m_apiHash, m_encSessionStore, m_phoneNumber); + } + + public void Dispose() + { + m_telegramClient?.Dispose(); + } + + public string DisplayName { get; } = Strings.DisplayName; + public string ProtocolKey { get; } = "telegram"; + + public IEnumerable List() + { + return SafeExecute>(() => + { + Authenticate(); + EnsureChannelCreated(); + var fileInfos = ListChannelFileInfos(); + var result = fileInfos.Select(fi => fi.ToFileEntry()); + return result; + }, + nameof(List)); + } + + public Task PutAsync(string remotename, Stream stream, CancellationToken cancelToken) + { + SafeExecute(() => + { + cancelToken.ThrowIfCancellationRequested(); + + Authenticate(); + + cancelToken.ThrowIfCancellationRequested(); + + var channel = GetChannel(); + + cancelToken.ThrowIfCancellationRequested(); + + using (var sr = new StreamReader(new StreamReadHelper(stream))) + { + cancelToken.ThrowIfCancellationRequested(); + EnsureConnected(cancelToken); + + cancelToken.ThrowIfCancellationRequested(); + var file = m_telegramClient.UploadFile(remotename, sr, cancelToken).GetAwaiter().GetResult(); + + cancelToken.ThrowIfCancellationRequested(); + var inputPeerChannel = new TLInputPeerChannel {ChannelId = channel.Id, AccessHash = (long)channel.AccessHash}; + var fileNameAttribute = new TLDocumentAttributeFilename + { + FileName = remotename + }; + + EnsureConnected(cancelToken); + m_telegramClient.SendUploadedDocument(inputPeerChannel, file, remotename, "application/zip", new TLVector {fileNameAttribute}, cancelToken).GetAwaiter().GetResult(); + } + }, + nameof(PutAsync)); + + return Task.CompletedTask; + } + + public void Get(string remotename, Stream stream) + { + SafeExecute(() => + { + var fileInfo = ListChannelFileInfos().First(fi => fi.Name == remotename); + var fileLocation = fileInfo.ToFileLocation(); + + var limit = BYTES_IN_MEBIBYTE; + var currentOffset = 0; + + while (currentOffset < fileInfo.Size) + { + try + { + EnsureConnected(); + var file = m_telegramClient.GetFile(fileLocation, limit, currentOffset).GetAwaiter().GetResult(); + stream.Write(file.Bytes, 0, file.Bytes.Length); + currentOffset += file.Bytes.Length; + } + catch (InvalidOperationException e) + { + if (e.Message.Contains("Couldn't read the packet length") == false) + { + throw; + } + } + } + }, + $"{nameof(Get)}"); + } + + public Task PutAsync(string remotename, string filename, CancellationToken cancelToken) + { + using (var fs = File.OpenRead(filename)) + { + PutAsync(remotename, fs, cancelToken).GetAwaiter().GetResult(); + } + + return Task.CompletedTask; + } + + public void Get(string remotename, string filename) + { + using (var fs = new FileStream(filename, FileMode.Create, FileAccess.Write)) + { + Get(remotename, fs); + } + } + + public void Delete(string remotename) + { + SafeExecute(() => + { + var channel = GetChannel(); + var fileInfo = ListChannelFileInfos().FirstOrDefault(fi => fi.Name == remotename); + if (fileInfo == null) + { + return; + } + + var request = new TLRequestDeleteMessages + { + Channel = new TLInputChannel + { + ChannelId = channel.Id, + AccessHash = channel.AccessHash.Value + }, + Id = new TLVector {fileInfo.MessageId} + }; + + EnsureConnected(); + m_telegramClient.SendRequestAsync(request).GetAwaiter().GetResult(); + }, + $"{nameof(Delete)}({remotename})"); + } + + public List ListChannelFileInfos() + { + var channel = GetChannel(); + + var inputPeerChannel = new TLInputPeerChannel {ChannelId = channel.Id, AccessHash = channel.AccessHash.Value}; + var result = new List(); + var oldMinDate = 0L; + var newMinDate = (long?)null; + + while (oldMinDate != newMinDate) + { + oldMinDate = newMinDate ?? 0L; + RetrieveMessages(inputPeerChannel, result, oldMinDate); + if (result.Any() == false) + { + break; + } + + newMinDate = result.Min(cfi => Utility.Utility.NormalizeDateTimeToEpochSeconds(cfi.Date)); + } + + result = result.Distinct().ToList(); + return result; + } + + private void RetrieveMessages(TLInputPeerChannel inputPeerChannel, List result, long offsetDate) + { + EnsureConnected(); + var absHistory = m_telegramClient.GetHistoryAsync(inputPeerChannel, offsetDate: (int)offsetDate).GetAwaiter().GetResult(); + var history = ((TLChannelMessages)absHistory).Messages.OfType(); + + foreach (var msg in history) + { + if (msg.Media is TLMessageMediaDocument media && + media.Document is TLDocument mediaDoc) + { + var fileInfo = new ChannelFileInfo( + msg.Id, + mediaDoc.AccessHash, + mediaDoc.Id, + mediaDoc.Version, + mediaDoc.Size, + media.Caption, + DateTimeOffset.FromUnixTimeSeconds(msg.Date).UtcDateTime); + + result.Add(fileInfo); + } + } + } + + public IList SupportedCommands { get; } = new List + { + new CommandLineArgument(Strings.API_ID_KEY, CommandLineArgument.ArgumentType.Integer, Strings.ApiIdShort, Strings.ApiIdLong), + new CommandLineArgument(Strings.API_HASH_KEY, CommandLineArgument.ArgumentType.String, Strings.ApiHashShort, Strings.ApiHashLong), + new CommandLineArgument(Strings.PHONE_NUMBER_KEY, CommandLineArgument.ArgumentType.String, Strings.PhoneNumberShort, Strings.PhoneNumberLong), + new CommandLineArgument(Strings.AUTH_CODE_KEY, CommandLineArgument.ArgumentType.String, Strings.AuthCodeShort, Strings.AuthCodeLong), + new CommandLineArgument(Strings.AUTH_PASSWORD, CommandLineArgument.ArgumentType.String, Strings.PasswordShort, Strings.PasswordLong), + new CommandLineArgument(Strings.CHANNEL_NAME, CommandLineArgument.ArgumentType.String, Strings.ChannelName, Strings.ChannelName) + }; + + public string Description { get; } = Strings.Description; + + public string[] DNSName { get; } + + public void Test() + { + SafeExecute(Authenticate, nameof(Authenticate)); + } + + public void CreateFolder() + { + SafeExecute(() => + { + Authenticate(); + EnsureChannelCreated(); + }, + nameof(CreateFolder)); + } + + private TLChannel GetChannel() + { + if (m_channelCache != null) + { + return m_channelCache; + } + + var absChats = GetChats(); + + var channel = (TLChannel)absChats.FirstOrDefault(chat => chat is TLChannel tlChannel && tlChannel.Title == m_channelName); + m_channelCache = channel; + return channel; + } + + private IEnumerable GetChats() + { + var lastDate = 0; + while (true) + { + EnsureConnected(); + var dialogs = m_telegramClient.GetUserDialogsAsync(lastDate).GetAwaiter().GetResult(); + var tlDialogs = dialogs as TLDialogs; + var tlDialogsSlice = dialogs as TLDialogsSlice; + + foreach (var chat in tlDialogs?.Chats ?? tlDialogsSlice?.Chats) + { + switch (chat) + { + case TLChannelForbidden _: + case TLChatForbidden _: + break; + case TLChat c: + lastDate = c.Date; + break; + case TLChannel c: + lastDate = c.Date; + break; + + default: + throw new NotSupportedException($"Unsupported chat type {chat.GetType()}"); + } + + yield return chat; + } + + if (tlDialogs?.Dialogs?.Count < 100 || tlDialogsSlice?.Dialogs?.Count < 100) + { + yield break; + } + } + } + + private void EnsureChannelCreated() + { + var channel = GetChannel(); + if (channel == null) + { + var newGroup = new TLRequestCreateChannel + { + Broadcast = false, + Megagroup = false, + Title = m_channelName, + About = string.Empty + }; + + EnsureConnected(); + m_telegramClient.SendRequestAsync(newGroup).GetAwaiter().GetResult(); + } + } + + private void Authenticate() + { + EnsureConnected(); + + if (IsAuthenticated()) + { + return; + } + + try + { + if (m_phoneCodeHash == null) + { + EnsureConnected(); + var phoneCodeHash = m_telegramClient.SendCodeRequestAsync(m_phoneNumber).GetAwaiter().GetResult(); + SetPhoneCodeHash(phoneCodeHash); + m_telegramClient.Session.Save(); + + if (string.IsNullOrEmpty(m_authCode)) + { + throw new UserInformationException(Strings.NoAuthCodeError, nameof(Strings.NoAuthCodeError)); + } + + throw new UserInformationException(Strings.WrongAuthCodeError, nameof(Strings.WrongAuthCodeError)); + } + + m_telegramClient.MakeAuthAsync(m_phoneNumber, m_phoneCodeHash, m_authCode).GetAwaiter().GetResult(); + } + catch (CloudPasswordNeededException) + { + if (string.IsNullOrEmpty(m_password)) + { + m_telegramClient.Session.Save(); + throw new UserInformationException(Strings.NoPasswordError, nameof(Strings.NoPasswordError)); + } + + EnsureConnected(); + var passwordSetting = m_telegramClient.GetPasswordSetting().GetAwaiter().GetResult(); + m_telegramClient.MakeAuthWithPasswordAsync(passwordSetting, m_password).GetAwaiter().GetResult(); + } + + m_telegramClient.Session.Save(); + } + + private void EnsureConnected(CancellationToken cancelToken = default(CancellationToken)) + { + if (m_telegramClient.IsReallyConnected()) + { + return; + } + + cancelToken.ThrowIfCancellationRequested(); + + m_telegramClient.WrapperConnectAsync(cancelToken).GetAwaiter().GetResult(); + + cancelToken.ThrowIfCancellationRequested(); + + if (m_telegramClient.IsReallyConnected() == false) + { + throw new WebException("Unable to connect to telegram"); + } + } + + private bool IsAuthenticated() + { + var isAuthorized = m_telegramClient.IsUserAuthorized(); + return isAuthorized; + } + + private static void SetPhoneCodeHash(string phoneCodeHash) + { + m_phoneCodeHash = phoneCodeHash; + } + + private void SafeExecute(Action action, string actionName) + { + lock (m_lockObj) + { + Log.WriteInformationMessage(m_logTag, nameof(Strings.STARTING_EXECUTING), Strings.STARTING_EXECUTING, actionName); + try + { + action(); + } + catch (UserInformationException uiExc) + { + Log.WriteWarningMessage(m_logTag, nameof(Strings.USER_INFO_EXC), uiExc, Strings.USER_INFO_EXC); + throw; + } + catch (FloodException floodExc) + { + var randSeconds = new Random().Next(2, 15); + Log.WriteInformationMessage(m_logTag, nameof(Strings.TELEGRAM_FLOOD), Strings.TELEGRAM_FLOOD, floodExc.TimeToWait.TotalSeconds + randSeconds); + Thread.Sleep(floodExc.TimeToWait + TimeSpan.FromSeconds(randSeconds)); + SafeExecute(action, actionName); + } + } + + Log.WriteInformationMessage(m_logTag, nameof(Strings.DONE_EXECUTING), Strings.DONE_EXECUTING, actionName); + } + + private T SafeExecute(Func func, string actionName) + { + lock (m_lockObj) + { + Log.WriteInformationMessage(m_logTag, nameof(Strings.STARTING_EXECUTING), Strings.STARTING_EXECUTING, actionName); + try + { + var res = func(); + Log.WriteInformationMessage(m_logTag, nameof(Strings.DONE_EXECUTING), Strings.DONE_EXECUTING, actionName); + return res; + } + catch (UserInformationException uiExc) + { + Log.WriteWarningMessage(m_logTag, nameof(Strings.USER_INFO_EXC), uiExc, Strings.USER_INFO_EXC); + throw; + } + catch (FloodException floodExc) + { + var randSeconds = new Random().Next(2, 15); + Log.WriteInformationMessage(m_logTag, nameof(Strings.TELEGRAM_FLOOD), Strings.TELEGRAM_FLOOD, floodExc.TimeToWait.TotalSeconds + randSeconds); + Thread.Sleep(floodExc.TimeToWait); + var res = SafeExecute(func, actionName); + Log.WriteInformationMessage(m_logTag, nameof(Strings.DONE_EXECUTING), Strings.DONE_EXECUTING, actionName); + return res; + } + } + } + } +} \ No newline at end of file diff --git a/Duplicati/Library/Backend/Telegram/app.config b/Duplicati/Library/Backend/Telegram/app.config new file mode 100644 index 000000000..ba98fc967 --- /dev/null +++ b/Duplicati/Library/Backend/Telegram/app.config @@ -0,0 +1,6 @@ + + + + + + diff --git a/Duplicati/Library/Backend/TencentCOS/COSBackend.cs b/Duplicati/Library/Backend/TencentCOS/COSBackend.cs new file mode 100644 index 000000000..59d5a719b --- /dev/null +++ b/Duplicati/Library/Backend/TencentCOS/COSBackend.cs @@ -0,0 +1,411 @@ +using COSXML; +using COSXML.Auth; +using COSXML.Model.Bucket; +using COSXML.Model.Object; +using COSXML.Model.Tag; +using COSXML.Utils; +using Duplicati.Library.Common.IO; +using Duplicati.Library.Interface; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Duplicati.Library.Backend.TencentCOS +{ + /// + /// Tencent COS + /// https://cloud.tencent.com/document/product/436 + /// https://cloud.tencent.com/document/product/436/32869 + /// + public class COS : IBackend, IStreamingBackend, IRenameEnabledBackend + { + private static readonly string LOGTAG = Logging.Log.LogTagFromType(); + + private const string COS_APP_ID = "cos-app-id"; + private const string COS_REGION = "cos-region"; + private const string COS_SECRET_ID = "cos-secret-id"; + private const string COS_SECRET_KEY = "cos-secret-key"; + private const string COS_BUCKET = "cos-bucket"; + + /// + /// Set default HTTPS request + /// + private const bool COS_USE_SSL = true; + /// + /// Connection timeout time, in milliseconds, default 45000ms + /// + private const int CONNECTION_TIMEOUT_MS = 1000 * 60 * 60; + /// + /// Read and write timeout time, in milliseconds, default 45000ms + /// + private const int READ_WRITE_TIMEOUT_MS = 1000 * 60 * 60; + /// + /// The valid time of each request signature, unit: second + /// + private const int KEY_DURATION_SECOND = 60 * 60; + + private readonly CosOptions _cosOptions; + + CosXml cosXml; + + /// + /// Tencent Cloud Object Storage Configuration + /// + public class CosOptions + { + /// + /// Tencent Cloud Account APPID + /// + public string Appid { get; set; } + /// + /// Cloud API Secret Id + /// + public string SecretId { get; set; } + /// + /// Cloud API Secret Key + /// + public string SecretKey { get; set; } + /// + /// Bucket region ap-guangzhou ap-hongkong + /// https://cloud.tencent.com/document/product/436/6224 + /// + public string Region { get; set; } + /// + /// Bucket, format: BucketName-APPID + /// + public string Bucket { get; set; } + /// + /// A path or subfolder in a bucket + /// + public string Path { get; set; } + } + + public COS() { } + + public COS(string url, Dictionary options) + { + _cosOptions = new CosOptions(); + + var uri = new Utility.Uri(url?.Trim()); + var prefix = uri.HostAndPath?.Trim()?.Trim('/')?.Trim('\\'); + + if (!string.IsNullOrEmpty(prefix)) + { + _cosOptions.Path = prefix + "/"; + } + + if (options.ContainsKey(COS_APP_ID)) + { + _cosOptions.Appid = options[COS_APP_ID]; + } + + if (options.ContainsKey(COS_REGION)) + { + _cosOptions.Region = options[COS_REGION]; + } + + if (options.ContainsKey(COS_SECRET_ID)) + { + _cosOptions.SecretId = options[COS_SECRET_ID]; + } + + if (options.ContainsKey(COS_SECRET_KEY)) + { + _cosOptions.SecretKey = options[COS_SECRET_KEY]; + } + + if (options.ContainsKey(COS_BUCKET)) + { + _cosOptions.Bucket = options[COS_BUCKET]; + } + } + + CosXml GetCosXml() + { + string appid = _cosOptions.Appid; + string region = _cosOptions.Region; + string secretId = _cosOptions.SecretId; + string secretKey = _cosOptions.SecretKey; + + CosXmlConfig config = new CosXmlConfig.Builder() + .SetConnectionTimeoutMs(CONNECTION_TIMEOUT_MS) + .SetReadWriteTimeoutMs(READ_WRITE_TIMEOUT_MS) + .IsHttps(COS_USE_SSL) + .SetAppid(appid) + .SetRegion(region) + .Build(); + + // Initialization QCloudCredentialProvider, COS SDK provides three ways: the permanent temporary keys custom + QCloudCredentialProvider cosCredentialProvider = new DefaultQCloudCredentialProvider(secretId, secretKey, KEY_DURATION_SECOND); + + return new CosXmlServer(config, cosCredentialProvider); + } + + public IEnumerable List() + { + bool isTruncated = true; + string filename = null; + + while (isTruncated) + { + cosXml = GetCosXml(); + string bucket = _cosOptions.Bucket; + string prefix = _cosOptions.Path; + + GetBucketRequest request = new GetBucketRequest(bucket); + + request.SetSign(TimeUtils.GetCurrentTime(TimeUnit.SECONDS), 600); + + if (!string.IsNullOrEmpty(filename)) + { + request.SetMarker(filename); + } + + if (!string.IsNullOrEmpty(prefix)) + { + request.SetPrefix(prefix); + } + + GetBucketResult result = cosXml.GetBucket(request); + + ListBucket info = result.listBucket; + + isTruncated = result.listBucket.isTruncated; + filename = result.listBucket.nextMarker; + + foreach (var item in info.contentsList) + { + var last = DateTime.Parse(item.lastModified); + + var fileName = item.key; + if (!string.IsNullOrWhiteSpace(prefix)) + { + fileName = fileName.Substring(prefix.Length); + + if (fileName.StartsWith("/", StringComparison.Ordinal)) + { + fileName = fileName.Trim('/'); + } + } + + yield return new FileEntry(fileName, item.size, last, last); + } + } + } + + public Task PutAsync(string remotename, string filename, CancellationToken cancelToken) + { + using (FileStream fs = File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.Read)) + return PutAsync(remotename, fs, cancelToken); + } + + public void Get(string remotename, string filename) + { + using (var fs = File.Open(filename, FileMode.Create, FileAccess.Write, FileShare.None)) + Get(remotename, fs); + } + + public void Delete(string remotename) + { + try + { + cosXml = GetCosXml(); + string bucket = _cosOptions.Bucket; + string key = GetFullKey(remotename); + + DeleteObjectRequest request = new DeleteObjectRequest(bucket, key); + + request.SetSign(TimeUtils.GetCurrentTime(TimeUnit.SECONDS), 600); + + DeleteObjectResult result = cosXml.DeleteObject(request); + } + catch (COSXML.CosException.CosClientException clientEx) + { + Logging.Log.WriteErrorMessage(LOGTAG, "Delete", clientEx, "Delete failed: {0}", remotename); + throw; + } + catch (COSXML.CosException.CosServerException serverEx) + { + Logging.Log.WriteErrorMessage(LOGTAG, "Delete", serverEx, "Delete failed: {0}, {1}", remotename, serverEx.GetInfo()); + throw; + } + } + + public void Test() + { + var json = JsonConvert.SerializeObject(_cosOptions); + try + { + cosXml = GetCosXml(); + string bucket = _cosOptions.Bucket; + HeadBucketRequest request = new HeadBucketRequest(bucket); + request.SetSign(TimeUtils.GetCurrentTime(TimeUnit.SECONDS), 600); + HeadBucketResult result = cosXml.HeadBucket(request); + + Logging.Log.WriteInformationMessage(LOGTAG, "Test", "Request complete {0}: {1}, {2}", result.httpCode, json, result.GetResultInfo()); + } + catch (COSXML.CosException.CosClientException clientEx) + { + Logging.Log.WriteErrorMessage(LOGTAG, "Test", clientEx, "Request failed: {0}", json); + throw; + } + catch (COSXML.CosException.CosServerException serverEx) + { + Logging.Log.WriteErrorMessage(LOGTAG, "Test", serverEx, "Request failed: {0}, {1}", json, serverEx.GetInfo()); + throw; + } + } + + public void CreateFolder() + { + // No need to create folders + } + + public void Dispose() + { + if (cosXml != null) + { + cosXml = null; + } + } + + public Task PutAsync(string remotename, Stream stream, CancellationToken cancelToken) + { + try + { + cosXml = GetCosXml(); + string bucket = _cosOptions.Bucket; + string key = GetFullKey(remotename); + + byte[] buffer = new byte[stream.Length]; + if (Utility.Utility.ForceStreamRead(stream, buffer, buffer.Length) != stream.Length) + { + throw new Exception("Bad file read"); + } + + PutObjectRequest request = new PutObjectRequest(bucket, key, buffer); + + request.SetSign(TimeUtils.GetCurrentTime(TimeUnit.SECONDS), KEY_DURATION_SECOND); + request.SetRequestHeader("Content-Type", "application/octet-stream"); + + PutObjectResult result = cosXml.PutObject(request); + } + catch (COSXML.CosException.CosClientException clientEx) + { + Logging.Log.WriteErrorMessage(LOGTAG, "PutAsync", clientEx, "Put failed: {0}", remotename); + throw; + } + catch (COSXML.CosException.CosServerException serverEx) + { + Logging.Log.WriteErrorMessage(LOGTAG, "PutAsync", serverEx, "Put failed: {0}, {1}", remotename, serverEx.GetInfo()); + throw; + } + + return Task.CompletedTask; + } + + public void Get(string remotename, Stream stream) + { + try + { + cosXml = GetCosXml(); + string bucket = _cosOptions.Bucket; + string key = GetFullKey(remotename); + + GetObjectBytesRequest request = new GetObjectBytesRequest(bucket, key); + + request.SetSign(TimeUtils.GetCurrentTime(TimeUnit.SECONDS), KEY_DURATION_SECOND); + + GetObjectBytesResult result = cosXml.GetObject(request); + + byte[] bytes = result.content; + + Stream ms = new MemoryStream(bytes); + Utility.Utility.CopyStream(ms, stream); + } + catch (COSXML.CosException.CosClientException clientEx) + { + Logging.Log.WriteErrorMessage(LOGTAG, "Get", clientEx, "Get failed: {0}", remotename); + throw; + } + catch (COSXML.CosException.CosServerException serverEx) + { + Logging.Log.WriteErrorMessage(LOGTAG, "Get", serverEx, "Get failed: {0}, {1}", remotename, serverEx.GetInfo()); + throw; + } + } + + public void Rename(string oldname, string newname) + { + try + { + cosXml = GetCosXml(); + string sourceAppid = _cosOptions.Appid; + string sourceBucket = _cosOptions.Bucket; + string sourceRegion = _cosOptions.Region; + string sourceKey = GetFullKey(oldname); + + CopySourceStruct copySource = new CopySourceStruct(sourceAppid, sourceBucket, + sourceRegion, sourceKey); + + string bucket = _cosOptions.Bucket; + string key = GetFullKey(newname); + CopyObjectRequest request = new CopyObjectRequest(bucket, key); + + request.SetSign(TimeUtils.GetCurrentTime(TimeUnit.SECONDS), KEY_DURATION_SECOND); + request.SetCopySource(copySource); + request.SetCopyMetaDataDirective(COSXML.Common.CosMetaDataDirective.COPY); + + CopyObjectResult result = cosXml.CopyObject(request); + + //Console.WriteLine(result.GetResultInfo()); + + Delete(oldname); + } + catch (COSXML.CosException.CosClientException clientEx) + { + Logging.Log.WriteErrorMessage(LOGTAG, "Rename", clientEx, "Rename failed: {0} to {1}", oldname, newname); + throw; + } + catch (COSXML.CosException.CosServerException serverEx) + { + Logging.Log.WriteErrorMessage(LOGTAG, "Rename", serverEx, "Rename failed: {0} to {1}, {2}", oldname, newname, serverEx.GetInfo()); + throw; + } + } + + public string DisplayName => Strings.COSBackend.DisplayName; + + public string Description => Strings.COSBackend.Description; + + public string ProtocolKey => "cos"; + + public IList SupportedCommands + { + get + { + return new List(new ICommandLineArgument[] { + new CommandLineArgument(COS_APP_ID, CommandLineArgument.ArgumentType.String, Strings.COSBackend.COSAccountDescriptionShort,Strings.COSBackend.COSAccountDescriptionLong), + new CommandLineArgument(COS_REGION, CommandLineArgument.ArgumentType.String, Strings.COSBackend.COSLocationDescriptionShort,Strings.COSBackend.COSLocationDescriptionLong), + new CommandLineArgument(COS_SECRET_ID, CommandLineArgument.ArgumentType.String, Strings.COSBackend.COSAPISecretIdDescriptionShort,Strings.COSBackend.COSAPISecretIdDescriptionLong), + new CommandLineArgument(COS_SECRET_KEY, CommandLineArgument.ArgumentType.Password, Strings.COSBackend.COSAPISecretKeyDescriptionShort,Strings.COSBackend.COSAPISecretKeyDescriptionLong), + new CommandLineArgument(COS_BUCKET, CommandLineArgument.ArgumentType.String,Strings.COSBackend.COSBucketDescriptionShort,Strings.COSBackend.COSBucketDescriptionLong), + }); + } + } + + public string[] DNSName => null; + + private string GetFullKey(string name) + { + if (string.IsNullOrWhiteSpace(_cosOptions?.Path)) + { + return name; + } + return _cosOptions.Path + name; + } + } +} diff --git a/Duplicati/Library/Backend/TencentCOS/Duplicati.Library.Backend.TencentCOS.csproj b/Duplicati/Library/Backend/TencentCOS/Duplicati.Library.Backend.TencentCOS.csproj new file mode 100644 index 000000000..e6e6a144b --- /dev/null +++ b/Duplicati/Library/Backend/TencentCOS/Duplicati.Library.Backend.TencentCOS.csproj @@ -0,0 +1,22 @@ + + + net5.0 + Library + false + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Duplicati/Library/Backend/TencentCOS/Properties/AssemblyInfo.cs b/Duplicati/Library/Backend/TencentCOS/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..2ce1f3932 --- /dev/null +++ b/Duplicati/Library/Backend/TencentCOS/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// 有关程序集的一般信息由以下 +// 控制。更改这些特性值可修改 +// 与程序集关联的信息。 +[assembly: AssemblyTitle("Duplicati.Library.Backend.TencentCOS")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Duplicati.Library.Backend.TencentCOS")] +[assembly: AssemblyCopyright("Copyright © 2020")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// 将 ComVisible 设置为 false 会使此程序集中的类型 +//对 COM 组件不可见。如果需要从 COM 访问此程序集中的类型 +//请将此类型的 ComVisible 特性设置为 true。 +[assembly: ComVisible(false)] + +// 如果此项目向 COM 公开,则下列 GUID 用于类型库的 ID +[assembly: Guid("545dd6d4-9476-42d6-b51c-a28e000c489e")] + +// 程序集的版本信息由下列四个值组成: +// +// 主版本 +// 次版本 +// 生成号 +// 修订号 +// +//可以指定所有这些值,也可以使用“生成号”和“修订号”的默认值 +//通过使用 "*",如下所示: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Duplicati/Library/Backend/TencentCOS/Strings.cs b/Duplicati/Library/Backend/TencentCOS/Strings.cs new file mode 100644 index 000000000..65fe0ff37 --- /dev/null +++ b/Duplicati/Library/Backend/TencentCOS/Strings.cs @@ -0,0 +1,26 @@ +using Duplicati.Library.Localization.Short; + +namespace Duplicati.Library.Backend.Strings +{ + internal static class COSBackend + { + public static string DisplayName { get { return LC.L(@"Tencent COS"); } } + public static string Description { get { return LC.L(@"Object storage (Cloud Object Storage, COS) is a distributed storage service for storing massive files provided by Tencent Cloud, which has the advantages of high scalability, low cost, reliability and security."); } } + + public static string COSAccountDescriptionShort { get { return LC.L(@"Account ID"); } } + public static string COSAccountDescriptionLong { get { return LC.L(@"Account ID of Tencent Cloud Account"); } } + + public static string COSAPISecretIdDescriptionShort { get { return LC.L(@"Secret Id"); } } + public static string COSAPISecretIdDescriptionLong { get { return LC.L(@"Cloud API Secret Id"); } } + + public static string COSAPISecretKeyDescriptionShort { get { return LC.L(@"Secret Key"); } } + public static string COSAPISecretKeyDescriptionLong { get { return LC.L(@"Cloud API Secret Key"); } } + + public static string COSBucketDescriptionShort { get { return LC.L(@"Bucket"); } } + public static string COSBucketDescriptionLong { get { return LC.L(@"Bucket, format: BucketName-APPID"); } } + + public static string COSLocationDescriptionShort { get { return LC.L(@"Specifies COS location constraints"); } } + public static string COSLocationDescriptionLong { get { return LC.L(@"Region (Region) is the distribution area of ​​the Tencent cloud hosting machine room, the object storage COS data is stored in the storage buckets of these regions. https://cloud.tencent.com/document/product/436/6224"); } } + } + +} diff --git a/Duplicati/Library/Common/IO/ISystemIO.cs b/Duplicati/Library/Common/IO/ISystemIO.cs index 2c3196324..654e72449 100644 --- a/Duplicati/Library/Common/IO/ISystemIO.cs +++ b/Duplicati/Library/Common/IO/ISystemIO.cs @@ -29,7 +29,9 @@ namespace Duplicati.Library.Common.IO { IFileEntry DirectoryEntry(string path); void DirectoryCreate(string path); + void DirectoryDelete(string path, bool recursive); bool DirectoryExists(string path); + void DirectoryMove(string sourceDirName, string destDirName); void DirectorySetLastWriteTimeUtc(string path, DateTime time); void DirectorySetCreationTimeUtc(string path, DateTime time); @@ -64,6 +66,7 @@ namespace Duplicati.Library.Common.IO DateTime GetLastWriteTimeUtc(string path); IEnumerable EnumerateFileSystemEntries(string path); IEnumerable EnumerateFiles(string path); + IEnumerable EnumerateFiles(string path, string searchPattern, SearchOption searchOption); IEnumerable EnumerateDirectories(string path); void SetMetadata(string path, Dictionary metdata, bool restorePermissions); diff --git a/Duplicati/Library/Common/IO/SystemIOLinux.cs b/Duplicati/Library/Common/IO/SystemIOLinux.cs index 180867b48..1acb95b47 100644 --- a/Duplicati/Library/Common/IO/SystemIOLinux.cs +++ b/Duplicati/Library/Common/IO/SystemIOLinux.cs @@ -32,11 +32,21 @@ namespace Duplicati.Library.Common.IO Directory.CreateDirectory(NormalizePath(path)); } + public void DirectoryDelete(string path, bool recursive) + { + Directory.Delete(NormalizePath(path), recursive); + } + public bool DirectoryExists(string path) { return Directory.Exists(NormalizePath(path)); } + public void DirectoryMove(string sourceDirName, string destDirName) + { + System.IO.Directory.Move(NormalizePath(sourceDirName), NormalizePath(destDirName)); + } + public void FileDelete(string path) { File.Delete(path); @@ -117,6 +127,10 @@ namespace Duplicati.Library.Common.IO return Directory.EnumerateFiles(path); } + public IEnumerable EnumerateFiles(string path, string searchPattern, SearchOption searchOption) + { + return Directory.EnumerateFiles(path, searchPattern, searchOption); + } public string PathGetFileName(string path) { diff --git a/Duplicati/Library/Common/IO/SystemIOWindows.cs b/Duplicati/Library/Common/IO/SystemIOWindows.cs index f7b5d891f..4758ba1cf 100644 --- a/Duplicati/Library/Common/IO/SystemIOWindows.cs +++ b/Duplicati/Library/Common/IO/SystemIOWindows.cs @@ -34,28 +34,48 @@ namespace Duplicati.Library.Common.IO private const string PATHPREFIX_SERVER = @"\\"; private static readonly string DIRSEP = Util.DirectorySeparatorString; - private static bool IsPathTooLong(string path) - { - // Use 258 for length check instead of 260 (MAX_PATH) - we need to leave room for the 16-bit (wide) null terminator - return path.StartsWith(UNCPREFIX, StringComparison.Ordinal) || path.StartsWith(UNCPREFIX_SERVER, StringComparison.Ordinal) || path.Length > 258; - } - public static string PrefixWithUNC(string path) { - if (path.StartsWith(UNCPREFIX_SERVER, StringComparison.Ordinal)) + if (IsPrefixedWithUNC(path)) + { return path; - - if (path.StartsWith(UNCPREFIX, StringComparison.Ordinal)) - return path; - + } return path.StartsWith(PATHPREFIX_SERVER, StringComparison.Ordinal) - ? UNCPREFIX_SERVER + path.Remove(0, PATHPREFIX_SERVER.Length) + ? UNCPREFIX_SERVER + path.Substring(PATHPREFIX_SERVER.Length) : UNCPREFIX + path; } + private static bool IsPrefixedWithUNC(string path) + { + return path.StartsWith(UNCPREFIX_SERVER, StringComparison.Ordinal) || + path.StartsWith(UNCPREFIX, StringComparison.Ordinal); + } + public static string StripUNCPrefix(string path) { - return path.StartsWith(UNCPREFIX, StringComparison.Ordinal) ? path.Substring(UNCPREFIX.Length) : path; + if (path.StartsWith(UNCPREFIX_SERVER, StringComparison.Ordinal)) + { + // @"\\?\UNC\example.com\share\file.txt" to @"\\example.com\share\file.txt" + return PATHPREFIX_SERVER + path.Substring(UNCPREFIX_SERVER.Length); + } + else if (path.StartsWith(UNCPREFIX, StringComparison.Ordinal)) + { + // @"\\?\C:\file.txt" to @"C:\file.txt" + return path.Substring(UNCPREFIX.Length); + } + else + { + return path; + } + } + + /// + /// Convert forward slashes to backslashes. + /// + /// Path with forward slashes replaced by backslashes. + private static string ConvertSlashes(string path) + { + return path.Replace("/", Util.DirectorySeparatorString); } private class FileSystemAccess @@ -139,175 +159,100 @@ namespace Duplicati.Library.Common.IO private System.Security.AccessControl.FileSystemSecurity GetAccessControlDir(string path) { - if (!IsPathTooLong(path)) - try - { - var di = new DirectoryInfo(path); - return di.GetAccessControl(); - } - catch (System.IO.PathTooLongException) { } - catch (System.ArgumentException) { } - - //TODO-DNC Revert back to proper call when Library will be netstandard ready - return (System.Security.AccessControl.FileSystemSecurity) - typeof(Alphaleonis.Win32.Filesystem.Directory).GetMethod("GetAccessControl", new Type[] { typeof(string) }) - .Invoke(null, new object[] { PrefixWithUNC(path) }); + return new DirectoryInfo(PrefixWithUNC(path)).GetAccessControl(); } private System.Security.AccessControl.FileSystemSecurity GetAccessControlFile(string path) { - if (!IsPathTooLong(path)) - try - { - var fi = new FileInfo(path); - return fi.GetAccessControl(); - } - catch (System.IO.PathTooLongException) { } - catch (System.ArgumentException) { } - - //TODO-DNC Revert back to proper call when Library will be netstandard ready - return (System.Security.AccessControl.FileSystemSecurity) - typeof(Alphaleonis.Win32.Filesystem.File).GetMethod("GetAccessControl", new Type[] { typeof(string) }) - .Invoke(null, new object[] { PrefixWithUNC(path) }); + return new FileInfo(PrefixWithUNC(path)).GetAccessControl(); } private void SetAccessControlFile(string path, FileSecurity rules) { - if (!IsPathTooLong(path)) - try - { - new FileInfo(path).SetAccessControl(rules); - return; - } - catch (System.IO.PathTooLongException) { } - catch (System.ArgumentException) { } - - //TODO-DNC Revert back to proper call when Library will be netstandard ready - typeof(Alphaleonis.Win32.Filesystem.File).GetMethod("SetAccessControl", new Type[] { typeof(string), typeof(System.Security.AccessControl.FileSystemSecurity), typeof(AccessControlSections) }) - .Invoke(null, new object[] { PrefixWithUNC(path), rules, AccessControlSections.All }); + new FileInfo(PrefixWithUNC(path)).SetAccessControl(rules); } private void SetAccessControlDir(string path, DirectorySecurity rules) { - if (!IsPathTooLong(path)) - try - { - new DirectoryInfo(path).SetAccessControl(rules); - return; - } - catch (System.IO.PathTooLongException) { } - catch (System.ArgumentException) { } - - //TODO-DNC Revert back to proper call when Library will be netstandard ready - typeof(Alphaleonis.Win32.Filesystem.Directory).GetMethod("SetAccessControl", new Type[] { typeof(string), typeof(System.Security.AccessControl.FileSystemSecurity), typeof(AccessControlSections) }) - .Invoke(null, new object[] { PrefixWithUNC(path), rules, AccessControlSections.All }); - } - - private static void PathTooLongVoidFuncWrapper(Func nativeIOFunc, - Func alternativeIOFunc, - string path, bool prefixWithUnc = false) - { - // Wrap void into bool return type to avoid code duplication. Code at anytime available for replacement. - PathTooLongFuncWrapper(p => { nativeIOFunc(p); return true; }, p => { alternativeIOFunc(p); return true; } , path, prefixWithUnc); - } - - private static void PathTooLongActionWrapper(Action nativeIOFunc, - Action alternativeIOFunc, - string path, bool prefixWithUnc = false) - { - // Wrap void into bool return type to avoid code duplication. Code at anytime available for replacement. - PathTooLongFuncWrapper(p => { nativeIOFunc(p); return true; }, p => { alternativeIOFunc(p); return true; }, path, prefixWithUnc); - } - - private static T PathTooLongFuncWrapper(Func nativeIOFunc, - Func alternativeIOFunc, - string path, bool prefixWithUnc = false) - { - if (!IsPathTooLong(path)) - try { return nativeIOFunc(path); } - catch (System.IO.PathTooLongException) { } - catch (System.ArgumentException) { } - - return !prefixWithUnc ? alternativeIOFunc(path) : alternativeIOFunc(PrefixWithUNC(path)); + new DirectoryInfo(PrefixWithUNC(path)).SetAccessControl(rules); } #region ISystemIO implementation public void DirectoryCreate(string path) { - PathTooLongVoidFuncWrapper(System.IO.Directory.CreateDirectory, - Alphaleonis.Win32.Filesystem.Directory.CreateDirectory, path, true); + System.IO.Directory.CreateDirectory(PrefixWithUNC(path)); + } + + public void DirectoryDelete(string path, bool recursive) + { + System.IO.Directory.Delete(PrefixWithUNC(path), recursive); } public bool DirectoryExists(string path) { - return PathTooLongFuncWrapper(System.IO.Directory.Exists, - Alphaleonis.Win32.Filesystem.Directory.Exists, path, true); + return System.IO.Directory.Exists(PrefixWithUNC(path)); + } + + public void DirectoryMove(string sourceDirName, string destDirName) + { + System.IO.Directory.Move(PrefixWithUNC(sourceDirName), PrefixWithUNC(destDirName)); } public void FileDelete(string path) { - PathTooLongActionWrapper(System.IO.File.Delete, - Alphaleonis.Win32.Filesystem.File.Delete, path, true); + System.IO.File.Delete(PrefixWithUNC(path)); } public void FileSetLastWriteTimeUtc(string path, DateTime time) { - PathTooLongActionWrapper(p => System.IO.File.SetLastWriteTimeUtc(p, time), - p => Alphaleonis.Win32.Filesystem.File.SetLastWriteTimeUtc(p, time), path, true); + System.IO.File.SetLastWriteTimeUtc(PrefixWithUNC(path), time); } public void FileSetCreationTimeUtc(string path, DateTime time) { - PathTooLongActionWrapper(p => System.IO.File.SetCreationTimeUtc(p, time), - p => Alphaleonis.Win32.Filesystem.File.SetCreationTimeUtc(p, time), path, true); + System.IO.File.SetCreationTimeUtc(PrefixWithUNC(path), time); } public DateTime FileGetLastWriteTimeUtc(string path) { - return PathTooLongFuncWrapper(System.IO.File.GetLastWriteTimeUtc, - Alphaleonis.Win32.Filesystem.File.GetLastWriteTimeUtc, path, true); + return System.IO.File.GetLastWriteTimeUtc(PrefixWithUNC(path)); } public DateTime FileGetCreationTimeUtc(string path) { - return PathTooLongFuncWrapper(System.IO.File.GetCreationTimeUtc, - Alphaleonis.Win32.Filesystem.File.GetCreationTimeUtc, path, true); + return System.IO.File.GetCreationTimeUtc(PrefixWithUNC(path)); } public bool FileExists(string path) { - return PathTooLongFuncWrapper(System.IO.File.Exists, - Alphaleonis.Win32.Filesystem.File.Exists, path, true); + return System.IO.File.Exists(PrefixWithUNC(path)); } public System.IO.FileStream FileOpenRead(string path) { - return PathTooLongFuncWrapper(p => System.IO.File.Open(p, System.IO.FileMode.Open, System.IO.FileAccess.Read, System.IO.FileShare.ReadWrite), - p => Alphaleonis.Win32.Filesystem.File.Open(p, System.IO.FileMode.Open, System.IO.FileAccess.Read, System.IO.FileShare.ReadWrite), - path, true); + return System.IO.File.Open(PrefixWithUNC(path), System.IO.FileMode.Open, System.IO.FileAccess.Read, System.IO.FileShare.ReadWrite); } public System.IO.FileStream FileOpenWrite(string path) { return !FileExists(path) ? FileCreate(path) - : PathTooLongFuncWrapper(System.IO.File.OpenWrite, Alphaleonis.Win32.Filesystem.File.OpenWrite, path, true); + : System.IO.File.OpenWrite(PrefixWithUNC(path)); } public System.IO.FileStream FileCreate(string path) { - return PathTooLongFuncWrapper(System.IO.File.Create, Alphaleonis.Win32.Filesystem.File.Create, path, true); + return System.IO.File.Create(PrefixWithUNC(path)); } public System.IO.FileAttributes GetFileAttributes(string path) { - return PathTooLongFuncWrapper(System.IO.File.GetAttributes, AlphaFS.File.GetAttributes, path, true); + return System.IO.File.GetAttributes(PrefixWithUNC(path)); } public void SetFileAttributes(string path, System.IO.FileAttributes attributes) { - PathTooLongActionWrapper(p => System.IO.File.SetAttributes(p, attributes), - p => Alphaleonis.Win32.Filesystem.File.SetAttributes(p, attributes), path, true); + System.IO.File.SetAttributes(PrefixWithUNC(path), attributes); } /// @@ -319,7 +264,7 @@ namespace Duplicati.Library.Common.IO { try { - return PathTooLongFuncWrapper(AlphaFS.File.GetLinkTargetInfo, AlphaFS.File.GetLinkTargetInfo, file, true).PrintName; + return AlphaFS.File.GetLinkTargetInfo(PrefixWithUNC(file)).PrintName; } catch (AlphaFS.NotAReparsePointException) { } catch (AlphaFS.UnrecognizedReparsePointException) { } @@ -332,156 +277,159 @@ namespace Duplicati.Library.Common.IO public IEnumerable EnumerateFileSystemEntries(string path) { - return PathTooLongFuncWrapper(System.IO.Directory.EnumerateFileSystemEntries, - Alphaleonis.Win32.Filesystem.Directory.GetFileSystemEntries, - path, true).Select(StripUNCPrefix).AsEnumerable(); + return System.IO.Directory.EnumerateFileSystemEntries(PrefixWithUNC(path)).Select(StripUNCPrefix); } public IEnumerable EnumerateFiles(string path) { - return PathTooLongFuncWrapper(System.IO.Directory.EnumerateFiles, - Alphaleonis.Win32.Filesystem.Directory.GetFiles, path, - true).Select(StripUNCPrefix).AsEnumerable(); + return System.IO.Directory.EnumerateFiles(PrefixWithUNC(path)).Select(StripUNCPrefix); + } + + public IEnumerable EnumerateFiles(string path, string searchPattern, SearchOption searchOption) + { + return System.IO.Directory.EnumerateFiles(PrefixWithUNC(path), searchPattern, searchOption).Select(StripUNCPrefix); } public string PathGetFileName(string path) { - return StripUNCPrefix(PathTooLongFuncWrapper(System.IO.Path.GetFileName, - Alphaleonis.Win32.Filesystem.Path.GetFileName, - path, true)); + return StripUNCPrefix(System.IO.Path.GetFileName(PrefixWithUNC(path))); } public string PathGetDirectoryName(string path) { - return StripUNCPrefix(PathTooLongFuncWrapper(System.IO.Path.GetDirectoryName, - Alphaleonis.Win32.Filesystem.Path.GetDirectoryName, - path, true)); + return StripUNCPrefix(System.IO.Path.GetDirectoryName(PrefixWithUNC(path))); } public string PathGetExtension(string path) { - return StripUNCPrefix(PathTooLongFuncWrapper(System.IO.Path.GetExtension, - Alphaleonis.Win32.Filesystem.Path.GetExtension, - path, true)); + return StripUNCPrefix(System.IO.Path.GetExtension(PrefixWithUNC(path))); } public string PathChangeExtension(string path, string extension) { - return StripUNCPrefix(PathTooLongFuncWrapper(p => System.IO.Path.ChangeExtension(p, extension), - p => Alphaleonis.Win32.Filesystem.Path.ChangeExtension(p, extension), - path, true)); + return StripUNCPrefix(System.IO.Path.ChangeExtension(PrefixWithUNC(path), extension)); } public void DirectorySetLastWriteTimeUtc(string path, DateTime time) { - PathTooLongActionWrapper(p => System.IO.Directory.SetLastWriteTimeUtc(p, time), - p => Alphaleonis.Win32.Filesystem.File.SetLastWriteTimeUtc(p, time), path, true); + System.IO.Directory.SetLastWriteTimeUtc(PrefixWithUNC(path), time); } public void DirectorySetCreationTimeUtc(string path, DateTime time) { - PathTooLongActionWrapper(p => System.IO.Directory.SetCreationTimeUtc(p, time), - p => Alphaleonis.Win32.Filesystem.File.SetCreationTimeUtc(p, time), path, true); + System.IO.Directory.SetCreationTimeUtc(PrefixWithUNC(path), time); } public void FileMove(string source, string target) { - // We do not check if path is too long on target. If so, then we catch an exception. - PathTooLongActionWrapper(p => System.IO.File.Move(p, target), - p => Alphaleonis.Win32.Filesystem.File.Move(p, PrefixWithUNC(target)), - source, true); + System.IO.File.Move(PrefixWithUNC(source), PrefixWithUNC(target)); } public long FileLength(string path) { - return PathTooLongFuncWrapper(p => new System.IO.FileInfo(p).Length, - p => new Alphaleonis.Win32.Filesystem.FileInfo(p).Length, - path, true); + return new System.IO.FileInfo(PrefixWithUNC(path)).Length; } public string GetPathRoot(string path) { - return PathTooLongFuncWrapper(Path.GetPathRoot, AlphaFS.Path.GetPathRoot, path, false); + if (IsPrefixedWithUNC(path)) + { + return Path.GetPathRoot(path); + } + else + { + return StripUNCPrefix(Path.GetPathRoot(PrefixWithUNC(path))); + } } public string[] GetDirectories(string path) { - return PathTooLongFuncWrapper(Directory.GetDirectories, AlphaFS.Directory.GetDirectories, path, false); + if (IsPrefixedWithUNC(path)) + { + return Directory.GetDirectories(path); + } + else + { + return Directory.GetDirectories(PrefixWithUNC(path)).Select(StripUNCPrefix).ToArray(); + } } public string[] GetFiles(string path) { - return PathTooLongFuncWrapper(Directory.GetFiles, AlphaFS.Directory.GetFiles, path, false); + if (IsPrefixedWithUNC(path)) + { + return Directory.GetFiles(path); + } + else + { + return Directory.GetFiles(PrefixWithUNC(path)).Select(StripUNCPrefix).ToArray(); + } } public string[] GetFiles(string path, string searchPattern) { - return PathTooLongFuncWrapper(p => Directory.GetFiles(p, searchPattern), p => AlphaFS.Directory.GetFiles(p, searchPattern), path, false); + if (IsPrefixedWithUNC(path)) + { + return Directory.GetFiles(path, searchPattern); + } + else + { + return Directory.GetFiles(PrefixWithUNC(path), searchPattern).Select(StripUNCPrefix).ToArray(); + } } public DateTime GetCreationTimeUtc(string path) { - return PathTooLongFuncWrapper(Directory.GetCreationTimeUtc, AlphaFS.File.GetCreationTimeUtc, path, false); + return Directory.GetCreationTimeUtc(PrefixWithUNC(path)); } public DateTime GetLastWriteTimeUtc(string path) { - return PathTooLongFuncWrapper(Directory.GetLastWriteTimeUtc, AlphaFS.File.GetLastWriteTimeUtc, path, false); + return Directory.GetLastWriteTimeUtc(PrefixWithUNC(path)); } public IEnumerable EnumerateDirectories(string path) { - return PathTooLongFuncWrapper(Directory.EnumerateDirectories, AlphaFS.Directory.EnumerateDirectories, path, false); + if (IsPrefixedWithUNC(path)) + { + return Directory.EnumerateDirectories(path); + } + else + { + return Directory.EnumerateDirectories(PrefixWithUNC(path)).Select(StripUNCPrefix); + } } public void FileCopy(string source, string target, bool overwrite) { - // We do not check if path is too long on target. If so, then we catch an exception. - PathTooLongActionWrapper(p => File.Copy(p, target, overwrite), p => AlphaFS.File.Copy(p, target, overwrite), source, false); + File.Copy(PrefixWithUNC(source), PrefixWithUNC(target), overwrite); } public string PathGetFullPath(string path) { - return PathTooLongFuncWrapper(System.IO.Path.GetFullPath, AlphaFS.Path.GetFullPath, path, false); + if (IsPrefixedWithUNC(path)) + { + return System.IO.Path.GetFullPath(ConvertSlashes(path)); + } + else + { + return StripUNCPrefix(System.IO.Path.GetFullPath(PrefixWithUNC(ConvertSlashes(path)))); + } } public IFileEntry DirectoryEntry(string path) { - IFileEntry DirectoryEntryNative(string p) { - var dInfo = new DirectoryInfo(p); - return new FileEntry(dInfo.Name, 0, dInfo.LastAccessTime, dInfo.LastWriteTime) - { - IsFolder = true - }; - } - - IFileEntry DirectoryEntryAlphaFS(string p) + var dInfo = new DirectoryInfo(PrefixWithUNC(path)); + return new FileEntry(dInfo.Name, 0, dInfo.LastAccessTime, dInfo.LastWriteTime) { - var dInfoAlphaFS = new AlphaFS.DirectoryInfo(p); - return new FileEntry(dInfoAlphaFS.Name, 0, dInfoAlphaFS.LastAccessTime, dInfoAlphaFS.LastWriteTime) - { - IsFolder = true - }; - } - - return PathTooLongFuncWrapper(DirectoryEntryNative, DirectoryEntryAlphaFS, path, false); + IsFolder = true + }; } public IFileEntry FileEntry(string path) { - IFileEntry FileEntryNative(string p) - { - var fileInfo = new FileInfo(p); - return new FileEntry(fileInfo.Name, fileInfo.Length, fileInfo.LastAccessTime, fileInfo.LastWriteTime); - } - - IFileEntry FileEntryAlphaFS(string p) - { - var fInfoAlphaFS = new AlphaFS.FileInfo(p); - return new FileEntry(fInfoAlphaFS.Name, fInfoAlphaFS.Length, fInfoAlphaFS.LastAccessTime, fInfoAlphaFS.LastWriteTime); - } - - return PathTooLongFuncWrapper(FileEntryNative, FileEntryAlphaFS, path, false); + var fileInfo = new FileInfo(PrefixWithUNC(path)); + return new FileEntry(fileInfo.Name, fileInfo.Length, fileInfo.LastAccessTime, fileInfo.LastWriteTime); } public Dictionary GetMetadata(string path, bool isSymlink, bool followSymlink) @@ -562,31 +510,7 @@ namespace Duplicati.Library.Common.IO public string PathCombine(params string[] paths) { - var combinedPath = ""; - for (int i = 0; i < paths.Length; i++) - { - if (i == 0) - { - combinedPath = paths[i]; - } - else - { - if (!IsPathTooLong(combinedPath + "\\" + paths[i])) - { - try - { - combinedPath = Path.Combine(combinedPath, paths[i]); - } - catch (Exception ex) when (ex is System.IO.PathTooLongException || ex is System.ArgumentException) - { - //TODO: Explain why we need to keep prefixing and stripping UNC's. - combinedPath = StripUNCPrefix(Alphaleonis.Win32.Filesystem.Path.Combine(PrefixWithUNC(combinedPath), paths[i])); - } - } - } - } - - return combinedPath; + return Path.Combine(paths); } public void CreateSymlink(string symlinkfile, string target, bool asDir) diff --git a/Duplicati/Library/Main/Controller.cs b/Duplicati/Library/Main/Controller.cs index ea2c93dce..18669a994 100644 --- a/Duplicati/Library/Main/Controller.cs +++ b/Duplicati/Library/Main/Controller.cs @@ -25,6 +25,7 @@ using Duplicati.Library.Utility; using Duplicati.Library.Common.IO; using Duplicati.Library.Common; using Duplicati.Library.Interface; +using Duplicati.Library.Main.Database; namespace Duplicati.Library.Main { @@ -205,8 +206,6 @@ namespace Duplicati.Library.Main return RunAction(new ListRemoteResults(), (result) => { result.OperationProgressUpdater.UpdatePhase(OperationPhase.Delete_Listing); - using (var tf = System.IO.File.Exists(m_options.Dbpath) ? null : new Library.Utility.TempFile()) - using (var db = new Database.LocalDatabase(((string)tf) ?? m_options.Dbpath, "list-remote", true)) using (var bk = new BackendManager(m_backend, m_options, result.BackendWriter, null)) { // Only delete files that match the expected pattern and prefix @@ -216,6 +215,20 @@ namespace Duplicati.Library.Main .Where(x => x.Prefix == m_options.Prefix) .ToList(); + // If the local database is available, we will use it to avoid deleting unrelated files + // from the backend. Otherwise, we may accidentally delete non-Duplicati files, or + // files from a different Duplicati configuration that points to the same backend location + // and uses the same prefix (see issues #2678, #3845, and #4244). + if (System.IO.File.Exists(m_options.Dbpath)) + { + using (LocalDatabase db = new LocalDatabase(m_options.Dbpath, "list-remote", true)) + { + IEnumerable dbRemoteVolumes = db.GetRemoteVolumes(); + HashSet dbRemoteFiles = new HashSet(dbRemoteVolumes.Select(x => x.Name)); + list = list.Where(x => dbRemoteFiles.Contains(x.File.Name)).ToList(); + } + } + result.OperationProgressUpdater.UpdatePhase(OperationPhase.Delete_Deleting); result.OperationProgressUpdater.UpdateProgress(0); for (var i = 0; i < list.Count; i++) diff --git a/Duplicati/Library/Main/Database/LocalBackupDatabase.cs b/Duplicati/Library/Main/Database/LocalBackupDatabase.cs index dcccea279..10b8f737e 100644 --- a/Duplicati/Library/Main/Database/LocalBackupDatabase.cs +++ b/Duplicati/Library/Main/Database/LocalBackupDatabase.cs @@ -691,57 +691,55 @@ namespace Duplicati.Library.Main.Database public void AppendFilesFromPreviousSetWithPredicate(System.Data.IDbTransaction transaction, Func exclusionPredicate, long fileSetId, long prevFileSetId, DateTime timestamp) { - if (exclusionPredicate == null) - { - AppendFilesFromPreviousSet(transaction, null, fileSetId, prevFileSetId, timestamp); + if (exclusionPredicate == null) + { + AppendFilesFromPreviousSet(transaction, null, fileSetId, prevFileSetId, timestamp); return; } using (var cmd = m_connection.CreateCommand()) - using (var cmdAdd = m_connection.CreateCommand()) + using (var cmdDelete = m_connection.CreateCommand()) using (var tr = new TemporaryTransactionWrapper(m_connection, transaction)) { - var lastFilesetId = - prevFileSetId < 0 ? GetPreviousFilesetID(cmd, timestamp, fileSetId) : prevFileSetId; + long lastFilesetId = prevFileSetId < 0 ? GetPreviousFilesetID(cmd, timestamp, fileSetId) : prevFileSetId; - // prepare command for adding new entries - cmdAdd.Transaction = tr.Parent; - cmdAdd.CommandText = - @"INSERT INTO ""FilesetEntry"" (""FilesetID"", ""FileID"", ""Lastmodified"") VALUES (?, ?, ?)"; - cmdAdd.AddParameters(3); - cmdAdd.SetParameterValue(0, fileSetId); + // copy entries from previous file set into a temporary table, except those file IDs already added by the current backup + var tempFileSetTable = "FilesetEntry-" + Library.Utility.Utility.ByteArrayAsHexString(Guid.NewGuid().ToByteArray()); + cmd.Transaction = tr.Parent; + cmd.ExecuteNonQuery($@"CREATE TEMPORARY TABLE ""{tempFileSetTable}"" AS SELECT ""FileID"", ""Lastmodified"" FROM (SELECT DISTINCT ""FilesetID"", ""FileID"", ""Lastmodified"" FROM ""FilesetEntry"" WHERE ""FilesetID"" = ? AND ""FileID"" NOT IN (SELECT ""FileID"" FROM ""FilesetEntry"" WHERE ""FilesetID"" = ?))", lastFilesetId, fileSetId); - // enumerate files from previous set + // now we need to remove, from the above, any entries that were enumerated by the + // UNC-driven backup + cmdDelete.Transaction = tr.Parent; + cmdDelete.CommandText = $@"DELETE FROM ""{tempFileSetTable}"" WHERE ""FileID"" = ?"; + cmdDelete.AddParameters(1); + + // enumerate files from new temporary file set, and remove any entries handled by UNC cmd.Transaction = tr.Parent; foreach (var row in cmd.ExecuteReaderEnumerable( - @"SELECT - f.""Path"", fs.""FileID"", fs.""Lastmodified"", COALESCE(bs.""Length"", -1) - FROM ( SELECT DISTINCT ""FileID"", ""Lastmodified"" - FROM ""FilesetEntry"" - WHERE ""FilesetID"" = ? - AND ""FileID"" NOT IN ( - SELECT ""FileID"" - FROM ""FilesetEntry"" - WHERE ""FilesetID"" = ? - )) AS fs + $@"SELECT f.""Path"", fs.""FileID"", fs.""Lastmodified"", COALESCE(bs.""Length"", -1) + FROM (SELECT DISTINCT ""FileID"", ""Lastmodified"" FROM ""{tempFileSetTable}"") AS fs LEFT JOIN ""File"" AS f ON fs.""FileID"" = f.""ID"" - LEFT JOIN ""Blockset"" AS bs ON f.""BlocksetID"" = bs.""ID"";", - lastFilesetId, fileSetId)) + LEFT JOIN ""Blockset"" AS bs ON f.""BlocksetID"" = bs.""ID"";")) { var path = row.GetString(0); var size = row.GetInt64(3); - if (!exclusionPredicate(path, size)) + + if (exclusionPredicate(path, size)) { - cmdAdd.SetParameterValue(1, row.GetInt64(1)); - cmdAdd.SetParameterValue(2, row.GetInt64(2)); - cmdAdd.ExecuteNonQuery(); + cmdDelete.SetParameterValue(0, row.GetInt64(1)); + cmdDelete.ExecuteNonQuery(); } } - tr.Commit(); + // now copy the temporary table into the FileSetEntry table + cmd.ExecuteNonQuery($@"INSERT INTO ""FilesetEntry"" (""FilesetID"", ""FileID"", ""Lastmodified"") + SELECT ?, ""FileID"", ""Lastmodified"" FROM ""{tempFileSetTable}""", fileSetId); + + tr.Commit(); } - } - + } + /// /// Creates a timestamped backup operation to correctly associate the fileset with the time it was created. /// diff --git a/Duplicati/Library/Main/Database/LocalDatabase.cs b/Duplicati/Library/Main/Database/LocalDatabase.cs index 9a11f11a2..b28a96833 100644 --- a/Duplicati/Library/Main/Database/LocalDatabase.cs +++ b/Duplicati/Library/Main/Database/LocalDatabase.cs @@ -1199,7 +1199,7 @@ ORDER BY var args = new List(); foreach (var f in ((Library.Utility.FilterExpression)filter).GetSimpleList()) { - if (f.Contains('*') || f.Contains('?')) + if (type == FilterType.Wildcard) { sb.Append(@"""Path"" LIKE ? OR "); args.Add(f.Replace('*', '%').Replace('?', '_')); diff --git a/Duplicati/Library/Main/Database/LocalDeleteDatabase.cs b/Duplicati/Library/Main/Database/LocalDeleteDatabase.cs index 7ff65885b..f97d5ff4d 100644 --- a/Duplicati/Library/Main/Database/LocalDeleteDatabase.cs +++ b/Duplicati/Library/Main/Database/LocalDeleteDatabase.cs @@ -102,10 +102,14 @@ namespace Duplicati.Library.Main.Database if (deleted != updated) throw new Exception(string.Format("Unexpected number of remote volumes marked as deleted. Found {0} filesets, but {1} volumes", deleted, updated)); - + using (var rd = cmd.ExecuteReader(@"SELECT ""Name"", ""Size"" FROM ""RemoteVolume"" WHERE ""Type"" = ? AND ""State"" = ? ", RemoteVolumeType.Files.ToString(), RemoteVolumeState.Deleting.ToString())) - while (rd.Read()) - yield return new KeyValuePair(rd.GetString(0), rd.ConvertValueToInt64(1)); + { + while (rd.Read()) + { + yield return new KeyValuePair(rd.GetString(0), rd.ConvertValueToInt64(1)); + } + } } } diff --git a/Duplicati/Library/Main/Database/LocalRecreateDatabase.cs b/Duplicati/Library/Main/Database/LocalRecreateDatabase.cs index ea31137df..53db0845f 100644 --- a/Duplicati/Library/Main/Database/LocalRecreateDatabase.cs +++ b/Duplicati/Library/Main/Database/LocalRecreateDatabase.cs @@ -607,12 +607,12 @@ DELETE FROM ""RemoteVolume"" WHERE ""Type"" = ""{RemoteVolumeType.Blocks}"" AND if (m_tempsmalllist != null) try - { - cmd.CommandText = string.Format(@"DROP TABLE IF EXISTS ""{0}""", m_tempsmalllist); - cmd.ExecuteNonQuery(); - } - catch { } - finally { m_tempsmalllist = null; } + { + cmd.CommandText = string.Format(@"DROP TABLE IF EXISTS ""{0}""", m_tempsmalllist); + cmd.ExecuteNonQuery(); + } + catch { } + finally { m_tempsmalllist = null; } } diff --git a/Duplicati/Library/Main/Database/LocalRestoreDatabase.cs b/Duplicati/Library/Main/Database/LocalRestoreDatabase.cs index bc17a8596..2f3d9d3f9 100644 --- a/Duplicati/Library/Main/Database/LocalRestoreDatabase.cs +++ b/Duplicati/Library/Main/Database/LocalRestoreDatabase.cs @@ -1241,11 +1241,20 @@ namespace Duplicati.Library.Main.Database public IEnumerable GetFilesAndSourceBlocksFast(long blocksize) { + var latestBlockTable = "LatestBlocksetIds-" + m_temptabsetguid; var whereclause = string.Format(@" ""{0}"".""ID"" = ""{1}"".""FileID"" AND ""{1}"".""Restored"" = 0 AND ""{1}"".""Metadata"" = 0 AND ""{0}"".""TargetPath"" != ""{0}"".""Path"" ", m_tempfiletable, m_tempblocktable); - var sourepaths = string.Format(@"SELECT DISTINCT ""{0}"".""Path"" FROM ""{0}"", ""{1}"" WHERE " + whereclause, m_tempfiletable, m_tempblocktable); - var latestBlocksetIds = @"SELECT ""File"".""Path"", ""File"".""BlocksetID"", MAX(""Fileset"".""Timestamp"") FROM ""Fileset"", ""FilesetEntry"", ""File"" WHERE ""FilesetEntry"".""FileID"" = ""File"".""ID"" AND ""FilesetEntry"".""FilesetID"" = ""Fileset"".""ID"" AND ""File"".""Path"" IN (" + sourepaths + @") GROUP BY ""File"".""Path"" "; - var sources = string.Format(@"SELECT DISTINCT ""{0}"".""TargetPath"", ""{0}"".""Path"", ""{0}"".""ID"", ""{1}"".""Index"", ""{1}"".""Hash"", ""{1}"".""Size"" FROM ""{0}"", ""{1}"", ""File"", (" + latestBlocksetIds + @") S, ""Block"", ""BlocksetEntry"" WHERE ""BlocksetEntry"".""BlocksetID"" = ""S"".""BlocksetID"" AND ""BlocksetEntry"".""BlocksetID"" = ""File"".""BlocksetID"" AND ""BlocksetEntry"".""BlockID"" = ""Block"".""ID"" AND ""{1}"".""Index"" = ""BlocksetEntry"".""Index"" AND ""{1}"".""Hash"" = ""Block"".""Hash"" AND ""{1}"".""Size"" = ""Block"".""Size"" AND ""S"".""Path"" = ""{0}"".""Path"" AND " + whereclause + @" ORDER BY ""{0}"".""ID"", ""{1}"".""Index"" ", m_tempfiletable, m_tempblocktable); - using(var cmd = m_connection.CreateCommand()) + var sourcePaths = string.Format(@"SELECT DISTINCT ""{0}"".""Path"" FROM ""{0}"", ""{1}"" WHERE " + whereclause, m_tempfiletable, m_tempblocktable); + var sources = string.Format(@"SELECT DISTINCT ""{0}"".""TargetPath"", ""{0}"".""Path"", ""{0}"".""ID"", ""{1}"".""Index"", ""{1}"".""Hash"", ""{1}"".""Size"" FROM ""{0}"", ""{1}"", ""{2}"" S, ""Block"", ""BlocksetEntry"" WHERE ""BlocksetEntry"".""BlocksetID"" = ""S"".""BlocksetID"" AND ""BlocksetEntry"".""BlockID"" = ""Block"".""ID"" AND ""{1}"".""Hash"" = ""Block"".""Hash"" AND ""{1}"".""Size"" = ""Block"".""Size"" AND ""S"".""Path"" = ""{0}"".""Path"" AND ""{1}"".""Index"" = ""BlocksetEntry"".""Index"" AND " + whereclause + @" ORDER BY ""{0}"".""ID"", ""{1}"".""Index"" ", m_tempfiletable, m_tempblocktable, latestBlockTable); + var latestBlocksetIds = @"SELECT ""File"".""Path"", ""File"".""BlocksetID"", MAX(""Fileset"".""Timestamp"") FROM ""Fileset"", ""FilesetEntry"", ""File"" WHERE ""FilesetEntry"".""FileID"" = ""File"".""ID"" AND ""FilesetEntry"".""FilesetID"" = ""Fileset"".""ID"" AND ""File"".""Path"" IN (" + sourcePaths + @") GROUP BY ""File"".""Path"" "; + + using (var cmd = m_connection.CreateCommand()) + { + cmd.ExecuteNonQuery(string.Format(@"DROP TABLE IF EXISTS ""{0}"" ", latestBlockTable)); + cmd.ExecuteNonQuery(string.Format(@"CREATE TEMPORARY TABLE ""{0}"" AS {1}", latestBlockTable, latestBlocksetIds)); + } + + + using (var cmd = m_connection.CreateCommand()) using(var rd = cmd.ExecuteReader(sources)) { if (rd.Read()) diff --git a/Duplicati/Library/Main/Operation/Backup/BackendUploader.cs b/Duplicati/Library/Main/Operation/Backup/BackendUploader.cs index b7639fa6c..307d60a52 100644 --- a/Duplicati/Library/Main/Operation/Backup/BackendUploader.cs +++ b/Duplicati/Library/Main/Operation/Backup/BackendUploader.cs @@ -39,12 +39,12 @@ namespace Duplicati.Library.Main.Operation.Backup { public Task LastWriteSizeAsync { get { return m_tcs.Task; } } private readonly TaskCompletionSource m_tcs = new TaskCompletionSource(); - + public void SetFlushed(long size) { m_tcs.TrySetResult(size); } - + public void TrySetCanceled() { m_tcs.TrySetCanceled(); @@ -108,7 +108,6 @@ namespace Duplicati.Library.Main.Operation.Backup private readonly StatsCollector m_stats; private readonly ITaskReader m_taskReader; - public BackendUploader(Func backendFactory, Options options, DatabaseCommon database, ITaskReader taskReader, StatsCollector stats) { m_backendFactory = backendFactory; @@ -135,6 +134,7 @@ namespace Duplicati.Library.Main.Operation.Backup var uploadsInProgress = 0; m_cancelTokenSource = new CancellationTokenSource(); m_progressUpdater.Run(m_cancelTokenSource.Token); + var filesetsToUpload = new List(); try { @@ -145,12 +145,7 @@ namespace Duplicati.Library.Main.Operation.Backup if (!await m_taskReader.ProgressAsync) break; - var worker = workers.FirstOrDefault(w => w.Task.IsCompleted && !w.Task.IsFaulted); - if (worker == null) - { - worker = new Worker(m_backendFactory()); - workers.Add(worker); - } + Worker worker = GetWorker(workers); if (req is VolumeUploadRequest volumeUpload) { @@ -164,8 +159,7 @@ namespace Duplicati.Library.Main.Operation.Backup } else if (req is FilesetUploadRequest filesetUpload) { - worker.Task = Task.Run(() => UploadVolumeWriter(filesetUpload.Fileset, worker, m_cancelTokenSource.Token)); - uploadsInProgress++; + filesetsToUpload.Add(filesetUpload); } else if (req is IndexVolumeUploadRequest indexUpload) { @@ -174,20 +168,10 @@ namespace Duplicati.Library.Main.Operation.Backup } else if (req is FlushRequest flush) { - while (workers.Any()) - { - Task finishedTask = await Task.WhenAny(workers.Select(w => w.Task)).ConfigureAwait(false); - if (finishedTask.IsFaulted) - { - flush.TrySetCanceled(); - ExceptionDispatchInfo.Capture(finishedTask.Exception).Throw(); - } + await WaitForWorkers(workers, flush).ConfigureAwait(false); + UploadFilesets(workers, filesetsToUpload); + await WaitForWorkers(workers, flush).ConfigureAwait(false); - Worker finishedWorker = workers.Single(w => w.Task == finishedTask); - workers.Remove(finishedWorker); - finishedWorker.Dispose(); - } - flush.SetFlushed(lastSize); uploadsInProgress = 0; break; @@ -240,6 +224,46 @@ namespace Duplicati.Library.Main.Operation.Backup }); } + private Worker GetWorker(List workers) + { + var worker = workers.FirstOrDefault(w => w.Task.IsCompleted && !w.Task.IsFaulted); + if (worker == null) + { + worker = new Worker(m_backendFactory()); + workers.Add(worker); + } + + return worker; + } + + private async Task WaitForWorkers(List workers, FlushRequest flush) + { + while (workers.Any()) + { + Task finishedTask = await Task.WhenAny(workers.Select(w => w.Task)).ConfigureAwait(false); + if (finishedTask.IsFaulted) + { + flush.TrySetCanceled(); + ExceptionDispatchInfo.Capture(finishedTask.Exception).Throw(); + } + + Worker finishedWorker = workers.Single(w => w.Task == finishedTask); + workers.Remove(finishedWorker); + finishedWorker.Dispose(); + } + } + + private void UploadFilesets(List workers, List filesets) + { + foreach (var fileset in filesets) + { + Worker worker = GetWorker(workers); + worker.Task = Task.Run(() => UploadVolumeWriter(fileset.Fileset, worker, m_cancelTokenSource.Token)); + } + + filesets.Clear(); + } + private static Exception GetInnerMostException(Exception ex) { while (ex.InnerException != null) @@ -481,4 +505,4 @@ namespace Duplicati.Library.Main.Operation.Backup } } } -} +} \ No newline at end of file diff --git a/Duplicati/Library/Main/Operation/Backup/CountFilesHandler.cs b/Duplicati/Library/Main/Operation/Backup/CountFilesHandler.cs index 919e5d28f..59d4e999e 100644 --- a/Duplicati/Library/Main/Operation/Backup/CountFilesHandler.cs +++ b/Duplicati/Library/Main/Operation/Backup/CountFilesHandler.cs @@ -20,20 +20,21 @@ using System.Collections.Generic; using System.Threading.Tasks; using Duplicati.Library.Utility; +using Duplicati.Library.Snapshots; using CoCoL; namespace Duplicati.Library.Main.Operation.Backup { internal static class CountFilesHandler { - public static async Task Run(IEnumerable sources, Snapshots.ISnapshotService snapshot, BackupResults result, Options options, IFilter sourcefilter, IFilter filter, Common.ITaskReader taskreader, System.Threading.CancellationToken token) + public static async Task Run(IEnumerable sources, Snapshots.ISnapshotService snapshot, UsnJournalService journalService, BackupResults result, Options options, IFilter sourcefilter, IFilter filter, Common.ITaskReader taskreader, System.Threading.CancellationToken token) { // Make sure we create the enumeration process in a separate scope, // but keep the log channel from the parent scope using(Logging.Log.StartIsolatingScope(true)) using (new IsolatedChannelScope()) { - var enumeratorTask = Backup.FileEnumerationProcess.Run(sources, snapshot, null, options.FileAttributeFilter, sourcefilter, filter, options.SymlinkPolicy, options.HardlinkPolicy, options.ExcludeEmptyFolders, options.IgnoreFilenames, options.ChangedFilelist, taskreader, token); + var enumeratorTask = Backup.FileEnumerationProcess.Run(sources, snapshot, journalService, options.FileAttributeFilter, sourcefilter, filter, options.SymlinkPolicy, options.HardlinkPolicy, options.ExcludeEmptyFolders, options.IgnoreFilenames, options.ChangedFilelist, taskreader, token); var counterTask = AutomationExtensions.RunTask(new { Input = Backup.Channels.SourcePaths.ForRead diff --git a/Duplicati/Library/Main/Operation/Backup/MetadataPreProcess.cs b/Duplicati/Library/Main/Operation/Backup/MetadataPreProcess.cs index bc72b7be7..a9e598d07 100644 --- a/Duplicati/Library/Main/Operation/Backup/MetadataPreProcess.cs +++ b/Duplicati/Library/Main/Operation/Backup/MetadataPreProcess.cs @@ -179,8 +179,17 @@ namespace Duplicati.Library.Main.Operation.Backup // Not all reparse points are symlinks. // For example, on Windows 10 Fall Creator's Update, the OneDrive folder (and all subfolders) // are reparse points, which allows the folder to hook into the OneDrive service and download things on-demand. - // If we can't find a symlink target for the current path, we won't treat it as a symlink. - string symlinkTarget = snapshot.GetSymlinkTarget(path); + // If we can't find a symlink target for the current path, we won't treat it as a symlink. + string symlinkTarget = null; + try + { + symlinkTarget = snapshot.GetSymlinkTarget(path); + } + catch (Exception ex) + { + Logging.Log.WriteExplicitMessage(FILELOGTAG, "SymlinkTargetReadFailure", ex, "Failed to read symlink target for path: {0}", path); + } + if (!string.IsNullOrWhiteSpace(symlinkTarget)) { if (options.SymlinkPolicy == Options.SymlinkStrategy.Ignore) diff --git a/Duplicati/Library/Main/Operation/Backup/StreamBlockSplitter.cs b/Duplicati/Library/Main/Operation/Backup/StreamBlockSplitter.cs index 84f4a16d6..c85a2c5a8 100644 --- a/Duplicati/Library/Main/Operation/Backup/StreamBlockSplitter.cs +++ b/Duplicati/Library/Main/Operation/Backup/StreamBlockSplitter.cs @@ -51,145 +51,161 @@ namespace Duplicati.Library.Main.Operation.Backup var filehasher = HashAlgorithm.Create(options.FileHashAlgorithm); var blockhasher = HashAlgorithm.Create(options.BlockHashAlgorithm); var emptymetadata = Utility.WrapMetadata(new Dictionary(), options); - var maxmetadatasize = (options.Blocksize / (long)options.BlockhashSize) * options.Blocksize; + var maxmetadatasize = (options.Blocksize / (long) options.BlockhashSize) * options.Blocksize; - if (blockhasher == null) + if (blockhasher == null) throw new UserInformationException(Strings.Common.InvalidHashAlgorithm(options.BlockHashAlgorithm), "BlockHashAlgorithmNotSupported"); - if (filehasher == null) + if (filehasher == null) throw new UserInformationException(Strings.Common.InvalidHashAlgorithm(options.FileHashAlgorithm), "FileHashAlgorithmNotSupported"); - - if (!blockhasher.CanReuseTransform) + + if (!blockhasher.CanReuseTransform) throw new UserInformationException(Strings.Common.InvalidCryptoSystem(options.BlockHashAlgorithm), "BlockHashAlgorithmNotSupported"); - if (!filehasher.CanReuseTransform) + if (!filehasher.CanReuseTransform) throw new UserInformationException(Strings.Common.InvalidCryptoSystem(options.FileHashAlgorithm), "FileHashAlgorithmNotSupported"); using (var empty_metadata_stream = new MemoryStream(emptymetadata.Blob)) - while (await taskreader.ProgressAsync) { - var send_close = false; - var filesize = 0L; - - var e = await self.Input.ReadAsync(); - var cur = e.Result; - - try + while (await taskreader.ProgressAsync) { - var stream = e.Stream; + var send_close = false; + var filesize = 0L; - using (var blocklisthashes = new Library.Utility.FileBackedStringList()) - using (var hashcollector = new Library.Utility.FileBackedStringList()) - { - var blocklistbuffer = new byte[blocksize]; - var blocklistoffset = 0L; + var e = await self.Input.ReadAsync(); + var cur = e.Result; - long fslen = -1; - try { fslen = stream.Length; } - catch (Exception ex) { Logging.Log.WriteWarningMessage(FILELOGTAG, "FileLengthFailure", ex, "Failed to read file length for file {0}", e.Path); } - - if (e.IsMetadata && fslen > maxmetadatasize) - { - //TODO: To fix this, the "WriteFileset" method in BackupHandler needs to - // be updated such that it can select sets even when there are multiple - // blocklist hashes for the metadata. - // This could be done such that an extra query is made if the metadata - // spans multiple blocklist hashes, as it is not expected to be common - - Logging.Log.WriteWarningMessage(LOGTAG, "TooLargeMetadata", null, "Metadata size is {0}, but the largest accepted size is {1}, recording empty metadata for {2}", fslen, maxmetadatasize, e.Path); - empty_metadata_stream.Position = 0; - stream = empty_metadata_stream; - fslen = stream.Length; - } - - // Don't send progress reports for metadata - if (!e.IsMetadata) - { - await self.ProgressChannel.WriteAsync(new ProgressEvent() { Filepath = e.Path, Length = fslen, Type = EventType.FileStarted }); - send_close = true; - } - - filehasher.Initialize(); - var lastread = 0; - var buf = new byte[blocksize]; - var lastupdate = DateTime.Now; - - // Core processing loop, read blocks of data and hash individually - while (((lastread = await stream.ForceStreamReadAsync(buf, blocksize)) != 0)) - { - // Run file hashing concurrently to squeeze a little extra concurrency out of it - var pftask = Task.Run(() => filehasher.TransformBlock(buf, 0, lastread, buf, 0)); - - var hashdata = blockhasher.ComputeHash(buf, 0, lastread); - var hashkey = Convert.ToBase64String(hashdata); - - // If we have too many hashes, flush the blocklist - if (blocklistbuffer.Length - blocklistoffset < hashdata.Length) - { - var blkey = Convert.ToBase64String(blockhasher.ComputeHash(blocklistbuffer, 0, (int)blocklistoffset)); - blocklisthashes.Add(blkey); - await DataBlock.AddBlockToOutputAsync(self.BlockOutput, blkey, blocklistbuffer, 0, blocklistoffset, CompressionHint.Noncompressible, true); - blocklistoffset = 0; - blocklistbuffer = new byte[blocksize]; - } - - // Store the current hash in the blocklist - Array.Copy(hashdata, 0, blocklistbuffer, blocklistoffset, hashdata.Length); - blocklistoffset += hashdata.Length; - hashcollector.Add(hashkey); - filesize += lastread; - - // Don't spam updates - if (send_close && (DateTime.Now - lastupdate).TotalSeconds > 5) - { - await self.ProgressChannel.WriteAsync(new ProgressEvent() { Filepath = e.Path, Length = filesize, Type = EventType.FileProgressUpdate }); - lastupdate = DateTime.Now; - } - - // Make sure the filehasher is done with the buf instance before we pass it on - await pftask.ConfigureAwait(false); - await DataBlock.AddBlockToOutputAsync(self.BlockOutput, hashkey, buf, 0, lastread, e.Hint, false); - buf = new byte[blocksize]; - } - - // If we have more than a single block of data, output the (trailing) blocklist - if (hashcollector.Count > 1) - { - var blkey = Convert.ToBase64String(blockhasher.ComputeHash(blocklistbuffer, 0, (int)blocklistoffset)); - blocklisthashes.Add(blkey); - await DataBlock.AddBlockToOutputAsync(self.BlockOutput, blkey, blocklistbuffer, 0, blocklistoffset, CompressionHint.Noncompressible, true); - } - - filehasher.TransformFinalBlock(new byte[0], 0, 0); - var filehash = Convert.ToBase64String(filehasher.Hash); - var blocksetid = await database.AddBlocksetAsync(filehash, filesize, blocksize, hashcollector, blocklisthashes); - cur.SetResult(new StreamProcessResult() { Streamlength = filesize, Streamhash = filehash, Blocksetid = blocksetid }); - cur = null; - } - } - catch (Exception ex) - { try { - if (cur != null) - cur.TrySetException(ex); - } - catch { } + var stream = e.Stream; - // Rethrow - if (ex.IsRetiredException()) - throw; - } - finally - { - if (cur != null) + using (var blocklisthashes = new Library.Utility.FileBackedStringList()) + using (var hashcollector = new Library.Utility.FileBackedStringList()) + { + var blocklistbuffer = new byte[blocksize]; + var blocklistoffset = 0L; + + long fslen = -1; + try + { + fslen = stream.Length; + } + catch (Exception ex) + { + Logging.Log.WriteWarningMessage(FILELOGTAG, "FileLengthFailure", ex, "Failed to read file length for file {0}", e.Path); + } + + if (e.IsMetadata && fslen > maxmetadatasize) + { + //TODO: To fix this, the "WriteFileset" method in BackupHandler needs to + // be updated such that it can select sets even when there are multiple + // blocklist hashes for the metadata. + // This could be done such that an extra query is made if the metadata + // spans multiple blocklist hashes, as it is not expected to be common + + Logging.Log.WriteWarningMessage(LOGTAG, "TooLargeMetadata", null, "Metadata size is {0}, but the largest accepted size is {1}, recording empty metadata for {2}", fslen, maxmetadatasize, e.Path); + empty_metadata_stream.Position = 0; + stream = empty_metadata_stream; + fslen = stream.Length; + } + + // Don't send progress reports for metadata + if (!e.IsMetadata) + { + await self.ProgressChannel.WriteAsync(new ProgressEvent() {Filepath = e.Path, Length = fslen, Type = EventType.FileStarted}); + send_close = true; + } + + filehasher.Initialize(); + var lastread = 0; + var buf = new byte[blocksize]; + var lastupdate = DateTime.Now; + + // Core processing loop, read blocks of data and hash individually + while (((lastread = await stream.ForceStreamReadAsync(buf, blocksize)) != 0)) + { + // Run file hashing concurrently to squeeze a little extra concurrency out of it + var pftask = Task.Run(() => filehasher.TransformBlock(buf, 0, lastread, buf, 0)); + + var hashdata = blockhasher.ComputeHash(buf, 0, lastread); + var hashkey = Convert.ToBase64String(hashdata); + + // If we have too many hashes, flush the blocklist + if (blocklistbuffer.Length - blocklistoffset < hashdata.Length) + { + var blkey = Convert.ToBase64String(blockhasher.ComputeHash(blocklistbuffer, 0, (int) blocklistoffset)); + blocklisthashes.Add(blkey); + await DataBlock.AddBlockToOutputAsync(self.BlockOutput, blkey, blocklistbuffer, 0, blocklistoffset, CompressionHint.Noncompressible, true); + blocklistoffset = 0; + blocklistbuffer = new byte[blocksize]; + } + + // Store the current hash in the blocklist + Array.Copy(hashdata, 0, blocklistbuffer, blocklistoffset, hashdata.Length); + blocklistoffset += hashdata.Length; + hashcollector.Add(hashkey); + filesize += lastread; + + // Don't spam updates + if (send_close && (DateTime.Now - lastupdate).TotalSeconds > 5) + { + await self.ProgressChannel.WriteAsync(new ProgressEvent() {Filepath = e.Path, Length = filesize, Type = EventType.FileProgressUpdate}); + lastupdate = DateTime.Now; + } + + // Make sure the filehasher is done with the buf instance before we pass it on + await pftask.ConfigureAwait(false); + await DataBlock.AddBlockToOutputAsync(self.BlockOutput, hashkey, buf, 0, lastread, e.Hint, false); + buf = new byte[blocksize]; + } + + // If we have more than a single block of data, output the (trailing) blocklist + if (hashcollector.Count > 1) + { + var blkey = Convert.ToBase64String(blockhasher.ComputeHash(blocklistbuffer, 0, (int) blocklistoffset)); + blocklisthashes.Add(blkey); + await DataBlock.AddBlockToOutputAsync(self.BlockOutput, blkey, blocklistbuffer, 0, blocklistoffset, CompressionHint.Noncompressible, true); + } + + filehasher.TransformFinalBlock(new byte[0], 0, 0); + var filehash = Convert.ToBase64String(filehasher.Hash); + var blocksetid = await database.AddBlocksetAsync(filehash, filesize, blocksize, hashcollector, blocklisthashes); + cur.SetResult(new StreamProcessResult() {Streamlength = filesize, Streamhash = filehash, Blocksetid = blocksetid}); + cur = null; + } + } + catch (Exception ex) { - try { cur.TrySetCanceled(); } - catch { } - cur = null; - } + try + { + if (cur != null) + cur.TrySetException(ex); + } + catch + { + } - if (send_close) - await self.ProgressChannel.WriteAsync(new ProgressEvent() { Filepath = e.Path, Length = filesize, Type = EventType.FileClosed }); - send_close = false; + // Rethrow + if (ex.IsRetiredException()) + throw; + } + finally + { + if (cur != null) + { + try + { + cur.TrySetCanceled(); + } + catch + { + } + + cur = null; + } + + if (send_close) + await self.ProgressChannel.WriteAsync(new ProgressEvent() {Filepath = e.Path, Length = filesize, Type = EventType.FileClosed}); + send_close = false; + } } } }); diff --git a/Duplicati/Library/Main/Operation/BackupHandler.cs b/Duplicati/Library/Main/Operation/BackupHandler.cs index 8a4bd2768..f78246cdd 100644 --- a/Duplicati/Library/Main/Operation/BackupHandler.cs +++ b/Duplicati/Library/Main/Operation/BackupHandler.cs @@ -442,17 +442,6 @@ namespace Duplicati.Library.Main.Operation { try { - // Start parallel scan, or use the database - if (m_options.DisableFileScanner) - { - var d = m_database.GetLastBackupFileCountAndSize(); - m_result.OperationProgressUpdater.UpdatefileCount(d.Item1, d.Item2, true); - } - else - { - parallelScanner = Backup.CountFilesHandler.Run(sources, snapshot, m_result, m_options, m_sourceFilter, m_filter, m_result.TaskReader, counterToken.Token); - } - // Make sure the database is sane await db.VerifyConsistencyAsync(m_options.Blocksize, m_options.BlockhashSize, !m_options.DisableFilelistConsistencyChecks); @@ -505,6 +494,17 @@ namespace Duplicati.Library.Main.Operation // create USN-based scanner if enabled var journalService = GetJournalService(sources, snapshot, filter, lastfilesetid); + // Start parallel scan, or use the database + if (m_options.DisableFileScanner) + { + var d = m_database.GetLastBackupFileCountAndSize(); + m_result.OperationProgressUpdater.UpdatefileCount(d.Item1, d.Item2, true); + } + else + { + parallelScanner = Backup.CountFilesHandler.Run(sources, snapshot, journalService, m_result, m_options, m_sourceFilter, m_filter, m_result.TaskReader, counterToken.Token); + } + // Run the backup operation if (await m_result.TaskReader.ProgressAsync) { diff --git a/Duplicati/Library/Main/Operation/DeleteHandler.cs b/Duplicati/Library/Main/Operation/DeleteHandler.cs index bb1cc8f88..c3693a1fe 100644 --- a/Duplicati/Library/Main/Operation/DeleteHandler.cs +++ b/Duplicati/Library/Main/Operation/DeleteHandler.cs @@ -281,7 +281,6 @@ namespace Duplicati.Library.Main.Operation /// /// Remove backups according to the --retention-policy option. - /// Backups that are not within any of the specified time frames will will NOT be deleted. /// Partial backups are not removed. /// public class RetentionPolicyRemover : FilesetRemover diff --git a/Duplicati/Library/Main/Operation/PurgeBrokenFilesHandler.cs b/Duplicati/Library/Main/Operation/PurgeBrokenFilesHandler.cs index 7458477c1..e90a44070 100644 --- a/Duplicati/Library/Main/Operation/PurgeBrokenFilesHandler.cs +++ b/Duplicati/Library/Main/Operation/PurgeBrokenFilesHandler.cs @@ -85,6 +85,11 @@ namespace Duplicati.Library.Main.Operation SetCount = db.GetFilesetFileCount(x.Item2, tr) }).ToArray(); + if (m_options.Dryrun) + tr.Rollback(); + else + tr.Commit(); + var fully_emptied = compare_list.Where(x => x.RemoveCount == x.SetCount).ToArray(); var to_purge = compare_list.Where(x => x.RemoveCount != x.SetCount).ToArray(); @@ -167,11 +172,6 @@ namespace Duplicati.Library.Main.Operation } } - if (m_options.Dryrun) - tr.Rollback(); - else - tr.Commit(); - m_result.OperationProgressUpdater.UpdateProgress(0.95f); if (!m_options.Dryrun && db.RepairInProgress) diff --git a/Duplicati/Library/Main/Operation/RestoreHandler.cs b/Duplicati/Library/Main/Operation/RestoreHandler.cs index 041439b10..6065e90cc 100644 --- a/Duplicati/Library/Main/Operation/RestoreHandler.cs +++ b/Duplicati/Library/Main/Operation/RestoreHandler.cs @@ -413,11 +413,27 @@ namespace Duplicati.Library.Main.Operation throw; } + var fileErrors = 0L; + // Restore empty files. They might not have any blocks so don't appear in any volume. - foreach (var file in database.GetFilesToRestore(true).Where(item => item.Length == 0)) { - // Just create the file and close it right away, empty statement is intentional. - using (SystemIO.IO_OS.FileCreate(file.Path)) + foreach (var file in database.GetFilesToRestore(true).Where(item => item.Length == 0)) + { + Logging.Log.WriteVerboseMessage(LOGTAG, "RestoreEmptyFile", "Restoring empty file \"{0}\"", file.Path); + + try { + SystemIO.IO_OS.DirectoryCreate(SystemIO.IO_OS.PathGetDirectoryName(file.Path)); + // Just create the file and close it right away, empty statement is intentional. + using (SystemIO.IO_OS.FileCreate(file.Path)) + { + } + } + catch (Exception ex) + { + fileErrors++; + Logging.Log.WriteErrorMessage(LOGTAG, "RestoreFileFailed", ex, "Failed to restore empty file: \"{0}\". Error message was: {1}", file.Path, ex.Message); + if (ex is System.Threading.ThreadAbortException) + throw; } } @@ -435,8 +451,6 @@ namespace Duplicati.Library.Main.Operation return; m_result.OperationProgressUpdater.UpdatePhase(OperationPhase.Restore_PostRestoreVerify); - - var fileErrors = 0L; if (m_options.PerformRestoredFileVerification) { @@ -470,7 +484,7 @@ namespace Duplicati.Library.Main.Operation catch (Exception ex) { fileErrors++; - Logging.Log.WriteErrorMessage(LOGTAG, "RestoreFileFailed", ex, ex.Message); + Logging.Log.WriteErrorMessage(LOGTAG, "RestoreFileFailed", ex, "Failed to restore file: \"{0}\". Error message was: {1}", file.Path, ex.Message); if (ex is System.Threading.ThreadAbortException) throw; } @@ -734,8 +748,8 @@ namespace Duplicati.Library.Main.Operation } } - if ((++updateCount) % 20 == 0) - blockmarker.UpdateProcessed(result.OperationProgressUpdater); + if ((++updateCount) % 20 == 0) + blockmarker.UpdateProcessed(result.OperationProgressUpdater); } catch (Exception ex) { diff --git a/Duplicati/Library/Main/ProcessController.cs b/Duplicati/Library/Main/ProcessController.cs index cda5f0866..c3f301913 100755 --- a/Duplicati/Library/Main/ProcessController.cs +++ b/Duplicati/Library/Main/ProcessController.cs @@ -208,12 +208,14 @@ namespace Duplicati.Library.Main if (string.Equals(ioclass, "idle", StringComparison.OrdinalIgnoreCase)) { m_originalNiceClass = 3; + // Only allowed for "best-effort" and "realtime" m_originalNiceLevel = -1; } else if (string.Equals(ioclass, "none", StringComparison.OrdinalIgnoreCase)) { m_originalNiceClass = 0; - m_originalNiceLevel = int.Parse(results.Last()); + // Only allowed for "best-effort" and "realtime" + m_originalNiceLevel = -1; } else if (string.Equals(ioclass, "best-effort", StringComparison.OrdinalIgnoreCase)) { diff --git a/Duplicati/Library/Main/Strings.cs b/Duplicati/Library/Main/Strings.cs index 462be3566..6208c40fe 100644 --- a/Duplicati/Library/Main/Strings.cs +++ b/Duplicati/Library/Main/Strings.cs @@ -10,7 +10,7 @@ namespace Duplicati.Library.Main.Strings public static string DeprecatedOptionUsedWarning(string optionname, string message) { return LC.L(@"The option {0} is deprecated: {1}", optionname, message); } public static string DuplicateOptionNameWarning(string optionname) { return LC.L(@"The option --{0} exists more than once, please report this to the developers", optionname); } public static string NoSourceFoldersError { get { return LC.L(@"No source folders specified for backup"); } } - public static string SourceIsMissingError(string foldername) { return LC.L(@"The source folder {0} does not exist, aborting backup", foldername); } + public static string SourceIsMissingError(string foldername) { return LC.L(@"Backup aborted since the source path {0} does not exist. Please verify that the source path exists, or remove the source path from the backup configuration, or set the allow-missing-source option.", foldername); } public static string SourceUnauthorizedError(string foldername) { return LC.L(@"Unauthorized to access source folder {0}, aborting backup", foldername); } public static string UnsupportedBooleanValue(string optionname, string value) { return LC.L(@"The value ""{1}"" supplied to --{0} does not parse into a valid boolean, this will be treated as if it was set to ""true""", optionname, value); } public static string UnsupportedEnumerationValue(string optionname, string value, string[] values) { return LC.L(@"The option --{0} does not support the value ""{1}"", supported values are: {2}", optionname, value, string.Join(", ", values)); } diff --git a/Duplicati/Library/Main/Volumes/FilesetVolumeWriter.cs b/Duplicati/Library/Main/Volumes/FilesetVolumeWriter.cs index ab0f505dc..df57b4ebd 100644 --- a/Duplicati/Library/Main/Volumes/FilesetVolumeWriter.cs +++ b/Duplicati/Library/Main/Volumes/FilesetVolumeWriter.cs @@ -135,9 +135,6 @@ namespace Duplicati.Library.Main.Volumes if (m_streamwriter != null) { this.AddFilelistFile(); - m_writer.Close(); - m_streamwriter.Dispose(); - m_streamwriter = null; } if (m_tempStream != null) @@ -159,11 +156,20 @@ namespace Duplicati.Library.Main.Volumes m_writer.Flush(); m_streamwriter.Flush(); - using (Stream sr = m_compression.CreateFile(FILELIST, CompressionHint.Compressible, DateTime.UtcNow)) + try { - m_tempStream.Seek(0, SeekOrigin.Begin); - m_tempStream.CopyTo(sr); - sr.Flush(); + using (Stream sr = m_compression.CreateFile(FILELIST, CompressionHint.Compressible, DateTime.UtcNow)) + { + m_tempStream.Seek(0, SeekOrigin.Begin); + m_tempStream.CopyTo(sr); + sr.Flush(); + } + } + finally + { + m_writer.Close(); + m_streamwriter.Dispose(); + m_streamwriter = null; } } diff --git a/Duplicati/Library/Modules/Builtin/RunScript.cs b/Duplicati/Library/Modules/Builtin/RunScript.cs index be70ec078..e8188df22 100644 --- a/Duplicati/Library/Modules/Builtin/RunScript.cs +++ b/Duplicati/Library/Modules/Builtin/RunScript.cs @@ -153,16 +153,17 @@ namespace Duplicati.Library.Modules.Builtin public void OnStart(string operationname, ref string remoteurl, ref string[] localpath) { + if (!string.IsNullOrEmpty(m_requiredScript)) + Execute(m_requiredScript, "BEFORE", operationname, ref remoteurl, ref localpath, m_timeout, true, m_options, null, null); + + if (!string.IsNullOrEmpty(m_startScript)) + Execute(m_startScript, "BEFORE", operationname, ref remoteurl, ref localpath, m_timeout, false, m_options, null, null); + + // Save options that might be set by a --run-script-before script so that the OnFinish method + // references the same values. m_operationName = operationname; m_remoteurl = remoteurl; m_localpath = localpath; - - - if (!string.IsNullOrEmpty(m_requiredScript)) - Execute(m_requiredScript, "BEFORE", m_operationName, ref m_remoteurl, ref m_localpath, m_timeout, true, m_options, null, null); - - if (!string.IsNullOrEmpty(m_startScript)) - Execute(m_startScript, "BEFORE", m_operationName, ref m_remoteurl, ref m_localpath, m_timeout, false, m_options, null, null); } public void OnFinish (object result) diff --git a/Duplicati/Library/Modules/Builtin/Strings.cs b/Duplicati/Library/Modules/Builtin/Strings.cs index 227c89ae2..a20e53451 100644 --- a/Duplicati/Library/Modules/Builtin/Strings.cs +++ b/Duplicati/Library/Modules/Builtin/Strings.cs @@ -90,8 +90,6 @@ Example with 3 recipients: Peter Sample , John Sample , admin@example.com"); } } public static string OptionRecipientShort { get { return LC.L(@"Email recipient(s)"); } } - public static string OptionSendallLong { get { return LC.L(@"By default, mail will only be sent after a Backup operation. Use this option to send mail for all operations."); } } - public static string OptionSendallShort { get { return LC.L(@"Send email for all operations"); } } public static string OptionSenderLong { get { return LC.L(@"Address of the email sender. If no host is supplied, the hostname of the first recipient is used. Examples of allowed formats: sender @@ -110,7 +108,6 @@ To enable SMTP over SSL, use the format smtps://example.com. To enable SMTP STAR public static string OptionSubjectShort { get { return LC.L(@"The email subject"); } } public static string OptionUsernameLong { get { return LC.L(@"The username used to authenticate with the SMTP server if required."); } } public static string OptionUsernameShort { get { return LC.L(@"SMTP Username"); } } - public static string SendMailFailedError(string message) { return LC.L(@"Failed to send email: {0}", message); } public static string SendMailLog(string message) { return LC.L(@"Whole SMTP communication: {0}", message); } public static string SendMailFailedRetryError(string failedserver, string message, string retryserver) { return LC.L(@"Failed to send email with server: {0}, message: {1}, retrying with {2}", failedserver, message, retryserver); } public static string SendMailSuccess(string server) { return LC.L(@"Email sent successfully using server: {0}", server); } @@ -140,7 +137,6 @@ You can supply multiple options with a comma separator, e.g. ""{0},{1}"". The sp public static string DisplayName { get { return LC.L(@"XMPP report module"); } } public static string Description { get { return LC.L(@"This module provides support for sending status reports via XMPP messages"); } } public static string LoginTimeoutError { get { return LC.L(@"Timeout occurred while logging in to jabber server"); } } - public static string SendMessageError(string message) { return LC.L(@"Failed to send jabber message: {0}", message); } } internal static class SendHttpMessage { @@ -167,9 +163,6 @@ All command line options are also reported within %value%, e.g. %volsize%. Any u You can supply multiple options with a comma separator, e.g. ""{0},{1}"". The special value ""{4}"" is a shorthand for ""{0},{1},{2},{3}"" and will cause all backup operations to send a message.", success, warning, error, fatal, all); } public static string SendhttpanyoperationShort { get { return LC.L(@"Send messages for all operations"); } } public static string SendhttpanyoperationLong { get { return LC.L(@"By default, messages will only be sent after a Backup operation. Use this option to send messages for all operations"); } } - public static string SendMessageError(string message) { return LC.L(@"Failed to send http message: {0}", message); } - public static string SendasjsonShort { get { return LC.L(@"Send data as JSON body"); } } - public static string SendasjsonLong { get { return LC.L(@"Use this flag to send the result data as a JSON object"); } } public static string HttpverbShort { get { return LC.L(@"Sets the HTTP verb to use"); } } public static string HttpverbLong { get { return LC.L(@"Use this option to change the default HTTP verb used to submit a report"); } } } diff --git a/Duplicati/Library/Modules/Builtin/app.config b/Duplicati/Library/Modules/Builtin/app.config index ba3677f42..3ec9d6a3d 100644 --- a/Duplicati/Library/Modules/Builtin/app.config +++ b/Duplicati/Library/Modules/Builtin/app.config @@ -12,4 +12,4 @@ - + diff --git a/Duplicati/Library/SQLiteHelper/packages.config b/Duplicati/Library/SQLiteHelper/packages.config index 6466e2794..e88fdce35 100644 --- a/Duplicati/Library/SQLiteHelper/packages.config +++ b/Duplicati/Library/SQLiteHelper/packages.config @@ -1,8 +1,8 @@  - - - - - - \ No newline at end of file + + + + + + diff --git a/Duplicati/Library/Snapshots/MSSQLUtility.cs b/Duplicati/Library/Snapshots/MSSQLUtility.cs index 0e43a340d..6149581c3 100644 --- a/Duplicati/Library/Snapshots/MSSQLUtility.cs +++ b/Duplicati/Library/Snapshots/MSSQLUtility.cs @@ -1,5 +1,5 @@ using Duplicati.Library.Common; -using Duplicati.Library.Common.IO; +using Duplicati.Library.Common.IO; using System; using System.Collections.Generic; using System.Linq; @@ -45,8 +45,8 @@ namespace Duplicati.Library.Snapshots public static bool operator ==(MSSQLDB db1, MSSQLDB db2) { if (object.ReferenceEquals(db1, db2)) return true; - if (object.ReferenceEquals(db1, null)) return false; - if (object.ReferenceEquals(db2, null)) return false; + if (object.ReferenceEquals(db1, null)) return false; + if (object.ReferenceEquals(db2, null)) return false; return db1.Equals(db2); } @@ -58,7 +58,7 @@ namespace Duplicati.Library.Snapshots if (object.ReferenceEquals(db2, null)) return true; return !db1.Equals(db2); - } + } } public class MSSQLUtility @@ -79,8 +79,8 @@ namespace Duplicati.Library.Snapshots /// Enumerated MS SQL DBs /// public List DBs { get { return m_DBs; } } - private readonly List m_DBs; - + private readonly List m_DBs; + public MSSQLUtility() { m_DBs = new List(); @@ -118,7 +118,7 @@ namespace Duplicati.Library.Snapshots else if (installed32on64 != null) try { arrInstalledInstances = (string[])installed32on64; } catch { } - } + } IsMSSQLInstalled = arrInstalledInstances != null && arrInstalledInstances.Length > 0; @@ -138,23 +138,23 @@ namespace Duplicati.Library.Snapshots m_DBs.Clear(); using (var vssBackupComponents = new VssBackupComponents()) - { - var writerGUIDS = new [] { MSSQLWriterGuid }; - try - { - vssBackupComponents.SetupWriters(writerGUIDS, null); - } - catch (Exception) - { - throw new Interface.UserInformationException("Microsoft SQL Server VSS Writer not found - cannot backup SQL databases.", "NoMsSqlVssWriter"); - } - - foreach (var o in vssBackupComponents.ParseWriterMetaData(writerGUIDS)) - { - m_DBs.Add(new MSSQLDB(o.Name, o.LogicalPath + "\\" + o.Name, o.Paths.ConvertAll(m => m[0].ToString().ToUpperInvariant() + m.Substring(1)) - .Distinct(Utility.Utility.ClientFilenameStringComparer) - .OrderBy(a => a).ToList())); - } + { + var writerGUIDS = new [] { MSSQLWriterGuid }; + try + { + vssBackupComponents.SetupWriters(writerGUIDS, null); + } + catch (Exception) + { + throw new Interface.UserInformationException("Microsoft SQL Server VSS Writer not found - cannot backup SQL databases.", "NoMsSqlVssWriter"); + } + + foreach (var o in vssBackupComponents.ParseWriterMetaData(writerGUIDS)) + { + m_DBs.Add(new MSSQLDB(o.Name, o.LogicalPath + "\\" + o.Name, o.Paths.ConvertAll(m => m[0].ToString().ToUpperInvariant() + m.Substring(1)) + .Distinct(Utility.Utility.ClientFilenameStringComparer) + .OrderBy(a => a).ToList())); + } } } } diff --git a/Duplicati/Library/Snapshots/NoSnapshotWindows.cs b/Duplicati/Library/Snapshots/NoSnapshotWindows.cs index 103d245d1..59e1230c2 100644 --- a/Duplicati/Library/Snapshots/NoSnapshotWindows.cs +++ b/Duplicati/Library/Snapshots/NoSnapshotWindows.cs @@ -17,6 +17,7 @@ // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA using System; using System.Collections.Generic; +using System.Linq; using Duplicati.Library.Common.IO; namespace Duplicati.Library.Snapshots @@ -56,6 +57,18 @@ namespace Duplicati.Library.Snapshots return SystemIO.IO_WIN.FileLength(localPath); } + /// + /// Enumerates all files and folders in the snapshot, restricted to sources + /// + /// Sources to enumerate + /// The callback to invoke with each found path + /// The callback used to report errors + public override IEnumerable EnumerateFilesAndFolders(IEnumerable sources, Utility.Utility.EnumerationFilterDelegate callback, Utility.Utility.ReportAccessError errorCallback) + { + // For Windows, ensure we don't store paths with UNC prefix + return base.EnumerateFilesAndFolders(sources.Select(SystemIOWindows.StripUNCPrefix), callback, errorCallback); + } + /// /// Gets the last write time of a given file in UTC /// @@ -138,7 +151,8 @@ namespace Duplicati.Library.Snapshots /// public override string ConvertToSnapshotPath(string localPath) { - return localPath; + // For Windows, ensure we don't store paths with UNC prefix + return SystemIOWindows.StripUNCPrefix(localPath); } } } diff --git a/Duplicati/Library/Snapshots/SnapshotBase.cs b/Duplicati/Library/Snapshots/SnapshotBase.cs index 639561360..939499338 100644 --- a/Duplicati/Library/Snapshots/SnapshotBase.cs +++ b/Duplicati/Library/Snapshots/SnapshotBase.cs @@ -48,7 +48,7 @@ namespace Duplicati.Library.Snapshots /// Source paths to enumerate /// The callback to invoke with each found path /// The callback used to report errors - public IEnumerable EnumerateFilesAndFolders(IEnumerable sources, Utility.Utility.EnumerationFilterDelegate callback, Utility.Utility.ReportAccessError errorCallback) + public virtual IEnumerable EnumerateFilesAndFolders(IEnumerable sources, Utility.Utility.EnumerationFilterDelegate callback, Utility.Utility.ReportAccessError errorCallback) { return sources.SelectMany(s => Utility.Utility.EnumerateFileSystemEntries(s, callback, ListFolders, ListFiles, GetAttributes, errorCallback)); } diff --git a/Duplicati/Library/Snapshots/USNJournal.cs b/Duplicati/Library/Snapshots/USNJournal.cs index 111a871b3..75aeffe51 100644 --- a/Duplicati/Library/Snapshots/USNJournal.cs +++ b/Duplicati/Library/Snapshots/USNJournal.cs @@ -37,9 +37,15 @@ namespace Duplicati.Library.Snapshots /// public sealed class USNJournal : IDisposable { + /// + /// The log tag to use + /// + private static readonly string FILTER_LOGTAG = Logging.Log.LogTagFromType(typeof(USNJournal)); + [Flags] public enum ChangeReason { + None = 0, Modified = 1, Created = 2, Deleted = 4, @@ -79,7 +85,7 @@ namespace Duplicati.Library.Snapshots /// /// Start USN of current cached m_entryList /// - private long m_minUsn; + private long m_startUsn; /// /// The safe filehandle @@ -133,27 +139,29 @@ namespace Duplicati.Library.Snapshots /// Returns a list of files or folders that have changed since the recorded USN /// /// The file or folder to find entries for - /// Minimum USN of entry + /// USN of first entry to consider /// A list of tuples with changed files and folders and their type - public IEnumerable> GetChangedFileSystemEntries(string sourceFileOrFolder, long minUsn) + public IEnumerable> GetChangedFileSystemEntries(string sourceFileOrFolder, long startUsn) { - return GetChangedFileSystemEntries(sourceFileOrFolder, minUsn, ChangeReason.Any); + return GetChangedFileSystemEntries(sourceFileOrFolder, startUsn, ChangeReason.Any); } /// /// Returns a list of files or folders that have changed since the recorded USN /// /// The file or folder to find entries for - /// Minimum USN of entry + /// USN of first entry to consider /// Filter expression for change reason /// A list of tuples with changed files and folders and their type - public IEnumerable> GetChangedFileSystemEntries(string sourceFileOrFolder, long minUsn, ChangeReason reason) + public IEnumerable> GetChangedFileSystemEntries(string sourceFileOrFolder, long startUsn, ChangeReason reason) { + Logging.Log.WriteVerboseMessage(FILTER_LOGTAG, "UsnInitialize", "Determine file system changes from USN for: {0}", sourceFileOrFolder); + var isFolder = sourceFileOrFolder.EndsWith(Util.DirectorySeparatorString, StringComparison.Ordinal); - foreach (var r in GetRecords(minUsn)) + foreach (var r in GetRecords(startUsn)) { - if (r.UsnRecord.Usn >= minUsn + if (r.UsnRecord.Usn >= startUsn && (reason == ChangeReason.Any || (MapChangeReason(r.UsnRecord.Reason) & reason) != 0) && (r.FullPath.Equals(sourceFileOrFolder, Utility.Utility.ClientFilenameStringComparison) || isFolder && Utility.Utility.IsPathBelowFolder(r.FullPath, sourceFileOrFolder))) @@ -181,13 +189,19 @@ namespace Duplicati.Library.Snapshots /// /// Internal method to initially create and then access the cached version of the file entry list + /// USN of first entry to consider /// - private IEnumerable GetRecords(long minUsn) + private IEnumerable GetRecords(long startUsn) { - if (m_entryList == null || m_minUsn != minUsn) + const Win32USN.USNReason InclusionFlags = Win32USN.USNReason.USN_REASON_ANY + & ~(Win32USN.USNReason.USN_REASON_INDEXABLE_CHANGE | Win32USN.USNReason.USN_REASON_COMPRESSION_CHANGE | + Win32USN.USNReason.USN_REASON_ENCRYPTION_CHANGE | Win32USN.USNReason.USN_REASON_EA_CHANGE | + Win32USN.USNReason.USN_REASON_REPARSE_POINT_CHANGE | Win32USN.USNReason.USN_REASON_CLOSE); + + if (m_entryList == null || m_startUsn != startUsn) { - m_minUsn = minUsn; - m_entryList = ResolveFullPaths(GetRawRecords(minUsn)); + m_startUsn = startUsn; + m_entryList = ResolveFullPaths(GetRawRecords(startUsn, rec => (rec.UsnRecord.Reason & InclusionFlags) != 0)); } return m_entryList; @@ -311,29 +325,6 @@ namespace Duplicati.Library.Snapshots private static IEnumerable EnumerateRecords(IReadOnlyCollection entryData) { return new RecordEnumerator(entryData); - - //if (entryData.Count <= sizeof(long)) - // yield break; - - //var bufferHandle = GCHandle.Alloc(entryData, GCHandleType.Pinned); - //try - //{ - // var bufferPointer = bufferHandle.AddrOfPinnedObject(); - - // long offset = sizeof(long); - // while (offset < entryData.Count) - // { - // var entry = GetBufferedEntry(bufferPointer, offset, out var fileName); - - // yield return new Record(entry, fileName); - - // offset += entry.RecordLength; - // } - //} - //finally - //{ - // bufferHandle.Free(); - //} } /// @@ -341,7 +332,7 @@ namespace Duplicati.Library.Snapshots /// /// The USN number to start the list from, set to zero to get all /// A list of files and folders changed since the USN - private ICollection GetRawRecords(long startUsn) + private ICollection GetRawRecords(long startUsn, Func inclusionPredicate) { var records = new List(); @@ -352,7 +343,6 @@ namespace Duplicati.Library.Snapshots Win32USN.USNReason.USN_REASON_DATA_EXTEND | Win32USN.USNReason.USN_REASON_DATA_OVERWRITE | Win32USN.USNReason.USN_REASON_DATA_TRUNCATION | - Win32USN.USNReason.USN_REASON_EA_CHANGE | Win32USN.USNReason.USN_REASON_FILE_CREATE | Win32USN.USNReason.USN_REASON_FILE_DELETE | Win32USN.USNReason.USN_REASON_HARD_LINK_CHANGE | @@ -361,8 +351,6 @@ namespace Duplicati.Library.Snapshots Win32USN.USNReason.USN_REASON_NAMED_DATA_TRUNCATION | Win32USN.USNReason.USN_REASON_RENAME_NEW_NAME | Win32USN.USNReason.USN_REASON_RENAME_OLD_NAME | - Win32USN.USNReason.USN_REASON_REPARSE_POINT_CHANGE | - Win32USN.USNReason.USN_REASON_SECURITY_CHANGE | Win32USN.USNReason.USN_REASON_STREAM_CHANGE, ReturnOnlyOnClose = 0, Timeout = 0, @@ -382,7 +370,7 @@ namespace Duplicati.Library.Snapshots if (e == Win32USN.ERROR_INSUFFICIENT_BUFFER) { - bufferSize = bufferSize * 2; + bufferSize *= 2; continue; } @@ -392,7 +380,9 @@ namespace Duplicati.Library.Snapshots throw new Win32Exception(e); } - records.AddRange(EnumerateRecords(entryData).TakeWhile(rec => rec.UsnRecord.Usn >= startUsn && rec.UsnRecord.Usn < m_journal.NextUsn)); + records.AddRange(EnumerateRecords(entryData) + .TakeWhile(rec => rec.UsnRecord.Usn < m_journal.NextUsn) + .Where(rec => rec.UsnRecord.Usn >= startUsn && (inclusionPredicate == null || inclusionPredicate(rec)))); readData.StartUsn = Marshal.ReadInt64(entryData, 0); } @@ -423,13 +413,16 @@ namespace Duplicati.Library.Snapshots return null; // retry, increasing buffer size - bufferSize = bufferSize * 2; + bufferSize *= 2; } // not really a foreach: we only check the first record foreach (var rec in EnumerateRecords(entryData)) + { if (rec.UsnRecord.FileReferenceNumber == frn) return rec; + break; + } return null; } @@ -439,7 +432,7 @@ namespace Duplicati.Library.Snapshots /// /// The list of records with local names /// A list of USN entries with full path - private List ResolveFullPaths(ICollection records) + private IReadOnlyCollection ResolveFullPaths(ICollection records) { // initialize file ref-nr (FRN) to path/parent-FRN look-up table var cache = new Dictionary(); @@ -457,10 +450,21 @@ namespace Duplicati.Library.Snapshots } } + // List of unresolved USN records, with FileReferenceNumber as a key + Dictionary> recordsByFileRefNumber = new Dictionary>(); + // iterate through USN records - var result = new List(); + var result = new Dictionary(); foreach (var rec in records) { + // Add entry to list of unresolved entries, and try to resolve them at the end of the scan + if (!recordsByFileRefNumber.TryGetValue(rec.UsnRecord.FileReferenceNumber, out List fileRefHistory)) + { + fileRefHistory = new List(); + recordsByFileRefNumber.Add(rec.UsnRecord.FileReferenceNumber, fileRefHistory); + } + fileRefHistory.Add(rec); + var pathList = new LinkedList(); pathList.AddFirst(rec); @@ -476,8 +480,17 @@ namespace Duplicati.Library.Snapshots { // parent FRN not found in look-up table, fetch it from change journal var parentRecord = GetRecordByFileRef(parentRefNr); - parents = new SortedRecords(new List { parentRecord }); - cache.Add(parentRefNr, parents); + + if (parentRecord == null) + { + pathList.Clear(); + break; + } + else + { + parents = new SortedRecords(new List { parentRecord }); + cache.Add(parentRefNr, parents); + } } // take parent entry having next smaller USN @@ -490,25 +503,87 @@ namespace Duplicati.Library.Snapshots cur = parent; } - // generate full path - Debug.Assert(m_volume != null, nameof(m_volume) + " != null"); - var path = m_volume; - foreach (var r in pathList) + if (pathList.Count > 0) { - path = SystemIO.IO_WIN.PathCombine(path, r.FileName); - } + // generate full path + Debug.Assert(m_volume != null, nameof(m_volume) + " != null"); + var path = m_volume; + foreach (var r in pathList) + { + path = SystemIO.IO_WIN.PathCombine(path, r.FileName); + } - if (rec.UsnRecord.FileAttributes.HasFlag(Win32USN.FileAttributes.Directory)) - { - path = Util.AppendDirSeparator(path); - } + if (rec.UsnRecord.FileAttributes.HasFlag(Win32USN.FileAttributes.Directory)) + { + path = Util.AppendDirSeparator(path); + } - // set resolved path - rec.FullPath = path; - result.Add(rec); + // set resolved path + rec.FullPath = path; + } } - return result; + // parse all records + foreach (var entry in recordsByFileRefNumber) + { + bool wasCreated = false; + var tempRecords = new List(); + + foreach (var rec in entry.Value) + { + // add entry to intermediate result set + tempRecords.Add(rec); + + var reason = rec.UsnRecord.Reason; + + if (reason.HasFlag(Win32USN.USNReason.USN_REASON_FILE_CREATE) || reason.HasFlag(Win32USN.USNReason.USN_REASON_RENAME_NEW_NAME)) + { + wasCreated = true; + } + + if (reason.HasFlag(Win32USN.USNReason.USN_REASON_FILE_DELETE) || reason.HasFlag(Win32USN.USNReason.USN_REASON_RENAME_OLD_NAME)) + { + if (!wasCreated) + { + FlushRecords(tempRecords, result); + } + + tempRecords.Clear(); + wasCreated = false; + } + } + + FlushRecords(tempRecords, result); + } + + return result.Values; + } + + private static void FlushRecords(List tempRecords, Dictionary resultRecords) + { + foreach (var rec in tempRecords) + { + if (!string.IsNullOrEmpty(rec.FullPath)) + { + resultRecords[rec.FullPath] = rec; + } + // Ignore entries below \$Extend\. A clean implementation would now + // parse the MFT and look up the actual entry. But for for now, we check against + // the well-known file names. + else if (!(rec.FileName.Length == 24 + && rec.UsnRecord.FileReferenceNumber.ToString("X16") == rec.FileName.Substring(0, 16) + || rec.FileName.Equals("$TxfLog") + || rec.FileName.Equals("$TxfLog.blf"))) + { + Logging.Log.WriteVerboseMessage(FILTER_LOGTAG, "UsnInitialize", + "Unable to use USN due to unresolvable entry \"{0}\" with ParentFileReferenceNumber {0:X24}", + rec.FileName, rec.UsnRecord.ParentFileReferenceNumber); + + throw new UsnJournalSoftFailureException(Strings.USNHelper.PathResolveError); + } + } + + tempRecords.Clear(); } private static ChangeReason MapChangeReason(Win32USN.USNReason reason) diff --git a/Duplicati/Library/Snapshots/UsnJournalService.cs b/Duplicati/Library/Snapshots/UsnJournalService.cs index 5aef2a52d..acff99883 100644 --- a/Duplicati/Library/Snapshots/UsnJournalService.cs +++ b/Duplicati/Library/Snapshots/UsnJournalService.cs @@ -33,6 +33,11 @@ namespace Duplicati.Library.Snapshots { public class UsnJournalService { + /// + /// The log tag to use + /// + private static readonly string FILTER_LOGTAG = Logging.Log.LogTagFromType(typeof(UsnJournalService)); + private readonly ISnapshotService m_snapshot; private readonly IEnumerable m_sources; private readonly Dictionary m_volumeDataDict; @@ -89,6 +94,8 @@ namespace Duplicati.Library.Snapshots // iterate over volumes foreach (var sourcesPerVolume in SortByVolume(m_sources)) { + Logging.Log.WriteVerboseMessage(FILTER_LOGTAG, "UsnInitialize", "Reading USN journal for volume: {0}", sourcesPerVolume.Key); + if (m_token.IsCancellationRequested) break; var volume = sourcesPerVolume.Key; diff --git a/Duplicati/Library/Snapshots/WindowsSnapshot.cs b/Duplicati/Library/Snapshots/WindowsSnapshot.cs index 4fd37d1e9..3d77e749e 100644 --- a/Duplicati/Library/Snapshots/WindowsSnapshot.cs +++ b/Duplicati/Library/Snapshots/WindowsSnapshot.cs @@ -53,6 +53,8 @@ namespace Duplicati.Library.Snapshots /// A set of commandline options public WindowsSnapshot(IEnumerable sources, IDictionary options) { + // For Windows, ensure we don't store paths with UNC prefix + sources = sources.Select(SystemIOWindows.StripUNCPrefix); try { _vssBackupComponents = new VssBackupComponents(); @@ -110,17 +112,7 @@ namespace Duplicati.Library.Snapshots { string[] tmp = null; var spath = ConvertToSnapshotPath(localFolderPath); - - try - { - tmp = SystemIO.IO_WIN.GetDirectories(spath); - } - catch (DirectoryNotFoundException) //TODO: Do we really need this?? - { - spath = SystemIOWindows.PrefixWithUNC(spath); - tmp = SystemIO.IO_WIN.GetDirectories(spath); - } - + tmp = SystemIO.IO_WIN.GetDirectories(spath); var root = Util.AppendDirSeparator(SystemIO.IO_WIN.GetPathRoot(localFolderPath)); var volumePath = Util.AppendDirSeparator(ConvertToSnapshotPath(root)); volumePath = SystemIOWindows.PrefixWithUNC(volumePath); @@ -145,17 +137,7 @@ namespace Duplicati.Library.Snapshots string[] files = null; var spath = ConvertToSnapshotPath(localFolderPath); - - // First try without long UNC prefixed - try - { - files = SystemIO.IO_WIN.GetFiles(spath); - } - catch (DirectoryNotFoundException) //TODO: Do we really need this?? - { - spath = SystemIOWindows.PrefixWithUNC(spath); - files = SystemIO.IO_WIN.GetFiles(spath); - } + files = SystemIO.IO_WIN.GetFiles(spath); // convert back to non-shadow, i.e., non-vss version var root = Util.AppendDirSeparator(SystemIO.IO_WIN.GetPathRoot(localFolderPath)); @@ -173,6 +155,18 @@ namespace Duplicati.Library.Snapshots #region ISnapshotService Members + /// + /// Enumerates all files and folders in the snapshot, restricted to sources + /// + /// Sources to enumerate + /// The callback to invoke with each found path + /// The callback used to report errors + public override IEnumerable EnumerateFilesAndFolders(IEnumerable sources, Utility.Utility.EnumerationFilterDelegate callback, Utility.Utility.ReportAccessError errorCallback) + { + // For Windows, ensure we don't store paths with UNC prefix + return base.EnumerateFilesAndFolders(sources.Select(SystemIOWindows.StripUNCPrefix), callback, errorCallback); + } + /// /// Gets the last write time of a given file in UTC /// @@ -280,13 +274,19 @@ namespace Duplicati.Library.Snapshots /// public override string ConvertToSnapshotPath(string localPath) { + // For Windows, ensure we don't store paths with UNC prefix + localPath = SystemIOWindows.StripUNCPrefix(localPath); + if (!Path.IsPathRooted(localPath)) throw new InvalidOperationException(); var root = SystemIO.IO_WIN.GetPathRoot(localPath); var volumePath = _vssBackupComponents.GetVolumeFromCache(root); - // Note: Do NOT use Path.Combine as it strips the UNC path prefix + // Note that using a simple Path.Combine() for the following code + // can result in invalid snapshot paths; e.g., if localPath is + // @"C:\", mappedPath would not have the required trailing + // directory separator. var subPath = localPath.Substring(root.Length); if (!subPath.StartsWith(Util.DirectorySeparatorString, StringComparison.Ordinal)) { diff --git a/Duplicati/Library/Utility/AsyncHttpRequest.cs b/Duplicati/Library/Utility/AsyncHttpRequest.cs index 7a276230a..0ba349d71 100644 --- a/Duplicati/Library/Utility/AsyncHttpRequest.cs +++ b/Duplicati/Library/Utility/AsyncHttpRequest.cs @@ -230,11 +230,11 @@ namespace Duplicati.Library.Utility { WebResponse resp = null; - try { resp = (WebResponse)r.GetType().GetProperty("Response", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic).GetValue(r); } + try { resp = r.GetType().GetProperty("Response", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)?.GetValue(r) as WebResponse; } catch {} if (resp == null) - try { resp = (WebResponse)m_owner.m_request.GetType().GetField("webResponse", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic).GetValue(m_owner.m_request); } + try { resp = m_owner.m_request.GetType().GetField("webResponse", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)?.GetValue(m_owner.m_request) as WebResponse; } catch { } if (resp != null) diff --git a/Duplicati/Library/Utility/FilterExpression.cs b/Duplicati/Library/Utility/FilterExpression.cs index 0c70f7ef7..fc4eb1745 100644 --- a/Duplicati/Library/Utility/FilterExpression.cs +++ b/Duplicati/Library/Utility/FilterExpression.cs @@ -119,6 +119,14 @@ namespace Duplicati.Library.Utility this.Filter = filter.Substring(1, filter.Length - 2); this.Regexp = GetFilterGroupRegex(this.Filter); } + else if (filter.StartsWith("@", StringComparison.Ordinal)) + { + // Take filter literally; i.e., don't treat wildcard + // characters as globbing characters + this.Type = FilterType.Simple; + this.Filter = filter.Substring(1); + this.Regexp = new Regex(Utility.ConvertLiteralToRegExp(this.Filter), REGEXP_OPTIONS); + } else { this.Type = (filter.Contains(MULTIPLE_WILDCARD) || filter.Contains(SINGLE_WILDCARD)) ? FilterType.Wildcard : FilterType.Simple; @@ -150,6 +158,10 @@ namespace Duplicati.Library.Utility { return filterString.Substring(1, filterString.Length - 2); } + else if (filterString.StartsWith("@", StringComparison.Ordinal)) + { + return Utility.ConvertLiteralToRegExp(filterString.Substring(1)); + } else { return Utility.ConvertGlobbingToRegExp(filterString); @@ -284,6 +296,8 @@ namespace Duplicati.Library.Utility return "[" + this.Filter + "]"; case FilterType.Group: return "{" + this.Filter + "}"; + case FilterType.Simple: + return "@" + this.Filter; default: return this.Filter; } diff --git a/Duplicati/Library/Utility/SslCertificateValidator.cs b/Duplicati/Library/Utility/SslCertificateValidator.cs index 52e09b079..c995aac25 100644 --- a/Duplicati/Library/Utility/SslCertificateValidator.cs +++ b/Duplicati/Library/Utility/SslCertificateValidator.cs @@ -20,8 +20,8 @@ using System; using System.Security.Cryptography.X509Certificates; using System.Net.Security; -using Duplicati.Library.Common; - +using Duplicati.Library.Common; + namespace Duplicati.Library.Utility { public class SslCertificateValidator @@ -65,12 +65,16 @@ namespace Duplicati.Library.Utility try { certHash = Utility.ByteArrayAsHexString(cert.GetCertHash()); - if (certHash != null && m_validHashes != null) - foreach(var hash in m_validHashes) + if (certHash != null && m_validHashes != null) + { + foreach (var hash in m_validHashes) { if (!string.IsNullOrEmpty(hash) && certHash.Equals(hash, StringComparison.OrdinalIgnoreCase)) - return true; + { + return true; + } } + } } catch (Exception ex) { diff --git a/Duplicati/Library/Utility/Uri.cs b/Duplicati/Library/Utility/Uri.cs index 6ba4e75ee..19be6be5b 100644 --- a/Duplicati/Library/Utility/Uri.cs +++ b/Duplicati/Library/Utility/Uri.cs @@ -19,7 +19,6 @@ using System; using System.Collections.Specialized; using System.Linq; using System.Text; -using System.Web; namespace Duplicati.Library.Utility { @@ -445,6 +444,18 @@ namespace Duplicati.Library.Utility /// The parsed query string /// The query to parse public static NameValueCollection ParseQueryString(string query) + { + return Library.Utility.Uri.ParseQueryString(query, true); + } + + /// + /// Parses the query string. + /// This is a duplicate of the System.Web.HttpUtility.ParseQueryString that does not work well on Mono + /// + /// The parsed query string + /// The query to parse + /// Whether to the parameter values should be decoded or not. + public static NameValueCollection ParseQueryString(string query, bool decodeValues) { if (query == null) throw new ArgumentNullException(nameof(query)); @@ -455,7 +466,15 @@ namespace Duplicati.Library.Utility var result = new NameValueCollection(StringComparer.OrdinalIgnoreCase); foreach (System.Text.RegularExpressions.Match m in RE_URLPARAM.Matches(query)) - result.Add(UrlDecode(m.Groups["key"].Value), UrlDecode(m.Groups["value"].Success ? m.Groups["value"].Value : "")); + { + string value = m.Groups["value"].Success ? m.Groups["value"].Value : ""; + if (decodeValues) + { + value = UrlDecode(value); + } + + result.Add(UrlDecode(m.Groups["key"].Value), value); + } return result; } diff --git a/Duplicati/Library/Utility/Utility.cs b/Duplicati/Library/Utility/Utility.cs index 88bee634d..a582fc397 100644 --- a/Duplicati/Library/Utility/Utility.cs +++ b/Duplicati/Library/Utility/Utility.cs @@ -165,6 +165,15 @@ namespace Duplicati.Library.Utility return globexp; } + /// + /// Convert literal path to the equivalent regular expression. + /// + public static string ConvertLiteralToRegExp(string literalPath) + { + // Escape all special characters + return Regex.Escape(literalPath); + } + /// /// Returns a list of all files found in the given folder. /// The search is recursive. diff --git a/Duplicati/License/Duplicati.License.csproj b/Duplicati/License/Duplicati.License.csproj index 3fe81df2f..e29878d9d 100644 --- a/Duplicati/License/Duplicati.License.csproj +++ b/Duplicati/License/Duplicati.License.csproj @@ -43,6 +43,21 @@ licenses\SQLite\License.txt PreserveNewest + + licenses\Tencentyun\download.txt + + + licenses\Tencentyun\LICENSE.txt + + + licenses\TLSharp\Homepage.txt + + + licenses\TLSharp\License.txt + + + licenses\TLSharp\licensedata.json + licenses\uplink.NET\Homepage.txt diff --git a/Duplicati/Server/Database/Backup.cs b/Duplicati/Server/Database/Backup.cs index 962fb1081..117253942 100644 --- a/Duplicati/Server/Database/Backup.cs +++ b/Duplicati/Server/Database/Backup.cs @@ -18,6 +18,7 @@ using System; using Duplicati.Server.Serialization.Interface; using System.Collections.Generic; +using System.Collections.Specialized; using System.Linq; namespace Duplicati.Server.Database @@ -121,11 +122,19 @@ namespace Duplicati.Server.Database public void SanitizeTargetUrl() { var url = new Duplicati.Library.Utility.Uri(this.TargetURL); - var filteredParameters = url.QueryParameters; - foreach (string field in UrlPasswords) { - filteredParameters.Remove(field); + NameValueCollection filteredParameters = new NameValueCollection(); + if (url.Query != null) + { + // We cannot use url.QueryParameters since it contains decoded parameter values, which + // breaks assumptions made by the decode_uri function in AppUtils.js. Since we are simply + // removing password parameters, we will leave the parameters as they are in the target URL. + filteredParameters = Library.Utility.Uri.ParseQueryString(url.Query, false); + foreach (string field in this.UrlPasswords) + { + filteredParameters.Remove(field); + } } - url = url.SetQuery(Duplicati.Library.Utility.Uri.BuildUriQuery(url.QueryParameters)); + url = url.SetQuery(Duplicati.Library.Utility.Uri.BuildUriQuery(filteredParameters)); this.TargetURL = url.ToString(); } diff --git a/Duplicati/Server/Database/Connection.cs b/Duplicati/Server/Database/Connection.cs index 7f7bcb212..ce9ed53da 100644 --- a/Duplicati/Server/Database/Connection.cs +++ b/Duplicati/Server/Database/Connection.cs @@ -476,21 +476,25 @@ namespace Duplicati.Server.Database internal void UpdateBackupDBPath(IBackup item, string path) { - lock(m_lock) - using(var tr = m_connection.BeginTransaction()) - using(var cmd = m_connection.CreateCommand()) + lock (m_lock) { - cmd.Transaction = tr; - cmd.Parameters.Add(cmd.CreateParameter()); - ((System.Data.IDbDataParameter)cmd.Parameters[0]).Value = path; - cmd.Parameters.Add(cmd.CreateParameter()); - ((System.Data.IDbDataParameter)cmd.Parameters[1]).Value = item.ID; + using (var tr = m_connection.BeginTransaction()) + { + using (var cmd = m_connection.CreateCommand()) + { + cmd.Transaction = tr; + cmd.Parameters.Add(cmd.CreateParameter()); + ((System.Data.IDbDataParameter) cmd.Parameters[0]).Value = path; + cmd.Parameters.Add(cmd.CreateParameter()); + ((System.Data.IDbDataParameter) cmd.Parameters[1]).Value = item.ID; - cmd.CommandText = @"UPDATE ""Backup"" SET ""DBPath""=? WHERE ""ID""=?"; - cmd.ExecuteNonQuery(); - tr.Commit(); + cmd.CommandText = @"UPDATE ""Backup"" SET ""DBPath""=? WHERE ""ID""=?"; + cmd.ExecuteNonQuery(); + tr.Commit(); + } + } } - + System.Threading.Interlocked.Increment(ref Program.LastDataUpdateID); Program.StatusEventNotifyer.SignalNewEvent(); } @@ -1104,15 +1108,15 @@ namespace Duplicati.Server.Database var prop = properties[i]; if (prop.PropertyType.IsEnum) - prop.SetValue(item, ConvertToEnum(prop.PropertyType, rd, i, Enum.GetValues(prop.PropertyType).GetValue(0)), null); + prop.SetValue(item, ConvertToEnum(prop.PropertyType, rd, i, Enum.GetValues(prop.PropertyType).GetValue(0)), null); else if (prop.PropertyType == typeof(string)) - prop.SetValue(item, ConvertToString(rd, i), null); + prop.SetValue(item, ConvertToString(rd, i), null); else if (prop.PropertyType == typeof(long)) - prop.SetValue(item, ConvertToInt64(rd, i), null); + prop.SetValue(item, ConvertToInt64(rd, i), null); else if (prop.PropertyType == typeof(bool)) - prop.SetValue(item, ConvertToBoolean(rd, i), null); + prop.SetValue(item, ConvertToBoolean(rd, i), null); else if (prop.PropertyType == typeof(DateTime)) - prop.SetValue(item, ConvertToDateTime(rd, i), null); + prop.SetValue(item, ConvertToDateTime(rd, i), null); } return item; diff --git a/Duplicati/Server/Program.cs b/Duplicati/Server/Program.cs index 3826fa357..69d7c9fc8 100644 --- a/Duplicati/Server/Program.cs +++ b/Duplicati/Server/Program.cs @@ -273,8 +273,6 @@ namespace Duplicati.Server Console.WriteLine(Strings.Program.SeriousError(mex.ToString())); return 100; - - throw; } catch (Exception ex) { diff --git a/Duplicati/Server/Runner.cs b/Duplicati/Server/Runner.cs index 56bb6abb0..33a230381 100644 --- a/Duplicati/Server/Runner.cs +++ b/Duplicati/Server/Runner.cs @@ -795,17 +795,28 @@ namespace Duplicati.Server if (r.FilesWithError > 0 || r.Warnings.Any() || r.Errors.Any()) { + string message; + string titleType; + if (r.FilesWithError > 0) + { + message = $"Errors affected {r.FilesWithError} file(s)."; + titleType = "Error"; + } + else if (r.Errors.Any()) + { + message = r.Errors.Count() == 1 ? r.Errors.Single() : $"Encountered {r.Errors.Count()} errors."; + titleType = "Error"; + } + else + { + message = r.Warnings.Count() == 1 ? r.Warnings.Single() : $"Encountered {r.Warnings.Count()} warnings."; + titleType = "Warning"; + } + Program.DataConnection.RegisterNotification( - r.FilesWithError == 0 && !r.Errors.Any() ? NotificationType.Warning : NotificationType.Error, - backup.IsTemporary ? - "Warning" : string.Format("Warning while running {0}", backup.Name), - r.FilesWithError > 0 ? - string.Format("Errors affected {0} file(s) ", r.FilesWithError) : - (r.Errors.Any() ? - string.Format("Got {0} error(s)", r.Errors.Count()) : - string.Format("Got {0} warning(s)", r.Warnings.Count()) - ) - , + r.FilesWithError == 0 && !r.Errors.Any() ? NotificationType.Warning : NotificationType.Error, + backup.IsTemporary ? "Warning" : $"{titleType} while running {backup.Name}", + message, null, backup.ID, "backup:show-log", @@ -942,11 +953,11 @@ namespace Duplicati.Server if (filters == null || filters.Length == 0) return null; - return + return (from n in filters - orderby n.Order - let exp = Environment.ExpandEnvironmentVariables(n.Expression) - select (Duplicati.Library.Utility.IFilter)(new Duplicati.Library.Utility.FilterExpression(exp, n.Include))) + orderby n.Order + let exp = Environment.ExpandEnvironmentVariables(n.Expression) + select (Duplicati.Library.Utility.IFilter)(new Duplicati.Library.Utility.FilterExpression(exp, n.Include))) .Aggregate((a, b) => Duplicati.Library.Utility.FilterExpression.Combine(a, b)); } } diff --git a/Duplicati/Server/SingleInstance.cs b/Duplicati/Server/SingleInstance.cs index 28d154ca2..bed2debec 100644 --- a/Duplicati/Server/SingleInstance.cs +++ b/Duplicati/Server/SingleInstance.cs @@ -251,8 +251,12 @@ namespace Duplicati.Server List args = new List(); using (System.IO.StreamReader sr = new System.IO.StreamReader(Platform.IsClientPosix ? UnixSupport.File.OpenExclusive(e.FullPath, System.IO.FileAccess.ReadWrite) : new System.IO.FileStream(e.FullPath, System.IO.FileMode.Open, System.IO.FileAccess.Read, System.IO.FileShare.None))) - while(!sr.EndOfStream) - args.Add(sr.ReadLine()); + { + while (!sr.EndOfStream) + { + args.Add(sr.ReadLine()); + } + } commandline = args.ToArray(); diff --git a/Duplicati/Server/WebServer/RESTHandler.cs b/Duplicati/Server/WebServer/RESTHandler.cs index 847545870..b09b78378 100644 --- a/Duplicati/Server/WebServer/RESTHandler.cs +++ b/Duplicati/Server/WebServer/RESTHandler.cs @@ -130,94 +130,98 @@ namespace Duplicati.Server.WebServer { var ci = ParseRequestCulture(info); - using ( Library.Localization.LocalizationService.TemporaryContext(ci)) - try + using (Library.Localization.LocalizationService.TemporaryContext(ci)) { - if (ci != null) - info.Response.AddHeader("Content-Language", ci.Name); - - IRESTMethod mod; - _modules.TryGetValue(module, out mod); - - if (mod == null) - { - info.Response.Status = System.Net.HttpStatusCode.NotFound; - info.Response.Reason = "No such module"; - } - else if (method == HttpServer.Method.Get && mod is IRESTMethodGET get) - { - if (info.Request.Form != HttpServer.HttpForm.EmptyForm) - { - if (info.Request.QueryString == HttpServer.HttpInput.Empty) - { - var r = info.Request.GetType().GetField("_queryString", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - r.SetValue(info.Request, new HttpServer.HttpInput("formdata")); - } - - foreach(HttpServer.HttpInputItem v in info.Request.Form) - if (!info.Request.QueryString.Contains(v.Name)) - info.Request.QueryString.Add(v.Name, v.Value); - } - get.GET(key, info); - } - else if (method == HttpServer.Method.Put && mod is IRESTMethodPUT put) - put.PUT(key, info); - else if (method == HttpServer.Method.Post && mod is IRESTMethodPOST post) - { - if (info.Request.Form == HttpServer.HttpForm.EmptyForm || info.Request.Form == HttpServer.HttpInput.Empty) - { - var r = info.Request.GetType().GetMethod("AssignForm", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance, null, new Type[] { typeof(HttpServer.HttpForm) }, null); - r.Invoke(info.Request, new object[] { new HttpServer.HttpForm(info.Request.QueryString) }); - } - else - { - foreach(HttpServer.HttpInputItem v in info.Request.QueryString) - if (!info.Request.Form.Contains(v.Name)) - info.Request.Form.Add(v.Name, v.Value); - } - post.POST(key, info); - } - else if (method == HttpServer.Method.Delete && mod is IRESTMethodDELETE delete) - delete.DELETE(key, info); - else if (method == "PATCH" && mod is IRESTMethodPATCH patch) - patch.PATCH(key, info); - else - { - info.Response.Status = System.Net.HttpStatusCode.MethodNotAllowed; - info.Response.Reason = "Method is not allowed"; - } - } - catch(Exception ex) - { - Program.DataConnection.LogError("", string.Format("Request for {0} gave error", info.Request.Uri), ex); - Console.WriteLine(ex); - try { - if (!info.Response.HeadersSent) - { - info.Response.Status = System.Net.HttpStatusCode.InternalServerError; - info.Response.Reason = "Error"; - info.Response.ContentType = "text/plain"; + if (ci != null) + info.Response.AddHeader("Content-Language", ci.Name); - var wex = ex; - while (wex is System.Reflection.TargetInvocationException && wex.InnerException != wex) - wex = wex.InnerException; - - info.BodyWriter.WriteJsonObject(new + IRESTMethod mod; + _modules.TryGetValue(module, out mod); + + if (mod == null) + { + info.Response.Status = System.Net.HttpStatusCode.NotFound; + info.Response.Reason = "No such module"; + } + else if (method == HttpServer.Method.Get && mod is IRESTMethodGET get) + { + if (info.Request.Form != HttpServer.HttpForm.EmptyForm) { - Message = wex.Message, - Type = wex.GetType().Name, - #if DEBUG - Stacktrace = wex.ToString() - #endif - }); - info.BodyWriter.Flush(); + if (info.Request.QueryString == HttpServer.HttpInput.Empty) + { + var r = info.Request.GetType().GetField("_queryString", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + r.SetValue(info.Request, new HttpServer.HttpInput("formdata")); + } + + foreach (HttpServer.HttpInputItem v in info.Request.Form) + if (!info.Request.QueryString.Contains(v.Name)) + info.Request.QueryString.Add(v.Name, v.Value); + } + + get.GET(key, info); + } + else if (method == HttpServer.Method.Put && mod is IRESTMethodPUT put) + put.PUT(key, info); + else if (method == HttpServer.Method.Post && mod is IRESTMethodPOST post) + { + if (info.Request.Form == HttpServer.HttpForm.EmptyForm || info.Request.Form == HttpServer.HttpInput.Empty) + { + var r = info.Request.GetType().GetMethod("AssignForm", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance, null, new Type[] {typeof(HttpServer.HttpForm)}, null); + r.Invoke(info.Request, new object[] {new HttpServer.HttpForm(info.Request.QueryString)}); + } + else + { + foreach (HttpServer.HttpInputItem v in info.Request.QueryString) + if (!info.Request.Form.Contains(v.Name)) + info.Request.Form.Add(v.Name, v.Value); + } + + post.POST(key, info); + } + else if (method == HttpServer.Method.Delete && mod is IRESTMethodDELETE delete) + delete.DELETE(key, info); + else if (method == "PATCH" && mod is IRESTMethodPATCH patch) + patch.PATCH(key, info); + else + { + info.Response.Status = System.Net.HttpStatusCode.MethodNotAllowed; + info.Response.Reason = "Method is not allowed"; } } - catch (Exception flex) + catch (Exception ex) { - Program.DataConnection.LogError("", "Reporting error gave error", flex); + Program.DataConnection.LogError("", string.Format("Request for {0} gave error", info.Request.Uri), ex); + Console.WriteLine(ex); + + try + { + if (!info.Response.HeadersSent) + { + info.Response.Status = System.Net.HttpStatusCode.InternalServerError; + info.Response.Reason = "Error"; + info.Response.ContentType = "text/plain"; + + var wex = ex; + while (wex is System.Reflection.TargetInvocationException && wex.InnerException != wex) + wex = wex.InnerException; + + info.BodyWriter.WriteJsonObject(new + { + Message = wex.Message, + Type = wex.GetType().Name, +#if DEBUG + Stacktrace = wex.ToString() +#endif + }); + info.BodyWriter.Flush(); + } + } + catch (Exception flex) + { + Program.DataConnection.LogError("", "Reporting error gave error", flex); + } } } } diff --git a/Duplicati/Server/WebServer/RESTMethods/Backup.cs b/Duplicati/Server/WebServer/RESTMethods/Backup.cs index e3f2069e5..926e7ba80 100644 --- a/Duplicati/Server/WebServer/RESTMethods/Backup.cs +++ b/Duplicati/Server/WebServer/RESTMethods/Backup.cs @@ -122,6 +122,12 @@ namespace Duplicati.Server.WebServer.RESTMethods info.OutputOK(new { inuse = Library.Main.DatabaseLocator.IsDatabasePathInUse(backup.DBPath) }); } + public static void RemovePasswords(IBackup backup) + { + backup.SanitizeSettings(); + backup.SanitizeTargetUrl(); + } + private void Export(IBackup backup, RequestInfo info) { var cmdline = Library.Utility.Utility.ParseBool(info.Request.QueryString["cmdline"].Value, false); @@ -129,8 +135,7 @@ namespace Duplicati.Server.WebServer.RESTMethods var exportPasswords = Library.Utility.Utility.ParseBool(info.Request.QueryString["export-passwords"].Value, false); if (!exportPasswords) { - backup.SanitizeSettings(); - backup.SanitizeTargetUrl(); + Backup.RemovePasswords(backup); } if (cmdline) diff --git a/Duplicati/Server/WebServer/SynologyAuthenticationHandler.cs b/Duplicati/Server/WebServer/SynologyAuthenticationHandler.cs index 928e36144..77fef2a19 100644 --- a/Duplicati/Server/WebServer/SynologyAuthenticationHandler.cs +++ b/Duplicati/Server/WebServer/SynologyAuthenticationHandler.cs @@ -193,7 +193,11 @@ namespace Duplicati.Server.WebServer var groups = GetEnvArg("SYNO_GROUP_IDS"); if (string.IsNullOrWhiteSpace(groups)) + { groups = ShellExec("id", "-G '" + username.Trim().Replace("'", "\\'") + "'", exitcode: 0).Result ?? string.Empty; + groups = groups.Replace(Environment.NewLine, String.Empty); + } + if (!groups.Split(new char[] { ' ' }).Contains("101")) { response.Status = System.Net.HttpStatusCode.Forbidden; diff --git a/Duplicati/Server/webroot/ngax/img/_svg/arrow_collapse_blue.svg b/Duplicati/Server/webroot/ngax/img/_svg/arrow_collapse_blue.svg new file mode 100644 index 000000000..c5e6c678c --- /dev/null +++ b/Duplicati/Server/webroot/ngax/img/_svg/arrow_collapse_blue.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Duplicati/Server/webroot/ngax/img/_svg/arrow_collapse_green.svg b/Duplicati/Server/webroot/ngax/img/_svg/arrow_collapse_green.svg new file mode 100644 index 000000000..452e14d22 --- /dev/null +++ b/Duplicati/Server/webroot/ngax/img/_svg/arrow_collapse_green.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Duplicati/Server/webroot/ngax/img/_svg/arrow_expand_blue.svg b/Duplicati/Server/webroot/ngax/img/_svg/arrow_expand_blue.svg new file mode 100644 index 000000000..f5c4707ab --- /dev/null +++ b/Duplicati/Server/webroot/ngax/img/_svg/arrow_expand_blue.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Duplicati/Server/webroot/ngax/img/_svg/arrow_expand_green.svg b/Duplicati/Server/webroot/ngax/img/_svg/arrow_expand_green.svg new file mode 100644 index 000000000..08af7f60b --- /dev/null +++ b/Duplicati/Server/webroot/ngax/img/_svg/arrow_expand_green.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Duplicati/Server/webroot/ngax/img/_svg/backup.svg b/Duplicati/Server/webroot/ngax/img/_svg/backup.svg new file mode 100644 index 000000000..d76a75a11 --- /dev/null +++ b/Duplicati/Server/webroot/ngax/img/_svg/backup.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Duplicati/Server/webroot/ngax/img/_svg/backuppause.svg b/Duplicati/Server/webroot/ngax/img/_svg/backuppause.svg new file mode 100644 index 000000000..39ce448bf --- /dev/null +++ b/Duplicati/Server/webroot/ngax/img/_svg/backuppause.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Duplicati/Server/webroot/ngax/img/_svg/backuprunning.svg b/Duplicati/Server/webroot/ngax/img/_svg/backuprunning.svg new file mode 100644 index 000000000..93fe10a99 --- /dev/null +++ b/Duplicati/Server/webroot/ngax/img/_svg/backuprunning.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Duplicati/Server/webroot/ngax/img/_svg/backupschedule.svg b/Duplicati/Server/webroot/ngax/img/_svg/backupschedule.svg new file mode 100644 index 000000000..5fc55a86f --- /dev/null +++ b/Duplicati/Server/webroot/ngax/img/_svg/backupschedule.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Duplicati/Server/webroot/ngax/img/_svg/clippy.svg b/Duplicati/Server/webroot/ngax/img/_svg/clippy.svg new file mode 100644 index 000000000..3e93580c6 --- /dev/null +++ b/Duplicati/Server/webroot/ngax/img/_svg/clippy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Duplicati/Server/webroot/ngax/img/_svg/external-link-hover.svg b/Duplicati/Server/webroot/ngax/img/_svg/external-link-hover.svg new file mode 100644 index 000000000..75a1bf74b --- /dev/null +++ b/Duplicati/Server/webroot/ngax/img/_svg/external-link-hover.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Duplicati/Server/webroot/ngax/img/_svg/external-link.svg b/Duplicati/Server/webroot/ngax/img/_svg/external-link.svg new file mode 100644 index 000000000..8af71e04f --- /dev/null +++ b/Duplicati/Server/webroot/ngax/img/_svg/external-link.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Duplicati/Server/webroot/ngax/img/_svg/mainmenu/about.svg b/Duplicati/Server/webroot/ngax/img/_svg/mainmenu/about.svg new file mode 100644 index 000000000..d2350f9ba --- /dev/null +++ b/Duplicati/Server/webroot/ngax/img/_svg/mainmenu/about.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Duplicati/Server/webroot/ngax/img/_svg/mainmenu/add.svg b/Duplicati/Server/webroot/ngax/img/_svg/mainmenu/add.svg new file mode 100644 index 000000000..38a71ae1f --- /dev/null +++ b/Duplicati/Server/webroot/ngax/img/_svg/mainmenu/add.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Duplicati/Server/webroot/ngax/img/_svg/mainmenu/home.svg b/Duplicati/Server/webroot/ngax/img/_svg/mainmenu/home.svg new file mode 100644 index 000000000..eef22a907 --- /dev/null +++ b/Duplicati/Server/webroot/ngax/img/_svg/mainmenu/home.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Duplicati/Server/webroot/ngax/img/_svg/mainmenu/logout.svg b/Duplicati/Server/webroot/ngax/img/_svg/mainmenu/logout.svg new file mode 100644 index 000000000..14cdb8600 --- /dev/null +++ b/Duplicati/Server/webroot/ngax/img/_svg/mainmenu/logout.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Duplicati/Server/webroot/ngax/img/_svg/mainmenu/over/about.svg b/Duplicati/Server/webroot/ngax/img/_svg/mainmenu/over/about.svg new file mode 100644 index 000000000..e2ef58138 --- /dev/null +++ b/Duplicati/Server/webroot/ngax/img/_svg/mainmenu/over/about.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Duplicati/Server/webroot/ngax/img/_svg/mainmenu/over/add.svg b/Duplicati/Server/webroot/ngax/img/_svg/mainmenu/over/add.svg new file mode 100644 index 000000000..d2cbc6ed2 --- /dev/null +++ b/Duplicati/Server/webroot/ngax/img/_svg/mainmenu/over/add.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Duplicati/Server/webroot/ngax/img/_svg/mainmenu/over/home.svg b/Duplicati/Server/webroot/ngax/img/_svg/mainmenu/over/home.svg new file mode 100644 index 000000000..219f16f27 --- /dev/null +++ b/Duplicati/Server/webroot/ngax/img/_svg/mainmenu/over/home.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Duplicati/Server/webroot/ngax/img/_svg/mainmenu/over/logout.svg b/Duplicati/Server/webroot/ngax/img/_svg/mainmenu/over/logout.svg new file mode 100644 index 000000000..f7d347c9f --- /dev/null +++ b/Duplicati/Server/webroot/ngax/img/_svg/mainmenu/over/logout.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Duplicati/Server/webroot/ngax/img/_svg/mainmenu/over/restore.svg b/Duplicati/Server/webroot/ngax/img/_svg/mainmenu/over/restore.svg new file mode 100644 index 000000000..b7df4b378 --- /dev/null +++ b/Duplicati/Server/webroot/ngax/img/_svg/mainmenu/over/restore.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Duplicati/Server/webroot/ngax/img/_svg/mainmenu/over/resume.svg b/Duplicati/Server/webroot/ngax/img/_svg/mainmenu/over/resume.svg new file mode 100644 index 000000000..73cb8e080 --- /dev/null +++ b/Duplicati/Server/webroot/ngax/img/_svg/mainmenu/over/resume.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Duplicati/Server/webroot/ngax/img/_svg/mainmenu/over/settings.svg b/Duplicati/Server/webroot/ngax/img/_svg/mainmenu/over/settings.svg new file mode 100644 index 000000000..16a3356d1 --- /dev/null +++ b/Duplicati/Server/webroot/ngax/img/_svg/mainmenu/over/settings.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Duplicati/Server/webroot/ngax/img/_svg/mainmenu/restore.svg b/Duplicati/Server/webroot/ngax/img/_svg/mainmenu/restore.svg new file mode 100644 index 000000000..eef87986e --- /dev/null +++ b/Duplicati/Server/webroot/ngax/img/_svg/mainmenu/restore.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Duplicati/Server/webroot/ngax/img/_svg/mainmenu/resume.svg b/Duplicati/Server/webroot/ngax/img/_svg/mainmenu/resume.svg new file mode 100644 index 000000000..0c68b17b7 --- /dev/null +++ b/Duplicati/Server/webroot/ngax/img/_svg/mainmenu/resume.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Duplicati/Server/webroot/ngax/img/_svg/mainmenu/settings.svg b/Duplicati/Server/webroot/ngax/img/_svg/mainmenu/settings.svg new file mode 100644 index 000000000..8958c1617 --- /dev/null +++ b/Duplicati/Server/webroot/ngax/img/_svg/mainmenu/settings.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Duplicati/Server/webroot/ngax/img/_svg/menu.svg b/Duplicati/Server/webroot/ngax/img/_svg/menu.svg new file mode 100644 index 000000000..2293ad164 --- /dev/null +++ b/Duplicati/Server/webroot/ngax/img/_svg/menu.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Duplicati/Server/webroot/ngax/img/_svg/menu_active.svg b/Duplicati/Server/webroot/ngax/img/_svg/menu_active.svg new file mode 100644 index 000000000..c0cea7698 --- /dev/null +++ b/Duplicati/Server/webroot/ngax/img/_svg/menu_active.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Duplicati/Server/webroot/ngax/img/_svg/pause.svg b/Duplicati/Server/webroot/ngax/img/_svg/pause.svg new file mode 100644 index 000000000..9cccd9e82 --- /dev/null +++ b/Duplicati/Server/webroot/ngax/img/_svg/pause.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Duplicati/Server/webroot/ngax/img/_svg/progress-resume.svg b/Duplicati/Server/webroot/ngax/img/_svg/progress-resume.svg new file mode 100644 index 000000000..84fec1bf2 --- /dev/null +++ b/Duplicati/Server/webroot/ngax/img/_svg/progress-resume.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Duplicati/Server/webroot/ngax/img/_svg/progress-stop.svg b/Duplicati/Server/webroot/ngax/img/_svg/progress-stop.svg new file mode 100644 index 000000000..4c122759a --- /dev/null +++ b/Duplicati/Server/webroot/ngax/img/_svg/progress-stop.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Duplicati/Server/webroot/ngax/img/_svg/resume.svg b/Duplicati/Server/webroot/ngax/img/_svg/resume.svg new file mode 100644 index 000000000..0c68b17b7 --- /dev/null +++ b/Duplicati/Server/webroot/ngax/img/_svg/resume.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Duplicati/Server/webroot/ngax/img/_svg/steps/line-out.svg b/Duplicati/Server/webroot/ngax/img/_svg/steps/line-out.svg new file mode 100644 index 000000000..320663d78 --- /dev/null +++ b/Duplicati/Server/webroot/ngax/img/_svg/steps/line-out.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Duplicati/Server/webroot/ngax/img/_svg/three_dots.svg b/Duplicati/Server/webroot/ngax/img/_svg/three_dots.svg new file mode 100644 index 000000000..370f729db --- /dev/null +++ b/Duplicati/Server/webroot/ngax/img/_svg/three_dots.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Duplicati/Server/webroot/ngax/img/_svg/throttle.svg b/Duplicati/Server/webroot/ngax/img/_svg/throttle.svg new file mode 100644 index 000000000..ccc37a5e6 --- /dev/null +++ b/Duplicati/Server/webroot/ngax/img/_svg/throttle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Duplicati/Server/webroot/ngax/img/_svg/treeicons.svg b/Duplicati/Server/webroot/ngax/img/_svg/treeicons.svg new file mode 100644 index 000000000..9ef6f7502 --- /dev/null +++ b/Duplicati/Server/webroot/ngax/img/_svg/treeicons.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Duplicati/Server/webroot/ngax/img/arrow_collapse_blue.png b/Duplicati/Server/webroot/ngax/img/arrow_collapse_blue.png index 0bf726599..45f1dd4a1 100644 Binary files a/Duplicati/Server/webroot/ngax/img/arrow_collapse_blue.png and b/Duplicati/Server/webroot/ngax/img/arrow_collapse_blue.png differ diff --git a/Duplicati/Server/webroot/ngax/img/arrow_collapse_blue_2x.png b/Duplicati/Server/webroot/ngax/img/arrow_collapse_blue_2x.png new file mode 100644 index 000000000..15fa2ea2f Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/arrow_collapse_blue_2x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/arrow_collapse_blue_3x.png b/Duplicati/Server/webroot/ngax/img/arrow_collapse_blue_3x.png new file mode 100644 index 000000000..edf1d2955 Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/arrow_collapse_blue_3x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/arrow_collapse_green.png b/Duplicati/Server/webroot/ngax/img/arrow_collapse_green.png index 8282f154d..0f79cfaed 100644 Binary files a/Duplicati/Server/webroot/ngax/img/arrow_collapse_green.png and b/Duplicati/Server/webroot/ngax/img/arrow_collapse_green.png differ diff --git a/Duplicati/Server/webroot/ngax/img/arrow_collapse_green_2x.png b/Duplicati/Server/webroot/ngax/img/arrow_collapse_green_2x.png new file mode 100644 index 000000000..bb932301d Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/arrow_collapse_green_2x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/arrow_collapse_green_3x.png b/Duplicati/Server/webroot/ngax/img/arrow_collapse_green_3x.png new file mode 100644 index 000000000..660eba45e Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/arrow_collapse_green_3x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/arrow_expand_blue.png b/Duplicati/Server/webroot/ngax/img/arrow_expand_blue.png index 5d7e101a2..ef7a216d1 100644 Binary files a/Duplicati/Server/webroot/ngax/img/arrow_expand_blue.png and b/Duplicati/Server/webroot/ngax/img/arrow_expand_blue.png differ diff --git a/Duplicati/Server/webroot/ngax/img/arrow_expand_blue_2x.png b/Duplicati/Server/webroot/ngax/img/arrow_expand_blue_2x.png new file mode 100644 index 000000000..bac78cf06 Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/arrow_expand_blue_2x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/arrow_expand_blue_3x.png b/Duplicati/Server/webroot/ngax/img/arrow_expand_blue_3x.png new file mode 100644 index 000000000..b9f8f4b9e Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/arrow_expand_blue_3x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/arrow_expand_green.png b/Duplicati/Server/webroot/ngax/img/arrow_expand_green.png index 370eb5f3d..bf2526488 100644 Binary files a/Duplicati/Server/webroot/ngax/img/arrow_expand_green.png and b/Duplicati/Server/webroot/ngax/img/arrow_expand_green.png differ diff --git a/Duplicati/Server/webroot/ngax/img/arrow_expand_green_2x.png b/Duplicati/Server/webroot/ngax/img/arrow_expand_green_2x.png new file mode 100644 index 000000000..a483be383 Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/arrow_expand_green_2x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/arrow_expand_green_3x.png b/Duplicati/Server/webroot/ngax/img/arrow_expand_green_3x.png new file mode 100644 index 000000000..5007d9012 Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/arrow_expand_green_3x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/backup.png b/Duplicati/Server/webroot/ngax/img/backup.png index 157fdb9c3..d34dd4e64 100755 Binary files a/Duplicati/Server/webroot/ngax/img/backup.png and b/Duplicati/Server/webroot/ngax/img/backup.png differ diff --git a/Duplicati/Server/webroot/ngax/img/backup_2x.png b/Duplicati/Server/webroot/ngax/img/backup_2x.png new file mode 100644 index 000000000..dc9ad4f80 Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/backup_2x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/backup_3x.png b/Duplicati/Server/webroot/ngax/img/backup_3x.png new file mode 100644 index 000000000..f5a8534a7 Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/backup_3x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/backuppause.png b/Duplicati/Server/webroot/ngax/img/backuppause.png index cffabaeb9..acbf63a06 100644 Binary files a/Duplicati/Server/webroot/ngax/img/backuppause.png and b/Duplicati/Server/webroot/ngax/img/backuppause.png differ diff --git a/Duplicati/Server/webroot/ngax/img/backuppause_2x.png b/Duplicati/Server/webroot/ngax/img/backuppause_2x.png new file mode 100644 index 000000000..78e744303 Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/backuppause_2x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/backuppause_3x.png b/Duplicati/Server/webroot/ngax/img/backuppause_3x.png new file mode 100644 index 000000000..d038769a8 Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/backuppause_3x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/backuprunning.png b/Duplicati/Server/webroot/ngax/img/backuprunning.png index f275eee6c..3001f910b 100644 Binary files a/Duplicati/Server/webroot/ngax/img/backuprunning.png and b/Duplicati/Server/webroot/ngax/img/backuprunning.png differ diff --git a/Duplicati/Server/webroot/ngax/img/backuprunning_2x.png b/Duplicati/Server/webroot/ngax/img/backuprunning_2x.png new file mode 100644 index 000000000..88905316b Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/backuprunning_2x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/backuprunning_3x.png b/Duplicati/Server/webroot/ngax/img/backuprunning_3x.png new file mode 100644 index 000000000..a2d382232 Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/backuprunning_3x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/backupschedule.png b/Duplicati/Server/webroot/ngax/img/backupschedule.png index f6833b73c..22e6a4ba8 100644 Binary files a/Duplicati/Server/webroot/ngax/img/backupschedule.png and b/Duplicati/Server/webroot/ngax/img/backupschedule.png differ diff --git a/Duplicati/Server/webroot/ngax/img/backupschedule_2x.png b/Duplicati/Server/webroot/ngax/img/backupschedule_2x.png new file mode 100644 index 000000000..8918d45d9 Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/backupschedule_2x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/backupschedule_3x.png b/Duplicati/Server/webroot/ngax/img/backupschedule_3x.png new file mode 100644 index 000000000..e1b7c114f Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/backupschedule_3x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/clippy.png b/Duplicati/Server/webroot/ngax/img/clippy.png index 0410c163b..e4a284f46 100644 Binary files a/Duplicati/Server/webroot/ngax/img/clippy.png and b/Duplicati/Server/webroot/ngax/img/clippy.png differ diff --git a/Duplicati/Server/webroot/ngax/img/clippy_2x.png b/Duplicati/Server/webroot/ngax/img/clippy_2x.png new file mode 100644 index 000000000..8aee8c875 Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/clippy_2x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/clippy_3x.png b/Duplicati/Server/webroot/ngax/img/clippy_3x.png new file mode 100644 index 000000000..d338ad704 Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/clippy_3x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/donate/bitcoin.png b/Duplicati/Server/webroot/ngax/img/donate/bitcoin.png deleted file mode 100644 index 439758efe..000000000 Binary files a/Duplicati/Server/webroot/ngax/img/donate/bitcoin.png and /dev/null differ diff --git a/Duplicati/Server/webroot/ngax/img/donate/bountysource.png b/Duplicati/Server/webroot/ngax/img/donate/bountysource.png deleted file mode 100644 index 87412b983..000000000 Binary files a/Duplicati/Server/webroot/ngax/img/donate/bountysource.png and /dev/null differ diff --git a/Duplicati/Server/webroot/ngax/img/donate/opencollective.png b/Duplicati/Server/webroot/ngax/img/donate/opencollective.png deleted file mode 100644 index cd7b1b2f2..000000000 Binary files a/Duplicati/Server/webroot/ngax/img/donate/opencollective.png and /dev/null differ diff --git a/Duplicati/Server/webroot/ngax/img/donate/paypal.png b/Duplicati/Server/webroot/ngax/img/donate/paypal.png deleted file mode 100644 index 96193d2d5..000000000 Binary files a/Duplicati/Server/webroot/ngax/img/donate/paypal.png and /dev/null differ diff --git a/Duplicati/Server/webroot/ngax/img/external-link-hover.png b/Duplicati/Server/webroot/ngax/img/external-link-hover.png index 2159277c6..8dad4e30a 100644 Binary files a/Duplicati/Server/webroot/ngax/img/external-link-hover.png and b/Duplicati/Server/webroot/ngax/img/external-link-hover.png differ diff --git a/Duplicati/Server/webroot/ngax/img/external-link-hover_2x.png b/Duplicati/Server/webroot/ngax/img/external-link-hover_2x.png new file mode 100644 index 000000000..00cc7d867 Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/external-link-hover_2x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/external-link-hover_3x.png b/Duplicati/Server/webroot/ngax/img/external-link-hover_3x.png new file mode 100644 index 000000000..5591ecb62 Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/external-link-hover_3x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/external-link.png b/Duplicati/Server/webroot/ngax/img/external-link.png index bd62fd66f..7bade3d51 100644 Binary files a/Duplicati/Server/webroot/ngax/img/external-link.png and b/Duplicati/Server/webroot/ngax/img/external-link.png differ diff --git a/Duplicati/Server/webroot/ngax/img/external-link_2x.png b/Duplicati/Server/webroot/ngax/img/external-link_2x.png new file mode 100644 index 000000000..011ca0427 Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/external-link_2x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/external-link_3x.png b/Duplicati/Server/webroot/ngax/img/external-link_3x.png new file mode 100644 index 000000000..27d23766c Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/external-link_3x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/loader-16.gif b/Duplicati/Server/webroot/ngax/img/loader-16.gif index 1b5b2fde4..ea4f8f18b 100755 Binary files a/Duplicati/Server/webroot/ngax/img/loader-16.gif and b/Duplicati/Server/webroot/ngax/img/loader-16.gif differ diff --git a/Duplicati/Server/webroot/ngax/img/loader-32.gif b/Duplicati/Server/webroot/ngax/img/loader-32.gif index 3288d1035..d02ff9c96 100644 Binary files a/Duplicati/Server/webroot/ngax/img/loader-32.gif and b/Duplicati/Server/webroot/ngax/img/loader-32.gif differ diff --git a/Duplicati/Server/webroot/ngax/img/mainmenu/about.png b/Duplicati/Server/webroot/ngax/img/mainmenu/about.png index 34fe4f2c2..91f316d5a 100644 Binary files a/Duplicati/Server/webroot/ngax/img/mainmenu/about.png and b/Duplicati/Server/webroot/ngax/img/mainmenu/about.png differ diff --git a/Duplicati/Server/webroot/ngax/img/mainmenu/about_2x.png b/Duplicati/Server/webroot/ngax/img/mainmenu/about_2x.png new file mode 100644 index 000000000..2efa4fc7c Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/mainmenu/about_2x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/mainmenu/about_3x.png b/Duplicati/Server/webroot/ngax/img/mainmenu/about_3x.png new file mode 100644 index 000000000..e1b79a308 Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/mainmenu/about_3x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/mainmenu/add.png b/Duplicati/Server/webroot/ngax/img/mainmenu/add.png index c621caa91..8582f3e59 100755 Binary files a/Duplicati/Server/webroot/ngax/img/mainmenu/add.png and b/Duplicati/Server/webroot/ngax/img/mainmenu/add.png differ diff --git a/Duplicati/Server/webroot/ngax/img/mainmenu/add_2x.png b/Duplicati/Server/webroot/ngax/img/mainmenu/add_2x.png new file mode 100644 index 000000000..2d991de5a Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/mainmenu/add_2x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/mainmenu/add_3x.png b/Duplicati/Server/webroot/ngax/img/mainmenu/add_3x.png new file mode 100644 index 000000000..74487df6d Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/mainmenu/add_3x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/mainmenu/arrow_down.png b/Duplicati/Server/webroot/ngax/img/mainmenu/arrow_down.png deleted file mode 100644 index 494cff086..000000000 Binary files a/Duplicati/Server/webroot/ngax/img/mainmenu/arrow_down.png and /dev/null differ diff --git a/Duplicati/Server/webroot/ngax/img/mainmenu/arrow_right.png b/Duplicati/Server/webroot/ngax/img/mainmenu/arrow_right.png deleted file mode 100644 index 4ed6ccd6a..000000000 Binary files a/Duplicati/Server/webroot/ngax/img/mainmenu/arrow_right.png and /dev/null differ diff --git a/Duplicati/Server/webroot/ngax/img/mainmenu/home.png b/Duplicati/Server/webroot/ngax/img/mainmenu/home.png index 25a5bec72..a528b2644 100644 Binary files a/Duplicati/Server/webroot/ngax/img/mainmenu/home.png and b/Duplicati/Server/webroot/ngax/img/mainmenu/home.png differ diff --git a/Duplicati/Server/webroot/ngax/img/mainmenu/home_2x.png b/Duplicati/Server/webroot/ngax/img/mainmenu/home_2x.png new file mode 100644 index 000000000..95844dc3d Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/mainmenu/home_2x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/mainmenu/home_3x.png b/Duplicati/Server/webroot/ngax/img/mainmenu/home_3x.png new file mode 100644 index 000000000..d23648725 Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/mainmenu/home_3x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/mainmenu/log.png b/Duplicati/Server/webroot/ngax/img/mainmenu/log.png deleted file mode 100755 index e245b4137..000000000 Binary files a/Duplicati/Server/webroot/ngax/img/mainmenu/log.png and /dev/null differ diff --git a/Duplicati/Server/webroot/ngax/img/mainmenu/logout.png b/Duplicati/Server/webroot/ngax/img/mainmenu/logout.png index 3827da043..f45f1bc01 100644 Binary files a/Duplicati/Server/webroot/ngax/img/mainmenu/logout.png and b/Duplicati/Server/webroot/ngax/img/mainmenu/logout.png differ diff --git a/Duplicati/Server/webroot/ngax/img/mainmenu/logout_2x.png b/Duplicati/Server/webroot/ngax/img/mainmenu/logout_2x.png new file mode 100644 index 000000000..63d711bcf Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/mainmenu/logout_2x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/mainmenu/logout_3x.png b/Duplicati/Server/webroot/ngax/img/mainmenu/logout_3x.png new file mode 100644 index 000000000..eb4b7676f Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/mainmenu/logout_3x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/mainmenu/over/about.png b/Duplicati/Server/webroot/ngax/img/mainmenu/over/about.png index 9e0249e77..2bb03828c 100644 Binary files a/Duplicati/Server/webroot/ngax/img/mainmenu/over/about.png and b/Duplicati/Server/webroot/ngax/img/mainmenu/over/about.png differ diff --git a/Duplicati/Server/webroot/ngax/img/mainmenu/over/about_2x.png b/Duplicati/Server/webroot/ngax/img/mainmenu/over/about_2x.png new file mode 100644 index 000000000..febdfdd75 Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/mainmenu/over/about_2x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/mainmenu/over/about_3x.png b/Duplicati/Server/webroot/ngax/img/mainmenu/over/about_3x.png new file mode 100644 index 000000000..9f18b923f Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/mainmenu/over/about_3x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/mainmenu/over/add.png b/Duplicati/Server/webroot/ngax/img/mainmenu/over/add.png index 1978b1400..9ac2f2c35 100755 Binary files a/Duplicati/Server/webroot/ngax/img/mainmenu/over/add.png and b/Duplicati/Server/webroot/ngax/img/mainmenu/over/add.png differ diff --git a/Duplicati/Server/webroot/ngax/img/mainmenu/over/add_2x.png b/Duplicati/Server/webroot/ngax/img/mainmenu/over/add_2x.png new file mode 100644 index 000000000..625e40182 Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/mainmenu/over/add_2x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/mainmenu/over/add_3x.png b/Duplicati/Server/webroot/ngax/img/mainmenu/over/add_3x.png new file mode 100644 index 000000000..44a676412 Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/mainmenu/over/add_3x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/mainmenu/over/arrow_down.png b/Duplicati/Server/webroot/ngax/img/mainmenu/over/arrow_down.png deleted file mode 100644 index 6930aca8a..000000000 Binary files a/Duplicati/Server/webroot/ngax/img/mainmenu/over/arrow_down.png and /dev/null differ diff --git a/Duplicati/Server/webroot/ngax/img/mainmenu/over/arrow_right.png b/Duplicati/Server/webroot/ngax/img/mainmenu/over/arrow_right.png deleted file mode 100644 index 16810af6f..000000000 Binary files a/Duplicati/Server/webroot/ngax/img/mainmenu/over/arrow_right.png and /dev/null differ diff --git a/Duplicati/Server/webroot/ngax/img/mainmenu/over/home.png b/Duplicati/Server/webroot/ngax/img/mainmenu/over/home.png index c77fd8910..5e6921131 100644 Binary files a/Duplicati/Server/webroot/ngax/img/mainmenu/over/home.png and b/Duplicati/Server/webroot/ngax/img/mainmenu/over/home.png differ diff --git a/Duplicati/Server/webroot/ngax/img/mainmenu/over/home_2x.png b/Duplicati/Server/webroot/ngax/img/mainmenu/over/home_2x.png new file mode 100644 index 000000000..4c7396138 Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/mainmenu/over/home_2x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/mainmenu/over/home_3x.png b/Duplicati/Server/webroot/ngax/img/mainmenu/over/home_3x.png new file mode 100644 index 000000000..385678cca Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/mainmenu/over/home_3x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/mainmenu/over/log.png b/Duplicati/Server/webroot/ngax/img/mainmenu/over/log.png deleted file mode 100755 index c4ae7d7b5..000000000 Binary files a/Duplicati/Server/webroot/ngax/img/mainmenu/over/log.png and /dev/null differ diff --git a/Duplicati/Server/webroot/ngax/img/mainmenu/over/logout.png b/Duplicati/Server/webroot/ngax/img/mainmenu/over/logout.png index 64ee4ab65..07c9cdb83 100644 Binary files a/Duplicati/Server/webroot/ngax/img/mainmenu/over/logout.png and b/Duplicati/Server/webroot/ngax/img/mainmenu/over/logout.png differ diff --git a/Duplicati/Server/webroot/ngax/img/mainmenu/over/logout_2x.png b/Duplicati/Server/webroot/ngax/img/mainmenu/over/logout_2x.png new file mode 100644 index 000000000..daa5fca81 Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/mainmenu/over/logout_2x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/mainmenu/over/logout_3x.png b/Duplicati/Server/webroot/ngax/img/mainmenu/over/logout_3x.png new file mode 100644 index 000000000..f9424087f Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/mainmenu/over/logout_3x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/mainmenu/over/pause.png b/Duplicati/Server/webroot/ngax/img/mainmenu/over/pause.png deleted file mode 100755 index fd8943fa1..000000000 Binary files a/Duplicati/Server/webroot/ngax/img/mainmenu/over/pause.png and /dev/null differ diff --git a/Duplicati/Server/webroot/ngax/img/mainmenu/over/restore.png b/Duplicati/Server/webroot/ngax/img/mainmenu/over/restore.png index 4d26cd53d..230af425b 100755 Binary files a/Duplicati/Server/webroot/ngax/img/mainmenu/over/restore.png and b/Duplicati/Server/webroot/ngax/img/mainmenu/over/restore.png differ diff --git a/Duplicati/Server/webroot/ngax/img/mainmenu/over/restore_2x.png b/Duplicati/Server/webroot/ngax/img/mainmenu/over/restore_2x.png new file mode 100644 index 000000000..e52edafe5 Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/mainmenu/over/restore_2x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/mainmenu/over/restore_3x.png b/Duplicati/Server/webroot/ngax/img/mainmenu/over/restore_3x.png new file mode 100644 index 000000000..3f57ba0bc Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/mainmenu/over/restore_3x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/mainmenu/over/resume.png b/Duplicati/Server/webroot/ngax/img/mainmenu/over/resume.png index 0eb5d077b..011928ca6 100644 Binary files a/Duplicati/Server/webroot/ngax/img/mainmenu/over/resume.png and b/Duplicati/Server/webroot/ngax/img/mainmenu/over/resume.png differ diff --git a/Duplicati/Server/webroot/ngax/img/mainmenu/over/resume_2x.png b/Duplicati/Server/webroot/ngax/img/mainmenu/over/resume_2x.png new file mode 100644 index 000000000..908b506ed Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/mainmenu/over/resume_2x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/mainmenu/over/resume_3x.png b/Duplicati/Server/webroot/ngax/img/mainmenu/over/resume_3x.png new file mode 100644 index 000000000..653607628 Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/mainmenu/over/resume_3x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/mainmenu/over/settings.png b/Duplicati/Server/webroot/ngax/img/mainmenu/over/settings.png index d42959aad..4a827b0e3 100755 Binary files a/Duplicati/Server/webroot/ngax/img/mainmenu/over/settings.png and b/Duplicati/Server/webroot/ngax/img/mainmenu/over/settings.png differ diff --git a/Duplicati/Server/webroot/ngax/img/mainmenu/over/settings_2x.png b/Duplicati/Server/webroot/ngax/img/mainmenu/over/settings_2x.png new file mode 100644 index 000000000..54424e6f5 Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/mainmenu/over/settings_2x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/mainmenu/over/settings_3x.png b/Duplicati/Server/webroot/ngax/img/mainmenu/over/settings_3x.png new file mode 100644 index 000000000..c63060882 Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/mainmenu/over/settings_3x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/mainmenu/pause.png b/Duplicati/Server/webroot/ngax/img/mainmenu/pause.png deleted file mode 100755 index 4af2b2f9b..000000000 Binary files a/Duplicati/Server/webroot/ngax/img/mainmenu/pause.png and /dev/null differ diff --git a/Duplicati/Server/webroot/ngax/img/mainmenu/restore.png b/Duplicati/Server/webroot/ngax/img/mainmenu/restore.png index a356d0982..d03bfdc00 100755 Binary files a/Duplicati/Server/webroot/ngax/img/mainmenu/restore.png and b/Duplicati/Server/webroot/ngax/img/mainmenu/restore.png differ diff --git a/Duplicati/Server/webroot/ngax/img/mainmenu/restore_2x.png b/Duplicati/Server/webroot/ngax/img/mainmenu/restore_2x.png new file mode 100644 index 000000000..2d805187e Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/mainmenu/restore_2x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/mainmenu/restore_3x.png b/Duplicati/Server/webroot/ngax/img/mainmenu/restore_3x.png new file mode 100644 index 000000000..bcc8237a2 Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/mainmenu/restore_3x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/mainmenu/resume.png b/Duplicati/Server/webroot/ngax/img/mainmenu/resume.png index 1252a9884..20de588b6 100644 Binary files a/Duplicati/Server/webroot/ngax/img/mainmenu/resume.png and b/Duplicati/Server/webroot/ngax/img/mainmenu/resume.png differ diff --git a/Duplicati/Server/webroot/ngax/img/mainmenu/resume_2x.png b/Duplicati/Server/webroot/ngax/img/mainmenu/resume_2x.png new file mode 100644 index 000000000..65e4d01d6 Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/mainmenu/resume_2x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/mainmenu/resume_3x.png b/Duplicati/Server/webroot/ngax/img/mainmenu/resume_3x.png new file mode 100644 index 000000000..0b59a3609 Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/mainmenu/resume_3x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/mainmenu/settings.png b/Duplicati/Server/webroot/ngax/img/mainmenu/settings.png index 43f2a5408..704270c58 100755 Binary files a/Duplicati/Server/webroot/ngax/img/mainmenu/settings.png and b/Duplicati/Server/webroot/ngax/img/mainmenu/settings.png differ diff --git a/Duplicati/Server/webroot/ngax/img/mainmenu/settings_2x.png b/Duplicati/Server/webroot/ngax/img/mainmenu/settings_2x.png new file mode 100644 index 000000000..ab31e9a64 Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/mainmenu/settings_2x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/mainmenu/settings_3x.png b/Duplicati/Server/webroot/ngax/img/mainmenu/settings_3x.png new file mode 100644 index 000000000..08fb5d4ac Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/mainmenu/settings_3x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/menu.png b/Duplicati/Server/webroot/ngax/img/menu.png index 0276764d1..66edc28d9 100755 Binary files a/Duplicati/Server/webroot/ngax/img/menu.png and b/Duplicati/Server/webroot/ngax/img/menu.png differ diff --git a/Duplicati/Server/webroot/ngax/img/menu_2x.png b/Duplicati/Server/webroot/ngax/img/menu_2x.png new file mode 100644 index 000000000..3ccb688e9 Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/menu_2x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/menu_3x.png b/Duplicati/Server/webroot/ngax/img/menu_3x.png new file mode 100644 index 000000000..c998f97f3 Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/menu_3x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/menu_active.png b/Duplicati/Server/webroot/ngax/img/menu_active.png index 91af174bd..906ebaf5f 100755 Binary files a/Duplicati/Server/webroot/ngax/img/menu_active.png and b/Duplicati/Server/webroot/ngax/img/menu_active.png differ diff --git a/Duplicati/Server/webroot/ngax/img/menu_active_2x.png b/Duplicati/Server/webroot/ngax/img/menu_active_2x.png new file mode 100644 index 000000000..94c95e3be Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/menu_active_2x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/menu_active_3x.png b/Duplicati/Server/webroot/ngax/img/menu_active_3x.png new file mode 100644 index 000000000..613f5a2a0 Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/menu_active_3x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/pause.png b/Duplicati/Server/webroot/ngax/img/pause.png index 4af2b2f9b..a58b83d02 100755 Binary files a/Duplicati/Server/webroot/ngax/img/pause.png and b/Duplicati/Server/webroot/ngax/img/pause.png differ diff --git a/Duplicati/Server/webroot/ngax/img/pause_2x.png b/Duplicati/Server/webroot/ngax/img/pause_2x.png new file mode 100644 index 000000000..7b4529b54 Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/pause_2x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/pause_3x.png b/Duplicati/Server/webroot/ngax/img/pause_3x.png new file mode 100644 index 000000000..42808651a Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/pause_3x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/progress-resume.png b/Duplicati/Server/webroot/ngax/img/progress-resume.png index afd6e9035..02218bbc5 100644 Binary files a/Duplicati/Server/webroot/ngax/img/progress-resume.png and b/Duplicati/Server/webroot/ngax/img/progress-resume.png differ diff --git a/Duplicati/Server/webroot/ngax/img/progress-resume_2x.png b/Duplicati/Server/webroot/ngax/img/progress-resume_2x.png new file mode 100644 index 000000000..18181b6ac Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/progress-resume_2x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/progress-resume_3x.png b/Duplicati/Server/webroot/ngax/img/progress-resume_3x.png new file mode 100644 index 000000000..46580b552 Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/progress-resume_3x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/progress-stop.png b/Duplicati/Server/webroot/ngax/img/progress-stop.png index 2d916ebb8..998aad6da 100644 Binary files a/Duplicati/Server/webroot/ngax/img/progress-stop.png and b/Duplicati/Server/webroot/ngax/img/progress-stop.png differ diff --git a/Duplicati/Server/webroot/ngax/img/progress-stop_2x.png b/Duplicati/Server/webroot/ngax/img/progress-stop_2x.png new file mode 100644 index 000000000..337571a55 Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/progress-stop_2x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/progress-stop_3x.png b/Duplicati/Server/webroot/ngax/img/progress-stop_3x.png new file mode 100644 index 000000000..d2f75ce79 Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/progress-stop_3x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/resume.png b/Duplicati/Server/webroot/ngax/img/resume.png index 1252a9884..85281a3ea 100644 Binary files a/Duplicati/Server/webroot/ngax/img/resume.png and b/Duplicati/Server/webroot/ngax/img/resume.png differ diff --git a/Duplicati/Server/webroot/ngax/img/resume_2x.png b/Duplicati/Server/webroot/ngax/img/resume_2x.png new file mode 100644 index 000000000..e5f06a7e2 Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/resume_2x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/resume_3x.png b/Duplicati/Server/webroot/ngax/img/resume_3x.png new file mode 100644 index 000000000..803a2f363 Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/resume_3x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/social/duplicati.png b/Duplicati/Server/webroot/ngax/img/social/duplicati.png index 3d8b785c5..447b06a6e 100644 Binary files a/Duplicati/Server/webroot/ngax/img/social/duplicati.png and b/Duplicati/Server/webroot/ngax/img/social/duplicati.png differ diff --git a/Duplicati/Server/webroot/ngax/img/social/duplicati_2x.png b/Duplicati/Server/webroot/ngax/img/social/duplicati_2x.png new file mode 100644 index 000000000..d60b23e46 Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/social/duplicati_2x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/social/duplicati_3x.png b/Duplicati/Server/webroot/ngax/img/social/duplicati_3x.png new file mode 100644 index 000000000..5e28d149e Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/social/duplicati_3x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/social/facebook.png b/Duplicati/Server/webroot/ngax/img/social/facebook.png index 57e98ebf0..bed3e0c7a 100755 Binary files a/Duplicati/Server/webroot/ngax/img/social/facebook.png and b/Duplicati/Server/webroot/ngax/img/social/facebook.png differ diff --git a/Duplicati/Server/webroot/ngax/img/social/facebook_2x.png b/Duplicati/Server/webroot/ngax/img/social/facebook_2x.png new file mode 100644 index 000000000..25ca03ce5 Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/social/facebook_2x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/social/facebook_3x.png b/Duplicati/Server/webroot/ngax/img/social/facebook_3x.png new file mode 100644 index 000000000..a696c9f21 Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/social/facebook_3x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/social/ggroups.png b/Duplicati/Server/webroot/ngax/img/social/ggroups.png deleted file mode 100644 index 1a976ee76..000000000 Binary files a/Duplicati/Server/webroot/ngax/img/social/ggroups.png and /dev/null differ diff --git a/Duplicati/Server/webroot/ngax/img/social/github.png b/Duplicati/Server/webroot/ngax/img/social/github.png index 8141ba806..1670e8d11 100755 Binary files a/Duplicati/Server/webroot/ngax/img/social/github.png and b/Duplicati/Server/webroot/ngax/img/social/github.png differ diff --git a/Duplicati/Server/webroot/ngax/img/social/github_2x.png b/Duplicati/Server/webroot/ngax/img/social/github_2x.png new file mode 100644 index 000000000..6f4a3b55e Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/social/github_2x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/social/github_3x.png b/Duplicati/Server/webroot/ngax/img/social/github_3x.png new file mode 100644 index 000000000..c273f8fa4 Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/social/github_3x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/steps/line-out.png b/Duplicati/Server/webroot/ngax/img/steps/line-out.png index 105481955..f93157855 100755 Binary files a/Duplicati/Server/webroot/ngax/img/steps/line-out.png and b/Duplicati/Server/webroot/ngax/img/steps/line-out.png differ diff --git a/Duplicati/Server/webroot/ngax/img/steps/line-out_2x.png b/Duplicati/Server/webroot/ngax/img/steps/line-out_2x.png new file mode 100644 index 000000000..ae9fb76d9 Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/steps/line-out_2x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/steps/line-out_3x.png b/Duplicati/Server/webroot/ngax/img/steps/line-out_3x.png new file mode 100644 index 000000000..e01f618db Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/steps/line-out_3x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/three_dots.png b/Duplicati/Server/webroot/ngax/img/three_dots.png index 036bc008b..1cddf9080 100644 Binary files a/Duplicati/Server/webroot/ngax/img/three_dots.png and b/Duplicati/Server/webroot/ngax/img/three_dots.png differ diff --git a/Duplicati/Server/webroot/ngax/img/three_dots_2x.png b/Duplicati/Server/webroot/ngax/img/three_dots_2x.png new file mode 100644 index 000000000..c2a70fd6d Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/three_dots_2x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/three_dots_3x.png b/Duplicati/Server/webroot/ngax/img/three_dots_3x.png new file mode 100644 index 000000000..8304fc59d Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/three_dots_3x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/throttle-inactive.png b/Duplicati/Server/webroot/ngax/img/throttle-inactive.png deleted file mode 100644 index 046ada18e..000000000 Binary files a/Duplicati/Server/webroot/ngax/img/throttle-inactive.png and /dev/null differ diff --git a/Duplicati/Server/webroot/ngax/img/throttle.png b/Duplicati/Server/webroot/ngax/img/throttle.png index a87690622..8a348aba9 100644 Binary files a/Duplicati/Server/webroot/ngax/img/throttle.png and b/Duplicati/Server/webroot/ngax/img/throttle.png differ diff --git a/Duplicati/Server/webroot/ngax/img/throttle_2x.png b/Duplicati/Server/webroot/ngax/img/throttle_2x.png new file mode 100644 index 000000000..6cb921f2f Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/throttle_2x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/throttle_3x.png b/Duplicati/Server/webroot/ngax/img/throttle_3x.png new file mode 100644 index 000000000..22e56c065 Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/throttle_3x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/treeicons.png b/Duplicati/Server/webroot/ngax/img/treeicons.png index 71110da04..aa8f87d55 100644 Binary files a/Duplicati/Server/webroot/ngax/img/treeicons.png and b/Duplicati/Server/webroot/ngax/img/treeicons.png differ diff --git a/Duplicati/Server/webroot/ngax/img/treeicons_2x.png b/Duplicati/Server/webroot/ngax/img/treeicons_2x.png new file mode 100644 index 000000000..af71b395a Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/treeicons_2x.png differ diff --git a/Duplicati/Server/webroot/ngax/img/treeicons_3x.png b/Duplicati/Server/webroot/ngax/img/treeicons_3x.png new file mode 100644 index 000000000..ff4125f26 Binary files /dev/null and b/Duplicati/Server/webroot/ngax/img/treeicons_3x.png differ diff --git a/Duplicati/Server/webroot/ngax/index.html b/Duplicati/Server/webroot/ngax/index.html index 2afaff9b2..bbed7a651 100755 --- a/Duplicati/Server/webroot/ngax/index.html +++ b/Duplicati/Server/webroot/ngax/index.html @@ -263,16 +263,16 @@ {{'Visit us on' | translate}}
  • - {{'Duplicati forum' | translate}} + {{'Duplicati forum' | translate}}
  • - Github + Github
  • - Facebook + Facebook
diff --git a/Duplicati/Server/webroot/ngax/less/style.less b/Duplicati/Server/webroot/ngax/less/style.less index 3cbeffbdc..4382e63cd 100755 --- a/Duplicati/Server/webroot/ngax/less/style.less +++ b/Duplicati/Server/webroot/ngax/less/style.less @@ -73,12 +73,40 @@ textarea { width: 16px; background: url('../img/external-link-hover.png'); background-size: 16px; + + @media + only screen and (-webkit-min-device-pixel-ratio: 1.25), + only screen and (min-resolution: 192dpi), + only screen and (min-resolution: 1.25dppx) { + background-image: url('../img/external-link-hover_2x.png'); + } + + @media + only screen and (-webkit-min-device-pixel-ratio: 1.25), + only screen and (min-resolution: 288dpi), + only screen and (min-resolution: 2.25dppx) { + background-image: url('../img/external-link-hover_3x.png'); + } } a { .external-link-image { background: url('../img/external-link.png'); background-size: 16px; + + @media + only screen and (-webkit-min-device-pixel-ratio: 1.25), + only screen and (min-resolution: 192dpi), + only screen and (min-resolution: 1.25dppx) { + background-image: url('../img/external-link_2x.png'); + } + + @media + only screen and (-webkit-min-device-pixel-ratio: 2.25), + only screen and (min-resolution: 288dpi), + only screen and (min-resolution: 2.25dppx) { + background-image: url('../img/external-link_3x.png'); + } } } @@ -87,6 +115,20 @@ a { .external-link-image { background: url('../img/external-link-hover.png'); background-size: 16px; + + @media + only screen and (-webkit-min-device-pixel-ratio: 1.25), + only screen and (min-resolution: 192dpi), + only screen and (min-resolution: 1.25dppx) { + background-image: url('../img/external-link-hover_2x.png'); + } + + @media + only screen and (-webkit-min-device-pixel-ratio: 2.25), + only screen and (min-resolution: 288dpi), + only screen and (min-resolution: 2.25dppx) { + background-image: url('../img/external-link-hover_3x.png'); + } } } } @@ -631,6 +673,20 @@ body { background: url('../img/progress-stop.png'); background-size: 26px; cursor: pointer; + + @media + only screen and (-webkit-min-device-pixel-ratio: 1.25), + only screen and (min-resolution: 192dpi), + only screen and (min-resolution: 1.25dppx) { + background-image: url('../img/progress-stop_2x.png'); + } + + @media + only screen and (-webkit-min-device-pixel-ratio: 2.25), + only screen and (min-resolution: 288dpi), + only screen and (min-resolution: 2.25dppx) { + background-image: url('../img/progress-stop_3x.png'); + } } .resume { @@ -640,6 +696,20 @@ body { background: url('../img/progress-resume.png'); background-size: 26px; cursor: pointer; + + @media + only screen and (-webkit-min-device-pixel-ratio: 1.25), + only screen and (min-resolution: 192dpi), + only screen and (min-resolution: 1.25dppx) { + background-image: url('../img/progress-resume_2x.png'); + } + + @media + only screen and (-webkit-min-device-pixel-ratio: 2.25), + only screen and (min-resolution: 288dpi), + only screen and (min-resolution: 2.25dppx) { + background-image: url('../img/progress-resume_3x.png'); + } } } @@ -686,10 +756,40 @@ body { display: inline-block; cursor: pointer; background: url('../img/pause.png'); + background-size: 26px; + + @media + only screen and (-webkit-min-device-pixel-ratio: 1.25), + only screen and (min-resolution: 192dpi), + only screen and (min-resolution: 1.25dppx) { + background-image: url('../img/pause_2x.png'); + } + + @media + only screen and (-webkit-min-device-pixel-ratio: 2.25), + only screen and (min-resolution: 288dpi), + only screen and (min-resolution: 2.25dppx) { + background-image: url('../img/pause_3x.png'); + } } > .pause.active { background: url('../img/resume.png'); + background-size: 26px; + + @media + only screen and (-webkit-min-device-pixel-ratio: 1.25), + only screen and (min-resolution: 192dpi), + only screen and (min-resolution: 1.25dppx) { + background-image: url('../img/resume_2x.png'); + } + + @media + only screen and (-webkit-min-device-pixel-ratio: 2.25), + only screen and (min-resolution: 288dpi), + only screen and (min-resolution: 2.25dppx) { + background-image: url('../img/resume_3x.png'); + } } > .throttle { @@ -698,6 +798,21 @@ body { display: inline-block; cursor: pointer; background: url('../img/throttle.png'); + background-size: 26px; + + @media + only screen and (-webkit-min-device-pixel-ratio: 1.25), + only screen and (min-resolution: 192dpi), + only screen and (min-resolution: 1.25dppx) { + background-image: url('../img/throttle_2x.png'); + } + + @media + only screen and (-webkit-min-device-pixel-ratio: 2.25), + only screen and (min-resolution: 288dpi), + only screen and (min-resolution: 2.25dppx) { + background-image: url('../img/throttle_3x.png'); + } } > .throttle.inactive { @@ -787,137 +902,382 @@ body { > a.add { background: url('../img/mainmenu/add.png') no-repeat 8px 7px; + background-size: 27px 26px; + + @media + only screen and (-webkit-min-device-pixel-ratio: 1.25), + only screen and (min-resolution: 192dpi), + only screen and (min-resolution: 1.25dppx) { + background-image: url('../img/mainmenu/add_2x.png'); + } + + @media + only screen and (-webkit-min-device-pixel-ratio: 2.25), + only screen and (min-resolution: 288dpi), + only screen and (min-resolution: 2.25dppx) { + background-image: url('../img/mainmenu/add_3x.png'); + } } > a.restore { background: url('../img/mainmenu/restore.png') no-repeat 8px 7px; - } + background-size: 27px 26px; - > a.pause { - background: url('../img/mainmenu/pause.png') no-repeat 8px 7px; + @media + only screen and (-webkit-min-device-pixel-ratio: 1.25), + only screen and (min-resolution: 192dpi), + only screen and (min-resolution: 1.25dppx) { + background-image: url('../img/mainmenu/restore_2x.png'); + } + + @media + only screen and (-webkit-min-device-pixel-ratio: 2.25), + only screen and (min-resolution: 288dpi), + only screen and (min-resolution: 2.25dppx) { + background-image: url('../img/mainmenu/restore_3x.png'); + } } > a.resume { background: url('../img/mainmenu/resume.png') no-repeat 8px 7px; + background-size: 27px 26px; + + @media + only screen and (-webkit-min-device-pixel-ratio: 1.25), + only screen and (min-resolution: 192dpi), + only screen and (min-resolution: 1.25dppx) { + background-image: url('../img/mainmenu/resume_2x.png'); + } + + @media + only screen and (-webkit-min-device-pixel-ratio: 2.25), + only screen and (min-resolution: 288dpi), + only screen and (min-resolution: 2.25dppx) { + background-image: url('../img/mainmenu/resume_3x.png'); + } } > a.settings { background: url('../img/mainmenu/settings.png') no-repeat 8px 7px; - } + background-size: 27px 26px; - > a.log { - background: url('../img/mainmenu/log.png') no-repeat 8px 7px; + @media + only screen and (-webkit-min-device-pixel-ratio: 1.25), + only screen and (min-resolution: 192dpi), + only screen and (min-resolution: 1.25dppx) { + background-image: url('../img/mainmenu/settings_2x.png'); + } + + @media + only screen and (-webkit-min-device-pixel-ratio: 2.25), + only screen and (min-resolution: 288dpi), + only screen and (min-resolution: 2.25dppx) { + background-image: url('../img/mainmenu/settings_3x.png'); + } } > a.logout { background: url('../img/mainmenu/logout.png') no-repeat 8px 7px; + background-size: 27px 26px; + + @media + only screen and (-webkit-min-device-pixel-ratio: 1.25), + only screen and (min-resolution: 192dpi), + only screen and (min-resolution: 1.25dppx) { + background-image: url('../img/mainmenu/logout_2x.png'); + } + + @media + only screen and (-webkit-min-device-pixel-ratio: 2.25), + only screen and (min-resolution: 288dpi), + only screen and (min-resolution: 2.25dppx) { + background-image: url('../img/mainmenu/logout_3x.png'); + } } > a.home { background: url('../img/mainmenu/home.png') no-repeat 8px 7px; + background-size: 27px 26px; + + @media + only screen and (-webkit-min-device-pixel-ratio: 1.25), + only screen and (min-resolution: 192dpi), + only screen and (min-resolution: 1.25dppx) { + background-image: url('../img/mainmenu/home_2x.png'); + } + + @media + only screen and (-webkit-min-device-pixel-ratio: 2.25), + only screen and (min-resolution: 288dpi), + only screen and (min-resolution: 2.25dppx) { + background-image: url('../img/mainmenu/home_3x.png'); + } } > a.about { background: url('../img/mainmenu/about.png') no-repeat 8px 7px; - } + background-size: 27px 26px; - > a.pause { - > span { - padding-right: 25px; - background: url('../img/mainmenu/arrow_right.png') right center no-repeat; + @media + only screen and (-webkit-min-device-pixel-ratio: 1.25), + only screen and (min-resolution: 192dpi), + only screen and (min-resolution: 1.25dppx) { + background-image: url('../img/mainmenu/about_2x.png'); + } + + @media + only screen and (-webkit-min-device-pixel-ratio: 2.25), + only screen and (min-resolution: 288dpi), + only screen and (min-resolution: 2.25dppx) { + background-image: url('../img/mainmenu/about_3x.png'); } } > a.home.active { background: lighten(@lColor, 15%) url('../img/mainmenu/over/home.png') no-repeat 8px 7px; + background-size: 27px 26px; + + @media + only screen and (-webkit-min-device-pixel-ratio: 1.25), + only screen and (min-resolution: 192dpi), + only screen and (min-resolution: 1.25dppx) { + background-image: url('../img/mainmenu/over/home_2x.png'); + } + + @media + only screen and (-webkit-min-device-pixel-ratio: 2.25), + only screen and (min-resolution: 288dpi), + only screen and (min-resolution: 2.25dppx) { + background-image: url('../img/mainmenu/over/home_3x.png'); + } } > a.add.active { background: lighten(@lColor, 15%) url('../img/mainmenu/over/add.png') no-repeat 8px 7px; + background-size: 27px 26px; + + @media + only screen and (-webkit-min-device-pixel-ratio: 1.25), + only screen and (min-resolution: 192dpi), + only screen and (min-resolution: 1.25dppx) { + background-image: url('../img/mainmenu/over/add_2x.png'); + } + + @media + only screen and (-webkit-min-device-pixel-ratio: 2.25), + only screen and (min-resolution: 288dpi), + only screen and (min-resolution: 2.25dppx) { + background-image: url('../img/mainmenu/over/add_3x.png'); + } } > a.restore.active { background: lighten(@lColor, 15%) url('../img/mainmenu/over/restore.png') no-repeat 8px 7px; - } + background-size: 27px 26px; - > a.pause.active { - background: lighten(@lColor, 15%) url('../img/mainmenu/over/pause.png') no-repeat 8px 7px; + @media + only screen and (-webkit-min-device-pixel-ratio: 1.25), + only screen and (min-resolution: 192dpi), + only screen and (min-resolution: 1.25dppx) { + background-image: url('../img/mainmenu/over/restore_2x.png'); + } + + @media + only screen and (-webkit-min-device-pixel-ratio: 2.25), + only screen and (min-resolution: 288dpi), + only screen and (min-resolution: 2.25dppx) { + background-image: url('../img/mainmenu/over/restore_3x.png'); + } } > a.resume.active { background: lighten(@lColor, 15%) url('../img/mainmenu/over/resume.png') no-repeat 8px 7px; + background-size: 27px 26px; + + @media + only screen and (-webkit-min-device-pixel-ratio: 1.25), + only screen and (min-resolution: 192dpi), + only screen and (min-resolution: 1.25dppx) { + background-image: url('../img/mainmenu/over/resume_2x.png'); + } + + @media + only screen and (-webkit-min-device-pixel-ratio: 2.25), + only screen and (min-resolution: 288dpi), + only screen and (min-resolution: 2.25dppx) { + background-image: url('../img/mainmenu/over/resume_3x.png'); + } } > a.settings.active { background: lighten(@lColor, 15%) url('../img/mainmenu/over/settings.png') no-repeat 8px 7px; - } + background-size: 27px 26px; - > a.log.active { - background: lighten(@lColor, 15%) url('../img/mainmenu/over/log.png') no-repeat 8px 7px; + @media + only screen and (-webkit-min-device-pixel-ratio: 1.25), + only screen and (min-resolution: 192dpi), + only screen and (min-resolution: 1.25dppx) { + background-image: url('../img/mainmenu/over/settings_2x.png'); + } + + @media + only screen and (-webkit-min-device-pixel-ratio: 2.25), + only screen and (min-resolution: 288dpi), + only screen and (min-resolution: 2.25dppx) { + background-image: url('../img/mainmenu/over/settings_3x.png'); + } } > a.about.active { background: lighten(@lColor, 15%) url('../img/mainmenu/over/about.png') no-repeat 8px 7px; - } + background-size: 27px 26px; - > a.pause.open.active { - > span { - background: url('../img/mainmenu/over/arrow_down.png') right center no-repeat; + @media + only screen and (-webkit-min-device-pixel-ratio: 1.25), + only screen and (min-resolution: 192dpi), + only screen and (min-resolution: 1.25dppx) { + background-image: url('../img/mainmenu/over/about_2x.png'); + } + + @media + only screen and (-webkit-min-device-pixel-ratio: 2.25), + only screen and (min-resolution: 288dpi), + only screen and (min-resolution: 2.25dppx) { + background-image: url('../img/mainmenu/over/about_3x.png'); } } > a.add:hover { background: @lColor url('../img/mainmenu/over/add.png') no-repeat 8px 7px; + background-size: 27px 26px; + + @media + only screen and (-webkit-min-device-pixel-ratio: 1.25), + only screen and (min-resolution: 192dpi), + only screen and (min-resolution: 1.25dppx) { + background-image: url('../img/mainmenu/over/add_2x.png'); + } + + @media + only screen and (-webkit-min-device-pixel-ratio: 2.25), + only screen and (min-resolution: 288dpi), + only screen and (min-resolution: 2.25dppx) { + background-image: url('../img/mainmenu/over/add_3x.png'); + } } > a.restore:hover { background: @lColor url('../img/mainmenu/over/restore.png') no-repeat 8px 7px; - } + background-size: 27px 26px; - > a.pause:hover { - background: @lColor url('../img/mainmenu/over/pause.png') no-repeat 8px 7px; - } - - > a.pause:hover { - > span { - background: url('../img/mainmenu/over/arrow_right.png') right center no-repeat; + @media + only screen and (-webkit-min-device-pixel-ratio: 1.25), + only screen and (min-resolution: 192dpi), + only screen and (min-resolution: 1.25dppx) { + background-image: url('../img/mainmenu/over/restore_2x.png'); } - } - > a.pause.open:hover { - > span { - background: url('../img/mainmenu/over/arrow_down.png') right center no-repeat; - } - } - - > a.pause.open { - > span { - background: url('../img/mainmenu/arrow_down.png') right center no-repeat; + @media + only screen and (-webkit-min-device-pixel-ratio: 2.25), + only screen and (min-resolution: 288dpi), + only screen and (min-resolution: 2.25dppx) { + background-image: url('../img/mainmenu/over/restore_3x.png'); } } > a.resume:hover { background: @lColor url('../img/mainmenu/over/resume.png') no-repeat 8px 7px; + background-size: 27px 26px; + + @media + only screen and (-webkit-min-device-pixel-ratio: 1.25), + only screen and (min-resolution: 192dpi), + only screen and (min-resolution: 1.25dppx) { + background-image: url('../img/mainmenu/over/resume_2x.png'); + } + + @media + only screen and (-webkit-min-device-pixel-ratio: 2.25), + only screen and (min-resolution: 288dpi), + only screen and (min-resolution: 2.25dppx) { + background-image: url('../img/mainmenu/over/resume_3x.png'); + } } > a.settings:hover { background: @lColor url('../img/mainmenu/over/settings.png') no-repeat 8px 7px; - } + background-size: 27px 26px; - > a.log:hover { - background: @lColor url('../img/mainmenu/over/log.png') no-repeat 8px 7px; + @media + only screen and (-webkit-min-device-pixel-ratio: 1.25), + only screen and (min-resolution: 192dpi), + only screen and (min-resolution: 1.25dppx) { + background-image: url('../img/mainmenu/over/settings_2x.png'); + } + + @media + only screen and (-webkit-min-device-pixel-ratio: 2.25), + only screen and (min-resolution: 288dpi), + only screen and (min-resolution: 2.25dppx) { + background-image: url('../img/mainmenu/over/settings_3x.png'); + } } > a.logout:hover { background: @lColor url('../img/mainmenu/over/logout.png') no-repeat 8px 7px; + background-size: 27px 26px; + + @media + only screen and (-webkit-min-device-pixel-ratio: 1.25), + only screen and (min-resolution: 192dpi), + only screen and (min-resolution: 1.25dppx) { + background-image: url('../img/mainmenu/over/logout_2x.png'); + } + + @media + only screen and (-webkit-min-device-pixel-ratio: 2.25), + only screen and (min-resolution: 288dpi), + only screen and (min-resolution: 2.25dppx) { + background-image: url('../img/mainmenu/over/logout_3x.png'); + } } > a.home:hover { background: @lColor url('../img/mainmenu/over/home.png') no-repeat 8px 7px; + background-size: 27px 26px; + + @media + only screen and (-webkit-min-device-pixel-ratio: 1.25), + only screen and (min-resolution: 192dpi), + only screen and (min-resolution: 1.25dppx) { + background-image: url('../img/mainmenu/over/home_2x.png'); + } + + @media + only screen and (-webkit-min-device-pixel-ratio: 2.25), + only screen and (min-resolution: 288dpi), + only screen and (min-resolution: 2.25dppx) { + background-image: url('../img/mainmenu/over/home_3x.png'); + } } > a.about:hover { background: @lColor url('../img/mainmenu/over/about.png') no-repeat 8px 7px; + background-size: 27px 26px; + + @media + only screen and (-webkit-min-device-pixel-ratio: 1.25), + only screen and (min-resolution: 192dpi), + only screen and (min-resolution: 1.25dppx) { + background-image: url('../img/mainmenu/over/about_2x.png'); + } + + @media + only screen and (-webkit-min-device-pixel-ratio: 2.25), + only screen and (min-resolution: 288dpi), + only screen and (min-resolution: 2.25dppx) { + background-image: url('../img/mainmenu/over/about_3x.png'); + } } } @@ -1087,8 +1447,23 @@ body { .step { float: left; background: url('../img/steps/line-out.png') no-repeat top left; + background-size: 485px 24px; color: #c7e5f6; + @media + only screen and (-webkit-min-device-pixel-ratio: 1.25), + only screen and (min-resolution: 192dpi), + only screen and (min-resolution: 1.25dppx) { + background-image: url('../img/steps/line-out_2x.png'); + } + + @media + only screen and (-webkit-min-device-pixel-ratio: 2.25), + only screen and (min-resolution: 288dpi), + only screen and (min-resolution: 2.25dppx) { + background-image: url('../img/steps/line-out_3x.png'); + } + span { display: block; border: 4px #c7e5f6 solid; @@ -2091,6 +2466,21 @@ div.modal-dialog { background-image: url(../img/treeicons.png); background-repeat: no-repeat; background-position: -80px 0px; + background-size: 112px 64px; + + @media + only screen and (-webkit-min-device-pixel-ratio: 1.25), + only screen and (min-resolution: 192dpi), + only screen and (min-resolution: 1.25dppx) { + background-image: url('../img/treeicons_2x.png'); + } + + @media + only screen and (-webkit-min-device-pixel-ratio: 2.25), + only screen and (min-resolution: 288dpi), + only screen and (min-resolution: 2.25dppx) { + background-image: url('../img/treeicons_3x.png'); + } &.leaf { background: none; @@ -2110,6 +2500,21 @@ div.modal-dialog { background-image: url(../img/treeicons.png); background-repeat: no-repeat; background-position: 0px -16px; + background-size: 112px 64px; + + @media + only screen and (-webkit-min-device-pixel-ratio: 1.25), + only screen and (min-resolution: 192dpi), + only screen and (min-resolution: 1.25dppx) { + background-image: url('../img/treeicons_2x.png'); + } + + @media + only screen and (-webkit-min-device-pixel-ratio: 2.25), + only screen and (min-resolution: 288dpi), + only screen and (min-resolution: 2.25dppx) { + background-image: url('../img/treeicons_3x.png'); + } &.invisible { background-position: 0px -32px; @@ -2120,6 +2525,14 @@ div.modal-dialog { background-image: url(../img/loader-16.gif); background-repeat: no-repeat; background-position: 0px 0px; + background-size: 16px 16px; + + @media + only screen and (-webkit-min-device-pixel-ratio: 1.25), + only screen and (min-resolution: 192dpi), + only screen and (min-resolution: 1.25dppx) { + background-image: url('../img/loader-32.gif'); + } } &.x-tree-icon-drive { @@ -2211,8 +2624,23 @@ div.modal-dialog { cursor: pointer; background-image: url(../img/treeicons.png); background-repeat: no-repeat; - background-position: 0px 0px; + background-position: 0 0; vertical-align: middle; + background-size: 112px 64px; + + @media + only screen and (-webkit-min-device-pixel-ratio: 1.25), + only screen and (min-resolution: 192dpi), + only screen and (min-resolution: 1.25dppx) { + background-image: url('../img/treeicons_2x.png'); + } + + @media + only screen and (-webkit-min-device-pixel-ratio: 2.25), + only screen and (min-resolution: 288dpi), + only screen and (min-resolution: 2.25dppx) { + background-image: url('../img/treeicons_3x.png'); + } } &.partial { @@ -2315,6 +2743,7 @@ div.modal-dialog { margin-top: 5px; margin-right: 15px; background: url('../img/menu.png') no-repeat right top; + background-size: 39px 39px; position: relative; height: 40px; line-height: 40px; @@ -2324,11 +2753,40 @@ div.modal-dialog { padding-left: 20px; text-transform: uppercase; text-align: right; + + @media + (-webkit-min-device-pixel-ratio: 1.25), + (min-resolution: 192dpi), + (min-resolution: 1.25dppx) { + background-image: url('../img/menu_2x.png'); + } + + @media + (-webkit-min-device-pixel-ratio: 2.25), + (min-resolution: 288dpi), + (min-resolution: 2.25dppx) { + background-image: url('../img/menu_3x.png'); + } } .menubutton.active { background-image: url('../img/menu_active.png'); + background-size: 39px 39px; color: @lColor; + + @media + (-webkit-min-device-pixel-ratio: 1.25), + (min-resolution: 192dpi), + (min-resolution: 1.25dppx) { + background-image: url('../img/menu_active_2x.png'); + } + + @media + (-webkit-min-device-pixel-ratio: 2.25), + (min-resolution: 288dpi), + (min-resolution: 2.25dppx) { + background-image: url('../img/menu_active_3x.png'); + } } .donate { @@ -2641,14 +3099,6 @@ div.modal-dialog { } } - li.support { - background: url('../img/support.png') no-repeat center center; - } - - li.about { - background: url('../img/about.png') no-repeat center center; - } - li:first-child { padding-bottom: 0; } diff --git a/Duplicati/Server/webroot/ngax/scripts/controllers/RestoreController.js b/Duplicati/Server/webroot/ngax/scripts/controllers/RestoreController.js index 0ff98a378..346c64fdf 100644 --- a/Duplicati/Server/webroot/ngax/scripts/controllers/RestoreController.js +++ b/Duplicati/Server/webroot/ngax/scripts/controllers/RestoreController.js @@ -346,10 +346,22 @@ backupApp.controller('RestoreController', function ($rootScope, $scope, $routePa var paths = []; for(var n in $scope.Selected) { var item = $scope.Selected[n]; - if (item.substr(item.length - 1) == dirsep) - paths.push(item + '*'); - else - paths.push(item); + if (item.substr(item.length - 1) == dirsep) { + // To support the possibility of encountering paths + // with literal wildcard characters, but also being + // able to add the globbing "*" suffix, use a regular + // expression filter + + // Escape regular expression metacharacters + var itemRegex = item.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + // Add "globbing" suffix + paths.push('[' + itemRegex + '.*]'); + } else { + // To support the possibility of encountering paths + // with literal wildcard characters, create a literal + // filter + paths.push('@' + item); + } } if (paths.length > 0) diff --git a/Duplicati/Server/webroot/ngax/scripts/directives/backupEditUri.js b/Duplicati/Server/webroot/ngax/scripts/directives/backupEditUri.js index 8f44f7410..d20a8105e 100644 --- a/Duplicati/Server/webroot/ngax/scripts/directives/backupEditUri.js +++ b/Duplicati/Server/webroot/ngax/scripts/directives/backupEditUri.js @@ -209,7 +209,7 @@ backupApp.directive('backupEditUri', function(gettextCatalog) { var message = ((prev || '').trim().length == 0) ? (gettextCatalog.getString('No certificate was specified previously, please verify with the server administrator that the key is correct: {{key}} \n\nDo you want to approve the reported host key?', { key: key })) : - (gettextCatalog.getString('The host key has changed, please check with the server administrator if this is correct, otherwise you could be the victim of a MAN-IN-THE-MIDDLE attack.\n\nDo you want to REPLACE your CURRENT host key "{{prev}}" with the REPORTED host key: {{key}}?', { prev: pref, key: key })); + (gettextCatalog.getString('The host key has changed, please check with the server administrator if this is correct, otherwise you could be the victim of a MAN-IN-THE-MIDDLE attack.\n\nDo you want to REPLACE your CURRENT host key "{{prev}}" with the REPORTED host key: {{key}}?', { prev: prev, key: key })); DialogService.dialog(gettextCatalog.getString('Trust host certificate?'), message, [gettextCatalog.getString('No'), gettextCatalog.getString('Yes')], function(ix) { if (ix == 1) { diff --git a/Duplicati/Server/webroot/ngax/scripts/directives/restoreFilePicker.js b/Duplicati/Server/webroot/ngax/scripts/directives/restoreFilePicker.js index ef142f6bc..3e71e8648 100644 --- a/Duplicati/Server/webroot/ngax/scripts/directives/restoreFilePicker.js +++ b/Duplicati/Server/webroot/ngax/scripts/directives/restoreFilePicker.js @@ -35,7 +35,10 @@ backupApp.directive('restoreFilePicker', function() { if (!node.children && !node.loading) { node.loading = true; - 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) { + // Prefix filter with "@" to prevent Duplicati from + // mistaking literal '*' and '?' characters in paths + // for glob wildcard characters + 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 = [] for(var n in data.data.Files) diff --git a/Duplicati/Server/webroot/ngax/scripts/services/EditUriBuiltins.js b/Duplicati/Server/webroot/ngax/scripts/services/EditUriBuiltins.js index 217ff8a7a..82f97f95e 100644 --- a/Duplicati/Server/webroot/ngax/scripts/services/EditUriBuiltins.js +++ b/Duplicati/Server/webroot/ngax/scripts/services/EditUriBuiltins.js @@ -30,7 +30,9 @@ backupApp.service('EditUriBuiltins', function (AppService, AppUtils, SystemInfo, EditUriBackendConfig.templates['dropbox'] = 'templates/backends/oauth.html'; EditUriBackendConfig.templates['sia'] = 'templates/backends/sia.html'; EditUriBackendConfig.templates['tardigrade'] = 'templates/backends/tardigrade.html'; + EditUriBackendConfig.templates['telegram'] = 'templates/backends/telegram.html'; EditUriBackendConfig.templates['rclone'] = 'templates/backends/rclone.html'; + EditUriBackendConfig.templates['cos'] = 'templates/backends/cos.html'; EditUriBackendConfig.testers['s3'] = function(scope, callback) { @@ -502,6 +504,43 @@ backupApp.service('EditUriBuiltins', function (AppService, AppUtils, SystemInfo, delete options[nukeopts[x]]; }; + EditUriBackendConfig.parsers['telegram'] = function (scope, module, server, port, path, options) { + if (options['--api-id']) + scope.api_id = options['--api-id']; + if (options['--api-hash']) + scope.api_hash = options['--api-hash']; + if (options['--auth-code']) + scope.auth_code = options['--auth-code']; + if (options['--channel-name']) + scope.channel_name = options['--channel-name']; + if (options['--phone-number']) + scope.phone_number = options['--phone-number']; + if (options['--auth_password']) + scope.auth_password = options['--auth_password']; + + var nukeopts = ['--api-id','--api-hash', '--auth-code', '--channel-name', '--phone-number', '--auth_password']; + for (var x in nukeopts) + delete options[nukeopts[x]]; + }; + + EditUriBackendConfig.parsers['cos'] = function (scope, module, server, port, path, options) { + if (options['--cos-app-id']) + scope.cos_app_id = options['--cos-app-id']; + if (options['--cos-region']) + scope.cos_region = options['--cos-region']; + if (options['--cos-secret-id']) + scope.cos_secret_id = options['--cos-secret-id']; + if (options['--cos-secret-key']) + scope.cos_secret_key = options['--cos-secret-key']; + if (options['--cos-bucket']) + scope.cos_bucket = options['--cos-bucket']; + + var nukeopts = ['--cos-app-id', '--cos-region', '--cos-secret-id', '--cos-secret-key', '--cos-bucket']; + for (var x in nukeopts) + delete options[nukeopts[x]]; + + EditUriBackendConfig.mergeServerAndPath(scope); + } // Builders take the scope and produce the uri output EditUriBackendConfig.builders['s3'] = function (scope) { @@ -740,6 +779,26 @@ backupApp.service('EditUriBuiltins', function (AppService, AppUtils, SystemInfo, return url; }; + EditUriBackendConfig.builders['telegram'] = function (scope) { + var opts = { + 'api-id': scope.api_id, + 'api-hash': scope.api_hash, + 'auth-code': scope.auth_code, + 'channel-name': scope.channel_name, + 'phone-number': scope.phone_number, + 'auth-password': scope.auth_password, + }; + + EditUriBackendConfig.merge_in_advanced_options(scope, opts); + + var url = AppUtils.format('{0}://t.me/{1}', + scope.Backend.Key, + AppUtils.encodeDictAsUrl(opts) + ); + + return url; + }; + EditUriBackendConfig.builders['rclone'] = function (scope) { var opts = { @@ -785,6 +844,27 @@ backupApp.service('EditUriBuiltins', function (AppService, AppUtils, SystemInfo, return url; }; + + EditUriBackendConfig.builders['cos'] = function (scope) { + var opts = { + 'cos-app-id': scope.cos_app_id, + 'cos-region': scope.cos_region, + 'cos-secret-id': scope.cos_secret_id, + 'cos-secret-key': scope.cos_secret_key, + 'cos-bucket': scope.cos_bucket + }; + + EditUriBackendConfig.merge_in_advanced_options(scope, opts); + + var url = AppUtils.format('{0}://{1}{2}', + scope.Backend.Key, + scope.Path || '', + AppUtils.encodeDictAsUrl(opts) + ); + + return url; + } + EditUriBackendConfig.validaters['file'] = function (scope, continuation) { if (EditUriBackendConfig.require_path(scope)) continuation(); @@ -914,7 +994,7 @@ backupApp.service('EditUriBuiltins', function (AppService, AppUtils, SystemInfo, res = EditUriBackendConfig.show_error_dialog(gettextCatalog.getString('You must enter a domain name to use v3 API')); if (res && ((scope.openstack_tenantname) || '').trim().length == 0) - res = EditUriBackendCOnfig.show_error_dialog(gettextCatalog.getString('You must enter a tenant (aka project) name to use v3 API')); + res = EditUriBackendConfig.show_error_dialog(gettextCatalog.getString('You must enter a tenant (aka project) name to use v3 API')); if (res && (scope.openstack_apikey || '').trim().length != 0) res = EditUriBackendConfig.show_error_dialog(gettextCatalog.getString('Openstack API Key are not supported in v3 keystone API.')); @@ -1069,6 +1149,26 @@ backupApp.service('EditUriBuiltins', function (AppService, AppUtils, SystemInfo, continuation(); }; + EditUriBackendConfig.validaters['telegram'] = function (scope, continuation) { + var opts = { + 'api-id': scope.api_id, + 'api-hash': scope.api_hash, + 'auth-code': scope.auth_code, + 'channel-name': scope.channel_name, + 'phone-number': scope.phone_number, + 'auth-password': scope.auth_password, + }; + + var res = + EditUriBackendConfig.require_field(scope, 'api_id', gettextCatalog.getString('api_id')) && + EditUriBackendConfig.require_field(scope, 'api_hash', gettextCatalog.getString('api_hash')) && + EditUriBackendConfig.require_field(scope, 'channel_name', gettextCatalog.getString('channel_name')) && + EditUriBackendConfig.require_field(scope, 'phone_number', gettextCatalog.getString('phone_number')); + + if (res) + continuation(); + }; + EditUriBackendConfig.validaters['rclone'] = function (scope, continuation) { var res = EditUriBackendConfig.require_field(scope, 'Server', gettextCatalog.getString('Remote Repository')) && @@ -1079,4 +1179,15 @@ backupApp.service('EditUriBuiltins', function (AppService, AppUtils, SystemInfo, continuation(); }; + EditUriBackendConfig.validaters['cos'] = function (scope, continuation) { + var res = + EditUriBackendConfig.require_field(scope, 'cos_app_id', gettextCatalog.getString('cos_app_id')) && + EditUriBackendConfig.require_field(scope, 'cos_secret_id', gettextCatalog.getString('cos_secret_id')) && + EditUriBackendConfig.require_field(scope, 'cos_secret_key', gettextCatalog.getString('cos_secret_key')) && + EditUriBackendConfig.require_field(scope, 'cos_region', gettextCatalog.getString('cos_region')) && + EditUriBackendConfig.require_field(scope, 'cos_bucket', gettextCatalog.getString('cos_bucket')); + + if (res) + continuation(); + }; }); diff --git a/Duplicati/Server/webroot/ngax/scripts/services/SystemInfo.js b/Duplicati/Server/webroot/ngax/scripts/services/SystemInfo.js index dd1e91abb..5b117acf0 100644 --- a/Duplicati/Server/webroot/ngax/scripts/services/SystemInfo.js +++ b/Duplicati/Server/webroot/ngax/scripts/services/SystemInfo.js @@ -72,8 +72,10 @@ backupApp.service('SystemInfo', function($rootScope, $timeout, $cookies, AppServ 'dropbox': null, 'sia': null, 'tardigrade': null, + 'telegram': null, 'jottacloud': null, - 'rclone': null + 'rclone': null, + 'cos': null } }; diff --git a/Duplicati/Server/webroot/ngax/styles/style.css b/Duplicati/Server/webroot/ngax/styles/style.css index 02191013d..8caecc1f6 100644 --- a/Duplicati/Server/webroot/ngax/styles/style.css +++ b/Duplicati/Server/webroot/ngax/styles/style.css @@ -1,4 +1,4 @@ @font-face{font-family:'Clear Sans';src:url('../fonts/ClearSans-Light-webfont.eot');src:url('../fonts/ClearSans-Light-webfont.eot?#iefix') format('embedded-opentype'),url('../fonts/ClearSans-Light-webfont.woff') format('woff'),url('../fonts/ClearSans-Light-webfont.ttf') format('truetype'),url('../fonts/ClearSans-Light-webfont.svg#clear_sans_lightregular') format('svg');font-weight:300;font-style:normal}@font-face{font-family:'Clear Sans';src:url('../fonts/ClearSans-Regular-webfont.eot');src:url('../fonts/ClearSans-Regular-webfont.eot?#iefix') format('embedded-opentype'),url('../fonts/ClearSans-Regular-webfont.woff') format('woff'),url('../fonts/ClearSans-Regular-webfont.ttf') format('truetype'),url('../fonts/ClearSans-Regular-webfont.svg#clear_sansregular') format('svg');font-weight:normal;font-style:normal}@font-face{font-family:'Clear Sans';src:url('../fonts/ClearSans-Medium-webfont.eot');src:url('../fonts/ClearSans-Medium-webfont.eot?#iefix') format('embedded-opentype'),url('../fonts/ClearSans-Medium-webfont.woff') format('woff'),url('../fonts/ClearSans-Medium-webfont.ttf') format('truetype'),url('../fonts/ClearSans-Medium-webfont.svg#clear_sans_mediumregular') format('svg');font-weight:500;font-style:normal}@font-face{font-family:'Clear Sans';src:url('../fonts/ClearSans-Bold-webfont.eot');src:url('../fonts/ClearSans-Bold-webfont.eot?#iefix') format('embedded-opentype'),url('../fonts/ClearSans-Bold-webfont.woff') format('woff'),url('../fonts/ClearSans-Bold-webfont.ttf') format('truetype'),url('../fonts/ClearSans-Bold-webfont.svg#clear_sansbold') format('svg');font-weight:700;font-style:normal}form.styled div.leftflush input{width:auto;margin-top:10px}form.styled div.leftflush label{width:auto;min-width:190px}form.styled label{display:block;width:190px;float:left;line-height:37px}form.styled input,form.styled textarea,form.styled select{color:#8f8f8f;font-size:16px;font-weight:300;float:left;display:block;border:1px #d8d8d8 solid;border-radius:2px;width:420px}form.styled input:focus,form.styled textarea:focus,form.styled select:focus{border:1px #a5a5a5 solid}form.styled .input{padding-bottom:18px;overflow:hidden}form.styled .input.text input,form.styled .input.password input{height:35px;line-height:35px;padding:0 12px}form.styled .input.text.text-browse input{width:375px;border-top-right-radius:0;border-bottom-right-radius:0;border-right:0}form.styled .input.text.text-browse a.browse{width:45px;display:block;float:left;height:37px;border-radius:2px;border-top-left-radius:0;border-bottom-left-radius:0;color:white;background:#65b1dd;line-height:37px}form.styled .input.text.text-browse a.browse:hover{background:#2881b4}form.styled .input.textarea textarea{height:130px;padding:10px 12px}form.styled .input.select select{width:446px;padding:0 12px;-webkit-appearance:menulist-button;background:white;border-radius:2px;height:38px;line-height:38px}form.styled .buttons{overflow:hidden;float:right}form.styled .buttons input,form.styled .buttons a{display:block;background:#65b1dd;color:white !important;padding:5px 15px;float:right;margin-left:10px;cursor:pointer;width:auto;border:none;font-family:'Clear Sans',sans-serif;font-size:16px;font-weight:300;border-radius:0}form.styled .buttons input{padding:4px 15px}form.styled .buttons input:hover,form.styled .buttons a:hover{background:#23729f}@media (max-width:480px){form.styled input,form.styled textarea,form.styled select{font-size:15px}}/*! * Font Awesome 4.5.0 by @davegandy - http://fontawesome.io - @fontawesome * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) - */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.5.0');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.5.0') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff2?v=4.5.0') format('woff2'),url('../fonts/fontawesome-webfont.woff?v=4.5.0') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.5.0') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.5.0#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=1);-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2);-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=3);-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1);-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1);-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-intersex:before,.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-genderless:before{content:"\f22d"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-hotel:before,.fa-bed:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.fa-yc:before,.fa-y-combinator:before{content:"\f23b"}.fa-optin-monster:before{content:"\f23c"}.fa-opencart:before{content:"\f23d"}.fa-expeditedssl:before{content:"\f23e"}.fa-battery-4:before,.fa-battery-full:before{content:"\f240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-battery-2:before,.fa-battery-half:before{content:"\f242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-mouse-pointer:before{content:"\f245"}.fa-i-cursor:before{content:"\f246"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-sticky-note:before{content:"\f249"}.fa-sticky-note-o:before{content:"\f24a"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-diners-club:before{content:"\f24c"}.fa-clone:before{content:"\f24d"}.fa-balance-scale:before{content:"\f24e"}.fa-hourglass-o:before{content:"\f250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass:before{content:"\f254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:"\f256"}.fa-hand-scissors-o:before{content:"\f257"}.fa-hand-lizard-o:before{content:"\f258"}.fa-hand-spock-o:before{content:"\f259"}.fa-hand-pointer-o:before{content:"\f25a"}.fa-hand-peace-o:before{content:"\f25b"}.fa-trademark:before{content:"\f25c"}.fa-registered:before{content:"\f25d"}.fa-creative-commons:before{content:"\f25e"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-tripadvisor:before{content:"\f262"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-get-pocket:before{content:"\f265"}.fa-wikipedia-w:before{content:"\f266"}.fa-safari:before{content:"\f267"}.fa-chrome:before{content:"\f268"}.fa-firefox:before{content:"\f269"}.fa-opera:before{content:"\f26a"}.fa-internet-explorer:before{content:"\f26b"}.fa-tv:before,.fa-television:before{content:"\f26c"}.fa-contao:before{content:"\f26d"}.fa-500px:before{content:"\f26e"}.fa-amazon:before{content:"\f270"}.fa-calendar-plus-o:before{content:"\f271"}.fa-calendar-minus-o:before{content:"\f272"}.fa-calendar-times-o:before{content:"\f273"}.fa-calendar-check-o:before{content:"\f274"}.fa-industry:before{content:"\f275"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-map-o:before{content:"\f278"}.fa-map:before{content:"\f279"}.fa-commenting:before{content:"\f27a"}.fa-commenting-o:before{content:"\f27b"}.fa-houzz:before{content:"\f27c"}.fa-vimeo:before{content:"\f27d"}.fa-black-tie:before{content:"\f27e"}.fa-fonticons:before{content:"\f280"}.fa-reddit-alien:before{content:"\f281"}.fa-edge:before{content:"\f282"}.fa-credit-card-alt:before{content:"\f283"}.fa-codiepie:before{content:"\f284"}.fa-modx:before{content:"\f285"}.fa-fort-awesome:before{content:"\f286"}.fa-usb:before{content:"\f287"}.fa-product-hunt:before{content:"\f288"}.fa-mixcloud:before{content:"\f289"}.fa-scribd:before{content:"\f28a"}.fa-pause-circle:before{content:"\f28b"}.fa-pause-circle-o:before{content:"\f28c"}.fa-stop-circle:before{content:"\f28d"}.fa-stop-circle-o:before{content:"\f28e"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-hashtag:before{content:"\f292"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-percent:before{content:"\f295"}*{font-family:'Clear Sans',sans-serif}html,body{margin:0;padding:0;height:100%}h1,h2{font-weight:300;color:#81c401}h1{margin:10px 0}h3{font-weight:400}a{text-decoration:none}ul{list-style:none;margin:0;padding:0}hr{border:none;border-bottom:1px #ddd solid}textarea{max-width:94%}.external-link-image{display:inline-block;margin-left:8px;margin-right:8px;height:16px;width:16px;background:url('../img/external-link-hover.png');background-size:16px}a .external-link-image{background:url('../img/external-link.png');background-size:16px}.header a:hover .external-link-image{background:url('../img/external-link-hover.png');background-size:16px}.button{display:block;background:#65b1dd;color:white !important;padding:5px 15px;float:right;margin-left:10px;cursor:pointer;width:auto;border:none;font-family:'Clear Sans',sans-serif;font-size:16px;font-weight:300;border-radius:0}.button:hover{background:#3b9cd4}.step3 source-folder-picker,#folder_path_picker,#restore_file_picker{display:block;border:1px solid lightgray;padding:2px;height:100%;overflow:auto}.not-clickable{cursor:default !important}.not-clickable>a,.not-clickable span,.not-clickable div{cursor:default !important}.ui-match{font-weight:bold;color:darkgreen}wait-area{min-width:350px;text-align:center;display:block}.prewrapped-text{white-space:pre-wrap}.exceptiontext{background-color:lightgray;color:black}.backup-result{width:90%;display:grid;grid-template-columns:50% 50%;grid-auto-rows:minmax(50px, auto);margin:0 auto}.backup-result div .horizontal-rule{width:100%;border-bottom:1px solid #d8d8d8;margin:5px 0 5px 0}.backup-result .box{margin:10px;margin-bottom:0px}.backup-result .title{color:#609101;font-weight:bold;font-size:30px}.backup-result .item{display:block}.backup-result .item .key{color:#81c401;font-weight:bold}.backup-result .item .value{color:#8f8f8f}.backup-result .item .expanded{padding:0 10px 0 18px;margin-bottom:10px}.backup-result .one{border-right:1px solid #d8d8d8;grid-column:1;grid-row:1}.backup-result .two{grid-column:2;grid-row:1}.backup-result .wide{grid-column:span 2;border-top:1px solid #d8d8d8;padding-top:10px}.backup-result .three{grid-row:2}.backup-result .four{grid-row:3;margin-bottom:10px}.success-color{color:#339900}.error-color{color:#cc0000}.warning-color{color:#ffcc00}ul.tabs{margin-bottom:10px}ul.tabs>li{display:inline;margin-right:10px;border:1px solid #65b1dd;padding:5px}ul.tabs>li.active{background-color:#65b1dd;color:white}ul.tabs>li.active>a{background-color:#65b1dd;color:white}ul.tabs>li.active.disabled{border:1px solid lightgray;background-color:lightgray;color:grey;cursor:default}ul.tabs>li.active.disabled>a{background-color:lightgray;color:grey;cursor:default}.licenses>ul{list-style:initial;margin:10px;margin-left:20px}.licenses li{margin-bottom:10px}.licenses a.itemlink{font-weight:bold}.logpage ul.entries{list-style:initial;margin:10px;margin-left:20px}.logpage .entries div.entryline.clickable{cursor:pointer}.logpage .entries.livedata li.expanded{height:auto}.logpage .button{text-align:center;margin-right:10px;border:1px solid #65b1dd;padding:5px;background-color:#65b1dd;color:white;cursor:pointer}.exportpage .checkbox input{width:auto;margin-top:10px}.exportpage .commandline div{background-color:lightgray;color:black}.themelink{margin-left:20px}ul.notification{position:fixed;bottom:0px;left:0px;right:0px;margin:auto;width:480px}.notification .title{border:1px solid #65b1dd;background-color:#65b1dd;border-top-left-radius:5px;border-top-right-radius:5px;border-bottom-left-radius:0px;border-bottom-right-radius:0px;padding:2px;padding-left:5px;padding-right:5px;font-weight:bold;color:lightgray;width:100%;text-align:center;clear:both}.notification .content{background-color:white;border:1px solid #65b1dd;border-top-left-radius:0px;border-top-right-radius:0px;border-bottom-left-radius:5px;border-bottom-right-radius:5px;padding:2px;padding-left:5px;padding-right:5px;width:100%}.notification .message{width:100%;color:black}.notification .button{padding:2px 10px;margin-top:6px}.notification .clear{clear:right;height:1px}.notification .error .title{border-color:red;background-color:red}.notification .error .content{border-color:red}.notification .error .button{border-color:red;background-color:red}.notification .warning .title{background-color:orange;border-color:orange}.notification .warning .button{background-color:orange;border-color:orange}.notification .warning .content{border-color:orange}.filepicker{height:200px}.resizable{margin-bottom:6px;max-width:100%}.advanced-toggle{float:right;margin-right:25px;line-height:37px}.advancedoptions li{clear:both;margin-bottom:10px;padding:10px 0;border-top:1px lightgray solid}.advancedentry .multiple{display:inline}.advancedentry .shortname{font-weight:bold}.advancedentry input[type="text"]{width:300px}.advancedentry select{width:300px}.advancedentry input[type="checkbox"]{margin-top:13px;width:auto}.advancedentry .longdescription{margin-left:190px;clear:both;font-style:italic}.settings div.sublabel{clear:both;padding:0 31px;font-style:italic}.logo img.mainlogo{height:64px;width:64px;float:left;padding-right:8px;padding-top:2px}.logo div.logotext{float:left}.logo a{float:left;display:block;line-height:normal}.logo div.build-suffix{clear:both;display:inline;float:left;font-size:16px;line-height:16px}.logo div.powered-by{font-size:16px;margin:0px;line-height:16px;float:left;padding:0px;margin-left:5px}.fixed-width-font{font-family:monospace}.warning{margin:10px;font-style:italic;color:#f49b42}div.captcha .details{padding-top:10px;margin-left:auto;margin-right:auto;width:180px}.centered-text{text-align:center}body{color:#8f8f8f}body .container{min-height:100%;position:relative}body .container .header{line-height:70px;background:#ededed;overflow:hidden;height:70px;position:fixed;top:0px;left:0px;right:0px;z-index:100}body .container .header a{color:#65b1dd}body .container .header a:hover,body .container .header a.active{color:#4f4f4f}body .container .header .logo{font-size:30px;font-weight:700;float:left;padding-left:40px}body .container .header .statepadding{padding-right:90px;margin-left:320px}body .container .header .state{float:left;color:#609101;width:595px;padding:13px 15px;margin:10px 20px;border:1px #609101 solid;font-weight:300;font-size:18px;overflow:hidden;line-height:normal;display:inline-block;background-color:white;text-overflow:ellipsis;position:relative;height:25px}body .container .header .state strong{display:inline;margin-right:10px}body .container .header .state span{display:inline}body .container .header .state .button{position:static;margin-top:70px}body .container .header .state .content{position:relative;z-index:10;margin-right:40px;display:block;text-overflow:ellipsis;overflow:hidden;white-space:nowrap}body .container .header .state .buttons{position:absolute;right:0px;top:0px;bottom:0px;width:26px;margin:13px 15px}body .container .header .state .buttons .stop{display:block;width:26px;height:26px;background:url('../img/progress-stop.png');background-size:26px;cursor:pointer}body .container .header .state .buttons .resume{display:block;width:26px;height:26px;background:url('../img/progress-resume.png');background-size:26px;cursor:pointer}body .container .header .state .progress-bar{position:absolute;top:0px;bottom:0px;left:0px;background:rgba(129,196,1,0.25);z-index:5}body .container .header .state .task-name{overflow:hidden;text-overflow:ellipsis;cursor:help}body .container .header .state .task-state-info{display:flex}body .container .header .action-icons{display:inline-block;line-height:normal;margin:10px 0px;padding:13px 0px;float:left}body .container .header .action-icons-small{display:none;float:right;margin-top:21px;line-height:normal}body .container .header .action-icons>.pause,body .container .header .action-icons-small>.pause{width:26px;height:26px;display:inline-block;cursor:pointer;background:url('../img/pause.png')}body .container .header .action-icons>.pause.active,body .container .header .action-icons-small>.pause.active{background:url('../img/resume.png')}body .container .header .action-icons>.throttle,body .container .header .action-icons-small>.throttle{width:26px;height:26px;display:inline-block;cursor:pointer;background:url('../img/throttle.png')}body .container .header .action-icons>.throttle.inactive,body .container .header .action-icons-small>.throttle.inactive{opacity:.5}body .container .header .about-header{float:right;padding-right:20px;overflow:hidden}body .container .header .about-header ul{overflow:hidden;list-style:none}body .container .header .about-header ul li{float:right;padding-right:20px}body .container .header .donate{float:right}body .container .header .donate ul{overflow:hidden;float:right;padding-left:20px;padding-right:10px}body .container .header .donate ul li{float:right;margin-right:10px;padding-top:5px}body .container .header .donate img{opacity:.6}body .container .header .donate img:hover{opacity:1}body .container .body{width:100%;overflow:hidden;min-height:500px;padding-top:120px;padding-bottom:70px}body .container .body a{color:#65b1dd}body .container .body .mainmenu{width:260px;padding-left:40px;float:left;position:fixed}body .container .body .mainmenu>ul>li{position:relative}body .container .body .mainmenu>ul>li>a{font-size:22px;font-weight:300;padding:5px 10px 5px 55px;display:block}body .container .body .mainmenu>ul>li>a:hover{color:white}body .container .body .mainmenu>ul>li>a.active{color:white}body .container .body .mainmenu>ul>li>a.add{background:url('../img/mainmenu/add.png') no-repeat 8px 7px}body .container .body .mainmenu>ul>li>a.restore{background:url('../img/mainmenu/restore.png') no-repeat 8px 7px}body .container .body .mainmenu>ul>li>a.pause{background:url('../img/mainmenu/pause.png') no-repeat 8px 7px}body .container .body .mainmenu>ul>li>a.resume{background:url('../img/mainmenu/resume.png') no-repeat 8px 7px}body .container .body .mainmenu>ul>li>a.settings{background:url('../img/mainmenu/settings.png') no-repeat 8px 7px}body .container .body .mainmenu>ul>li>a.log{background:url('../img/mainmenu/log.png') no-repeat 8px 7px}body .container .body .mainmenu>ul>li>a.logout{background:url('../img/mainmenu/logout.png') no-repeat 8px 7px}body .container .body .mainmenu>ul>li>a.home{background:url('../img/mainmenu/home.png') no-repeat 8px 7px}body .container .body .mainmenu>ul>li>a.about{background:url('../img/mainmenu/about.png') no-repeat 8px 7px}body .container .body .mainmenu>ul>li>a.pause>span{padding-right:25px;background:url('../img/mainmenu/arrow_right.png') right center no-repeat}body .container .body .mainmenu>ul>li>a.home.active{background:#a4d1eb url('../img/mainmenu/over/home.png') no-repeat 8px 7px}body .container .body .mainmenu>ul>li>a.add.active{background:#a4d1eb url('../img/mainmenu/over/add.png') no-repeat 8px 7px}body .container .body .mainmenu>ul>li>a.restore.active{background:#a4d1eb url('../img/mainmenu/over/restore.png') no-repeat 8px 7px}body .container .body .mainmenu>ul>li>a.pause.active{background:#a4d1eb url('../img/mainmenu/over/pause.png') no-repeat 8px 7px}body .container .body .mainmenu>ul>li>a.resume.active{background:#a4d1eb url('../img/mainmenu/over/resume.png') no-repeat 8px 7px}body .container .body .mainmenu>ul>li>a.settings.active{background:#a4d1eb url('../img/mainmenu/over/settings.png') no-repeat 8px 7px}body .container .body .mainmenu>ul>li>a.log.active{background:#a4d1eb url('../img/mainmenu/over/log.png') no-repeat 8px 7px}body .container .body .mainmenu>ul>li>a.about.active{background:#a4d1eb url('../img/mainmenu/over/about.png') no-repeat 8px 7px}body .container .body .mainmenu>ul>li>a.pause.open.active>span{background:url('../img/mainmenu/over/arrow_down.png') right center no-repeat}body .container .body .mainmenu>ul>li>a.add:hover{background:#65b1dd url('../img/mainmenu/over/add.png') no-repeat 8px 7px}body .container .body .mainmenu>ul>li>a.restore:hover{background:#65b1dd url('../img/mainmenu/over/restore.png') no-repeat 8px 7px}body .container .body .mainmenu>ul>li>a.pause:hover{background:#65b1dd url('../img/mainmenu/over/pause.png') no-repeat 8px 7px}body .container .body .mainmenu>ul>li>a.pause:hover>span{background:url('../img/mainmenu/over/arrow_right.png') right center no-repeat}body .container .body .mainmenu>ul>li>a.pause.open:hover>span{background:url('../img/mainmenu/over/arrow_down.png') right center no-repeat}body .container .body .mainmenu>ul>li>a.pause.open>span{background:url('../img/mainmenu/arrow_down.png') right center no-repeat}body .container .body .mainmenu>ul>li>a.resume:hover{background:#65b1dd url('../img/mainmenu/over/resume.png') no-repeat 8px 7px}body .container .body .mainmenu>ul>li>a.settings:hover{background:#65b1dd url('../img/mainmenu/over/settings.png') no-repeat 8px 7px}body .container .body .mainmenu>ul>li>a.log:hover{background:#65b1dd url('../img/mainmenu/over/log.png') no-repeat 8px 7px}body .container .body .mainmenu>ul>li>a.logout:hover{background:#65b1dd url('../img/mainmenu/over/logout.png') no-repeat 8px 7px}body .container .body .mainmenu>ul>li>a.home:hover{background:#65b1dd url('../img/mainmenu/over/home.png') no-repeat 8px 7px}body .container .body .mainmenu>ul>li>a.about:hover{background:#65b1dd url('../img/mainmenu/over/about.png') no-repeat 8px 7px}body .container .body .mainmenu>ul li.hr-top{padding-top:25px;margin-top:25px;border-top:1px #ededed solid}body .container .body div.contextmenu_container{position:relative}body .container .body .contextmenu{display:none;position:absolute;background:white;border:1px #ededed solid;box-shadow:0 4px 8px rgba(0,0,0,0.3);z-index:200;padding:5px}body .container .body .contextmenu li a{color:#65b1dd;font-size:15px;font-weight:400;padding:0;display:block;min-width:200px;padding:4px 10px;white-space:nowrap;padding-left:45px;overflow:hidden;text-overflow:ellipsis}body .container .body .contextmenu li a:hover{background:#65b1dd;color:white}body .container .body .contextmenu.open{display:block}body .container .body .content{float:left;padding-left:350px;padding-bottom:50px;max-width:70%}body .container .body .content ul.tabs>li{display:inline-block}body .container .body .content .tasks .tasklist .task{border-top:1px solid #eee;padding-top:20px;margin-bottom:25px}body .container .body .content .tasks .tasklist .task:last-child{border-bottom:1px solid #eee;padding-bottom:20px}body .container .body .content .tasks .tasklist .task:first-child{padding-top:0px;border-top:0px none}body .container .body .content .tasks .tasklist .progress-small{text-align:center;height:18px;background:rgba(164,209,235,0.5)}body .container .body .content .tasks .tasklist .progress-small-bg{border:1px #65b1dd solid;width:200px}body .container .body .content .tasks .tasklist a{font-size:30px;font-weight:300;display:inline-block}body .container .body .content .tasks .tasklist a.action-link{font-size:14px;background:none;padding-left:0px}body .container .body .content .tasks .tasklist dl{padding-left:55px;overflow:hidden;font-size:14px}body .container .body .content .tasks .tasklist dl dt,body .container .body .content .tasks .tasklist dl dd{display:block;float:left}body .container .body .content .tasks .tasklist dl dt{clear:both;font-weight:500;margin-bottom:5px}body .container .body .content .tasks .tasklist dl dd{margin-left:10px}body .container .body .content .tasks .tasklist dl.taskmenu p{display:inline;margin-right:10px;color:#65b1dd;cursor:pointer}body .container .body .content .tasks .tasklist dl.taskmenu dt{float:left;margin-right:10px;margin-bottom:0;padding:5px 8px;color:#8f8f8f;cursor:pointer;clear:none}body .container .body .content .tasks .tasklist dl.taskmenu dd{clear:both;float:none;padding-bottom:8px;border-bottom:1px #ddd solid;margin-bottom:5px}body .container .body .content div.add .steps,body .container .body .content div.restore .steps{width:100%;overflow:hidden}body .container .body .content div.add .steps .step,body .container .body .content div.restore .steps .step{float:left;background:url('../img/steps/line-out.png') no-repeat top left;color:#c7e5f6}body .container .body .content div.add .steps .step span,body .container .body .content div.restore .steps .step span{display:block;border:4px #c7e5f6 solid;background:white;border-radius:50%;width:35px;height:35px;text-align:center;font-size:22px;line-height:35px;cursor:pointer}body .container .body .content div.add .steps .step.active,body .container .body .content div.restore .steps .step.active{color:#65b1dd}body .container .body .content div.add .steps .step.active span,body .container .body .content div.restore .steps .step.active span{border:4px #65b1dd solid;background:#65b1dd;color:white}body .container .body .content div.add .steps .step.active h2,body .container .body .content div.restore .steps .step.active h2{color:#65b1dd}body .container .body .content div.add .steps .step:first-child,body .container .body .content div.restore .steps .step:first-child{padding-left:0;background:transparent}body .container .body .content div.add .steps-legend,body .container .body .content div.restore .steps-legend{overflow:hidden;padding-bottom:50px;list-style:none;margin:0}body .container .body .content div.add .steps-legend li,body .container .body .content div.restore .steps-legend li{color:#c7e5f6;font-size:18px;text-align:center;float:left;padding-top:10px;cursor:pointer}body .container .body .content div.add .steps-legend li.active,body .container .body .content div.restore .steps-legend li.active{color:#65b1dd}body .container .body .content div.add .steps-boxes,body .container .body .content div.restore .steps-boxes{padding-left:40px}body .container .body .content div.add .steps-boxes .step,body .container .body .content div.restore .steps-boxes .step{display:none}body .container .body .content div.add .steps-boxes .step.active,body .container .body .content div.restore .steps-boxes .step.active{display:block}body .container .body .content div.add .steps-boxes .box.browser .checklinks a,body .container .body .content div.restore .steps-boxes .box.browser .checklinks a{float:left;margin-left:20px;color:#8f8f8f}body .container .body .content div.add .steps-boxes .box.browser .checklinks a i,body .container .body .content div.restore .steps-boxes .box.browser .checklinks a i{border:2px solid;border-color:#8f8f8f;border-radius:2px;font-size:18px;height:18px;vertical-align:top;width:18px}body .container .body .content div.add .steps-boxes .box.browser .checklinks a.inactive,body .container .body .content div.restore .steps-boxes .box.browser .checklinks a.inactive{color:#c2c2c2;cursor:default}body .container .body .content div.add .steps-boxes .box.browser .checklinks a.inactive i,body .container .body .content div.restore .steps-boxes .box.browser .checklinks a.inactive i{border-color:#c2c2c2}body .container .body .content div.add .steps-boxes .box.browser .checklinks a:first-child,body .container .body .content div.restore .steps-boxes .box.browser .checklinks a:first-child{margin-left:0}body .container .body .content div.add .steps-boxes .box.browser .input.overlayButton,body .container .body .content div.restore .steps-boxes .box.browser .input.overlayButton{padding-top:10px;max-width:100%}body .container .body .content div.add .steps-boxes .box.browser .input.overlayButton input#sourcePath,body .container .body .content div.restore .steps-boxes .box.browser .input.overlayButton input#sourcePath{width:100%;box-sizing:border-box;height:37px}body .container .body .content div.add .steps-boxes .box.browser .input.overlayButton a.button,body .container .body .content div.restore .steps-boxes .box.browser .input.overlayButton a.button{top:10px}body .container .body .content div.add .steps-boxes .box.filters .input.link a,body .container .body .content div.restore .steps-boxes .box.filters .input.link a{color:#8f8f8f}body .container .body .content div.add .steps-boxes .box.filters .input.link a i,body .container .body .content div.restore .steps-boxes .box.filters .input.link a i{border:2px solid;border-color:#8f8f8f;border-radius:2px;font-size:18px;height:18px;vertical-align:top;width:18px}body .container .body .content div.add .steps-boxes .box.filters ul#simplefilterlist,body .container .body .content div.restore .steps-boxes .box.filters ul#simplefilterlist{overflow:hidden;padding-bottom:15px}body .container .body .content div.add .steps-boxes .box.filters ul#simplefilterlist li,body .container .body .content div.restore .steps-boxes .box.filters ul#simplefilterlist li{overflow:hidden;clear:both;padding-bottom:5px}body .container .body .content div.add .steps-boxes .box.filters ul#simplefilterlist li select,body .container .body .content div.restore .steps-boxes .box.filters ul#simplefilterlist li select{width:200px;margin-right:5px;height:36px;line-height:36px}body .container .body .content div.add .steps-boxes .box.filters ul#simplefilterlist li input,body .container .body .content div.restore .steps-boxes .box.filters ul#simplefilterlist li input{width:280px;padding:5px}body .container .body .content div.add .steps-boxes .step1 li.strength.score-0,body .container .body .content div.restore .steps-boxes .step1 li.strength.score-0{color:#ff0000}body .container .body .content div.add .steps-boxes .step1 li.strength.score-1,body .container .body .content div.restore .steps-boxes .step1 li.strength.score-1{color:#ff7700}body .container .body .content div.add .steps-boxes .step1 li.strength.score-2,body .container .body .content div.restore .steps-boxes .step1 li.strength.score-2{color:#aaaa00}body .container .body .content div.add .steps-boxes .step1 li.strength.score-3,body .container .body .content div.restore .steps-boxes .step1 li.strength.score-3{color:#007700}body .container .body .content div.add .steps-boxes .step1 li.strength.score-4,body .container .body .content div.restore .steps-boxes .step1 li.strength.score-4{color:#427e27}body .container .body .content div.add .steps-boxes .step1 li.strength.score-x,body .container .body .content div.restore .steps-boxes .step1 li.strength.score-x{color:#ff0000}body .container .body .content div.add .steps-boxes .step5 div.input.maxSize input.number,body .container .body .content div.restore .steps-boxes .step5 div.input.maxSize input.number,body .container .body .content div.add .steps-boxes .step5 div.input.keepBackups input.number,body .container .body .content div.restore .steps-boxes .step5 div.input.keepBackups input.number{width:60px}body .container .body .content div.add .steps-boxes .step5 .hint,body .container .body .content div.restore .steps-boxes .step5 .hint,body .container .body .content div.add .steps-boxes .step5 .retention-options,body .container .body .content div.restore .steps-boxes .step5 .retention-options{clear:both;margin-left:190px;margin-top:50px;font-style:italic}body .container .body .content div.add .steps-boxes .step5 .retention-options input,body .container .body .content div.restore .steps-boxes .step5 .retention-options input{margin-bottom:10px}body .container .body .content div.add .steps-boxes .step5 .advancedoptions,body .container .body .content div.restore .steps-boxes .step5 .advancedoptions{padding-top:15px;clear:both}body .container .body .content div.add .steps-boxes .step5 .advancedoptions li,body .container .body .content div.restore .steps-boxes .step5 .advancedoptions li{border-top:none}body .container .body .content div.add .steps-boxes .step5 .advancedoptions li>a,body .container .body .content div.restore .steps-boxes .step5 .advancedoptions li>a{display:block;background:#65b1dd;color:white !important;padding:5px 15px;float:right;margin-left:10px;cursor:pointer;width:auto;border:none;font-family:'Clear Sans',sans-serif;font-size:16px;font-weight:300;border-radius:0}body .container .body .content div.add .steps-boxes .step5 .advancedoptions li.advancedentry,body .container .body .content div.restore .steps-boxes .step5 .advancedoptions li.advancedentry{border-bottom:1px solid lightgray}body .container .body .content div.add .steps-boxes .step5 .advancedoptions li:last-child,body .container .body .content div.restore .steps-boxes .step5 .advancedoptions li:last-child{padding-top:0}body .container .body .content div.add .steps-boxes .step5 .advancedoptions li:last-child select,body .container .body .content div.restore .steps-boxes .step5 .advancedoptions li:last-child select{max-width:400px}body .container .body .content div.add .steps-boxes .step5 .advancedoptions label,body .container .body .content div.restore .steps-boxes .step5 .advancedoptions label{line-height:normal}body .container .body .content div.add .steps-boxes .step5 .advancedoptions input,body .container .body .content div.restore .steps-boxes .step5 .advancedoptions input,body .container .body .content div.add .steps-boxes .step5 .advancedoptions select,body .container .body .content div.restore .steps-boxes .step5 .advancedoptions select{width:auto;max-width:100%;box-sizing:border-box}body .container .body .content div.add .steps-boxes .step5 .advancedoptions .longdescription,body .container .body .content div.restore .steps-boxes .step5 .advancedoptions .longdescription{margin-top:10px}body .container .body .content div.add .steps-boxes .step5 .advanced-toggle,body .container .body .content div.restore .steps-boxes .step5 .advanced-toggle{color:#8f8f8f;line-height:normal;margin-top:16px;clear:both;float:left}body .container .body .content div.add .steps-boxes .step5 .advanced-toggle i.fa,body .container .body .content div.restore .steps-boxes .step5 .advanced-toggle i.fa{border:2px solid;border-color:#8f8f8f;border-radius:2px;font-size:18px;height:18px;vertical-align:top;width:18px}body .container .body .content div.add .steps-boxes .step5 textarea,body .container .body .content div.restore .steps-boxes .step5 textarea{box-sizing:border-box;clear:both;margin-top:15px;width:100%}body .container .body .content div.add form,body .container .body .content div.restore form{padding-bottom:50px;overflow:hidden}body .container .body .content div.add form .input.password .tools,body .container .body .content div.restore form .input.password .tools{clear:both;padding-left:190px;padding-top:10px}body .container .body .content div.add form .input.password .tools ul,body .container .body .content div.restore form .input.password .tools ul{overflow:hidden}body .container .body .content div.add form .input.password .tools ul li,body .container .body .content div.restore form .input.password .tools ul li{float:left;padding-right:7px}body .container .body .content div.add form .input.password .tools ul li.strength.useless,body .container .body .content div.restore form .input.password .tools ul li.strength.useless{color:red}body .container .body .content div.add form .input.password .tools ul li.strength.average,body .container .body .content div.restore form .input.password .tools ul li.strength.average{color:yellow}body .container .body .content div.add form .input.password .tools ul li.strength.good,body .container .body .content div.restore form .input.password .tools ul li.strength.good{color:#65b1dd}body .container .body .content div.add form .input.multiple input,body .container .body .content div.restore form .input.multiple input,body .container .body .content div.add form .input.multiple select,body .container .body .content div.restore form .input.multiple select{width:auto;margin-right:5px}body .container .body .content div.add form .input.multiple select,body .container .body .content div.restore form .input.multiple select{padding:5px 12px}body .container .body .content div.add form .input.overlayButton,body .container .body .content div.restore form .input.overlayButton{overflow:hidden;position:relative;max-width:446px}body .container .body .content div.add form .input.overlayButton input,body .container .body .content div.restore form .input.overlayButton input{width:347px}body .container .body .content div.add form .input.overlayButton a.button,body .container .body .content div.restore form .input.overlayButton a.button{position:absolute;top:0;right:0;padding:7px 12px 8px}body .container .body .content div.add form .input.checkbox.multiple strong,body .container .body .content div.restore form .input.checkbox.multiple strong{display:block;padding-bottom:5px}body .container .body .content div.add form .input.checkbox.multiple label,body .container .body .content div.restore form .input.checkbox.multiple label{display:inline-block;float:none;width:auto;padding-right:10px}body .container .body .content div.add form .input.checkbox.multiple input,body .container .body .content div.restore form .input.checkbox.multiple input{width:auto;display:inline-block;float:none}body .container .body .content div.add form .buttons,body .container .body .content div.restore form .buttons{float:none;width:635px;padding-top:30px}body .container .body .content div.add .steps{margin-left:48.5px}body .container .body .content div.add .steps .step{padding-left:97px}body .container .body .content div.add .steps-legend{padding-left:0}body .container .body .content div.add .steps-legend li{width:140px}body .container .body .content div.restore .steps{margin-left:153.5px}body .container .body .content div.restore .steps .step{padding-left:307px}body .container .body .content div.restore .steps-legend{padding-left:0}body .container .body .content div.restore .steps-legend li{width:350px}body .container .body .content div.restore.restore-direct .steps{margin-left:66px}body .container .body .content div.restore.restore-direct .steps .step{padding-left:132px}body .container .body .content div.restore.restore-direct .steps-legend{padding-left:0}body .container .body .content div.restore.restore-direct .steps-legend li{width:175px}body .container .body .content div.restore.restore-direct .step:first-child{padding-left:0;background:transparent}body .container .body .content div.restore.restore-direct .steps-legend{padding-left:20px}body .container .body .content div.headerthreedotmenu{margin:20px 0 20px 0}body .container .body .content div.headerthreedotmenu h2{display:inline}body .container .body .content div.headerthreedotmenu .contextmenu_container{float:right}body .container .body .content div.headerthreedotmenu .contextmenu{left:auto;right:0;top:auto}body .container .body .content div.headerthreedotmenu .threedotmenubutton{padding:5px}body .container .body .content .expandable{margin:20px 0 20px 0}body .container .body .content .expandable h2{display:inline}body .container .body .content .expandable img{padding:0 6px}body .container .body .content div.settings .input.mixed.multiple input.checkbox,body .container .body .content div.settings .input.checkbox input.checkbox{width:auto}body .container .body .content div.settings .input.mixed.multiple select,body .container .body .content div.settings .input.checkbox select{width:auto;margin-right:5px}body .container .body .content div.settings .input.mixed.multiple label,body .container .body .content div.settings .input.checkbox label{line-height:normal;padding:0 15px;width:auto}body .container .body .content .logpage ul.tabs{padding:15px 0}body .container .body .content .logpage ul.entries li{padding:10px 0 10px 0;border-bottom:1px solid #d8d8d8}body .container .body .content .logpage ul.backuplog{list-style:none}body .container .body .content .prewrapped-text{white-space:pre-wrap;overflow-x:auto}body .container .footer{background:#ededed;min-height:70px;line-height:70px;overflow:hidden;position:absolute;bottom:0;width:100%}body .container .footer a{color:#65b1dd}body .container .footer .about-footer{float:left;overflow:hidden;padding-right:20px;display:none}body .container .footer .about-footer span{display:block;float:left;padding-left:20px}body .container .footer .about-footer ul{float:left}body .container .footer .about-footer li{float:left;padding-left:20px}body .container .footer .donate{display:none;float:right;padding-right:40px;overflow:hidden}body .container .footer .donate ul{overflow:hidden;float:right}body .container .footer .donate ul li{float:left;margin-left:10px}body .container .footer .donate ul li a img{margin-top:24px;display:inline-block;opacity:.6}body .container .footer .donate ul li a img:hover{opacity:1}body .container .footer .donate>a{float:left}body .container .footer .social{float:right}body .container .footer .social ul{overflow:hidden;float:right;padding-left:20px;padding-right:10px}body .container .footer .social ul li{float:right;margin-right:10px;padding-top:5px}body .container .footer .social ul li img{opacity:.6}body .container .footer .social ul li img:hover{opacity:1}body .container .footer .themelink{float:right;padding-right:20px}body #modal-menu{max-width:400px}body #modal-menu a{color:#65b1dd;font-size:20px;line-height:40px}.remodal{padding:30px;box-shadow:0 2px 7px rgba(0,0,0,0.3);background:white;display:none}.remodal form .buttons{float:none}.remodal-wrapper .remodal{display:block}span.info{font-size:10px;font-weight:500;display:inline-block;background:#65b1dd;border-radius:50%;width:15px;height:15px;vertical-align:super;color:white;line-height:15px;margin-left:5px;text-align:center}.hidden{display:none}.clear{clear:both}.nofloat{float:none !important}div.blocker,div.connection-lost,div.modal-dialog{position:fixed;top:0px;left:0px;right:0px;bottom:0px;margin:auto}div.blocker{z-index:5000;background-color:#000000;opacity:.65}#connection-lost-blocker{z-index:5100}#connection-lost-dialog{z-index:5200}div.connection-lost,div.modal-dialog{z-index:5001;display:-webkit-box;display:-moz-box;display:-ms-flexbox;display:-webkit-flex;display:flex;-webkit-box-pack:center;-moz-box-pack:center;-ms-flex-pack:center;-webkit-justify-content:center;justify-content:center;-webkit-box-align:center;-moz-box-align:center;-ms-flex-align:center;-webkit-align-items:center;align-items:center}div.connection-lost div.info,div.modal-dialog div.info{min-width:310px;max-width:650px;margin:5px}div.connection-lost div.title,div.modal-dialog div.title{border:1px solid #65b1dd;background-color:#65b1dd;border-radius:5px 5px 0 0;padding:10px 20px;font-weight:bold;color:lightgray;text-align:center}div.connection-lost div.content,div.modal-dialog div.content{background-color:white;border:1px solid white;padding:20px}div.connection-lost .buttons,div.modal-dialog .buttons{border-radius:0 0 5px 5px;padding-top:10px;overflow:auto}div.connection-lost form,div.modal-dialog form{margin-top:15px}div.connection-lost form textarea,div.modal-dialog form textarea{height:130px;width:420px;padding:10px 12px;border:1px #d8d8d8 solid;border-radius:2px;color:#8f8f8f;font-size:16px;font-weight:300}div.connection-lost form input,div.modal-dialog form input{height:35px;line-height:35px;padding:0 12px}div.modal-dialog .content.buttons ul{float:right}div.modal-dialog .content.buttons .tooltipped{position:relative}div.modal-dialog .content.buttons .tooltipped:after{position:absolute;z-index:1000000;display:none;padding:5px 8px;font:normal normal 11px/1.5 Helvetica,arial,nimbussansl,liberationsans,freesans,clean,sans-serif,"Segoe UI Emoji","Segoe UI Symbol";color:#fff;text-align:center;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-wrap:break-word;white-space:pre;pointer-events:none;content:attr(aria-label);background:rgba(0,0,0,0.8);border-radius:3px;-webkit-font-smoothing:subpixel-antialiased}div.modal-dialog .content.buttons .tooltipped:before{position:absolute;z-index:1000001;display:none;width:0;height:0;color:rgba(0,0,0,0.8);pointer-events:none;content:"";border:5px solid transparent}div.modal-dialog .content.buttons .tooltipped:hover:before,div.modal-dialog .content.buttons .tooltipped:hover:after,div.modal-dialog .content.buttons .tooltipped:active:before,div.modal-dialog .content.buttons .tooltipped:active:after,div.modal-dialog .content.buttons .tooltipped:focus:before,div.modal-dialog .content.buttons .tooltipped:focus:after{display:inline-block;text-decoration:none}div.modal-dialog .content.buttons .tooltipped-w:after{right:100%;bottom:50%;margin-right:5px;-webkit-transform:translateY(50%);-ms-transform:translateY(50%);transform:translateY(50%)}div.modal-dialog .content.buttons .tooltipped-w:before{top:50%;bottom:50%;left:-5px;margin-top:-5px;border-left-color:rgba(0,0,0,0.8)}.importpage form.styled input{margin-top:11px;margin-bottom:11px}.restorewizard form.styled ul,.addwizard form.styled ul{margin:20px;margin-left:0px}.restorewizard form.styled input[type="radio"],.addwizard form.styled input[type="radio"]{width:20px;margin-left:5px;margin-right:5px}.restorewizard form.styled label,.addwizard form.styled label{width:auto;line-height:normal}.restorewizard form.styled div.subtext,.addwizard form.styled div.subtext{clear:both;margin-left:30px;padding-top:5px;color:lightgray}.pauseoptions form.styled li{line-height:normal;padding:0px}.pauseoptions form.styled li input{height:auto;margin-top:8px;margin-right:8px;width:auto}.progress-bar-striped{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);-webkit-background-size:40px 40px;background-size:40px 40px}.progress-striped .progress-bar{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);-webkit-background-size:40px 40px;background-size:40px 40px}.progress{position:relative;min-height:25px}.progress>span{vertical-align:middle;display:block;width:100%;height:100%;text-align:center;z-index:100;padding-top:2px}.progress.active .progress-bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress .progress-bar{float:left;width:0;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#337ab7;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-webkit-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease;height:100%;position:absolute;top:0}.progress .progress-bar.active{-webkit-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.tree-view{list-style-type:none;margin-left:10px;padding-bottom:5px}.tree-view ul{margin-left:16px}.tree-view span.nodeLabel{cursor:pointer}.tree-view span.nodeLabel.selected{border:1px solid #aaa;background-color:#ddd;padding:1px 3px}.tree-view li .node{padding-bottom:5px}.tree-view li div.selected{border-color:lightblue;background-color:lightblue}.tree-view li>ul{display:none}.tree-view li>ul.expanded{display:block}.tree-view li a.nav{cursor:pointer;display:inline-block;width:16px;height:16px;vertical-align:middle;background-image:url(../img/treeicons.png);background-repeat:no-repeat;background-position:-80px 0}.tree-view li a.nav.leaf{background:none}.tree-view li a.nav.expanded{background-position:-80px -16px}.tree-view li a.type{cursor:auto;display:inline-block;width:16px;height:16px;vertical-align:middle;background-image:url(../img/treeicons.png);background-repeat:no-repeat;background-position:0 -16px}.tree-view li a.type.invisible{background-position:0 -32px}.tree-view li a.type.loading{cursor:progress;background-image:url(../img/loader-16.gif);background-repeat:no-repeat;background-position:0px 0px}.tree-view li a.type.x-tree-icon-drive{background-position:-16px -16px}.tree-view li a.type.x-tree-icon-leaf{background-position:-32px -16px}.tree-view li a.type.x-tree-icon-symlink{background-position:-48px -16px}.tree-view li a.type.x-tree-icon-userdata{background-position:-16px -48px}.tree-view li a.type.x-tree-icon-locked{background-position:-64px -16px}.tree-view li a.type.x-tree-icon-broken{background-position:-64px -16px}.tree-view li a.type.x-tree-icon-computer{background-position:0 -48px}.tree-view li a.type.x-tree-icon-hyperv{background-position:-96px -16px}.tree-view li a.type.x-tree-icon-hypervmachine{background-position:-96px 0}.tree-view li a.type.x-tree-icon-mssql{background-position:-96px -32px}.tree-view li a.type.x-tree-icon-mssqldb{background-position:-80px -32px}.tree-view li a.type.x-tree-icon-mydocuments{background-position:-32px -48px}.tree-view li a.type.x-tree-icon-mymusic{background-position:-48px -48px}.tree-view li a.type.x-tree-icon-mypictures{background-position:-64px -48px}.tree-view li a.type.x-tree-icon-desktop{background-position:-80px -48px}.tree-view li a.type.x-tree-icon-home{background-position:-96px -48px}.tree-view li a.type.x-tree-icon-drive.invisible{background-position:-16px -32px}.tree-view li a.type.x-tree-icon-leaf.invisible{background-position:-32px -32px}.tree-view li a.type.x-tree-icon-symlink.invisible{cursor:auto;background-position:-48px -32px}.tree-view li a.type.x-tree-icon-locked.invisible{background-position:-64px -32px}.tree-view li a.check{height:16px;width:16px;display:inline-block;cursor:pointer;background-image:url(../img/treeicons.png);background-repeat:no-repeat;background-position:0px 0px;vertical-align:middle}.tree-view li a.partial{background-position:-32px 0}.tree-view li a.include{background-position:-16px 0}.tree-view li a.exclude{background-position:-48px 0}.tree-view li a.root{background:none;display:none}.throttlesettings div.multiple select{width:auto;margin-right:5px}.throttlesettings div.multiple input{width:100px}.throttlesettings div.multiple input.checkbox{width:auto}.throttlesettings div.multiple label{line-height:35px;padding:0 15px;width:auto;min-width:150px}.throttlesettings .disabled{color:#cfcfcf}.throttlesettings .disabled input,.throttlesettings .disabled select{color:#cfcfcf}@media (max-width:1150px){body .container .header{height:140px}body .container .header .statepadding{padding-right:90px;margin-left:0px}body .container .header .state{width:100%;margin:10px 40px;clear:left;float:left}body .container .header .action-icons{display:none}body .container .header .action-icons-small{display:inline-block}body .container .header .menubutton{display:block;font-size:18px;padding-right:50px;margin-top:5px;margin-right:15px;background:url('../img/menu.png') no-repeat right top;position:relative;height:40px;line-height:40px;color:#8f8f8f;float:right;top:10px;padding-left:20px;text-transform:uppercase;text-align:right}body .container .header .menubutton.active{background-image:url('../img/menu_active.png');color:#65b1dd}body .container .header .donate{display:none}body .container .body{position:relative;padding-top:140px}body .container .body .mainmenu{display:none;position:fixed;background:none repeat scroll 0 0 white;box-shadow:0 4px 8px rgba(0,0,0,0.3);left:10px;padding:20px;top:60px}body .container .body .mainmenu.mobile-open{display:block;left:auto;right:0;top:0;z-index:1000}body .container .body .contextmenu{left:0;top:auto}body .container .body .content{float:none;padding:20px 20px;margin:0px auto 30px auto}body .container .body .content .state{width:auto}body .container .mobileOpen{display:block !important}}@media (max-width:768px){body .container .body .content .tasks .tasklist a{font-size:20px;background-size:24px;background-position:0 4px;padding-left:35px}body .container .body .content .tasks .tasklist dl{padding-left:35px}body .container .header .logo{padding-left:10px}body .container .header .statepadding{padding-right:50px}body .container .header .state{margin-left:10px}body .container .header .menubutton{margin-right:5px}body .container .body .content div.add .steps,body .container .body .content div.restore .steps,body .container .body .content div.settings .steps{display:none}body .container .body .content div.add .steps-legend,body .container .body .content div.restore .steps-legend,body .container .body .content div.settings .steps-legend{list-style:decimal;padding-left:20px;border-bottom:1px solid #eee;margin-bottom:30px;padding-bottom:20px}body .container .body .content div.add .steps-legend li,body .container .body .content div.restore .steps-legend li,body .container .body .content div.settings .steps-legend li{float:none;font-weight:500;width:auto !important;padding-right:0 !important}body .container .body .content div.add .steps-boxes,body .container .body .content div.restore .steps-boxes,body .container .body .content div.settings .steps-boxes{padding-left:0}body .container .body .content div.add form.styled .input input,body .container .body .content div.restore form.styled .input input,body .container .body .content div.settings form.styled .input input,body .container .body .content div.add form.styled .input select,body .container .body .content div.restore form.styled .input select,body .container .body .content div.settings form.styled .input select,body .container .body .content div.add form.styled .input textarea,body .container .body .content div.restore form.styled .input textarea,body .container .body .content div.settings form.styled .input textarea{max-width:100%;box-sizing:border-box}body .container .body .content div.add form.styled .input.select select,body .container .body .content div.restore form.styled .input.select select,body .container .body .content div.settings form.styled .input.select select{width:420px}body .container .body .content div.add form.styled .buttons,body .container .body .content div.restore form.styled .buttons,body .container .body .content div.settings form.styled .buttons{max-width:100%;width:auto}body .container .body .content div.add form.styled .tools,body .container .body .content div.restore form.styled .tools,body .container .body .content div.settings form.styled .tools{padding-left:0 !important}body .container .body .content div.add form.styled .input.checkbox.multiple,body .container .body .content div.restore form.styled .input.checkbox.multiple,body .container .body .content div.settings form.styled .input.checkbox.multiple{padding-bottom:5px}body .container .body .content div.add form.styled .input.checkbox.multiple input,body .container .body .content div.restore form.styled .input.checkbox.multiple input,body .container .body .content div.settings form.styled .input.checkbox.multiple input,body .container .body .content div.add form.styled .input.checkbox.multiple label,body .container .body .content div.restore form.styled .input.checkbox.multiple label,body .container .body .content div.settings form.styled .input.checkbox.multiple label{display:block !important;float:left !important;line-height:normal}body .container .body .content div.add form.styled .input.checkbox.multiple input,body .container .body .content div.restore form.styled .input.checkbox.multiple input,body .container .body .content div.settings form.styled .input.checkbox.multiple input{clear:both}body .container .body .content div.add form.styled .input.text.multiple input,body .container .body .content div.restore form.styled .input.text.multiple input,body .container .body .content div.settings form.styled .input.text.multiple input{max-width:48% !important}}@media (max-width:640px){body h2{font-size:20px;text-align:center}body .container .body{padding-bottom:10px}body .container .body .content{margin:0px auto}body .container .body .content div.add form .input.overlayButton,body .container .body .content div.restore form .input.overlayButton{padding-top:8px;padding-bottom:30px;margin-bottom:10px}body .container .body .content div.add form .input.overlayButton a.button,body .container .body .content div.restore form .input.overlayButton a.button{padding:7px 10px;right:1px;top:9px}body .container .body .content div.add form .input.checkbox.multiple div,body .container .body .content div.restore form .input.checkbox.multiple div{display:block}body .container .body .content div.add form .input.select.multiple input#exclude-larger-than-number,body .container .body .content div.restore form .input.select.multiple input#exclude-larger-than-number{width:75px}body .container .body .content div.add form .input.select.multiple select#exclude-larger-than-multiplier,body .container .body .content div.restore form .input.select.multiple select#exclude-larger-than-multiplier{width:140px}body .container .body .content div.add form .filters .input.textarea,body .container .body .content div.restore form .filters .input.textarea{padding-bottom:10px}body .container .body .content div.add form .filters h3,body .container .body .content div.restore form .filters h3{margin:5px 0}body .container .body .content div.add form .input.text.select.multiple.repeat label,body .container .body .content div.restore form .input.text.select.multiple.repeat label{float:none}body .container .body .content div.add form .input.text.select.multiple.repeat input#repeatRunNumber,body .container .body .content div.restore form .input.text.select.multiple.repeat input#repeatRunNumber{width:70px}body .container .body .content div.add form .input.text.select.multiple.repeat select#repeatRunMultiplier,body .container .body .content div.restore form .input.text.select.multiple.repeat select#repeatRunMultiplier{width:100px}body .container .body .content div.add form .input.multiple.text.select.maxSize input,body .container .body .content div.restore form .input.multiple.text.select.maxSize input{width:70px}body .container .body .content div.add form .input.multiple.text.select.maxSize select,body .container .body .content div.restore form .input.multiple.text.select.maxSize select{width:100px}body .container .body .content div.add form .input.multiple.text.select.keepBackups select,body .container .body .content div.restore form .input.multiple.text.select.keepBackups select{width:85px;padding:4px 6px}body .container .body .content div.add form .input.multiple.text.select.keepBackups input,body .container .body .content div.restore form .input.multiple.text.select.keepBackups input{width:60px}body .container .footer{position:static;padding:15px;line-height:normal;text-align:left;box-sizing:border-box}body .container .footer *{float:none !important;text-align:center;box-sizing:border-box}body .container .footer .about-footer{padding-right:0;display:block}body .container .footer .about-footer span{padding-left:0;padding-bottom:5px}body .container .footer .about-footer li{padding-left:0;float:none;display:inline-block;height:32px;width:32px;background-size:28px !important;border-bottom:none}body .container .footer .about-footer li.support{background:url('../img/support.png') no-repeat center center}body .container .footer .about-footer li.about{background:url('../img/about.png') no-repeat center center}body .container .footer .about-footer li:first-child{padding-bottom:0}body .container .footer .about-footer li:last-child{padding-bottom:20px}body .container .footer li,body .container .footer .donate,body .container .footer .social,body .container .footer .about-footer{padding:8px 0;border-bottom:1px #ddd solid}body .container .footer .donate{display:block}body .container .footer .donate ul li{display:inline-block;border:none;margin:0 5px}body .container .footer .donate ul li a img{margin-top:0}body .container .footer .social li{display:inline-block;border:none}body .container .footer .themelink{padding:5px 0}}@media (max-width:580px){.advancedentry .longdescription{margin-left:0}}@media (max-width:492px){ul.notification{width:auto}}@media (max-width:480px){body{font-size:15px}body .container .header .logo{padding-left:5px}body .container .header .menubutton{margin-right:5px}body .container .header .state{margin-left:5px}body .container .header .statepadding{padding-right:40px}body .container .header .menubutton{padding-left:10px}body .container .body .mainmenu{width:280px;box-sizing:border-box}body .container .body .mainmenu ul li a{font-size:22px}body .container .body .content{padding:15px}body .container .body .content div.add form .input.password .tools ul li,body .container .body .content div.restore form .input.password .tools ul li{font-size:14px}body .container .body .content div.add form .buttons a,body .container .body .content div.restore form .buttons a{float:none;text-align:center;margin-bottom:5px}body .container .body .content div.add .steps-boxes .box.browser .checklinks a,body .container .body .content div.restore .steps-boxes .box.browser .checklinks a{float:none;margin-bottom:8px;display:block}}@media (max-width:400px){body{font-size:15px}body .container .header .menubutton{margin-right:0px;padding-left:0px;padding-right:40px}body .container .header .menubutton span{display:none}}@media (max-width:325px){body{font-size:15px}body .container .header .logo div{display:none}}@media (max-width:200px){body{font-size:15px}body .container .header .menubutton{position:static;margin-top:0px}body .container .header .action-icons-small{clear:right;margin-top:0px}} \ No newline at end of file + */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.5.0');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.5.0') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff2?v=4.5.0') format('woff2'),url('../fonts/fontawesome-webfont.woff?v=4.5.0') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.5.0') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.5.0#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=1);-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2);-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=3);-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1);-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1);-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-intersex:before,.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-genderless:before{content:"\f22d"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-hotel:before,.fa-bed:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.fa-yc:before,.fa-y-combinator:before{content:"\f23b"}.fa-optin-monster:before{content:"\f23c"}.fa-opencart:before{content:"\f23d"}.fa-expeditedssl:before{content:"\f23e"}.fa-battery-4:before,.fa-battery-full:before{content:"\f240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-battery-2:before,.fa-battery-half:before{content:"\f242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-mouse-pointer:before{content:"\f245"}.fa-i-cursor:before{content:"\f246"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-sticky-note:before{content:"\f249"}.fa-sticky-note-o:before{content:"\f24a"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-diners-club:before{content:"\f24c"}.fa-clone:before{content:"\f24d"}.fa-balance-scale:before{content:"\f24e"}.fa-hourglass-o:before{content:"\f250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass:before{content:"\f254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:"\f256"}.fa-hand-scissors-o:before{content:"\f257"}.fa-hand-lizard-o:before{content:"\f258"}.fa-hand-spock-o:before{content:"\f259"}.fa-hand-pointer-o:before{content:"\f25a"}.fa-hand-peace-o:before{content:"\f25b"}.fa-trademark:before{content:"\f25c"}.fa-registered:before{content:"\f25d"}.fa-creative-commons:before{content:"\f25e"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-tripadvisor:before{content:"\f262"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-get-pocket:before{content:"\f265"}.fa-wikipedia-w:before{content:"\f266"}.fa-safari:before{content:"\f267"}.fa-chrome:before{content:"\f268"}.fa-firefox:before{content:"\f269"}.fa-opera:before{content:"\f26a"}.fa-internet-explorer:before{content:"\f26b"}.fa-tv:before,.fa-television:before{content:"\f26c"}.fa-contao:before{content:"\f26d"}.fa-500px:before{content:"\f26e"}.fa-amazon:before{content:"\f270"}.fa-calendar-plus-o:before{content:"\f271"}.fa-calendar-minus-o:before{content:"\f272"}.fa-calendar-times-o:before{content:"\f273"}.fa-calendar-check-o:before{content:"\f274"}.fa-industry:before{content:"\f275"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-map-o:before{content:"\f278"}.fa-map:before{content:"\f279"}.fa-commenting:before{content:"\f27a"}.fa-commenting-o:before{content:"\f27b"}.fa-houzz:before{content:"\f27c"}.fa-vimeo:before{content:"\f27d"}.fa-black-tie:before{content:"\f27e"}.fa-fonticons:before{content:"\f280"}.fa-reddit-alien:before{content:"\f281"}.fa-edge:before{content:"\f282"}.fa-credit-card-alt:before{content:"\f283"}.fa-codiepie:before{content:"\f284"}.fa-modx:before{content:"\f285"}.fa-fort-awesome:before{content:"\f286"}.fa-usb:before{content:"\f287"}.fa-product-hunt:before{content:"\f288"}.fa-mixcloud:before{content:"\f289"}.fa-scribd:before{content:"\f28a"}.fa-pause-circle:before{content:"\f28b"}.fa-pause-circle-o:before{content:"\f28c"}.fa-stop-circle:before{content:"\f28d"}.fa-stop-circle-o:before{content:"\f28e"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-hashtag:before{content:"\f292"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-percent:before{content:"\f295"}*{font-family:'Clear Sans',sans-serif}html,body{margin:0;padding:0;height:100%}h1,h2{font-weight:300;color:#81c401}h1{margin:10px 0}h3{font-weight:400}a{text-decoration:none}ul{list-style:none;margin:0;padding:0}hr{border:none;border-bottom:1px #ddd solid}textarea{max-width:94%}.external-link-image{display:inline-block;margin-left:8px;margin-right:8px;height:16px;width:16px;background:url('../img/external-link-hover.png');background-size:16px}@media only screen and (-webkit-min-device-pixel-ratio:1.25),only screen and (min-resolution:192dpi),only screen and (min-resolution:1.25dppx){.external-link-image{background-image:url('../img/external-link-hover_2x.png')}}@media only screen and (-webkit-min-device-pixel-ratio:1.25),only screen and (min-resolution:288dpi),only screen and (min-resolution:2.25dppx){.external-link-image{background-image:url('../img/external-link-hover_3x.png')}}a .external-link-image{background:url('../img/external-link.png');background-size:16px}@media only screen and (-webkit-min-device-pixel-ratio:1.25),only screen and (min-resolution:192dpi),only screen and (min-resolution:1.25dppx){a .external-link-image{background-image:url('../img/external-link_2x.png')}}@media only screen and (-webkit-min-device-pixel-ratio:2.25),only screen and (min-resolution:288dpi),only screen and (min-resolution:2.25dppx){a .external-link-image{background-image:url('../img/external-link_3x.png')}}.header a:hover .external-link-image{background:url('../img/external-link-hover.png');background-size:16px}@media only screen and (-webkit-min-device-pixel-ratio:1.25),only screen and (min-resolution:192dpi),only screen and (min-resolution:1.25dppx){.header a:hover .external-link-image{background-image:url('../img/external-link-hover_2x.png')}}@media only screen and (-webkit-min-device-pixel-ratio:2.25),only screen and (min-resolution:288dpi),only screen and (min-resolution:2.25dppx){.header a:hover .external-link-image{background-image:url('../img/external-link-hover_3x.png')}}.button{display:block;background:#65b1dd;color:white !important;padding:5px 15px;float:right;margin-left:10px;cursor:pointer;width:auto;border:none;font-family:'Clear Sans',sans-serif;font-size:16px;font-weight:300;border-radius:0}.button:hover{background:#3b9cd4}.step3 source-folder-picker,#folder_path_picker,#restore_file_picker{display:block;border:1px solid lightgray;padding:2px;height:100%;overflow:auto}.not-clickable{cursor:default !important}.not-clickable>a,.not-clickable span,.not-clickable div{cursor:default !important}.ui-match{font-weight:bold;color:darkgreen}wait-area{min-width:350px;text-align:center;display:block}.prewrapped-text{white-space:pre-wrap}.exceptiontext{background-color:lightgray;color:black}.backup-result{width:90%;display:grid;grid-template-columns:50% 50%;grid-auto-rows:minmax(50px, auto);margin:0 auto}.backup-result div .horizontal-rule{width:100%;border-bottom:1px solid #d8d8d8;margin:5px 0 5px 0}.backup-result .box{margin:10px;margin-bottom:0px}.backup-result .title{color:#609101;font-weight:bold;font-size:30px}.backup-result .item{display:block}.backup-result .item .key{color:#81c401;font-weight:bold}.backup-result .item .value{color:#8f8f8f}.backup-result .item .expanded{padding:0 10px 0 18px;margin-bottom:10px}.backup-result .one{border-right:1px solid #d8d8d8;grid-column:1;grid-row:1}.backup-result .two{grid-column:2;grid-row:1}.backup-result .wide{grid-column:span 2;border-top:1px solid #d8d8d8;padding-top:10px}.backup-result .three{grid-row:2}.backup-result .four{grid-row:3;margin-bottom:10px}.success-color{color:#339900}.error-color{color:#cc0000}.warning-color{color:#ffcc00}ul.tabs{margin-bottom:10px}ul.tabs>li{display:inline;margin-right:10px;border:1px solid #65b1dd;padding:5px}ul.tabs>li.active{background-color:#65b1dd;color:white}ul.tabs>li.active>a{background-color:#65b1dd;color:white}ul.tabs>li.active.disabled{border:1px solid lightgray;background-color:lightgray;color:grey;cursor:default}ul.tabs>li.active.disabled>a{background-color:lightgray;color:grey;cursor:default}.licenses>ul{list-style:initial;margin:10px;margin-left:20px}.licenses li{margin-bottom:10px}.licenses a.itemlink{font-weight:bold}.logpage ul.entries{list-style:initial;margin:10px;margin-left:20px}.logpage .entries div.entryline.clickable{cursor:pointer}.logpage .entries.livedata li.expanded{height:auto}.logpage .button{text-align:center;margin-right:10px;border:1px solid #65b1dd;padding:5px;background-color:#65b1dd;color:white;cursor:pointer}.exportpage .checkbox input{width:auto;margin-top:10px}.exportpage .commandline div{background-color:lightgray;color:black}.themelink{margin-left:20px}ul.notification{position:fixed;bottom:0px;left:0px;right:0px;margin:auto;width:480px}.notification .title{border:1px solid #65b1dd;background-color:#65b1dd;border-top-left-radius:5px;border-top-right-radius:5px;border-bottom-left-radius:0px;border-bottom-right-radius:0px;padding:2px;padding-left:5px;padding-right:5px;font-weight:bold;color:lightgray;width:100%;text-align:center;clear:both}.notification .content{background-color:white;border:1px solid #65b1dd;border-top-left-radius:0px;border-top-right-radius:0px;border-bottom-left-radius:5px;border-bottom-right-radius:5px;padding:2px;padding-left:5px;padding-right:5px;width:100%}.notification .message{width:100%;color:black}.notification .button{padding:2px 10px;margin-top:6px}.notification .clear{clear:right;height:1px}.notification .error .title{border-color:red;background-color:red}.notification .error .content{border-color:red}.notification .error .button{border-color:red;background-color:red}.notification .warning .title{background-color:orange;border-color:orange}.notification .warning .button{background-color:orange;border-color:orange}.notification .warning .content{border-color:orange}.filepicker{height:200px}.resizable{margin-bottom:6px;max-width:100%}.advanced-toggle{float:right;margin-right:25px;line-height:37px}.advancedoptions li{clear:both;margin-bottom:10px;padding:10px 0;border-top:1px lightgray solid}.advancedentry .multiple{display:inline}.advancedentry .shortname{font-weight:bold}.advancedentry input[type="text"]{width:300px}.advancedentry select{width:300px}.advancedentry input[type="checkbox"]{margin-top:13px;width:auto}.advancedentry .longdescription{margin-left:190px;clear:both;font-style:italic}.settings div.sublabel{clear:both;padding:0 31px;font-style:italic}.logo img.mainlogo{height:64px;width:64px;float:left;padding-right:8px;padding-top:2px}.logo div.logotext{float:left}.logo a{float:left;display:block;line-height:normal}.logo div.build-suffix{clear:both;display:inline;float:left;font-size:16px;line-height:16px}.logo div.powered-by{font-size:16px;margin:0px;line-height:16px;float:left;padding:0px;margin-left:5px}.fixed-width-font{font-family:monospace}.warning{margin:10px;font-style:italic;color:#f49b42}div.captcha .details{padding-top:10px;margin-left:auto;margin-right:auto;width:180px}.centered-text{text-align:center}body{color:#8f8f8f}body .container{min-height:100%;position:relative}body .container .header{line-height:70px;background:#ededed;overflow:hidden;height:70px;position:fixed;top:0px;left:0px;right:0px;z-index:100}body .container .header a{color:#65b1dd}body .container .header a:hover,body .container .header a.active{color:#4f4f4f}body .container .header .logo{font-size:30px;font-weight:700;float:left;padding-left:40px}body .container .header .statepadding{padding-right:90px;margin-left:320px}body .container .header .state{float:left;color:#609101;width:595px;padding:13px 15px;margin:10px 20px;border:1px #609101 solid;font-weight:300;font-size:18px;overflow:hidden;line-height:normal;display:inline-block;background-color:white;text-overflow:ellipsis;position:relative;height:25px}body .container .header .state strong{display:inline;margin-right:10px}body .container .header .state span{display:inline}body .container .header .state .button{position:static;margin-top:70px}body .container .header .state .content{position:relative;z-index:10;margin-right:40px;display:block;text-overflow:ellipsis;overflow:hidden;white-space:nowrap}body .container .header .state .buttons{position:absolute;right:0px;top:0px;bottom:0px;width:26px;margin:13px 15px}body .container .header .state .buttons .stop{display:block;width:26px;height:26px;background:url('../img/progress-stop.png');background-size:26px;cursor:pointer}@media only screen and (-webkit-min-device-pixel-ratio:1.25),only screen and (min-resolution:192dpi),only screen and (min-resolution:1.25dppx){body .container .header .state .buttons .stop{background-image:url('../img/progress-stop_2x.png')}}@media only screen and (-webkit-min-device-pixel-ratio:2.25),only screen and (min-resolution:288dpi),only screen and (min-resolution:2.25dppx){body .container .header .state .buttons .stop{background-image:url('../img/progress-stop_3x.png')}}body .container .header .state .buttons .resume{display:block;width:26px;height:26px;background:url('../img/progress-resume.png');background-size:26px;cursor:pointer}@media only screen and (-webkit-min-device-pixel-ratio:1.25),only screen and (min-resolution:192dpi),only screen and (min-resolution:1.25dppx){body .container .header .state .buttons .resume{background-image:url('../img/progress-resume_2x.png')}}@media only screen and (-webkit-min-device-pixel-ratio:2.25),only screen and (min-resolution:288dpi),only screen and (min-resolution:2.25dppx){body .container .header .state .buttons .resume{background-image:url('../img/progress-resume_3x.png')}}body .container .header .state .progress-bar{position:absolute;top:0px;bottom:0px;left:0px;background:rgba(129,196,1,0.25);z-index:5}body .container .header .state .task-name{overflow:hidden;text-overflow:ellipsis;cursor:help}body .container .header .state .task-state-info{display:flex}body .container .header .action-icons{display:inline-block;line-height:normal;margin:10px 0px;padding:13px 0px;float:left}body .container .header .action-icons-small{display:none;float:right;margin-top:21px;line-height:normal}body .container .header .action-icons>.pause,body .container .header .action-icons-small>.pause{width:26px;height:26px;display:inline-block;cursor:pointer;background:url('../img/pause.png');background-size:26px}@media only screen and (-webkit-min-device-pixel-ratio:1.25),only screen and (min-resolution:192dpi),only screen and (min-resolution:1.25dppx){body .container .header .action-icons>.pause,body .container .header .action-icons-small>.pause{background-image:url('../img/pause_2x.png')}}@media only screen and (-webkit-min-device-pixel-ratio:2.25),only screen and (min-resolution:288dpi),only screen and (min-resolution:2.25dppx){body .container .header .action-icons>.pause,body .container .header .action-icons-small>.pause{background-image:url('../img/pause_3x.png')}}body .container .header .action-icons>.pause.active,body .container .header .action-icons-small>.pause.active{background:url('../img/resume.png');background-size:26px}@media only screen and (-webkit-min-device-pixel-ratio:1.25),only screen and (min-resolution:192dpi),only screen and (min-resolution:1.25dppx){body .container .header .action-icons>.pause.active,body .container .header .action-icons-small>.pause.active{background-image:url('../img/resume_2x.png')}}@media only screen and (-webkit-min-device-pixel-ratio:2.25),only screen and (min-resolution:288dpi),only screen and (min-resolution:2.25dppx){body .container .header .action-icons>.pause.active,body .container .header .action-icons-small>.pause.active{background-image:url('../img/resume_3x.png')}}body .container .header .action-icons>.throttle,body .container .header .action-icons-small>.throttle{width:26px;height:26px;display:inline-block;cursor:pointer;background:url('../img/throttle.png');background-size:26px}@media only screen and (-webkit-min-device-pixel-ratio:1.25),only screen and (min-resolution:192dpi),only screen and (min-resolution:1.25dppx){body .container .header .action-icons>.throttle,body .container .header .action-icons-small>.throttle{background-image:url('../img/throttle_2x.png')}}@media only screen and (-webkit-min-device-pixel-ratio:2.25),only screen and (min-resolution:288dpi),only screen and (min-resolution:2.25dppx){body .container .header .action-icons>.throttle,body .container .header .action-icons-small>.throttle{background-image:url('../img/throttle_3x.png')}}body .container .header .action-icons>.throttle.inactive,body .container .header .action-icons-small>.throttle.inactive{opacity:.5}body .container .header .about-header{float:right;padding-right:20px;overflow:hidden}body .container .header .about-header ul{overflow:hidden;list-style:none}body .container .header .about-header ul li{float:right;padding-right:20px}body .container .header .donate{float:right}body .container .header .donate ul{overflow:hidden;float:right;padding-left:20px;padding-right:10px}body .container .header .donate ul li{float:right;margin-right:10px;padding-top:5px}body .container .header .donate img{opacity:.6}body .container .header .donate img:hover{opacity:1}body .container .body{width:100%;overflow:hidden;min-height:500px;padding-top:120px;padding-bottom:70px}body .container .body a{color:#65b1dd}body .container .body .mainmenu{width:260px;padding-left:40px;float:left;position:fixed}body .container .body .mainmenu>ul>li{position:relative}body .container .body .mainmenu>ul>li>a{font-size:22px;font-weight:300;padding:5px 10px 5px 55px;display:block}body .container .body .mainmenu>ul>li>a:hover{color:white}body .container .body .mainmenu>ul>li>a.active{color:white}body .container .body .mainmenu>ul>li>a.add{background:url('../img/mainmenu/add.png') no-repeat 8px 7px;background-size:27px 26px}@media only screen and (-webkit-min-device-pixel-ratio:1.25),only screen and (min-resolution:192dpi),only screen and (min-resolution:1.25dppx){body .container .body .mainmenu>ul>li>a.add{background-image:url('../img/mainmenu/add_2x.png')}}@media only screen and (-webkit-min-device-pixel-ratio:2.25),only screen and (min-resolution:288dpi),only screen and (min-resolution:2.25dppx){body .container .body .mainmenu>ul>li>a.add{background-image:url('../img/mainmenu/add_3x.png')}}body .container .body .mainmenu>ul>li>a.restore{background:url('../img/mainmenu/restore.png') no-repeat 8px 7px;background-size:27px 26px}@media only screen and (-webkit-min-device-pixel-ratio:1.25),only screen and (min-resolution:192dpi),only screen and (min-resolution:1.25dppx){body .container .body .mainmenu>ul>li>a.restore{background-image:url('../img/mainmenu/restore_2x.png')}}@media only screen and (-webkit-min-device-pixel-ratio:2.25),only screen and (min-resolution:288dpi),only screen and (min-resolution:2.25dppx){body .container .body .mainmenu>ul>li>a.restore{background-image:url('../img/mainmenu/restore_3x.png')}}body .container .body .mainmenu>ul>li>a.resume{background:url('../img/mainmenu/resume.png') no-repeat 8px 7px;background-size:27px 26px}@media only screen and (-webkit-min-device-pixel-ratio:1.25),only screen and (min-resolution:192dpi),only screen and (min-resolution:1.25dppx){body .container .body .mainmenu>ul>li>a.resume{background-image:url('../img/mainmenu/resume_2x.png')}}@media only screen and (-webkit-min-device-pixel-ratio:2.25),only screen and (min-resolution:288dpi),only screen and (min-resolution:2.25dppx){body .container .body .mainmenu>ul>li>a.resume{background-image:url('../img/mainmenu/resume_3x.png')}}body .container .body .mainmenu>ul>li>a.settings{background:url('../img/mainmenu/settings.png') no-repeat 8px 7px;background-size:27px 26px}@media only screen and (-webkit-min-device-pixel-ratio:1.25),only screen and (min-resolution:192dpi),only screen and (min-resolution:1.25dppx){body .container .body .mainmenu>ul>li>a.settings{background-image:url('../img/mainmenu/settings_2x.png')}}@media only screen and (-webkit-min-device-pixel-ratio:2.25),only screen and (min-resolution:288dpi),only screen and (min-resolution:2.25dppx){body .container .body .mainmenu>ul>li>a.settings{background-image:url('../img/mainmenu/settings_3x.png')}}body .container .body .mainmenu>ul>li>a.logout{background:url('../img/mainmenu/logout.png') no-repeat 8px 7px;background-size:27px 26px}@media only screen and (-webkit-min-device-pixel-ratio:1.25),only screen and (min-resolution:192dpi),only screen and (min-resolution:1.25dppx){body .container .body .mainmenu>ul>li>a.logout{background-image:url('../img/mainmenu/logout_2x.png')}}@media only screen and (-webkit-min-device-pixel-ratio:2.25),only screen and (min-resolution:288dpi),only screen and (min-resolution:2.25dppx){body .container .body .mainmenu>ul>li>a.logout{background-image:url('../img/mainmenu/logout_3x.png')}}body .container .body .mainmenu>ul>li>a.home{background:url('../img/mainmenu/home.png') no-repeat 8px 7px;background-size:27px 26px}@media only screen and (-webkit-min-device-pixel-ratio:1.25),only screen and (min-resolution:192dpi),only screen and (min-resolution:1.25dppx){body .container .body .mainmenu>ul>li>a.home{background-image:url('../img/mainmenu/home_2x.png')}}@media only screen and (-webkit-min-device-pixel-ratio:2.25),only screen and (min-resolution:288dpi),only screen and (min-resolution:2.25dppx){body .container .body .mainmenu>ul>li>a.home{background-image:url('../img/mainmenu/home_3x.png')}}body .container .body .mainmenu>ul>li>a.about{background:url('../img/mainmenu/about.png') no-repeat 8px 7px;background-size:27px 26px}@media only screen and (-webkit-min-device-pixel-ratio:1.25),only screen and (min-resolution:192dpi),only screen and (min-resolution:1.25dppx){body .container .body .mainmenu>ul>li>a.about{background-image:url('../img/mainmenu/about_2x.png')}}@media only screen and (-webkit-min-device-pixel-ratio:2.25),only screen and (min-resolution:288dpi),only screen and (min-resolution:2.25dppx){body .container .body .mainmenu>ul>li>a.about{background-image:url('../img/mainmenu/about_3x.png')}}body .container .body .mainmenu>ul>li>a.home.active{background:#a4d1eb url('../img/mainmenu/over/home.png') no-repeat 8px 7px;background-size:27px 26px}@media only screen and (-webkit-min-device-pixel-ratio:1.25),only screen and (min-resolution:192dpi),only screen and (min-resolution:1.25dppx){body .container .body .mainmenu>ul>li>a.home.active{background-image:url('../img/mainmenu/over/home_2x.png')}}@media only screen and (-webkit-min-device-pixel-ratio:2.25),only screen and (min-resolution:288dpi),only screen and (min-resolution:2.25dppx){body .container .body .mainmenu>ul>li>a.home.active{background-image:url('../img/mainmenu/over/home_3x.png')}}body .container .body .mainmenu>ul>li>a.add.active{background:#a4d1eb url('../img/mainmenu/over/add.png') no-repeat 8px 7px;background-size:27px 26px}@media only screen and (-webkit-min-device-pixel-ratio:1.25),only screen and (min-resolution:192dpi),only screen and (min-resolution:1.25dppx){body .container .body .mainmenu>ul>li>a.add.active{background-image:url('../img/mainmenu/over/add_2x.png')}}@media only screen and (-webkit-min-device-pixel-ratio:2.25),only screen and (min-resolution:288dpi),only screen and (min-resolution:2.25dppx){body .container .body .mainmenu>ul>li>a.add.active{background-image:url('../img/mainmenu/over/add_3x.png')}}body .container .body .mainmenu>ul>li>a.restore.active{background:#a4d1eb url('../img/mainmenu/over/restore.png') no-repeat 8px 7px;background-size:27px 26px}@media only screen and (-webkit-min-device-pixel-ratio:1.25),only screen and (min-resolution:192dpi),only screen and (min-resolution:1.25dppx){body .container .body .mainmenu>ul>li>a.restore.active{background-image:url('../img/mainmenu/over/restore_2x.png')}}@media only screen and (-webkit-min-device-pixel-ratio:2.25),only screen and (min-resolution:288dpi),only screen and (min-resolution:2.25dppx){body .container .body .mainmenu>ul>li>a.restore.active{background-image:url('../img/mainmenu/over/restore_3x.png')}}body .container .body .mainmenu>ul>li>a.resume.active{background:#a4d1eb url('../img/mainmenu/over/resume.png') no-repeat 8px 7px;background-size:27px 26px}@media only screen and (-webkit-min-device-pixel-ratio:1.25),only screen and (min-resolution:192dpi),only screen and (min-resolution:1.25dppx){body .container .body .mainmenu>ul>li>a.resume.active{background-image:url('../img/mainmenu/over/resume_2x.png')}}@media only screen and (-webkit-min-device-pixel-ratio:2.25),only screen and (min-resolution:288dpi),only screen and (min-resolution:2.25dppx){body .container .body .mainmenu>ul>li>a.resume.active{background-image:url('../img/mainmenu/over/resume_3x.png')}}body .container .body .mainmenu>ul>li>a.settings.active{background:#a4d1eb url('../img/mainmenu/over/settings.png') no-repeat 8px 7px;background-size:27px 26px}@media only screen and (-webkit-min-device-pixel-ratio:1.25),only screen and (min-resolution:192dpi),only screen and (min-resolution:1.25dppx){body .container .body .mainmenu>ul>li>a.settings.active{background-image:url('../img/mainmenu/over/settings_2x.png')}}@media only screen and (-webkit-min-device-pixel-ratio:2.25),only screen and (min-resolution:288dpi),only screen and (min-resolution:2.25dppx){body .container .body .mainmenu>ul>li>a.settings.active{background-image:url('../img/mainmenu/over/settings_3x.png')}}body .container .body .mainmenu>ul>li>a.about.active{background:#a4d1eb url('../img/mainmenu/over/about.png') no-repeat 8px 7px;background-size:27px 26px}@media only screen and (-webkit-min-device-pixel-ratio:1.25),only screen and (min-resolution:192dpi),only screen and (min-resolution:1.25dppx){body .container .body .mainmenu>ul>li>a.about.active{background-image:url('../img/mainmenu/over/about_2x.png')}}@media only screen and (-webkit-min-device-pixel-ratio:2.25),only screen and (min-resolution:288dpi),only screen and (min-resolution:2.25dppx){body .container .body .mainmenu>ul>li>a.about.active{background-image:url('../img/mainmenu/over/about_3x.png')}}body .container .body .mainmenu>ul>li>a.add:hover{background:#65b1dd url('../img/mainmenu/over/add.png') no-repeat 8px 7px;background-size:27px 26px}@media only screen and (-webkit-min-device-pixel-ratio:1.25),only screen and (min-resolution:192dpi),only screen and (min-resolution:1.25dppx){body .container .body .mainmenu>ul>li>a.add:hover{background-image:url('../img/mainmenu/over/add_2x.png')}}@media only screen and (-webkit-min-device-pixel-ratio:2.25),only screen and (min-resolution:288dpi),only screen and (min-resolution:2.25dppx){body .container .body .mainmenu>ul>li>a.add:hover{background-image:url('../img/mainmenu/over/add_3x.png')}}body .container .body .mainmenu>ul>li>a.restore:hover{background:#65b1dd url('../img/mainmenu/over/restore.png') no-repeat 8px 7px;background-size:27px 26px}@media only screen and (-webkit-min-device-pixel-ratio:1.25),only screen and (min-resolution:192dpi),only screen and (min-resolution:1.25dppx){body .container .body .mainmenu>ul>li>a.restore:hover{background-image:url('../img/mainmenu/over/restore_2x.png')}}@media only screen and (-webkit-min-device-pixel-ratio:2.25),only screen and (min-resolution:288dpi),only screen and (min-resolution:2.25dppx){body .container .body .mainmenu>ul>li>a.restore:hover{background-image:url('../img/mainmenu/over/restore_3x.png')}}body .container .body .mainmenu>ul>li>a.resume:hover{background:#65b1dd url('../img/mainmenu/over/resume.png') no-repeat 8px 7px;background-size:27px 26px}@media only screen and (-webkit-min-device-pixel-ratio:1.25),only screen and (min-resolution:192dpi),only screen and (min-resolution:1.25dppx){body .container .body .mainmenu>ul>li>a.resume:hover{background-image:url('../img/mainmenu/over/resume_2x.png')}}@media only screen and (-webkit-min-device-pixel-ratio:2.25),only screen and (min-resolution:288dpi),only screen and (min-resolution:2.25dppx){body .container .body .mainmenu>ul>li>a.resume:hover{background-image:url('../img/mainmenu/over/resume_3x.png')}}body .container .body .mainmenu>ul>li>a.settings:hover{background:#65b1dd url('../img/mainmenu/over/settings.png') no-repeat 8px 7px;background-size:27px 26px}@media only screen and (-webkit-min-device-pixel-ratio:1.25),only screen and (min-resolution:192dpi),only screen and (min-resolution:1.25dppx){body .container .body .mainmenu>ul>li>a.settings:hover{background-image:url('../img/mainmenu/over/settings_2x.png')}}@media only screen and (-webkit-min-device-pixel-ratio:2.25),only screen and (min-resolution:288dpi),only screen and (min-resolution:2.25dppx){body .container .body .mainmenu>ul>li>a.settings:hover{background-image:url('../img/mainmenu/over/settings_3x.png')}}body .container .body .mainmenu>ul>li>a.logout:hover{background:#65b1dd url('../img/mainmenu/over/logout.png') no-repeat 8px 7px;background-size:27px 26px}@media only screen and (-webkit-min-device-pixel-ratio:1.25),only screen and (min-resolution:192dpi),only screen and (min-resolution:1.25dppx){body .container .body .mainmenu>ul>li>a.logout:hover{background-image:url('../img/mainmenu/over/logout_2x.png')}}@media only screen and (-webkit-min-device-pixel-ratio:2.25),only screen and (min-resolution:288dpi),only screen and (min-resolution:2.25dppx){body .container .body .mainmenu>ul>li>a.logout:hover{background-image:url('../img/mainmenu/over/logout_3x.png')}}body .container .body .mainmenu>ul>li>a.home:hover{background:#65b1dd url('../img/mainmenu/over/home.png') no-repeat 8px 7px;background-size:27px 26px}@media only screen and (-webkit-min-device-pixel-ratio:1.25),only screen and (min-resolution:192dpi),only screen and (min-resolution:1.25dppx){body .container .body .mainmenu>ul>li>a.home:hover{background-image:url('../img/mainmenu/over/home_2x.png')}}@media only screen and (-webkit-min-device-pixel-ratio:2.25),only screen and (min-resolution:288dpi),only screen and (min-resolution:2.25dppx){body .container .body .mainmenu>ul>li>a.home:hover{background-image:url('../img/mainmenu/over/home_3x.png')}}body .container .body .mainmenu>ul>li>a.about:hover{background:#65b1dd url('../img/mainmenu/over/about.png') no-repeat 8px 7px;background-size:27px 26px}@media only screen and (-webkit-min-device-pixel-ratio:1.25),only screen and (min-resolution:192dpi),only screen and (min-resolution:1.25dppx){body .container .body .mainmenu>ul>li>a.about:hover{background-image:url('../img/mainmenu/over/about_2x.png')}}@media only screen and (-webkit-min-device-pixel-ratio:2.25),only screen and (min-resolution:288dpi),only screen and (min-resolution:2.25dppx){body .container .body .mainmenu>ul>li>a.about:hover{background-image:url('../img/mainmenu/over/about_3x.png')}}body .container .body .mainmenu>ul li.hr-top{padding-top:25px;margin-top:25px;border-top:1px #ededed solid}body .container .body div.contextmenu_container{position:relative}body .container .body .contextmenu{display:none;position:absolute;background:white;border:1px #ededed solid;box-shadow:0 4px 8px rgba(0,0,0,0.3);z-index:200;padding:5px}body .container .body .contextmenu li a{color:#65b1dd;font-size:15px;font-weight:400;padding:0;display:block;min-width:200px;padding:4px 10px;white-space:nowrap;padding-left:45px;overflow:hidden;text-overflow:ellipsis}body .container .body .contextmenu li a:hover{background:#65b1dd;color:white}body .container .body .contextmenu.open{display:block}body .container .body .content{float:left;padding-left:350px;padding-bottom:50px;max-width:70%}body .container .body .content ul.tabs>li{display:inline-block}body .container .body .content .tasks .tasklist .task{border-top:1px solid #eee;padding-top:20px;margin-bottom:25px}body .container .body .content .tasks .tasklist .task:last-child{border-bottom:1px solid #eee;padding-bottom:20px}body .container .body .content .tasks .tasklist .task:first-child{padding-top:0px;border-top:0px none}body .container .body .content .tasks .tasklist .progress-small{text-align:center;height:18px;background:rgba(164,209,235,0.5)}body .container .body .content .tasks .tasklist .progress-small-bg{border:1px #65b1dd solid;width:200px}body .container .body .content .tasks .tasklist a{font-size:30px;font-weight:300;display:inline-block}body .container .body .content .tasks .tasklist a.action-link{font-size:14px;background:none;padding-left:0px}body .container .body .content .tasks .tasklist dl{padding-left:55px;overflow:hidden;font-size:14px}body .container .body .content .tasks .tasklist dl dt,body .container .body .content .tasks .tasklist dl dd{display:block;float:left}body .container .body .content .tasks .tasklist dl dt{clear:both;font-weight:500;margin-bottom:5px}body .container .body .content .tasks .tasklist dl dd{margin-left:10px}body .container .body .content .tasks .tasklist dl.taskmenu p{display:inline;margin-right:10px;color:#65b1dd;cursor:pointer}body .container .body .content .tasks .tasklist dl.taskmenu dt{float:left;margin-right:10px;margin-bottom:0;padding:5px 8px;color:#8f8f8f;cursor:pointer;clear:none}body .container .body .content .tasks .tasklist dl.taskmenu dd{clear:both;float:none;padding-bottom:8px;border-bottom:1px #ddd solid;margin-bottom:5px}body .container .body .content div.add .steps,body .container .body .content div.restore .steps{width:100%;overflow:hidden}body .container .body .content div.add .steps .step,body .container .body .content div.restore .steps .step{float:left;background:url('../img/steps/line-out.png') no-repeat top left;background-size:485px 24px;color:#c7e5f6}@media only screen and (-webkit-min-device-pixel-ratio:1.25),only screen and (min-resolution:192dpi),only screen and (min-resolution:1.25dppx){body .container .body .content div.add .steps .step,body .container .body .content div.restore .steps .step{background-image:url('../img/steps/line-out_2x.png')}}@media only screen and (-webkit-min-device-pixel-ratio:2.25),only screen and (min-resolution:288dpi),only screen and (min-resolution:2.25dppx){body .container .body .content div.add .steps .step,body .container .body .content div.restore .steps .step{background-image:url('../img/steps/line-out_3x.png')}}body .container .body .content div.add .steps .step span,body .container .body .content div.restore .steps .step span{display:block;border:4px #c7e5f6 solid;background:white;border-radius:50%;width:35px;height:35px;text-align:center;font-size:22px;line-height:35px;cursor:pointer}body .container .body .content div.add .steps .step.active,body .container .body .content div.restore .steps .step.active{color:#65b1dd}body .container .body .content div.add .steps .step.active span,body .container .body .content div.restore .steps .step.active span{border:4px #65b1dd solid;background:#65b1dd;color:white}body .container .body .content div.add .steps .step.active h2,body .container .body .content div.restore .steps .step.active h2{color:#65b1dd}body .container .body .content div.add .steps .step:first-child,body .container .body .content div.restore .steps .step:first-child{padding-left:0;background:transparent}body .container .body .content div.add .steps-legend,body .container .body .content div.restore .steps-legend{overflow:hidden;padding-bottom:50px;list-style:none;margin:0}body .container .body .content div.add .steps-legend li,body .container .body .content div.restore .steps-legend li{color:#c7e5f6;font-size:18px;text-align:center;float:left;padding-top:10px;cursor:pointer}body .container .body .content div.add .steps-legend li.active,body .container .body .content div.restore .steps-legend li.active{color:#65b1dd}body .container .body .content div.add .steps-boxes,body .container .body .content div.restore .steps-boxes{padding-left:40px}body .container .body .content div.add .steps-boxes .step,body .container .body .content div.restore .steps-boxes .step{display:none}body .container .body .content div.add .steps-boxes .step.active,body .container .body .content div.restore .steps-boxes .step.active{display:block}body .container .body .content div.add .steps-boxes .box.browser .checklinks a,body .container .body .content div.restore .steps-boxes .box.browser .checklinks a{float:left;margin-left:20px;color:#8f8f8f}body .container .body .content div.add .steps-boxes .box.browser .checklinks a i,body .container .body .content div.restore .steps-boxes .box.browser .checklinks a i{border:2px solid;border-color:#8f8f8f;border-radius:2px;font-size:18px;height:18px;vertical-align:top;width:18px}body .container .body .content div.add .steps-boxes .box.browser .checklinks a.inactive,body .container .body .content div.restore .steps-boxes .box.browser .checklinks a.inactive{color:#c2c2c2;cursor:default}body .container .body .content div.add .steps-boxes .box.browser .checklinks a.inactive i,body .container .body .content div.restore .steps-boxes .box.browser .checklinks a.inactive i{border-color:#c2c2c2}body .container .body .content div.add .steps-boxes .box.browser .checklinks a:first-child,body .container .body .content div.restore .steps-boxes .box.browser .checklinks a:first-child{margin-left:0}body .container .body .content div.add .steps-boxes .box.browser .input.overlayButton,body .container .body .content div.restore .steps-boxes .box.browser .input.overlayButton{padding-top:10px;max-width:100%}body .container .body .content div.add .steps-boxes .box.browser .input.overlayButton input#sourcePath,body .container .body .content div.restore .steps-boxes .box.browser .input.overlayButton input#sourcePath{width:100%;box-sizing:border-box;height:37px}body .container .body .content div.add .steps-boxes .box.browser .input.overlayButton a.button,body .container .body .content div.restore .steps-boxes .box.browser .input.overlayButton a.button{top:10px}body .container .body .content div.add .steps-boxes .box.filters .input.link a,body .container .body .content div.restore .steps-boxes .box.filters .input.link a{color:#8f8f8f}body .container .body .content div.add .steps-boxes .box.filters .input.link a i,body .container .body .content div.restore .steps-boxes .box.filters .input.link a i{border:2px solid;border-color:#8f8f8f;border-radius:2px;font-size:18px;height:18px;vertical-align:top;width:18px}body .container .body .content div.add .steps-boxes .box.filters ul#simplefilterlist,body .container .body .content div.restore .steps-boxes .box.filters ul#simplefilterlist{overflow:hidden;padding-bottom:15px}body .container .body .content div.add .steps-boxes .box.filters ul#simplefilterlist li,body .container .body .content div.restore .steps-boxes .box.filters ul#simplefilterlist li{overflow:hidden;clear:both;padding-bottom:5px}body .container .body .content div.add .steps-boxes .box.filters ul#simplefilterlist li select,body .container .body .content div.restore .steps-boxes .box.filters ul#simplefilterlist li select{width:200px;margin-right:5px;height:36px;line-height:36px}body .container .body .content div.add .steps-boxes .box.filters ul#simplefilterlist li input,body .container .body .content div.restore .steps-boxes .box.filters ul#simplefilterlist li input{width:280px;padding:5px}body .container .body .content div.add .steps-boxes .step1 li.strength.score-0,body .container .body .content div.restore .steps-boxes .step1 li.strength.score-0{color:#ff0000}body .container .body .content div.add .steps-boxes .step1 li.strength.score-1,body .container .body .content div.restore .steps-boxes .step1 li.strength.score-1{color:#ff7700}body .container .body .content div.add .steps-boxes .step1 li.strength.score-2,body .container .body .content div.restore .steps-boxes .step1 li.strength.score-2{color:#aaaa00}body .container .body .content div.add .steps-boxes .step1 li.strength.score-3,body .container .body .content div.restore .steps-boxes .step1 li.strength.score-3{color:#007700}body .container .body .content div.add .steps-boxes .step1 li.strength.score-4,body .container .body .content div.restore .steps-boxes .step1 li.strength.score-4{color:#427e27}body .container .body .content div.add .steps-boxes .step1 li.strength.score-x,body .container .body .content div.restore .steps-boxes .step1 li.strength.score-x{color:#ff0000}body .container .body .content div.add .steps-boxes .step5 div.input.maxSize input.number,body .container .body .content div.restore .steps-boxes .step5 div.input.maxSize input.number,body .container .body .content div.add .steps-boxes .step5 div.input.keepBackups input.number,body .container .body .content div.restore .steps-boxes .step5 div.input.keepBackups input.number{width:60px}body .container .body .content div.add .steps-boxes .step5 .hint,body .container .body .content div.restore .steps-boxes .step5 .hint,body .container .body .content div.add .steps-boxes .step5 .retention-options,body .container .body .content div.restore .steps-boxes .step5 .retention-options{clear:both;margin-left:190px;margin-top:50px;font-style:italic}body .container .body .content div.add .steps-boxes .step5 .retention-options input,body .container .body .content div.restore .steps-boxes .step5 .retention-options input{margin-bottom:10px}body .container .body .content div.add .steps-boxes .step5 .advancedoptions,body .container .body .content div.restore .steps-boxes .step5 .advancedoptions{padding-top:15px;clear:both}body .container .body .content div.add .steps-boxes .step5 .advancedoptions li,body .container .body .content div.restore .steps-boxes .step5 .advancedoptions li{border-top:none}body .container .body .content div.add .steps-boxes .step5 .advancedoptions li>a,body .container .body .content div.restore .steps-boxes .step5 .advancedoptions li>a{display:block;background:#65b1dd;color:white !important;padding:5px 15px;float:right;margin-left:10px;cursor:pointer;width:auto;border:none;font-family:'Clear Sans',sans-serif;font-size:16px;font-weight:300;border-radius:0}body .container .body .content div.add .steps-boxes .step5 .advancedoptions li.advancedentry,body .container .body .content div.restore .steps-boxes .step5 .advancedoptions li.advancedentry{border-bottom:1px solid lightgray}body .container .body .content div.add .steps-boxes .step5 .advancedoptions li:last-child,body .container .body .content div.restore .steps-boxes .step5 .advancedoptions li:last-child{padding-top:0}body .container .body .content div.add .steps-boxes .step5 .advancedoptions li:last-child select,body .container .body .content div.restore .steps-boxes .step5 .advancedoptions li:last-child select{max-width:400px}body .container .body .content div.add .steps-boxes .step5 .advancedoptions label,body .container .body .content div.restore .steps-boxes .step5 .advancedoptions label{line-height:normal}body .container .body .content div.add .steps-boxes .step5 .advancedoptions input,body .container .body .content div.restore .steps-boxes .step5 .advancedoptions input,body .container .body .content div.add .steps-boxes .step5 .advancedoptions select,body .container .body .content div.restore .steps-boxes .step5 .advancedoptions select{width:auto;max-width:100%;box-sizing:border-box}body .container .body .content div.add .steps-boxes .step5 .advancedoptions .longdescription,body .container .body .content div.restore .steps-boxes .step5 .advancedoptions .longdescription{margin-top:10px}body .container .body .content div.add .steps-boxes .step5 .advanced-toggle,body .container .body .content div.restore .steps-boxes .step5 .advanced-toggle{color:#8f8f8f;line-height:normal;margin-top:16px;clear:both;float:left}body .container .body .content div.add .steps-boxes .step5 .advanced-toggle i.fa,body .container .body .content div.restore .steps-boxes .step5 .advanced-toggle i.fa{border:2px solid;border-color:#8f8f8f;border-radius:2px;font-size:18px;height:18px;vertical-align:top;width:18px}body .container .body .content div.add .steps-boxes .step5 textarea,body .container .body .content div.restore .steps-boxes .step5 textarea{box-sizing:border-box;clear:both;margin-top:15px;width:100%}body .container .body .content div.add form,body .container .body .content div.restore form{padding-bottom:50px;overflow:hidden}body .container .body .content div.add form .input.password .tools,body .container .body .content div.restore form .input.password .tools{clear:both;padding-left:190px;padding-top:10px}body .container .body .content div.add form .input.password .tools ul,body .container .body .content div.restore form .input.password .tools ul{overflow:hidden}body .container .body .content div.add form .input.password .tools ul li,body .container .body .content div.restore form .input.password .tools ul li{float:left;padding-right:7px}body .container .body .content div.add form .input.password .tools ul li.strength.useless,body .container .body .content div.restore form .input.password .tools ul li.strength.useless{color:red}body .container .body .content div.add form .input.password .tools ul li.strength.average,body .container .body .content div.restore form .input.password .tools ul li.strength.average{color:yellow}body .container .body .content div.add form .input.password .tools ul li.strength.good,body .container .body .content div.restore form .input.password .tools ul li.strength.good{color:#65b1dd}body .container .body .content div.add form .input.multiple input,body .container .body .content div.restore form .input.multiple input,body .container .body .content div.add form .input.multiple select,body .container .body .content div.restore form .input.multiple select{width:auto;margin-right:5px}body .container .body .content div.add form .input.multiple select,body .container .body .content div.restore form .input.multiple select{padding:5px 12px}body .container .body .content div.add form .input.overlayButton,body .container .body .content div.restore form .input.overlayButton{overflow:hidden;position:relative;max-width:446px}body .container .body .content div.add form .input.overlayButton input,body .container .body .content div.restore form .input.overlayButton input{width:347px}body .container .body .content div.add form .input.overlayButton a.button,body .container .body .content div.restore form .input.overlayButton a.button{position:absolute;top:0;right:0;padding:7px 12px 8px}body .container .body .content div.add form .input.checkbox.multiple strong,body .container .body .content div.restore form .input.checkbox.multiple strong{display:block;padding-bottom:5px}body .container .body .content div.add form .input.checkbox.multiple label,body .container .body .content div.restore form .input.checkbox.multiple label{display:inline-block;float:none;width:auto;padding-right:10px}body .container .body .content div.add form .input.checkbox.multiple input,body .container .body .content div.restore form .input.checkbox.multiple input{width:auto;display:inline-block;float:none}body .container .body .content div.add form .buttons,body .container .body .content div.restore form .buttons{float:none;width:635px;padding-top:30px}body .container .body .content div.add .steps{margin-left:48.5px}body .container .body .content div.add .steps .step{padding-left:97px}body .container .body .content div.add .steps-legend{padding-left:0}body .container .body .content div.add .steps-legend li{width:140px}body .container .body .content div.restore .steps{margin-left:153.5px}body .container .body .content div.restore .steps .step{padding-left:307px}body .container .body .content div.restore .steps-legend{padding-left:0}body .container .body .content div.restore .steps-legend li{width:350px}body .container .body .content div.restore.restore-direct .steps{margin-left:66px}body .container .body .content div.restore.restore-direct .steps .step{padding-left:132px}body .container .body .content div.restore.restore-direct .steps-legend{padding-left:0}body .container .body .content div.restore.restore-direct .steps-legend li{width:175px}body .container .body .content div.restore.restore-direct .step:first-child{padding-left:0;background:transparent}body .container .body .content div.restore.restore-direct .steps-legend{padding-left:20px}body .container .body .content div.headerthreedotmenu{margin:20px 0 20px 0}body .container .body .content div.headerthreedotmenu h2{display:inline}body .container .body .content div.headerthreedotmenu .contextmenu_container{float:right}body .container .body .content div.headerthreedotmenu .contextmenu{left:auto;right:0;top:auto}body .container .body .content div.headerthreedotmenu .threedotmenubutton{padding:5px}body .container .body .content .expandable{margin:20px 0 20px 0}body .container .body .content .expandable h2{display:inline}body .container .body .content .expandable img{padding:0 6px}body .container .body .content div.settings .input.mixed.multiple input.checkbox,body .container .body .content div.settings .input.checkbox input.checkbox{width:auto}body .container .body .content div.settings .input.mixed.multiple select,body .container .body .content div.settings .input.checkbox select{width:auto;margin-right:5px}body .container .body .content div.settings .input.mixed.multiple label,body .container .body .content div.settings .input.checkbox label{line-height:normal;padding:0 15px;width:auto}body .container .body .content .logpage ul.tabs{padding:15px 0}body .container .body .content .logpage ul.entries li{padding:10px 0 10px 0;border-bottom:1px solid #d8d8d8}body .container .body .content .logpage ul.backuplog{list-style:none}body .container .body .content .prewrapped-text{white-space:pre-wrap;overflow-x:auto}body .container .footer{background:#ededed;min-height:70px;line-height:70px;overflow:hidden;position:absolute;bottom:0;width:100%}body .container .footer a{color:#65b1dd}body .container .footer .about-footer{float:left;overflow:hidden;padding-right:20px;display:none}body .container .footer .about-footer span{display:block;float:left;padding-left:20px}body .container .footer .about-footer ul{float:left}body .container .footer .about-footer li{float:left;padding-left:20px}body .container .footer .donate{display:none;float:right;padding-right:40px;overflow:hidden}body .container .footer .donate ul{overflow:hidden;float:right}body .container .footer .donate ul li{float:left;margin-left:10px}body .container .footer .donate ul li a img{margin-top:24px;display:inline-block;opacity:.6}body .container .footer .donate ul li a img:hover{opacity:1}body .container .footer .donate>a{float:left}body .container .footer .social{float:right}body .container .footer .social ul{overflow:hidden;float:right;padding-left:20px;padding-right:10px}body .container .footer .social ul li{float:right;margin-right:10px;padding-top:5px}body .container .footer .social ul li img{opacity:.6}body .container .footer .social ul li img:hover{opacity:1}body .container .footer .themelink{float:right;padding-right:20px}body #modal-menu{max-width:400px}body #modal-menu a{color:#65b1dd;font-size:20px;line-height:40px}.remodal{padding:30px;box-shadow:0 2px 7px rgba(0,0,0,0.3);background:white;display:none}.remodal form .buttons{float:none}.remodal-wrapper .remodal{display:block}span.info{font-size:10px;font-weight:500;display:inline-block;background:#65b1dd;border-radius:50%;width:15px;height:15px;vertical-align:super;color:white;line-height:15px;margin-left:5px;text-align:center}.hidden{display:none}.clear{clear:both}.nofloat{float:none !important}div.blocker,div.connection-lost,div.modal-dialog{position:fixed;top:0px;left:0px;right:0px;bottom:0px;margin:auto}div.blocker{z-index:5000;background-color:#000000;opacity:.65}#connection-lost-blocker{z-index:5100}#connection-lost-dialog{z-index:5200}div.connection-lost,div.modal-dialog{z-index:5001;display:-webkit-box;display:-moz-box;display:-ms-flexbox;display:-webkit-flex;display:flex;-webkit-box-pack:center;-moz-box-pack:center;-ms-flex-pack:center;-webkit-justify-content:center;justify-content:center;-webkit-box-align:center;-moz-box-align:center;-ms-flex-align:center;-webkit-align-items:center;align-items:center}div.connection-lost div.info,div.modal-dialog div.info{min-width:310px;max-width:650px;margin:5px}div.connection-lost div.title,div.modal-dialog div.title{border:1px solid #65b1dd;background-color:#65b1dd;border-radius:5px 5px 0 0;padding:10px 20px;font-weight:bold;color:lightgray;text-align:center}div.connection-lost div.content,div.modal-dialog div.content{background-color:white;border:1px solid white;padding:20px}div.connection-lost .buttons,div.modal-dialog .buttons{border-radius:0 0 5px 5px;padding-top:10px;overflow:auto}div.connection-lost form,div.modal-dialog form{margin-top:15px}div.connection-lost form textarea,div.modal-dialog form textarea{height:130px;width:420px;padding:10px 12px;border:1px #d8d8d8 solid;border-radius:2px;color:#8f8f8f;font-size:16px;font-weight:300}div.connection-lost form input,div.modal-dialog form input{height:35px;line-height:35px;padding:0 12px}div.modal-dialog .content.buttons ul{float:right}div.modal-dialog .content.buttons .tooltipped{position:relative}div.modal-dialog .content.buttons .tooltipped:after{position:absolute;z-index:1000000;display:none;padding:5px 8px;font:normal normal 11px/1.5 Helvetica,arial,nimbussansl,liberationsans,freesans,clean,sans-serif,"Segoe UI Emoji","Segoe UI Symbol";color:#fff;text-align:center;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-wrap:break-word;white-space:pre;pointer-events:none;content:attr(aria-label);background:rgba(0,0,0,0.8);border-radius:3px;-webkit-font-smoothing:subpixel-antialiased}div.modal-dialog .content.buttons .tooltipped:before{position:absolute;z-index:1000001;display:none;width:0;height:0;color:rgba(0,0,0,0.8);pointer-events:none;content:"";border:5px solid transparent}div.modal-dialog .content.buttons .tooltipped:hover:before,div.modal-dialog .content.buttons .tooltipped:hover:after,div.modal-dialog .content.buttons .tooltipped:active:before,div.modal-dialog .content.buttons .tooltipped:active:after,div.modal-dialog .content.buttons .tooltipped:focus:before,div.modal-dialog .content.buttons .tooltipped:focus:after{display:inline-block;text-decoration:none}div.modal-dialog .content.buttons .tooltipped-w:after{right:100%;bottom:50%;margin-right:5px;-webkit-transform:translateY(50%);-ms-transform:translateY(50%);transform:translateY(50%)}div.modal-dialog .content.buttons .tooltipped-w:before{top:50%;bottom:50%;left:-5px;margin-top:-5px;border-left-color:rgba(0,0,0,0.8)}.importpage form.styled input{margin-top:11px;margin-bottom:11px}.restorewizard form.styled ul,.addwizard form.styled ul{margin:20px;margin-left:0px}.restorewizard form.styled input[type="radio"],.addwizard form.styled input[type="radio"]{width:20px;margin-left:5px;margin-right:5px}.restorewizard form.styled label,.addwizard form.styled label{width:auto;line-height:normal}.restorewizard form.styled div.subtext,.addwizard form.styled div.subtext{clear:both;margin-left:30px;padding-top:5px;color:lightgray}.pauseoptions form.styled li{line-height:normal;padding:0px}.pauseoptions form.styled li input{height:auto;margin-top:8px;margin-right:8px;width:auto}.progress-bar-striped{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);-webkit-background-size:40px 40px;background-size:40px 40px}.progress-striped .progress-bar{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);-webkit-background-size:40px 40px;background-size:40px 40px}.progress{position:relative;min-height:25px}.progress>span{vertical-align:middle;display:block;width:100%;height:100%;text-align:center;z-index:100;padding-top:2px}.progress.active .progress-bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress .progress-bar{float:left;width:0;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#337ab7;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-webkit-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease;height:100%;position:absolute;top:0}.progress .progress-bar.active{-webkit-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.tree-view{list-style-type:none;margin-left:10px;padding-bottom:5px}.tree-view ul{margin-left:16px}.tree-view span.nodeLabel{cursor:pointer}.tree-view span.nodeLabel.selected{border:1px solid #aaa;background-color:#ddd;padding:1px 3px}.tree-view li .node{padding-bottom:5px}.tree-view li div.selected{border-color:lightblue;background-color:lightblue}.tree-view li>ul{display:none}.tree-view li>ul.expanded{display:block}.tree-view li a.nav{cursor:pointer;display:inline-block;width:16px;height:16px;vertical-align:middle;background-image:url(../img/treeicons.png);background-repeat:no-repeat;background-position:-80px 0;background-size:112px 64px}@media only screen and (-webkit-min-device-pixel-ratio:1.25),only screen and (min-resolution:192dpi),only screen and (min-resolution:1.25dppx){.tree-view li a.nav{background-image:url('../img/treeicons_2x.png')}}@media only screen and (-webkit-min-device-pixel-ratio:2.25),only screen and (min-resolution:288dpi),only screen and (min-resolution:2.25dppx){.tree-view li a.nav{background-image:url('../img/treeicons_3x.png')}}.tree-view li a.nav.leaf{background:none}.tree-view li a.nav.expanded{background-position:-80px -16px}.tree-view li a.type{cursor:auto;display:inline-block;width:16px;height:16px;vertical-align:middle;background-image:url(../img/treeicons.png);background-repeat:no-repeat;background-position:0 -16px;background-size:112px 64px}@media only screen and (-webkit-min-device-pixel-ratio:1.25),only screen and (min-resolution:192dpi),only screen and (min-resolution:1.25dppx){.tree-view li a.type{background-image:url('../img/treeicons_2x.png')}}@media only screen and (-webkit-min-device-pixel-ratio:2.25),only screen and (min-resolution:288dpi),only screen and (min-resolution:2.25dppx){.tree-view li a.type{background-image:url('../img/treeicons_3x.png')}}.tree-view li a.type.invisible{background-position:0 -32px}.tree-view li a.type.loading{cursor:progress;background-image:url(../img/loader-16.gif);background-repeat:no-repeat;background-position:0px 0px;background-size:16px 16px}@media only screen and (-webkit-min-device-pixel-ratio:1.25),only screen and (min-resolution:192dpi),only screen and (min-resolution:1.25dppx){.tree-view li a.type.loading{background-image:url('../img/loader-32.gif')}}.tree-view li a.type.x-tree-icon-drive{background-position:-16px -16px}.tree-view li a.type.x-tree-icon-leaf{background-position:-32px -16px}.tree-view li a.type.x-tree-icon-symlink{background-position:-48px -16px}.tree-view li a.type.x-tree-icon-userdata{background-position:-16px -48px}.tree-view li a.type.x-tree-icon-locked{background-position:-64px -16px}.tree-view li a.type.x-tree-icon-broken{background-position:-64px -16px}.tree-view li a.type.x-tree-icon-computer{background-position:0 -48px}.tree-view li a.type.x-tree-icon-hyperv{background-position:-96px -16px}.tree-view li a.type.x-tree-icon-hypervmachine{background-position:-96px 0}.tree-view li a.type.x-tree-icon-mssql{background-position:-96px -32px}.tree-view li a.type.x-tree-icon-mssqldb{background-position:-80px -32px}.tree-view li a.type.x-tree-icon-mydocuments{background-position:-32px -48px}.tree-view li a.type.x-tree-icon-mymusic{background-position:-48px -48px}.tree-view li a.type.x-tree-icon-mypictures{background-position:-64px -48px}.tree-view li a.type.x-tree-icon-desktop{background-position:-80px -48px}.tree-view li a.type.x-tree-icon-home{background-position:-96px -48px}.tree-view li a.type.x-tree-icon-drive.invisible{background-position:-16px -32px}.tree-view li a.type.x-tree-icon-leaf.invisible{background-position:-32px -32px}.tree-view li a.type.x-tree-icon-symlink.invisible{cursor:auto;background-position:-48px -32px}.tree-view li a.type.x-tree-icon-locked.invisible{background-position:-64px -32px}.tree-view li a.check{height:16px;width:16px;display:inline-block;cursor:pointer;background-image:url(../img/treeicons.png);background-repeat:no-repeat;background-position:0 0;vertical-align:middle;background-size:112px 64px}@media only screen and (-webkit-min-device-pixel-ratio:1.25),only screen and (min-resolution:192dpi),only screen and (min-resolution:1.25dppx){.tree-view li a.check{background-image:url('../img/treeicons_2x.png')}}@media only screen and (-webkit-min-device-pixel-ratio:2.25),only screen and (min-resolution:288dpi),only screen and (min-resolution:2.25dppx){.tree-view li a.check{background-image:url('../img/treeicons_3x.png')}}.tree-view li a.partial{background-position:-32px 0}.tree-view li a.include{background-position:-16px 0}.tree-view li a.exclude{background-position:-48px 0}.tree-view li a.root{background:none;display:none}.throttlesettings div.multiple select{width:auto;margin-right:5px}.throttlesettings div.multiple input{width:100px}.throttlesettings div.multiple input.checkbox{width:auto}.throttlesettings div.multiple label{line-height:35px;padding:0 15px;width:auto;min-width:150px}.throttlesettings .disabled{color:#cfcfcf}.throttlesettings .disabled input,.throttlesettings .disabled select{color:#cfcfcf}@media (max-width:1150px){body .container .header{height:140px}body .container .header .statepadding{padding-right:90px;margin-left:0px}body .container .header .state{width:100%;margin:10px 40px;clear:left;float:left}body .container .header .action-icons{display:none}body .container .header .action-icons-small{display:inline-block}body .container .header .menubutton{display:block;font-size:18px;padding-right:50px;margin-top:5px;margin-right:15px;background:url('../img/menu.png') no-repeat right top;background-size:39px 39px;position:relative;height:40px;line-height:40px;color:#8f8f8f;float:right;top:10px;padding-left:20px;text-transform:uppercase;text-align:right}body .container .header .menubutton.active{background-image:url('../img/menu_active.png');background-size:39px 39px;color:#65b1dd}body .container .header .donate{display:none}body .container .body{position:relative;padding-top:140px}body .container .body .mainmenu{display:none;position:fixed;background:none repeat scroll 0 0 white;box-shadow:0 4px 8px rgba(0,0,0,0.3);left:10px;padding:20px;top:60px}body .container .body .mainmenu.mobile-open{display:block;left:auto;right:0;top:0;z-index:1000}body .container .body .contextmenu{left:0;top:auto}body .container .body .content{float:none;padding:20px 20px;margin:0px auto 30px auto}body .container .body .content .state{width:auto}body .container .mobileOpen{display:block !important}}@media (max-width:1150px) and (-webkit-min-device-pixel-ratio:1.25),(max-width:1150px) and (min-resolution:192dpi),(max-width:1150px) and (min-resolution:1.25dppx){body .container .header .menubutton{background-image:url('../img/menu_2x.png')}}@media (max-width:1150px) and (-webkit-min-device-pixel-ratio:2.25),(max-width:1150px) and (min-resolution:288dpi),(max-width:1150px) and (min-resolution:2.25dppx){body .container .header .menubutton{background-image:url('../img/menu_3x.png')}}@media (max-width:1150px) and (-webkit-min-device-pixel-ratio:1.25),(max-width:1150px) and (min-resolution:192dpi),(max-width:1150px) and (min-resolution:1.25dppx){body .container .header .menubutton.active{background-image:url('../img/menu_active_2x.png')}}@media (max-width:1150px) and (-webkit-min-device-pixel-ratio:2.25),(max-width:1150px) and (min-resolution:288dpi),(max-width:1150px) and (min-resolution:2.25dppx){body .container .header .menubutton.active{background-image:url('../img/menu_active_3x.png')}}@media (max-width:768px){body .container .body .content .tasks .tasklist a{font-size:20px;background-size:24px;background-position:0 4px;padding-left:35px}body .container .body .content .tasks .tasklist dl{padding-left:35px}body .container .header .logo{padding-left:10px}body .container .header .statepadding{padding-right:50px}body .container .header .state{margin-left:10px}body .container .header .menubutton{margin-right:5px}body .container .body .content div.add .steps,body .container .body .content div.restore .steps,body .container .body .content div.settings .steps{display:none}body .container .body .content div.add .steps-legend,body .container .body .content div.restore .steps-legend,body .container .body .content div.settings .steps-legend{list-style:decimal;padding-left:20px;border-bottom:1px solid #eee;margin-bottom:30px;padding-bottom:20px}body .container .body .content div.add .steps-legend li,body .container .body .content div.restore .steps-legend li,body .container .body .content div.settings .steps-legend li{float:none;font-weight:500;width:auto !important;padding-right:0 !important}body .container .body .content div.add .steps-boxes,body .container .body .content div.restore .steps-boxes,body .container .body .content div.settings .steps-boxes{padding-left:0}body .container .body .content div.add form.styled .input input,body .container .body .content div.restore form.styled .input input,body .container .body .content div.settings form.styled .input input,body .container .body .content div.add form.styled .input select,body .container .body .content div.restore form.styled .input select,body .container .body .content div.settings form.styled .input select,body .container .body .content div.add form.styled .input textarea,body .container .body .content div.restore form.styled .input textarea,body .container .body .content div.settings form.styled .input textarea{max-width:100%;box-sizing:border-box}body .container .body .content div.add form.styled .input.select select,body .container .body .content div.restore form.styled .input.select select,body .container .body .content div.settings form.styled .input.select select{width:420px}body .container .body .content div.add form.styled .buttons,body .container .body .content div.restore form.styled .buttons,body .container .body .content div.settings form.styled .buttons{max-width:100%;width:auto}body .container .body .content div.add form.styled .tools,body .container .body .content div.restore form.styled .tools,body .container .body .content div.settings form.styled .tools{padding-left:0 !important}body .container .body .content div.add form.styled .input.checkbox.multiple,body .container .body .content div.restore form.styled .input.checkbox.multiple,body .container .body .content div.settings form.styled .input.checkbox.multiple{padding-bottom:5px}body .container .body .content div.add form.styled .input.checkbox.multiple input,body .container .body .content div.restore form.styled .input.checkbox.multiple input,body .container .body .content div.settings form.styled .input.checkbox.multiple input,body .container .body .content div.add form.styled .input.checkbox.multiple label,body .container .body .content div.restore form.styled .input.checkbox.multiple label,body .container .body .content div.settings form.styled .input.checkbox.multiple label{display:block !important;float:left !important;line-height:normal}body .container .body .content div.add form.styled .input.checkbox.multiple input,body .container .body .content div.restore form.styled .input.checkbox.multiple input,body .container .body .content div.settings form.styled .input.checkbox.multiple input{clear:both}body .container .body .content div.add form.styled .input.text.multiple input,body .container .body .content div.restore form.styled .input.text.multiple input,body .container .body .content div.settings form.styled .input.text.multiple input{max-width:48% !important}}@media (max-width:640px){body h2{font-size:20px;text-align:center}body .container .body{padding-bottom:10px}body .container .body .content{margin:0px auto}body .container .body .content div.add form .input.overlayButton,body .container .body .content div.restore form .input.overlayButton{padding-top:8px;padding-bottom:30px;margin-bottom:10px}body .container .body .content div.add form .input.overlayButton a.button,body .container .body .content div.restore form .input.overlayButton a.button{padding:7px 10px;right:1px;top:9px}body .container .body .content div.add form .input.checkbox.multiple div,body .container .body .content div.restore form .input.checkbox.multiple div{display:block}body .container .body .content div.add form .input.select.multiple input#exclude-larger-than-number,body .container .body .content div.restore form .input.select.multiple input#exclude-larger-than-number{width:75px}body .container .body .content div.add form .input.select.multiple select#exclude-larger-than-multiplier,body .container .body .content div.restore form .input.select.multiple select#exclude-larger-than-multiplier{width:140px}body .container .body .content div.add form .filters .input.textarea,body .container .body .content div.restore form .filters .input.textarea{padding-bottom:10px}body .container .body .content div.add form .filters h3,body .container .body .content div.restore form .filters h3{margin:5px 0}body .container .body .content div.add form .input.text.select.multiple.repeat label,body .container .body .content div.restore form .input.text.select.multiple.repeat label{float:none}body .container .body .content div.add form .input.text.select.multiple.repeat input#repeatRunNumber,body .container .body .content div.restore form .input.text.select.multiple.repeat input#repeatRunNumber{width:70px}body .container .body .content div.add form .input.text.select.multiple.repeat select#repeatRunMultiplier,body .container .body .content div.restore form .input.text.select.multiple.repeat select#repeatRunMultiplier{width:100px}body .container .body .content div.add form .input.multiple.text.select.maxSize input,body .container .body .content div.restore form .input.multiple.text.select.maxSize input{width:70px}body .container .body .content div.add form .input.multiple.text.select.maxSize select,body .container .body .content div.restore form .input.multiple.text.select.maxSize select{width:100px}body .container .body .content div.add form .input.multiple.text.select.keepBackups select,body .container .body .content div.restore form .input.multiple.text.select.keepBackups select{width:85px;padding:4px 6px}body .container .body .content div.add form .input.multiple.text.select.keepBackups input,body .container .body .content div.restore form .input.multiple.text.select.keepBackups input{width:60px}body .container .footer{position:static;padding:15px;line-height:normal;text-align:left;box-sizing:border-box}body .container .footer *{float:none !important;text-align:center;box-sizing:border-box}body .container .footer .about-footer{padding-right:0;display:block}body .container .footer .about-footer span{padding-left:0;padding-bottom:5px}body .container .footer .about-footer li{padding-left:0;float:none;display:inline-block;height:32px;width:32px;background-size:28px !important;border-bottom:none}body .container .footer .about-footer li:first-child{padding-bottom:0}body .container .footer .about-footer li:last-child{padding-bottom:20px}body .container .footer li,body .container .footer .donate,body .container .footer .social,body .container .footer .about-footer{padding:8px 0;border-bottom:1px #ddd solid}body .container .footer .donate{display:block}body .container .footer .donate ul li{display:inline-block;border:none;margin:0 5px}body .container .footer .donate ul li a img{margin-top:0}body .container .footer .social li{display:inline-block;border:none}body .container .footer .themelink{padding:5px 0}}@media (max-width:580px){.advancedentry .longdescription{margin-left:0}}@media (max-width:492px){ul.notification{width:auto}}@media (max-width:480px){body{font-size:15px}body .container .header .logo{padding-left:5px}body .container .header .menubutton{margin-right:5px}body .container .header .state{margin-left:5px}body .container .header .statepadding{padding-right:40px}body .container .header .menubutton{padding-left:10px}body .container .body .mainmenu{width:280px;box-sizing:border-box}body .container .body .mainmenu ul li a{font-size:22px}body .container .body .content{padding:15px}body .container .body .content div.add form .input.password .tools ul li,body .container .body .content div.restore form .input.password .tools ul li{font-size:14px}body .container .body .content div.add form .buttons a,body .container .body .content div.restore form .buttons a{float:none;text-align:center;margin-bottom:5px}body .container .body .content div.add .steps-boxes .box.browser .checklinks a,body .container .body .content div.restore .steps-boxes .box.browser .checklinks a{float:none;margin-bottom:8px;display:block}}@media (max-width:400px){body{font-size:15px}body .container .header .menubutton{margin-right:0px;padding-left:0px;padding-right:40px}body .container .header .menubutton span{display:none}}@media (max-width:325px){body{font-size:15px}body .container .header .logo div{display:none}}@media (max-width:200px){body{font-size:15px}body .container .header .menubutton{position:static;margin-top:0px}body .container .header .action-icons-small{clear:right;margin-top:0px}} \ No newline at end of file diff --git a/Duplicati/Server/webroot/ngax/templates/addoredit.html b/Duplicati/Server/webroot/ngax/templates/addoredit.html old mode 100755 new mode 100644 index f54c026fb..bd59f0416 --- a/Duplicati/Server/webroot/ngax/templates/addoredit.html +++ b/Duplicati/Server/webroot/ngax/templates/addoredit.html @@ -92,7 +92,7 @@

Backup destination

- +
    @@ -124,7 +124,7 @@

    Source data

    - +
      @@ -161,12 +161,12 @@
      - +
        @@ -212,8 +212,8 @@ @@ -310,7 +310,9 @@
        - This option does not relate to your maximum backup or file size, nor does it affect deduplication rates. See this page before you change the remote volume size. + The backups will be split up into multiple files called volumes. Here + you can set the maximum size of the individual volume files. + See this page for more information.
      @@ -374,12 +376,12 @@
      - +
        diff --git a/Duplicati/Server/webroot/ngax/templates/backends/cos.html b/Duplicati/Server/webroot/ngax/templates/backends/cos.html new file mode 100644 index 000000000..feae7f551 --- /dev/null +++ b/Duplicati/Server/webroot/ngax/templates/backends/cos.html @@ -0,0 +1,35 @@ +
        + + +
        + +
        + + +
        + +
        + + +
        + +
        + + +
        + +
        + + +
        + +
        + + +
        + + diff --git a/Duplicati/Server/webroot/ngax/templates/backends/telegram.html b/Duplicati/Server/webroot/ngax/templates/backends/telegram.html new file mode 100644 index 000000000..f1987789d --- /dev/null +++ b/Duplicati/Server/webroot/ngax/templates/backends/telegram.html @@ -0,0 +1,24 @@ +
        + + +
        +
        + + +
        +
        + + +
        +
        + + +
        +
        + + +
        +
        + + +
        \ No newline at end of file diff --git a/Duplicati/Server/webroot/ngax/templates/copy_clipboard_buttons.html b/Duplicati/Server/webroot/ngax/templates/copy_clipboard_buttons.html index 943811fb9..b81a02f67 100644 --- a/Duplicati/Server/webroot/ngax/templates/copy_clipboard_buttons.html +++ b/Duplicati/Server/webroot/ngax/templates/copy_clipboard_buttons.html @@ -1,4 +1,4 @@ - + Copy diff --git a/Duplicati/Server/webroot/ngax/templates/edituri.html b/Duplicati/Server/webroot/ngax/templates/edituri.html index 77b2443a7..820169b8b 100644 --- a/Duplicati/Server/webroot/ngax/templates/edituri.html +++ b/Duplicati/Server/webroot/ngax/templates/edituri.html @@ -26,12 +26,12 @@
        - +
          diff --git a/Duplicati/Server/webroot/ngax/templates/home.html b/Duplicati/Server/webroot/ngax/templates/home.html index 0df649c8c..ef355f17d 100644 --- a/Duplicati/Server/webroot/ngax/templates/home.html +++ b/Duplicati/Server/webroot/ngax/templates/home.html @@ -3,13 +3,13 @@
          - - - - + + + + {{item.Backup.Name}} - - + +
          diff --git a/Duplicati/Server/webroot/ngax/templates/localdatabase.html b/Duplicati/Server/webroot/ngax/templates/localdatabase.html index 38f245b1c..8f3011223 100644 --- a/Duplicati/Server/webroot/ngax/templates/localdatabase.html +++ b/Duplicati/Server/webroot/ngax/templates/localdatabase.html @@ -5,12 +5,12 @@
          - {{'Each backup has a local database associated with it, which stores information about the remote backup on the local machine.\nThis makes it faster to perform many operations, and reduces the amount of data that needs to be downloaded for each operation.' | translate}} + {{'Each backup has a local database associated with it, which stores information about the remote backup on the local machine. This makes it faster to perform many operations, and reduces the amount of data that needs to be downloaded for each operation.' | translate}}

          Maintenance

          - {{'If the backup and the remote storage is out of sync, Duplicati will require that you perform a repair operation to synchronize the database.\nIf the repair is unsuccesful, you can delete the local database and re-generate.' | translate}} + {{'If the backup and the remote storage is out of sync, Duplicati will require that you perform a repair operation to synchronize the database. If the repair is unsuccesful, you can delete the local database and re-generate.' | translate}}
           
          diff --git a/Duplicati/Server/webroot/ngax/templates/restoredirect.html b/Duplicati/Server/webroot/ngax/templates/restoredirect.html index 508838b7b..cc7a4c482 100644 --- a/Duplicati/Server/webroot/ngax/templates/restoredirect.html +++ b/Duplicati/Server/webroot/ngax/templates/restoredirect.html @@ -31,7 +31,7 @@

          Backup location

          - +
            @@ -64,8 +64,8 @@ diff --git a/Duplicati/UnitTest/BasicSetupHelper.cs b/Duplicati/UnitTest/BasicSetupHelper.cs index 891bf727b..accc49944 100644 --- a/Duplicati/UnitTest/BasicSetupHelper.cs +++ b/Duplicati/UnitTest/BasicSetupHelper.cs @@ -18,6 +18,10 @@ using System; using NUnit.Framework; using System.IO; using System.Collections.Generic; +using System.IO.Compression; +using Duplicati.Library.Common; +using Duplicati.Library.Common.IO; +using Duplicati.Library.Utility; namespace Duplicati.UnitTest { @@ -61,6 +65,8 @@ namespace Duplicati.UnitTest ///
public static readonly bool DEBUG_OUTPUT = Library.Utility.Utility.ParseBool(Environment.GetEnvironmentVariable("DEBUG_OUTPUT"), false); + protected static readonly ISystemIO systemIO = SystemIO.IO_OS; + /// /// Writes a message to TestContext.Progress and Console.Out /// @@ -81,7 +87,7 @@ namespace Duplicati.UnitTest Console.SetOut(TestContext.Progress); } - Directory.CreateDirectory(BASEFOLDER); + systemIO.DirectoryCreate(BASEFOLDER); this.TearDown(); this.OneTimeTearDown(); } @@ -95,37 +101,37 @@ namespace Duplicati.UnitTest [SetUp] public virtual void SetUp() { - Directory.CreateDirectory(this.DATAFOLDER); - Directory.CreateDirectory(this.TARGETFOLDER); - Directory.CreateDirectory(this.RESTOREFOLDER); + systemIO.DirectoryCreate(this.DATAFOLDER); + systemIO.DirectoryCreate(this.TARGETFOLDER); + systemIO.DirectoryCreate(this.RESTOREFOLDER); } [TearDown] public virtual void TearDown() { - if (Directory.Exists(this.DATAFOLDER)) + if (systemIO.DirectoryExists(this.DATAFOLDER)) { - Directory.Delete(this.DATAFOLDER, true); + systemIO.DirectoryDelete(this.DATAFOLDER, true); } - if (Directory.Exists(this.TARGETFOLDER)) + if (systemIO.DirectoryExists(this.TARGETFOLDER)) { - Directory.Delete(this.TARGETFOLDER, true); + systemIO.DirectoryDelete(this.TARGETFOLDER, true); } - if (Directory.Exists(this.RESTOREFOLDER)) + if (systemIO.DirectoryExists(this.RESTOREFOLDER)) { - Directory.Delete(this.RESTOREFOLDER, true); + systemIO.DirectoryDelete(this.RESTOREFOLDER, true); } - if (File.Exists(this.LOGFILE)) + if (systemIO.FileExists(this.LOGFILE)) { - File.Delete(this.LOGFILE); + systemIO.FileDelete(this.LOGFILE); } - if (File.Exists(this.DBFILE)) + if (systemIO.FileExists(this.DBFILE)) { - File.Delete(this.DBFILE); + systemIO.FileDelete(this.DBFILE); } - if (File.Exists($"{this.DBFILE}-journal")) + if (systemIO.FileExists($"{this.DBFILE}-journal")) { - File.Delete($"{this.DBFILE}-journal"); + systemIO.FileDelete($"{this.DBFILE}-journal"); } } @@ -153,6 +159,59 @@ namespace Duplicati.UnitTest } } + /// + /// Alternative to System.IO.Compression.ZipFile.ExtractToDirectory() + /// that handles long paths. + /// + protected static void ZipFileExtractToDirectory(string sourceArchiveFileName, string destinationDirectoryName) + { + if (Platform.IsClientWindows) + { + // Handle long paths under Windows by extracting to a + // temporary file and moving the resulting file to the + // actual destination using functions that support + // long paths. + using (var archive = ZipFile.OpenRead(sourceArchiveFileName)) + { + foreach (var entry in archive.Entries) + { + // By the ZIP spec, directories end in a forward slash + var isDirectory = entry.FullName.EndsWith("/"); + var destination = + systemIO.PathGetFullPath(systemIO.PathCombine(destinationDirectoryName, entry.FullName)); + if (isDirectory) + { + systemIO.DirectoryCreate(destination); + } + else + { + // Not every directory is recorded separately, + // so create directories if needed + systemIO.DirectoryCreate(systemIO.PathGetDirectoryName(destination)); + // Extract file to temporary file, then move to + // the (possibly) long path destination + var tempFile = Path.GetTempFileName(); + try + { + entry.ExtractToFile(tempFile, true); + systemIO.FileMove(tempFile, destination); + } + finally + { + if (systemIO.FileExists(tempFile)) + { + systemIO.FileDelete(tempFile); + } + } + } + } + } + } + else + { + ZipFile.ExtractToDirectory(sourceArchiveFileName, destinationDirectoryName); + } + } } } diff --git a/Duplicati/UnitTest/BorderTests.cs b/Duplicati/UnitTest/BorderTests.cs index c67a0a299..1c4ceaf9e 100644 --- a/Duplicati/UnitTest/BorderTests.cs +++ b/Duplicati/UnitTest/BorderTests.cs @@ -19,6 +19,7 @@ using NUnit.Framework; using System.IO; using System.Linq; using System.Collections.Generic; +using Duplicati.Library.Interface; namespace Duplicati.UnitTest { @@ -212,8 +213,14 @@ namespace Duplicati.UnitTest var filenames = WriteTestFilesToFolder(DATAFOLDER, blocksize, basedatasize); - using(var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null)) - c.Backup(new string[] { DATAFOLDER }); + using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null)) + { + IBackupResults backupResults = c.Backup(new string[] {DATAFOLDER}); + Assert.AreEqual(0, backupResults.Errors.Count()); + + // TODO: This sometimes results in a "No block hash found for file: C:\projects\duplicati\testdata\backup-data\a-0" warning. + // Because of this, we don't check for warnings here. + } // After the first backup we remove the --blocksize argument as that should be auto-set testopts.Remove("blocksize"); @@ -222,7 +229,9 @@ namespace Duplicati.UnitTest using(var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts.Expand(new { version = 0 }), null)) { - c.List("*"); + IListResults listResults = c.List("*"); + Assert.AreEqual(0, listResults.Errors.Count()); + Assert.AreEqual(0, listResults.Warnings.Count()); //Console.WriteLine("In first backup:"); //Console.WriteLine(string.Join(Environment.NewLine, r.Files.Select(x => x.Path))); } @@ -240,6 +249,9 @@ namespace Duplicati.UnitTest using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null)) { var r = c.Backup(new string[] { DATAFOLDER }); + Assert.AreEqual(0, r.Errors.Count()); + Assert.AreEqual(0, r.Warnings.Count()); + if (!Library.Utility.Utility.ParseBoolOption(testopts, "disable-filetime-check")) { if (r.OpenedFiles != filenames.Count) @@ -255,14 +267,20 @@ namespace Duplicati.UnitTest rn.NextBytes(data); File.WriteAllBytes(Path.Combine(DATAFOLDER, "c" + k.Key), data.Take(k.Value).ToArray()); } - - using(var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null)) - c.Backup(new string[] { DATAFOLDER }); - + + using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null)) + { + IBackupResults backupResults = c.Backup(new string[] {DATAFOLDER}); + Assert.AreEqual(0, backupResults.Errors.Count()); + Assert.AreEqual(0, backupResults.Warnings.Count()); + } + using(var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts.Expand(new { version = 0 }), null)) { var r = c.List("*"); + Assert.AreEqual(0, r.Errors.Count()); + Assert.AreEqual(0, r.Warnings.Count()); //ProgressWriteLine("Newest before deleting:"); //ProgressWriteLine(string.Join(Environment.NewLine, r.Files.Select(x => x.Path))); Assert.AreEqual((filenames.Count * 3) + 1, r.Files.Count()); @@ -271,6 +289,8 @@ namespace Duplicati.UnitTest using(var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts.Expand(new { version = 0, no_local_db = true }), null)) { var r = c.List("*"); + Assert.AreEqual(0, r.Errors.Count()); + Assert.AreEqual(0, r.Warnings.Count()); //ProgressWriteLine("Newest without db:"); //ProgressWriteLine(string.Join(Environment.NewLine, r.Files.Select(x => x.Path))); Assert.AreEqual((filenames.Count * 3) + 1, r.Files.Count()); @@ -278,15 +298,28 @@ namespace Duplicati.UnitTest testopts["dbpath"] = this.recreatedDatabaseFile; - using(var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null)) - c.Repair(); + using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null)) + { + IRepairResults repairResults = c.Repair(); + Assert.AreEqual(0, repairResults.Errors.Count()); + + // TODO: This sometimes results in a "No block hash found for file: C:\projects\duplicati\testdata\backup-data\a-0" warning. + // Because of this, we don't check for warnings here. + } - using(var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null)) - Assert.AreEqual(3, c.List().Filesets.Count()); + using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null)) + { + IListResults listResults = c.List(); + Assert.AreEqual(0, listResults.Errors.Count()); + Assert.AreEqual(0, listResults.Warnings.Count()); + Assert.AreEqual(3, listResults.Filesets.Count()); + } using(var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts.Expand(new { version = 2 }), null)) { var r = c.List("*"); + Assert.AreEqual(0, r.Errors.Count()); + Assert.AreEqual(0, r.Warnings.Count()); //ProgressWriteLine("V2 after delete:"); //ProgressWriteLine(string.Join(Environment.NewLine, r.Files.Select(x => x.Path))); Assert.AreEqual((filenames.Count * 1) + 1, r.Files.Count()); @@ -295,6 +328,8 @@ namespace Duplicati.UnitTest using(var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts.Expand(new { version = 1 }), null)) { var r = c.List("*"); + Assert.AreEqual(0, r.Errors.Count()); + Assert.AreEqual(0, r.Warnings.Count()); //ProgressWriteLine("V1 after delete:"); //ProgressWriteLine(string.Join(Environment.NewLine, r.Files.Select(x => x.Path))); Assert.AreEqual((filenames.Count * 2) + 1, r.Files.Count()); @@ -303,6 +338,8 @@ namespace Duplicati.UnitTest using(var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts.Expand(new { version = 0 }), null)) { var r = c.List("*"); + Assert.AreEqual(0, r.Errors.Count()); + Assert.AreEqual(0, r.Warnings.Count()); //ProgressWriteLine("Newest after delete:"); //ProgressWriteLine(string.Join(Environment.NewLine, r.Files.Select(x => x.Path))); Assert.AreEqual((filenames.Count * 3) + 1, r.Files.Count()); @@ -311,16 +348,20 @@ namespace Duplicati.UnitTest using(var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts.Expand(new { restore_path = RESTOREFOLDER, no_local_blocks = true }), null)) { var r = c.Restore(null); + Assert.AreEqual(0, r.Errors.Count()); + Assert.AreEqual(0, r.Warnings.Count()); Assert.AreEqual(filenames.Count * 3, r.RestoredFiles); } - TestUtils.VerifyDir(DATAFOLDER, RESTOREFOLDER, !Library.Utility.Utility.ParseBoolOption(testopts, "skip-metadata")); + TestUtils.AssertDirectoryTreesAreEquivalent(DATAFOLDER, RESTOREFOLDER, !Library.Utility.Utility.ParseBoolOption(testopts, "skip-metadata"), "Restore"); using(var tf = new Library.Utility.TempFolder()) { using(var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts.Expand(new { restore_path = (string)tf, no_local_blocks = true }), null)) { var r = c.Restore(new string[] { Path.Combine(DATAFOLDER, "a") + "*" }); + Assert.AreEqual(0, r.Errors.Count()); + Assert.AreEqual(0, r.Warnings.Count()); Assert.AreEqual(filenames.Count, r.RestoredFiles); } } diff --git a/Duplicati/UnitTest/CommandLineOperationsTests.cs b/Duplicati/UnitTest/CommandLineOperationsTests.cs index 9bb0b1927..a60aa4685 100644 --- a/Duplicati/UnitTest/CommandLineOperationsTests.cs +++ b/Duplicati/UnitTest/CommandLineOperationsTests.cs @@ -25,7 +25,7 @@ namespace Duplicati.UnitTest { public class CommandLineOperationsTests : BasicSetupHelper { - private static readonly string S3_URL = $"https://s3.amazonaws.com/duplicati-test-file-hosting/"; + private static readonly string S3_URL = $"https://testfiles.duplicati.com/"; /// /// The log tag @@ -48,25 +48,25 @@ namespace Duplicati.UnitTest get { return - from x in Directory.EnumerateDirectories(SOURCEFOLDER) + from x in systemIO.EnumerateDirectories(SOURCEFOLDER) orderby x select x; } } - public override void OneTimeSetUp() + public override void SetUp() { - base.OneTimeSetUp(); + base.SetUp(); - if (!File.Exists(zipAlternativeFilepath)) + if (!systemIO.FileExists(zipAlternativeFilepath)) { var url = $"{S3_URL}{this.zipFilename}"; DownloadS3FileIfNewer(zipFilepath, url); - System.IO.Compression.ZipFile.ExtractToDirectory(this.zipFilepath, BASEFOLDER); + ZipFileExtractToDirectory(this.zipFilepath, BASEFOLDER); } else { - System.IO.Compression.ZipFile.ExtractToDirectory(this.zipAlternativeFilepath, BASEFOLDER); + ZipFileExtractToDirectory(this.zipAlternativeFilepath, BASEFOLDER); } } @@ -74,9 +74,9 @@ namespace Duplicati.UnitTest { var webRequest = (HttpWebRequest)WebRequest.Create(url); - if (File.Exists(destinationFilePath)) + if (systemIO.FileExists(destinationFilePath)) { - webRequest.IfModifiedSince = File.GetLastWriteTimeUtc(destinationFilePath); + webRequest.IfModifiedSince = systemIO.FileGetLastWriteTimeUtc(destinationFilePath); } try @@ -104,9 +104,9 @@ namespace Duplicati.UnitTest public override void OneTimeTearDown() { - if (Directory.Exists(this.SOURCEFOLDER)) + if (systemIO.DirectoryExists(this.SOURCEFOLDER)) { - Directory.Delete(this.SOURCEFOLDER, true); + systemIO.DirectoryDelete(this.SOURCEFOLDER, true); } } @@ -143,9 +143,9 @@ namespace Duplicati.UnitTest var targetfolder = Path.Combine(DATAFOLDER, foldername); ProgressWriteLine("Adding folder {0} to source", foldername); - Directory.Move(n, targetfolder); + systemIO.DirectoryMove(n, targetfolder); - var size = Directory.EnumerateFiles(targetfolder, "*", SearchOption.AllDirectories).Select(x => new FileInfo(x).Length).Sum(); + var size = systemIO.EnumerateFiles(targetfolder, "*", SearchOption.AllDirectories).Select(systemIO.FileLength).Sum(); ProgressWriteLine("Running backup with {0} data added ...", Duplicati.Library.Utility.Utility.FormatSizeString(size)); using (new Library.Logging.Timer(LOGTAG, "BackupWithDataAdded", string.Format("Backup with {0} data added", Duplicati.Library.Utility.Utility.FormatSizeString(size)))) @@ -161,29 +161,29 @@ namespace Duplicati.UnitTest using (new Library.Logging.Timer(LOGTAG, "UnchangedBackup", "Unchanged backup")) Duplicati.CommandLine.Program.RealMain(backupargs); - var datafolders = Directory.EnumerateDirectories(DATAFOLDER); + var datafolders = systemIO.EnumerateDirectories(DATAFOLDER); var f = datafolders.Skip(datafolders.Count() / 2).First(); ProgressWriteLine("Renaming folder {0}", Path.GetFileName(f)); - Directory.Move(f, Path.Combine(Path.GetDirectoryName(f), Path.GetFileName(f) + "-renamed")); + systemIO.DirectoryMove(f, Path.Combine(Path.GetDirectoryName(f), Path.GetFileName(f) + "-renamed")); ProgressWriteLine("Running backup with renamed folder..."); using (new Library.Logging.Timer(LOGTAG, "BackupWithRenamedFolder", "Backup with renamed folder")) Duplicati.CommandLine.Program.RealMain(backupargs); - datafolders = Directory.EnumerateDirectories(DATAFOLDER); + datafolders = systemIO.EnumerateDirectories(DATAFOLDER); ProgressWriteLine("Deleting data"); var rm1 = datafolders.First(); var rm2 = datafolders.Skip(1).First(); var rm3 = datafolders.Skip(2).First(); - Directory.Delete(rm1, true); - Directory.Delete(rm2, true); - var rmfiles = Directory.EnumerateFiles(rm3, "*", SearchOption.AllDirectories); + systemIO.DirectoryDelete(rm1, true); + systemIO.DirectoryDelete(rm2, true); + var rmfiles = systemIO.EnumerateFiles(rm3, "*", SearchOption.AllDirectories); foreach (var n in rmfiles.Take(rmfiles.Count() / 2)) - File.Delete(n); + systemIO.FileDelete(n); ProgressWriteLine("Running backup with deleted data..."); using (new Library.Logging.Timer(LOGTAG, "BackupWithDeletedData", "Backup with deleted data")) @@ -196,7 +196,7 @@ namespace Duplicati.UnitTest for (var i = 0; i < 5; i++) { ProgressWriteLine("Running backup with changed logfile {0} of {1} ...", i + 1, 5); - File.Copy(LOGFILE, Path.Combine(SOURCEFOLDER, Path.GetFileName(LOGFILE)), true); + systemIO.FileCopy(LOGFILE, Path.Combine(SOURCEFOLDER, Path.GetFileName(LOGFILE)), true); using (new Library.Logging.Timer(LOGTAG, "BackupWithLogfileChange", string.Format("Backup with logfilechange {0}", i + 1))) Duplicati.CommandLine.Program.RealMain(backupargs); @@ -207,7 +207,7 @@ namespace Duplicati.UnitTest Duplicati.CommandLine.Program.RealMain((new string[] { "compact", target, "--small-file-max-count=2" }.Union(opts)).ToArray()); - datafolders = Directory.EnumerateDirectories(DATAFOLDER); + datafolders = systemIO.EnumerateDirectories(DATAFOLDER); var rf = datafolders.Skip(datafolders.Count() - 2).First(); ProgressWriteLine("Partial restore of {0} ...", Path.GetFileName(rf)); @@ -215,10 +215,10 @@ namespace Duplicati.UnitTest Duplicati.CommandLine.Program.RealMain((new string[] { "restore", target, rf + "*", "--restore-path=\"" + RESTOREFOLDER + "\"" }.Union(opts)).ToArray()); ProgressWriteLine("Verifying partial restore ..."); - using (new Library.Logging.Timer(LOGTAG, "VerifiationOfPartialRestore", "Verification of partial restored files")) - TestUtils.VerifyDir(rf, RESTOREFOLDER, true); + using (new Library.Logging.Timer(LOGTAG, "VerificationOfPartialRestore", "Verification of partial restored files")) + TestUtils.AssertDirectoryTreesAreEquivalent(rf, RESTOREFOLDER, true, "VerificationOfPartialRestore"); - Directory.Delete(RESTOREFOLDER, true); + systemIO.DirectoryDelete(RESTOREFOLDER, true); ProgressWriteLine("Partial restore of {0} without local db...", Path.GetFileName(rf)); using (new Library.Logging.Timer(LOGTAG, "PartialRestoreWithoutLocalDb", "Partial restore without local db")) @@ -226,9 +226,9 @@ namespace Duplicati.UnitTest ProgressWriteLine("Verifying partial restore ..."); using (new Library.Logging.Timer(LOGTAG, "VerificationOfPartialRestore", "Verification of partial restored files")) - TestUtils.VerifyDir(rf, RESTOREFOLDER, true); + TestUtils.AssertDirectoryTreesAreEquivalent(rf, RESTOREFOLDER, true, "VerificationOfPartialRestore"); - Directory.Delete(RESTOREFOLDER, true); + systemIO.DirectoryDelete(RESTOREFOLDER, true); ProgressWriteLine("Full restore ..."); using (new Library.Logging.Timer(LOGTAG, "FullRestore", "Full restore")) @@ -236,10 +236,10 @@ namespace Duplicati.UnitTest ProgressWriteLine("Verifying full restore ..."); using (new Library.Logging.Timer(LOGTAG, "VerificationOfFullRestore", "Verification of restored files")) - foreach (var s in Directory.EnumerateDirectories(DATAFOLDER)) - TestUtils.VerifyDir(s, Path.Combine(RESTOREFOLDER, Path.GetFileName(s)), true); + foreach (var s in systemIO.EnumerateDirectories(DATAFOLDER)) + TestUtils.AssertDirectoryTreesAreEquivalent(s, Path.Combine(RESTOREFOLDER, Path.GetFileName(s)), true, "VerificationOfFullRestore"); - Directory.Delete(RESTOREFOLDER, true); + systemIO.DirectoryDelete(RESTOREFOLDER, true); ProgressWriteLine("Full restore without local db..."); using (new Library.Logging.Timer(LOGTAG, "FullRestoreWithoutDb", "Full restore without local db")) @@ -247,8 +247,8 @@ namespace Duplicati.UnitTest ProgressWriteLine("Verifying full restore ..."); using (new Library.Logging.Timer(LOGTAG, "VerificationOfFullRestoreWithoutDb", "Verification of restored files")) - foreach (var s in Directory.EnumerateDirectories(DATAFOLDER)) - TestUtils.VerifyDir(s, Path.Combine(RESTOREFOLDER, Path.GetFileName(s)), true); + foreach (var s in systemIO.EnumerateDirectories(DATAFOLDER)) + TestUtils.AssertDirectoryTreesAreEquivalent(s, Path.Combine(RESTOREFOLDER, Path.GetFileName(s)), true, "VerificationOfFullRestoreWithoutDb"); ProgressWriteLine("Testing data ..."); using (new Library.Logging.Timer(LOGTAG, "TestRemoteData", "Test remote data")) diff --git a/Duplicati/UnitTest/ControllerTests.cs b/Duplicati/UnitTest/ControllerTests.cs new file mode 100644 index 000000000..c50582928 --- /dev/null +++ b/Duplicati/UnitTest/ControllerTests.cs @@ -0,0 +1,95 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Duplicati.Library.Interface; +using Duplicati.Library.Main; +using NUnit.Framework; + +namespace Duplicati.UnitTest +{ + public class ControllerTests : BasicSetupHelper + { + [Test] + [Category("Controller")] + public void DeleteAllRemoteFiles() + { + string filePath = Path.Combine(this.DATAFOLDER, "file"); + File.WriteAllBytes(filePath, new byte[] {0, 1, 2}); + + Dictionary firstOptions = new Dictionary(this.TestOptions); + using (Controller c = new Controller("file://" + this.TARGETFOLDER, firstOptions, null)) + { + IBackupResults backupResults = c.Backup(new[] {this.DATAFOLDER}); + Assert.AreEqual(0, backupResults.Errors.Count()); + Assert.AreEqual(0, backupResults.Warnings.Count()); + } + + // Keep track of the backend files from the first backup configuration so that we can + // check that they remain after we remove the backend files from the second backup + // configuration. + string[] firstBackupFiles = Directory.GetFiles(this.TARGETFOLDER); + Assert.Greater(firstBackupFiles.Length, 0); + + Dictionary secondOptions = new Dictionary(this.TestOptions) + { + ["dbpath"] = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()) + }; + using (Controller c = new Controller("file://" + this.TARGETFOLDER, secondOptions, null)) + { + // An exception should be thrown due to unrecognized files in the target folder. + // ReSharper disable once AccessToDisposedClosure + Assert.That(() => c.Backup(new[] {this.DATAFOLDER}), Throws.Exception); + } + + // We should be able to safely remove backend files from the second backup by referring + // to the local database. + using (Controller c = new Controller("file://" + this.TARGETFOLDER, secondOptions, null)) + { + IListRemoteResults listResults = c.DeleteAllRemoteFiles(); + Assert.AreEqual(0, listResults.Errors.Count()); + Assert.AreEqual(0, listResults.Warnings.Count()); + } + + // After we delete backend files from the second backup configuration, those from the first + // configuration should remain (see issues #3845, and #4244). + foreach (string file in firstBackupFiles) + { + Assert.IsTrue(File.Exists(file)); + } + + // Configure and run a second backup with a different prefix. This should run without error. + secondOptions["prefix"] = new Options(firstOptions).Prefix + "2"; + using (Controller c = new Controller("file://" + this.TARGETFOLDER, secondOptions, null)) + { + IBackupResults backupResults = c.Backup(new[] {this.DATAFOLDER}); + Assert.AreEqual(0, backupResults.Errors.Count()); + Assert.AreEqual(0, backupResults.Warnings.Count()); + } + + // Even without a local database, we should be able to safely remove backend files from + // the second backup due to the prefix. + File.Delete(secondOptions["dbpath"]); + using (Controller c = new Controller("file://" + this.TARGETFOLDER, secondOptions, null)) + { + IListRemoteResults listResults = c.DeleteAllRemoteFiles(); + Assert.AreEqual(0, listResults.Errors.Count()); + Assert.AreEqual(0, listResults.Warnings.Count()); + } + + // After we delete backend files from the second backup configuration, those from the first + // configuration should remain (see issue #2678). + foreach (string file in firstBackupFiles) + { + Assert.IsTrue(File.Exists(file)); + } + + // The first backup configuration should still run normally. + using (Controller c = new Controller("file://" + this.TARGETFOLDER, firstOptions, null)) + { + IBackupResults backupResults = c.Backup(new[] {this.DATAFOLDER}); + Assert.AreEqual(0, backupResults.Errors.Count()); + Assert.AreEqual(0, backupResults.Warnings.Count()); + } + } + } +} \ No newline at end of file diff --git a/Duplicati/UnitTest/DisruptionTests.cs b/Duplicati/UnitTest/DisruptionTests.cs index eaf10ffd8..f9f0953b9 100644 --- a/Duplicati/UnitTest/DisruptionTests.cs +++ b/Duplicati/UnitTest/DisruptionTests.cs @@ -30,19 +30,19 @@ namespace Duplicati.UnitTest } } - private async Task RunPartialBackup(Controller controller) + private async Task RunPartialBackup(Controller controller) { this.ModifySourceFiles(); // ReSharper disable once AccessToDisposedClosure - Task backupTask = Task.Run(() => controller.Backup(new[] {this.DATAFOLDER})); + Task backupTask = Task.Run(() => controller.Backup(new[] {this.DATAFOLDER})); // Block for a small amount of time to allow the ITaskControl to be associated // with the Controller. Otherwise, the call to Stop will simply be a no-op. Thread.Sleep(1000); controller.Stop(true); - await backupTask.ConfigureAwait(false); + return await backupTask.ConfigureAwait(false); } public override void SetUp() @@ -67,13 +67,17 @@ namespace Duplicati.UnitTest // Run a full backup. using (Controller c = new Controller("file://" + this.TARGETFOLDER, options, null)) { - c.Backup(new[] {this.DATAFOLDER}); + IBackupResults backupResults = c.Backup(new[] {this.DATAFOLDER}); + Assert.AreEqual(0, backupResults.Errors.Count()); + Assert.AreEqual(0, backupResults.Warnings.Count()); } // Run a partial backup. using (Controller c = new Controller("file://" + this.TARGETFOLDER, options, null)) { - await this.RunPartialBackup(c).ConfigureAwait(false); + IBackupResults backupResults = await this.RunPartialBackup(c).ConfigureAwait(false); + Assert.AreEqual(0, backupResults.Errors.Count()); + Assert.AreEqual(1, backupResults.Warnings.Count()); } Dictionary GetBackupTypesFromRemoteFiles(Controller c, out List filelistFiles) @@ -102,7 +106,10 @@ namespace Duplicati.UnitTest Dictionary backupTypeMap; using (Controller c = new Controller("file://" + this.TARGETFOLDER, options, null)) { - c.PurgeFiles(new Library.Utility.FilterExpression($"*{this.fileSizes[0]}*")); + IPurgeFilesResults purgeResults = c.PurgeFiles(new Library.Utility.FilterExpression($"*{this.fileSizes[0]}*")); + Assert.AreEqual(0, purgeResults.Errors.Count()); + Assert.AreEqual(0, purgeResults.Warnings.Count()); + List filesets = c.List().Filesets.ToList(); Assert.AreEqual(2, filesets.Count); Assert.AreEqual(BackupType.FULL_BACKUP, filesets.Single(x => x.Version == 1).IsFullBackup); @@ -125,7 +132,10 @@ namespace Duplicati.UnitTest // Run a repair and verify that the fileset file exists in the new dlist files. using (Controller c = new Controller("file://" + this.TARGETFOLDER, options, null)) { - c.Repair(); + IRepairResults repairResults = c.Repair(); + Assert.AreEqual(0, repairResults.Errors.Count()); + Assert.AreEqual(0, repairResults.Warnings.Count()); + List filesets = c.List().Filesets.ToList(); Assert.AreEqual(2, filesets.Count); Assert.AreEqual(BackupType.FULL_BACKUP, filesets.Single(x => x.Version == 1).IsFullBackup); @@ -152,7 +162,9 @@ namespace Duplicati.UnitTest DateTime firstBackupTime; using (Controller c = new Controller("file://" + this.TARGETFOLDER, options, null)) { - c.Backup(new[] {this.DATAFOLDER}); + IBackupResults backupResults = c.Backup(new[] {this.DATAFOLDER}); + Assert.AreEqual(0, backupResults.Errors.Count()); + Assert.AreEqual(0, backupResults.Warnings.Count()); firstBackupTime = c.List().Filesets.First().Time; } @@ -163,7 +175,9 @@ namespace Duplicati.UnitTest using (Controller c = new Controller("file://" + this.TARGETFOLDER, options, null)) { this.ModifySourceFiles(); - c.Backup(new[] {this.DATAFOLDER}); + IBackupResults backupResults = c.Backup(new[] {this.DATAFOLDER}); + Assert.AreEqual(0, backupResults.Errors.Count()); + Assert.AreEqual(0, backupResults.Warnings.Count()); secondBackupTime = c.List().Filesets.First().Time; } @@ -171,7 +185,9 @@ namespace Duplicati.UnitTest DateTime thirdBackupTime; using (Controller c = new Controller("file://" + this.TARGETFOLDER, options, null)) { - await this.RunPartialBackup(c).ConfigureAwait(false); + IBackupResults backupResults = await this.RunPartialBackup(c).ConfigureAwait(false); + Assert.AreEqual(0, backupResults.Errors.Count()); + Assert.AreEqual(1, backupResults.Warnings.Count()); thirdBackupTime = c.List().Filesets.First().Time; } @@ -180,7 +196,9 @@ namespace Duplicati.UnitTest using (Controller c = new Controller("file://" + this.TARGETFOLDER, options, null)) { options["keep-time"] = $"{(int) ((DateTime.Now - firstBackupTime).TotalSeconds - (secondBackupTime - firstBackupTime).TotalSeconds / 2)}s"; - c.Delete(); + IDeleteResults deleteResults = c.Delete(); + Assert.AreEqual(0, deleteResults.Errors.Count()); + Assert.AreEqual(0, deleteResults.Warnings.Count()); List filesets = c.List().Filesets.ToList(); Assert.AreEqual(2, filesets.Count); @@ -194,13 +212,17 @@ namespace Duplicati.UnitTest // even when all the "recent" backups are partial. using (Controller c = new Controller("file://" + this.TARGETFOLDER, options, null)) { - await this.RunPartialBackup(c).ConfigureAwait(false); + IBackupResults backupResults = await this.RunPartialBackup(c).ConfigureAwait(false); + Assert.AreEqual(0, backupResults.Errors.Count()); + Assert.AreEqual(1, backupResults.Warnings.Count()); DateTime fourthBackupTime = c.List().Filesets.First().Time; // Set the keep-time option so that the threshold lies after the most recent full backup // and run the delete operation. options["keep-time"] = "1s"; - c.Delete(); + IDeleteResults deleteResults = c.Delete(); + Assert.AreEqual(0, deleteResults.Errors.Count()); + Assert.AreEqual(0, deleteResults.Warnings.Count()); List filesets = c.List().Filesets.ToList(); Assert.AreEqual(3, filesets.Count); @@ -224,20 +246,26 @@ namespace Duplicati.UnitTest DateTime firstBackupTime; using (Controller c = new Controller("file://" + this.TARGETFOLDER, options, null)) { - c.Backup(new[] {this.DATAFOLDER}); + IBackupResults backupResults = c.Backup(new[] {this.DATAFOLDER}); + Assert.AreEqual(0, backupResults.Errors.Count()); + Assert.AreEqual(0, backupResults.Warnings.Count()); firstBackupTime = c.List().Filesets.First().Time; } // Run a partial backup. using (Controller c = new Controller("file://" + this.TARGETFOLDER, options, null)) { - await this.RunPartialBackup(c).ConfigureAwait(false); + IBackupResults backupResults = await this.RunPartialBackup(c).ConfigureAwait(false); + Assert.AreEqual(0, backupResults.Errors.Count()); + Assert.AreEqual(1, backupResults.Warnings.Count()); } // Run a partial backup. using (Controller c = new Controller("file://" + this.TARGETFOLDER, options, null)) { - await this.RunPartialBackup(c).ConfigureAwait(false); + IBackupResults backupResults = await this.RunPartialBackup(c).ConfigureAwait(false); + Assert.AreEqual(0, backupResults.Errors.Count()); + Assert.AreEqual(1, backupResults.Warnings.Count()); } // Run a full backup. @@ -245,7 +273,9 @@ namespace Duplicati.UnitTest using (Controller c = new Controller("file://" + this.TARGETFOLDER, options, null)) { this.ModifySourceFiles(); - c.Backup(new[] {this.DATAFOLDER}); + IBackupResults backupResults = c.Backup(new[] {this.DATAFOLDER}); + Assert.AreEqual(0, backupResults.Errors.Count()); + Assert.AreEqual(0, backupResults.Warnings.Count()); fourthBackupTime = c.List().Filesets.First().Time; } @@ -253,7 +283,9 @@ namespace Duplicati.UnitTest using (Controller c = new Controller("file://" + this.TARGETFOLDER, options, null)) { options["keep-versions"] = "2"; - await this.RunPartialBackup(c).ConfigureAwait(false); + IBackupResults backupResults = await this.RunPartialBackup(c).ConfigureAwait(false); + Assert.AreEqual(0, backupResults.Errors.Count()); + Assert.AreEqual(1, backupResults.Warnings.Count()); DateTime fifthBackupTime = c.List().Filesets.First().Time; // Partial backups that are followed by a full backup can be deleted. @@ -271,7 +303,9 @@ namespace Duplicati.UnitTest using (Controller c = new Controller("file://" + this.TARGETFOLDER, options, null)) { this.ModifySourceFiles(); - c.Backup(new[] {this.DATAFOLDER}); + IBackupResults backupResults = c.Backup(new[] {this.DATAFOLDER}); + Assert.AreEqual(0, backupResults.Errors.Count()); + Assert.AreEqual(0, backupResults.Warnings.Count()); DateTime sixthBackupTime = c.List().Filesets.First().Time; // Since the last backup was full, we can now expect to have just the 2 most recent full backups. @@ -298,13 +332,17 @@ namespace Duplicati.UnitTest // Run a full backup. using (Controller c = new Controller("file://" + this.TARGETFOLDER, options, null)) { - c.Backup(new[] {this.DATAFOLDER}); + IBackupResults backupResults = c.Backup(new[] {this.DATAFOLDER}); + Assert.AreEqual(0, backupResults.Errors.Count()); + Assert.AreEqual(0, backupResults.Warnings.Count()); } // Run a partial backup. using (Controller c = new Controller("file://" + this.TARGETFOLDER, options, null)) { - await this.RunPartialBackup(c).ConfigureAwait(false); + IBackupResults backupResults = await this.RunPartialBackup(c).ConfigureAwait(false); + Assert.AreEqual(0, backupResults.Errors.Count()); + Assert.AreEqual(1, backupResults.Warnings.Count()); List filesets = c.List().Filesets.ToList(); Assert.AreEqual(2, filesets.Count); @@ -330,14 +368,18 @@ namespace Duplicati.UnitTest DateTime firstBackupTime; using (Controller c = new Controller("file://" + this.TARGETFOLDER, options, null)) { - c.Backup(new[] {this.DATAFOLDER}); + IBackupResults backupResults = c.Backup(new[] {this.DATAFOLDER}); + Assert.AreEqual(0, backupResults.Errors.Count()); + Assert.AreEqual(0, backupResults.Warnings.Count()); firstBackupTime = c.List().Filesets.First().Time; List filesets = c.List().Filesets.ToList(); Assert.AreEqual(1, filesets.Count); this.ModifySourceFiles(); - c.Backup(new[] {this.DATAFOLDER}); + backupResults = c.Backup(new[] {this.DATAFOLDER}); + Assert.AreEqual(0, backupResults.Errors.Count()); + Assert.AreEqual(0, backupResults.Warnings.Count()); DateTime secondBackupTime = c.List().Filesets.First().Time; // Since the most recent backup is not considered in the retention logic, the only backup in the first time frame @@ -356,7 +398,9 @@ namespace Duplicati.UnitTest DateTime thirdBackupTime; using (Controller c = new Controller("file://" + this.TARGETFOLDER, options, null)) { - await this.RunPartialBackup(c).ConfigureAwait(false); + IBackupResults backupResults = await this.RunPartialBackup(c).ConfigureAwait(false); + Assert.AreEqual(0, backupResults.Errors.Count()); + Assert.AreEqual(1, backupResults.Warnings.Count()); thirdBackupTime = c.List().Filesets.First().Time; // Since the most recent backup is not considered in the retention logic, there are no backups in the first time @@ -374,7 +418,9 @@ namespace Duplicati.UnitTest using (Controller c = new Controller("file://" + this.TARGETFOLDER, options, null)) { this.ModifySourceFiles(); - c.Backup(new[] {this.DATAFOLDER}); + IBackupResults backupResults = c.Backup(new[] {this.DATAFOLDER}); + Assert.AreEqual(0, backupResults.Errors.Count()); + Assert.AreEqual(0, backupResults.Warnings.Count()); fourthBackupTime = c.List().Filesets.First().Time; // Since the most recent backup is not considered in the retention logic, the third backup is the only backup @@ -390,7 +436,9 @@ namespace Duplicati.UnitTest Assert.AreEqual(BackupType.FULL_BACKUP, filesets[0].IsFullBackup); this.ModifySourceFiles(); - c.Backup(new[] {this.DATAFOLDER}); + backupResults = c.Backup(new[] {this.DATAFOLDER}); + Assert.AreEqual(0, backupResults.Errors.Count()); + Assert.AreEqual(0, backupResults.Warnings.Count()); DateTime fifthBackupTime = c.List().Filesets.First().Time; // Since the most recent backup is not considered in the retention logic, we now have two backups in the @@ -415,7 +463,9 @@ namespace Duplicati.UnitTest using (Controller c = new Controller("file://" + this.TARGETFOLDER, options, null)) { this.ModifySourceFiles(); - c.Backup(new[] {this.DATAFOLDER}); + IBackupResults backupResults = c.Backup(new[] {this.DATAFOLDER}); + Assert.AreEqual(0, backupResults.Errors.Count()); + Assert.AreEqual(0, backupResults.Warnings.Count()); DateTime sixthBackupTime = c.List().Filesets.First().Time; // Since the most recent backup is not considered in the retention logic, we now have three backups in the @@ -444,7 +494,10 @@ namespace Duplicati.UnitTest // Run a complete backup. using (Controller c = new Controller("file://" + this.TARGETFOLDER, options, null)) { - c.Backup(new[] {this.DATAFOLDER}); + IBackupResults backupResults = c.Backup(new[] {this.DATAFOLDER}); + Assert.AreEqual(0, backupResults.Errors.Count()); + Assert.AreEqual(0, backupResults.Warnings.Count()); + Assert.AreEqual(1, c.List().Filesets.Count()); Assert.AreEqual(BackupType.FULL_BACKUP, c.List().Filesets.Single(x => x.Version == 0).IsFullBackup); } @@ -452,7 +505,9 @@ namespace Duplicati.UnitTest // Run a partial backup. using (Controller c = new Controller("file://" + this.TARGETFOLDER, options, null)) { - await this.RunPartialBackup(c).ConfigureAwait(false); + IBackupResults backupResults = await this.RunPartialBackup(c).ConfigureAwait(false); + Assert.AreEqual(0, backupResults.Errors.Count()); + Assert.AreEqual(1, backupResults.Warnings.Count()); // If we interrupt the backup, the most recent Fileset should be marked as partial. Assert.AreEqual(2, c.List().Filesets.Count()); @@ -472,7 +527,7 @@ namespace Duplicati.UnitTest foreach (string filepath in partialVersionFiles) { string filename = Path.GetFileName(filepath); - Assert.IsTrue(TestUtils.CompareFiles(filepath, Path.Combine(this.RESTOREFOLDER, filename ?? String.Empty), filename, false)); + TestUtils.AssertFilesAreEqual(filepath, Path.Combine(this.RESTOREFOLDER, filename ?? String.Empty), false, filename); } } @@ -480,7 +535,10 @@ namespace Duplicati.UnitTest File.Delete(this.DBFILE); using (Controller c = new Controller("file://" + this.TARGETFOLDER, options, null)) { - c.Repair(); + IRepairResults repairResults = c.Repair(); + Assert.AreEqual(0, repairResults.Errors.Count()); + Assert.AreEqual(0, repairResults.Warnings.Count()); + Assert.AreEqual(2, c.List().Filesets.Count()); Assert.AreEqual(BackupType.FULL_BACKUP, c.List().Filesets.Single(x => x.Version == 1).IsFullBackup); Assert.AreEqual(BackupType.PARTIAL_BACKUP, c.List().Filesets.Single(x => x.Version == 0).IsFullBackup); @@ -489,8 +547,11 @@ namespace Duplicati.UnitTest // Run a complete backup. Listing the Filesets should include both full and partial backups. using (Controller c = new Controller("file://" + this.TARGETFOLDER, options, null)) { - c.Backup(new[] {this.DATAFOLDER}); + IBackupResults backupResults = c.Backup(new[] {this.DATAFOLDER}); + Assert.AreEqual(0, backupResults.Errors.Count()); + Assert.AreEqual(0, backupResults.Warnings.Count()); Assert.AreEqual(3, c.List().Filesets.Count()); + Assert.AreEqual(BackupType.FULL_BACKUP, c.List().Filesets.Single(x => x.Version == 2).IsFullBackup); Assert.AreEqual(BackupType.PARTIAL_BACKUP, c.List().Filesets.Single(x => x.Version == 1).IsFullBackup); Assert.AreEqual(BackupType.FULL_BACKUP, c.List().Filesets.Single(x => x.Version == 0).IsFullBackup); @@ -503,12 +564,15 @@ namespace Duplicati.UnitTest IListResults lastResults = c.List("*"); string[] fullVersionFiles = lastResults.Files.Select(x => x.Path).Where(x => !Utility.IsFolder(x, File.GetAttributes)).ToArray(); Assert.AreEqual(this.fileSizes.Length, fullVersionFiles.Length); - c.Restore(fullVersionFiles); + + IRestoreResults restoreResults = c.Restore(fullVersionFiles); + Assert.AreEqual(0, restoreResults.Errors.Count()); + Assert.AreEqual(0, restoreResults.Warnings.Count()); foreach (string filepath in fullVersionFiles) { string filename = Path.GetFileName(filepath); - Assert.IsTrue(TestUtils.CompareFiles(filepath, Path.Combine(this.RESTOREFOLDER, filename ?? String.Empty), filename, false)); + TestUtils.AssertFilesAreEqual(filepath, Path.Combine(this.RESTOREFOLDER, filename ?? String.Empty), false, filename); } } } @@ -523,7 +587,10 @@ namespace Duplicati.UnitTest // Run a complete backup. using (Controller c = new Controller("file://" + this.TARGETFOLDER, options, null)) { - c.Backup(new[] {this.DATAFOLDER}); + IBackupResults backupResults = c.Backup(new[] {this.DATAFOLDER}); + Assert.AreEqual(0, backupResults.Errors.Count()); + Assert.AreEqual(0, backupResults.Warnings.Count()); + List filesets = c.List().Filesets.ToList(); Assert.AreEqual(1, filesets.Count); Assert.AreEqual(BackupType.FULL_BACKUP, filesets[0].IsFullBackup); @@ -547,7 +614,10 @@ namespace Duplicati.UnitTest // The next backup should proceed without issues. using (Controller c = new Controller("file://" + this.TARGETFOLDER, options, null)) { - c.Backup(new[] {this.DATAFOLDER}); + IBackupResults backupResults = c.Backup(new[] {this.DATAFOLDER}); + Assert.AreEqual(0, backupResults.Errors.Count()); + Assert.AreEqual(0, backupResults.Warnings.Count()); + List filesets = c.List().Filesets.ToList(); Assert.AreEqual(2, filesets.Count); Assert.AreEqual(BackupType.FULL_BACKUP, filesets[1].IsFullBackup); @@ -561,12 +631,15 @@ namespace Duplicati.UnitTest IListResults lastResults = c.List("*"); string[] fullVersionFiles = lastResults.Files.Select(x => x.Path).Where(x => !Utility.IsFolder(x, File.GetAttributes)).ToArray(); Assert.AreEqual(this.fileSizes.Length, fullVersionFiles.Length); - c.Restore(fullVersionFiles); + + IRestoreResults restoreResults = c.Restore(fullVersionFiles); + Assert.AreEqual(0, restoreResults.Errors.Count()); + Assert.AreEqual(0, restoreResults.Warnings.Count()); foreach (string filepath in fullVersionFiles) { string filename = Path.GetFileName(filepath); - Assert.IsTrue(TestUtils.CompareFiles(filepath, Path.Combine(this.RESTOREFOLDER, filename ?? String.Empty), filename, false)); + TestUtils.AssertFilesAreEqual(filepath, Path.Combine(this.RESTOREFOLDER, filename ?? String.Empty), false, filename); } } } diff --git a/Duplicati/UnitTest/FilterTest.cs b/Duplicati/UnitTest/FilterTest.cs index 602afbdb7..a40da0a17 100644 --- a/Duplicati/UnitTest/FilterTest.cs +++ b/Duplicati/UnitTest/FilterTest.cs @@ -19,6 +19,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using Duplicati.Library.Common.IO; +using Duplicati.Library.Interface; using Duplicati.Library.Utility; using NUnit.Framework; @@ -60,12 +61,18 @@ namespace Duplicati.UnitTest // Create a fileset with all data present using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null)) - c.Backup(new string[] { DATAFOLDER }); + { + IBackupResults backupResults = c.Backup(new string[] {DATAFOLDER}); + Assert.AreEqual(0, backupResults.Errors.Count()); + Assert.AreEqual(0, backupResults.Warnings.Count()); + } // Check that we have 4 files and 7 folders using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts.Expand(new { version = 0 }), null)) { var r = c.List("*"); + Assert.AreEqual(0, r.Errors.Count()); + Assert.AreEqual(0, r.Warnings.Count()); var folders = r.Files.Count(x => x.Path.EndsWith(Util.DirectorySeparatorString, StringComparison.Ordinal)); var files = r.Files.Count(x => !x.Path.EndsWith(Util.DirectorySeparatorString, StringComparison.Ordinal)); @@ -79,12 +86,18 @@ namespace Duplicati.UnitTest System.Threading.Thread.Sleep(5000); testopts["ignore-filenames"] = "exclude.me"; using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null)) - c.Backup(new string[] { DATAFOLDER }); + { + IBackupResults backupResults = c.Backup(new string[] {DATAFOLDER}); + Assert.AreEqual(0, backupResults.Errors.Count()); + Assert.AreEqual(0, backupResults.Warnings.Count()); + } // Check that we have 2 files and 6 folders after excluding the "excludefile" folder using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts.Expand(new { version = 0 }), null)) { var r = c.List("*"); + Assert.AreEqual(0, r.Errors.Count()); + Assert.AreEqual(0, r.Warnings.Count()); var folders = r.Files.Count(x => x.Path.EndsWith(Util.DirectorySeparatorString, StringComparison.Ordinal)); var files = r.Files.Count(x => !x.Path.EndsWith(Util.DirectorySeparatorString, StringComparison.Ordinal)); @@ -98,12 +111,18 @@ namespace Duplicati.UnitTest System.Threading.Thread.Sleep(5000); testopts["exclude-empty-folders"] = "true"; using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null)) - c.Backup(new string[] { DATAFOLDER }); + { + IBackupResults backupResults = c.Backup(new string[] {DATAFOLDER}); + Assert.AreEqual(0, backupResults.Errors.Count()); + Assert.AreEqual(0, backupResults.Warnings.Count()); + } // Check that the two empty folders are now removed using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts.Expand(new { version = 0 }), null)) { var r = c.List("*"); + Assert.AreEqual(0, r.Errors.Count()); + Assert.AreEqual(0, r.Warnings.Count()); var folders = r.Files.Count(x => x.Path.EndsWith(Util.DirectorySeparatorString, StringComparison.Ordinal)); var files = r.Files.Count(x => !x.Path.EndsWith(Util.DirectorySeparatorString, StringComparison.Ordinal)); @@ -116,13 +135,19 @@ namespace Duplicati.UnitTest // Filter out one file and rerun the backup to exclude the folder System.Threading.Thread.Sleep(5000); var excludefilter = new Library.Utility.FilterExpression($"*{System.IO.Path.DirectorySeparatorChar}myfile.txt", false); - using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null )) - c.Backup(new string[] { DATAFOLDER }, excludefilter); + using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null)) + { + IBackupResults backupResults = c.Backup(new string[] {DATAFOLDER}, excludefilter); + Assert.AreEqual(0, backupResults.Errors.Count()); + Assert.AreEqual(0, backupResults.Warnings.Count()); + } // Check that the empty folder is now removed using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts.Expand(new { version = 0 }), null)) { var r = c.List("*"); + Assert.AreEqual(0, r.Errors.Count()); + Assert.AreEqual(0, r.Warnings.Count()); var folders = r.Files.Count(x => x.Path.EndsWith(Util.DirectorySeparatorString, StringComparison.Ordinal)); var files = r.Files.Count(x => !x.Path.EndsWith(Util.DirectorySeparatorString, StringComparison.Ordinal)); @@ -136,12 +161,18 @@ namespace Duplicati.UnitTest System.Threading.Thread.Sleep(5000); File.Delete(Path.Combine(source, "toplevel", "normal", "standard.txt")); using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null)) - c.Backup(new string[] { DATAFOLDER }, excludefilter); + { + IBackupResults backupResults = c.Backup(new string[] {DATAFOLDER}, excludefilter); + Assert.AreEqual(0, backupResults.Errors.Count()); + Assert.AreEqual(0, backupResults.Warnings.Count()); + } // Check we now have only one folder and no files using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts.Expand(new { version = 0 }), null)) { var r = c.List("*"); + Assert.AreEqual(0, r.Errors.Count()); + Assert.AreEqual(0, r.Warnings.Count()); var folders = r.Files.Count(x => x.Path.EndsWith(Util.DirectorySeparatorString, StringComparison.Ordinal)); var files = r.Files.Count(x => !x.Path.EndsWith(Util.DirectorySeparatorString, StringComparison.Ordinal)); diff --git a/Duplicati/UnitTest/GeneralBlackBoxTesting.cs b/Duplicati/UnitTest/GeneralBlackBoxTesting.cs index 75bb9fbf3..50a955957 100644 --- a/Duplicati/UnitTest/GeneralBlackBoxTesting.cs +++ b/Duplicati/UnitTest/GeneralBlackBoxTesting.cs @@ -68,7 +68,7 @@ namespace Duplicati.UnitTest using (WebClient client = new WebClient()) { - client.DownloadFile($"https://s3.amazonaws.com/duplicati-test-file-hosting/{this.zipFilename}", this.zipFilepath); + client.DownloadFile($"https://testfiles.duplicati.com/{this.zipFilename}", this.zipFilepath); } System.IO.Compression.ZipFile.ExtractToDirectory(this.zipFilepath, BASEFOLDER); diff --git a/Duplicati/UnitTest/IOTests.cs b/Duplicati/UnitTest/IOTests.cs index b89e0f23b..48b4ef5ca 100644 --- a/Duplicati/UnitTest/IOTests.cs +++ b/Duplicati/UnitTest/IOTests.cs @@ -44,16 +44,16 @@ namespace Duplicati.UnitTest } [Test] - public void TestUncBehaviourOfAlphaFS() + public void TestUncBehaviourOfGetPathRoot() { if (!Platform.IsClientWindows) { return; } - var root = @"C:"; + var root = @"C:" + Util.DirectorySeparatorString; var filename = "test.txt"; - var filePath = root + Util.DirectorySeparatorString + filename; + var filePath = root + filename; var filePathWithUNC = SystemIOWindows.PrefixWithUNC(filePath); var filePathWithUNCRoot = SystemIO.IO_WIN.GetPathRoot(filePathWithUNC); @@ -63,7 +63,7 @@ namespace Duplicati.UnitTest //Without UNC prefixed, no prefix. var filePathRoot = SystemIO.IO_WIN.GetPathRoot(filePath); - Assert.AreEqual(root + Util.DirectorySeparatorString, filePathRoot); + Assert.AreEqual(root, filePathRoot); } [Test] diff --git a/Duplicati/UnitTest/ImportExportTests.cs b/Duplicati/UnitTest/ImportExportTests.cs index 5d92f2e16..9d0a05c7a 100644 --- a/Duplicati/UnitTest/ImportExportTests.cs +++ b/Duplicati/UnitTest/ImportExportTests.cs @@ -14,12 +14,18 @@ // 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.Server.Database; -using Duplicati.Server.Serialization.Interface; -using NUnit.Framework; + using System; using System.Collections.Generic; using System.IO; +using Duplicati.Server; +using Duplicati.Server.Database; +using Duplicati.Server.Serializable; +using Duplicati.Server.Serialization; +using Duplicati.Server.Serialization.Interface; +using Duplicati.Server.WebServer.RESTMethods; +using NUnit.Framework; +using Backup = Duplicati.Server.Database.Backup; namespace Duplicati.UnitTest { @@ -47,62 +53,110 @@ namespace Duplicati.UnitTest } } + private IBackup CreateBackup(string name, string username, string password, Dictionary metadata) + { + return new Backup + { + Description = "Mock Backup Description", + Filters = new IFilter[0], + ID = "1", + Metadata = metadata, + Name = name, + Settings = new[] {new Setting {Name = "passphrase", Value = "12345"}}, + Sources = new[] {"Mock Backup Source"}, + Tags = new[] {"Tags"}, + TargetURL = $"file:///mock_backup_target?auth-username={username}&auth-password={password}" + }; + } + + [Test] + [Category("ImportExport")] + [TestCase(true)] + [TestCase(false)] + public void ExportToJSONEncoding(bool removePasswords) + { + Dictionary advancedOptions = new Dictionary {{"server-datafolder", this.serverDatafolder}}; + + string usernameKey = "auth-username"; + string passwordKey = "auth-password"; + string username = @"user%40email.com"; + string password = @"abcde12345!@#$%/\"; + + IBackup backup = this.CreateBackup("backup", username, password, new Dictionary()); + if (removePasswords) + { + Server.WebServer.RESTMethods.Backup.RemovePasswords(backup); + } + + Assert.That(backup.TargetURL, Does.Contain($"{usernameKey}={username}")); + if (removePasswords) + { + Assert.That(backup.TargetURL, Does.Not.Contain(passwordKey)); + Assert.That(backup.TargetURL, Does.Not.Contain(password)); + } + else + { + Assert.That(backup.TargetURL, Does.Contain(passwordKey)); + Assert.That(backup.TargetURL, Does.Contain(password)); + } + + byte[] jsonByteArray; + using (Program.DataConnection = Program.GetDatabaseConnection(advancedOptions)) + { + jsonByteArray = Server.WebServer.RESTMethods.Backup.ExportToJSON(backup, null); + } + + // The username should not have the '%40' converted to '@' since the import code + // cannot handle it (see issue #3619). + string json = System.Text.Encoding.Default.GetString(jsonByteArray); + Assert.That(json, Does.Not.Contain("user@email.com")); + + ImportExportStructure importedConfiguration = Serializer.Deserialize(new StreamReader(new MemoryStream(jsonByteArray))); + Assert.AreEqual(backup.Description, importedConfiguration.Backup.Description); + Assert.AreEqual(backup.Name, importedConfiguration.Backup.Name); + Assert.AreEqual(backup.TargetURL, importedConfiguration.Backup.TargetURL); + } + [Test] [Category("ImportExport")] public void RoundTrip() { - Dictionary metadata = new Dictionary { { "SourceFilesCount", "1" } }; - IBackup CreateBackup(string name) - { - return new Backup - { - Description = "Mock Backup Description", - Filters = new IFilter[0], - ID = "1", - Metadata = metadata, - Name = name, - Settings = new[] { new Setting { Name = "passphrase", Value = "12345" } }, - Sources = new[] { "Mock Backup Source" }, - Tags = new[] { "Tags" }, - TargetURL = "file:///mock_backup_target" - }; - } - - Dictionary advancedOptions = new Dictionary { { "server-datafolder", this.serverDatafolder } }; - using (Duplicati.Server.Program.DataConnection = Duplicati.Server.Program.GetDatabaseConnection(advancedOptions)) + Dictionary metadata = new Dictionary {{"SourceFilesCount", "1"}}; + Dictionary advancedOptions = new Dictionary {{"server-datafolder", this.serverDatafolder}}; + using (Program.DataConnection = Program.GetDatabaseConnection(advancedOptions)) { // Unencrypted file, don't import metadata. string unencryptedWithoutMetadata = Path.Combine(this.serverDatafolder, Path.GetRandomFileName()); - File.WriteAllBytes(unencryptedWithoutMetadata, Server.WebServer.RESTMethods.Backup.ExportToJSON(CreateBackup("unencrypted without metadata"), null)); - Duplicati.Server.WebServer.RESTMethods.Backups.ImportBackup(unencryptedWithoutMetadata, false, () => null, advancedOptions); - Assert.AreEqual(1, Duplicati.Server.Program.DataConnection.Backups.Length); - Assert.AreEqual(0, Duplicati.Server.Program.DataConnection.Backups[0].Metadata.Count); + File.WriteAllBytes(unencryptedWithoutMetadata, Server.WebServer.RESTMethods.Backup.ExportToJSON(this.CreateBackup("unencrypted without metadata", "user", "password", metadata), null)); + Backups.ImportBackup(unencryptedWithoutMetadata, false, () => null, advancedOptions); + Assert.AreEqual(1, Program.DataConnection.Backups.Length); + Assert.AreEqual(0, Program.DataConnection.Backups[0].Metadata.Count); // Unencrypted file, import metadata. string unencryptedWithMetadata = Path.Combine(this.serverDatafolder, Path.GetRandomFileName()); - File.WriteAllBytes(unencryptedWithMetadata, Server.WebServer.RESTMethods.Backup.ExportToJSON(CreateBackup("unencrypted with metadata"), null)); - Duplicati.Server.WebServer.RESTMethods.Backups.ImportBackup(unencryptedWithMetadata, true, () => null, advancedOptions); - Assert.AreEqual(2, Duplicati.Server.Program.DataConnection.Backups.Length); - Assert.AreEqual(metadata.Count, Duplicati.Server.Program.DataConnection.Backups[1].Metadata.Count); + File.WriteAllBytes(unencryptedWithMetadata, Server.WebServer.RESTMethods.Backup.ExportToJSON(this.CreateBackup("unencrypted with metadata", "user", "password", metadata), null)); + Backups.ImportBackup(unencryptedWithMetadata, true, () => null, advancedOptions); + Assert.AreEqual(2, Program.DataConnection.Backups.Length); + Assert.AreEqual(metadata.Count, Program.DataConnection.Backups[1].Metadata.Count); // Encrypted file, don't import metadata. string encryptedWithoutMetadata = Path.Combine(this.serverDatafolder, Path.GetRandomFileName()); string passphrase = "abcde"; - File.WriteAllBytes(encryptedWithoutMetadata, Server.WebServer.RESTMethods.Backup.ExportToJSON(CreateBackup("encrypted without metadata"), passphrase)); - Duplicati.Server.WebServer.RESTMethods.Backups.ImportBackup(encryptedWithoutMetadata, false, () => passphrase, advancedOptions); - Assert.AreEqual(3, Duplicati.Server.Program.DataConnection.Backups.Length); - Assert.AreEqual(0, Duplicati.Server.Program.DataConnection.Backups[2].Metadata.Count); + File.WriteAllBytes(encryptedWithoutMetadata, Server.WebServer.RESTMethods.Backup.ExportToJSON(this.CreateBackup("encrypted without metadata", "user", "password", metadata), passphrase)); + Backups.ImportBackup(encryptedWithoutMetadata, false, () => passphrase, advancedOptions); + Assert.AreEqual(3, Program.DataConnection.Backups.Length); + Assert.AreEqual(0, Program.DataConnection.Backups[2].Metadata.Count); // Encrypted file, import metadata. string encryptedWithMetadata = Path.Combine(this.serverDatafolder, Path.GetRandomFileName()); - File.WriteAllBytes(encryptedWithMetadata, Server.WebServer.RESTMethods.Backup.ExportToJSON(CreateBackup("encrypted with metadata"), passphrase)); - Duplicati.Server.WebServer.RESTMethods.Backups.ImportBackup(encryptedWithMetadata, true, () => passphrase, advancedOptions); - Assert.AreEqual(4, Duplicati.Server.Program.DataConnection.Backups.Length); - Assert.AreEqual(metadata.Count, Duplicati.Server.Program.DataConnection.Backups[3].Metadata.Count); + File.WriteAllBytes(encryptedWithMetadata, Server.WebServer.RESTMethods.Backup.ExportToJSON(this.CreateBackup("encrypted with metadata", "user", "password", metadata), passphrase)); + Backups.ImportBackup(encryptedWithMetadata, true, () => passphrase, advancedOptions); + Assert.AreEqual(4, Program.DataConnection.Backups.Length); + Assert.AreEqual(metadata.Count, Program.DataConnection.Backups[3].Metadata.Count); // Encrypted file, incorrect passphrase. - Assert.Throws(Is.InstanceOf(), () => Duplicati.Server.WebServer.RESTMethods.Backups.ImportBackup(encryptedWithMetadata, true, () => passphrase + " ", advancedOptions)); + Assert.Throws(Is.InstanceOf(), () => Backups.ImportBackup(encryptedWithMetadata, true, () => passphrase + " ", advancedOptions)); } } } -} +} \ No newline at end of file diff --git a/Duplicati/UnitTest/Issue1410.cs b/Duplicati/UnitTest/Issue1410.cs index 9effb49a0..d2060e4d5 100644 --- a/Duplicati/UnitTest/Issue1410.cs +++ b/Duplicati/UnitTest/Issue1410.cs @@ -19,6 +19,7 @@ using System.IO; using NUnit.Framework; using System.Linq; using System.Collections.Generic; +using Duplicati.Library.Interface; namespace Duplicati.UnitTest { @@ -32,24 +33,36 @@ namespace Duplicati.UnitTest var data = new byte[1024 * 1024 * 10]; File.WriteAllBytes(Path.Combine(DATAFOLDER, "a"), data); - using(var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null)) - c.Backup(new string[] { DATAFOLDER }); + using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null)) + { + IBackupResults backupResults = c.Backup(new string[] {DATAFOLDER}); + Assert.AreEqual(0, backupResults.Errors.Count()); + Assert.AreEqual(0, backupResults.Warnings.Count()); + } using(var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts.Expand(new { version = 0 }), null)) { var r = c.List("*"); + Assert.AreEqual(0, r.Errors.Count()); + Assert.AreEqual(0, r.Warnings.Count()); Console.WriteLine("In first backup:"); Console.WriteLine(string.Join(Environment.NewLine, r.Files.Select(x => x.Path))); } new Random().NextBytes(data); File.WriteAllBytes(Path.Combine(DATAFOLDER, "b"), data); - using(var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null)) - c.Backup(new string[] { DATAFOLDER }); + using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null)) + { + IBackupResults backupResults = c.Backup(new string[] {DATAFOLDER}); + Assert.AreEqual(0, backupResults.Errors.Count()); + Assert.AreEqual(0, backupResults.Warnings.Count()); + } using(var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts.Expand(new { version = 0 }), null)) { var r = c.List("*"); + Assert.AreEqual(0, r.Errors.Count()); + Assert.AreEqual(0, r.Warnings.Count()); Console.WriteLine("Newest before deleting:"); Console.WriteLine(string.Join(Environment.NewLine, r.Files.Select(x => x.Path))); Assert.AreEqual(3, r.Files.Count()); @@ -58,6 +71,8 @@ namespace Duplicati.UnitTest using(var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts.Expand(new { version = 0, no_local_db = true }), null)) { var r = c.List("*"); + Assert.AreEqual(0, r.Errors.Count()); + Assert.AreEqual(0, r.Warnings.Count()); Console.WriteLine("Newest without db:"); Console.WriteLine(string.Join(Environment.NewLine, r.Files.Select(x => x.Path))); Assert.AreEqual(3, r.Files.Count()); @@ -65,15 +80,26 @@ namespace Duplicati.UnitTest File.Delete(DBFILE); - using(var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null)) - c.Repair(); + using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null)) + { + IRepairResults repairResults = c.Repair(); + Assert.AreEqual(0, repairResults.Errors.Count()); + Assert.AreEqual(0, repairResults.Warnings.Count()); + } - using(var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null)) - Assert.AreEqual(c.List().Filesets.Count(), 2); + using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null)) + { + IListResults listResults = c.List(); + Assert.AreEqual(0, listResults.Errors.Count()); + Assert.AreEqual(0, listResults.Warnings.Count()); + Assert.AreEqual(listResults.Filesets.Count(), 2); + } using(var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts.Expand(new { version = 1 }), null)) { var r = c.List("*"); + Assert.AreEqual(0, r.Errors.Count()); + Assert.AreEqual(0, r.Warnings.Count()); Console.WriteLine("Oldest after delete:"); Console.WriteLine(string.Join(Environment.NewLine, r.Files.Select(x => x.Path))); Assert.AreEqual(2, r.Files.Count()); @@ -82,6 +108,8 @@ namespace Duplicati.UnitTest using(var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts.Expand(new { version = 0 }), null)) { var r = c.List("*"); + Assert.AreEqual(0, r.Errors.Count()); + Assert.AreEqual(0, r.Warnings.Count()); Console.WriteLine("Newest after delete:"); Console.WriteLine(string.Join(Environment.NewLine, r.Files.Select(x => x.Path))); Assert.AreEqual(3, r.Files.Count()); diff --git a/Duplicati/UnitTest/Issue1723.cs b/Duplicati/UnitTest/Issue1723.cs index 25aeb2159..91ec9ff16 100644 --- a/Duplicati/UnitTest/Issue1723.cs +++ b/Duplicati/UnitTest/Issue1723.cs @@ -16,6 +16,7 @@ // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA using System; using System.IO; +using System.Linq; using NUnit.Framework; namespace Duplicati.UnitTest @@ -34,6 +35,8 @@ namespace Duplicati.UnitTest using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null)) { var r = c.Backup(new string[] { DATAFOLDER }); + Assert.AreEqual(0, r.Errors.Count()); + Assert.AreEqual(0, r.Warnings.Count()); var pr = (Library.Interface.IParsedBackendStatistics)r.BackendStatistics; if (pr.KnownFileSize == 0 || pr.KnownFileCount != 3 || pr.BackupListCount != 1) throw new Exception(string.Format("Failed to get stats from remote backend: {0}, {1}, {2}", pr.KnownFileSize, pr.KnownFileCount, pr.BackupListCount)); diff --git a/Duplicati/UnitTest/ProblematicPathTests.cs b/Duplicati/UnitTest/ProblematicPathTests.cs new file mode 100644 index 000000000..e470f67c3 --- /dev/null +++ b/Duplicati/UnitTest/ProblematicPathTests.cs @@ -0,0 +1,296 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Duplicati.Library.Common; +using Duplicati.Library.Common.IO; +using Duplicati.Library.Interface; +using Duplicati.Library.Main; +using Duplicati.Library.Utility; +using NUnit.Framework; +using Utility = Duplicati.Library.Utility.Utility; + +namespace Duplicati.UnitTest +{ + [TestFixture] + public class ProblematicPathTests : BasicSetupHelper + { + [Test] + [Category("ProblematicPath")] + public void DirectoriesWithWildcards() + { + const string file = "file"; + List directories = new List(); + + // Keep expected match counts since they'll differ between + // Linux and Windows. + var questionMarkWildcardShouldMatchCount = 0; + var verbatimAsteriskShouldMatchCount = 0; + + const string asterisk = "*"; + string dirWithAsterisk = Path.Combine(this.DATAFOLDER, asterisk); + // Windows does not support literal asterisks in paths. + if (!Platform.IsClientWindows) + { + SystemIO.IO_OS.DirectoryCreate(dirWithAsterisk); + TestUtils.WriteFile(SystemIO.IO_OS.PathCombine(dirWithAsterisk, file), new byte[] {0}); + directories.Add(dirWithAsterisk); + questionMarkWildcardShouldMatchCount++; + verbatimAsteriskShouldMatchCount++; + } + + const string questionMark = "?"; + string dirWithQuestionMark = Path.Combine(this.DATAFOLDER, questionMark); + // Windows does not support literal question marks in paths. + if (!Platform.IsClientWindows) + { + SystemIO.IO_OS.DirectoryCreate(dirWithQuestionMark); + TestUtils.WriteFile(SystemIO.IO_OS.PathCombine(dirWithQuestionMark, file), new byte[] { 1 }); + directories.Add(dirWithQuestionMark); + questionMarkWildcardShouldMatchCount++; + } + + // Include at least one single character directory in Windows + // for a '?' wildcard can match on + const string singleCharacterDir = "X"; + string dirWithSingleCharacter = Path.Combine(this.DATAFOLDER, singleCharacterDir); + SystemIO.IO_OS.DirectoryCreate(dirWithSingleCharacter); + TestUtils.WriteFile(SystemIO.IO_OS.PathCombine(dirWithSingleCharacter, file), new byte[] { 2 }); + directories.Add(dirWithSingleCharacter); + questionMarkWildcardShouldMatchCount++; + + const string dir = "dir"; + string normalDir = Path.Combine(this.DATAFOLDER, dir); + SystemIO.IO_OS.DirectoryCreate(normalDir); + TestUtils.WriteFile(SystemIO.IO_OS.PathCombine(normalDir, file), new byte[] {3}); + directories.Add(normalDir); + + // Backup all files. + Dictionary options = new Dictionary(this.TestOptions); + using (Controller c = new Controller("file://" + this.TARGETFOLDER, options, null)) + { + IBackupResults backupResults = c.Backup(new[] {this.DATAFOLDER}); + Assert.AreEqual(0, backupResults.Errors.Count()); + Assert.AreEqual(0, backupResults.Warnings.Count()); + } + + // Restore all files. + Dictionary restoreOptions = new Dictionary(options) {["restore-path"] = this.RESTOREFOLDER}; + using (Controller c = new Controller("file://" + this.TARGETFOLDER, restoreOptions, null)) + { + IRestoreResults restoreResults = c.Restore(null); + Assert.AreEqual(0, restoreResults.Errors.Count()); + Assert.AreEqual(0, restoreResults.Warnings.Count()); + + TestUtils.AssertDirectoryTreesAreEquivalent(this.DATAFOLDER, this.RESTOREFOLDER, true, "Restore"); + + // List results using * should return a match for each directory. + IListResults listResults = c.List(SystemIO.IO_OS.PathCombine(dirWithAsterisk, file)); + Assert.AreEqual(0, listResults.Errors.Count()); + Assert.AreEqual(0, listResults.Warnings.Count()); + Assert.AreEqual(directories.Count, listResults.Files.Count()); + + listResults = c.List(SystemIO.IO_OS.PathCombine(dirWithQuestionMark, file)); + Assert.AreEqual(0, listResults.Errors.Count()); + Assert.AreEqual(0, listResults.Warnings.Count()); + // List results using ? should return 3 matches in Linux, + // one for the directory with '*' and one for the directory + // with '?', plus one for directory 'X'; but should return + // 1 matches in Windows just for directory 'X'. + Assert.AreEqual(questionMarkWildcardShouldMatchCount, listResults.Files.Count()); + } + + SystemIO.IO_OS.DirectoryDelete(this.RESTOREFOLDER, true); + + // Restore one file at a time using the verbatim identifier. + foreach (string directory in directories) + { + foreach (string expectedFilePath in SystemIO.IO_OS.EnumerateFiles(directory)) + { + using (Controller c = new Controller("file://" + this.TARGETFOLDER, restoreOptions, null)) + { + string verbatimFilePath = "@" + expectedFilePath; + + // Verify that list result using verbatim identifier contains only one file. + IListResults listResults = c.List(verbatimFilePath); + Assert.AreEqual(0, listResults.Errors.Count()); + Assert.AreEqual(0, listResults.Warnings.Count()); + Assert.AreEqual(1, listResults.Files.Count()); + Assert.AreEqual(expectedFilePath, listResults.Files.Single().Path); + + IRestoreResults restoreResults = c.Restore(new[] {verbatimFilePath}); + Assert.AreEqual(0, restoreResults.Errors.Count()); + Assert.AreEqual(0, restoreResults.Warnings.Count()); + + string fileName = SystemIO.IO_OS.PathGetFileName(expectedFilePath); + string restoredFilePath = SystemIO.IO_OS.PathCombine(this.RESTOREFOLDER, fileName); + TestUtils.AssertFilesAreEqual(expectedFilePath, restoredFilePath, false, expectedFilePath); + + SystemIO.IO_OS.FileDelete(restoredFilePath); + } + } + } + + // Backup with asterisk in include filter should include all directories. + FilterExpression filter = new FilterExpression(dirWithAsterisk); + using (Controller c = new Controller("file://" + this.TARGETFOLDER, options, null)) + { + IBackupResults backupResults = c.Backup(new[] {this.DATAFOLDER}, filter); + Assert.AreEqual(0, backupResults.Errors.Count()); + Assert.AreEqual(0, backupResults.Warnings.Count()); + Assert.AreEqual(directories.Count, backupResults.ExaminedFiles); + } + + // Block for a small amount of time to avoid clock issues when quickly running successive backups. + System.Threading.Thread.Sleep(1000); + + // Backup with verbatim asterisk in include filter should include + // one directory in Linux and zero directories in Windows. + filter = new FilterExpression("@" + SystemIO.IO_OS.PathCombine(dirWithAsterisk, file)); + using (Controller c = new Controller("file://" + this.TARGETFOLDER, options, null)) + { + IBackupResults backupResults = c.Backup(new[] {this.DATAFOLDER}, filter); + Assert.AreEqual(0, backupResults.Errors.Count()); + Assert.AreEqual(0, backupResults.Warnings.Count()); + Assert.AreEqual(verbatimAsteriskShouldMatchCount, backupResults.ExaminedFiles); + } + } + + [Test] + [Category("ProblematicPath")] + public void ExcludeProblematicPaths() + { + // A normal path that will be backed up. + string normalFilePath = Path.Combine(this.DATAFOLDER, "normal"); + File.WriteAllBytes(normalFilePath, new byte[] {0, 1, 2}); + + // A long path to exclude. + string longFile = SystemIO.IO_OS.PathCombine(this.DATAFOLDER, new string('y', 255)); + TestUtils.WriteFile(longFile, new byte[] {0, 1}); + + // A folder that ends with a dot to exclude. + string folderWithDot = Path.Combine(this.DATAFOLDER, "folder_with_dot."); + SystemIO.IO_OS.DirectoryCreate(folderWithDot); + + // A folder that ends with a space to exclude. + string folderWithSpace = Path.Combine(this.DATAFOLDER, "folder_with_space "); + SystemIO.IO_OS.DirectoryCreate(folderWithSpace); + + // A file that ends with a dot to exclude. + string fileWithDot = Path.Combine(this.DATAFOLDER, "file_with_dot."); + TestUtils.WriteFile(fileWithDot, new byte[] {0, 1}); + + // A file that ends with a space to exclude. + string fileWithSpace = Path.Combine(this.DATAFOLDER, "file_with_space "); + TestUtils.WriteFile(fileWithSpace, new byte[] {0, 1}); + + FilterExpression filter = new FilterExpression(longFile, false); + filter = FilterExpression.Combine(filter, new FilterExpression(Util.AppendDirSeparator(folderWithDot), false)); + filter = FilterExpression.Combine(filter, new FilterExpression(Util.AppendDirSeparator(folderWithSpace), false)); + filter = FilterExpression.Combine(filter, new FilterExpression(fileWithDot, false)); + filter = FilterExpression.Combine(filter, new FilterExpression(fileWithSpace, false)); + + Dictionary options = new Dictionary(this.TestOptions); + using (Controller c = new Controller("file://" + this.TARGETFOLDER, options, null)) + { + IBackupResults backupResults = c.Backup(new[] {this.DATAFOLDER}, filter); + Assert.AreEqual(0, backupResults.Errors.Count()); + Assert.AreEqual(0, backupResults.Warnings.Count()); + } + + using (Controller c = new Controller("file://" + this.TARGETFOLDER, options, null)) + { + IListResults listResults = c.List("*"); + Assert.AreEqual(0, listResults.Errors.Count()); + Assert.AreEqual(0, listResults.Warnings.Count()); + + string[] backedUpPaths = listResults.Files.Select(x => x.Path).ToArray(); + Assert.AreEqual(2, backedUpPaths.Length); + Assert.Contains(Util.AppendDirSeparator(this.DATAFOLDER), backedUpPaths); + Assert.Contains(normalFilePath, backedUpPaths); + } + } + + [Test] + [Category("ProblematicPath")] + public void LongPath() + { + string folderPath = Path.Combine(this.DATAFOLDER, new string('x', 10)); + SystemIO.IO_OS.DirectoryCreate(folderPath); + + string fileName = new string('y', 255); + string filePath = SystemIO.IO_OS.PathCombine(folderPath, fileName); + byte[] fileBytes = {0, 1, 2}; + TestUtils.WriteFile(filePath, fileBytes); + + Dictionary options = new Dictionary(this.TestOptions); + using (Controller c = new Controller("file://" + this.TARGETFOLDER, options, null)) + { + IBackupResults backupResults = c.Backup(new[] {this.DATAFOLDER}); + Assert.AreEqual(0, backupResults.Errors.Count()); + Assert.AreEqual(0, backupResults.Warnings.Count()); + } + + Dictionary restoreOptions = new Dictionary(this.TestOptions) {["restore-path"] = this.RESTOREFOLDER}; + using (Controller c = new Controller("file://" + this.TARGETFOLDER, restoreOptions, null)) + { + IRestoreResults restoreResults = c.Restore(new[] {filePath}); + Assert.AreEqual(0, restoreResults.Errors.Count()); + Assert.AreEqual(0, restoreResults.Warnings.Count()); + } + + string restoreFilePath = SystemIO.IO_OS.PathCombine(this.RESTOREFOLDER, fileName); + Assert.IsTrue(SystemIO.IO_OS.FileExists(restoreFilePath)); + + MemoryStream restoredStream = new MemoryStream(); + using (FileStream fileStream = SystemIO.IO_OS.FileOpenRead(restoreFilePath)) + { + Utility.CopyStream(fileStream, restoredStream); + } + + Assert.AreEqual(fileBytes, restoredStream.ToArray()); + } + + [Test] + [Category("ProblematicPath")] + [TestCase("ends_with_dot.")] + [TestCase("ends_with_dots..")] + [TestCase("ends_with_space ")] + [TestCase("ends_with_spaces ")] + public void ProblematicSuffixes(string pathComponent) + { + string folderPath = SystemIO.IO_OS.PathCombine(this.DATAFOLDER, pathComponent); + SystemIO.IO_OS.DirectoryCreate(folderPath); + + string filePath = SystemIO.IO_OS.PathCombine(folderPath, pathComponent); + byte[] fileBytes = {0, 1, 2}; + TestUtils.WriteFile(filePath, fileBytes); + + Dictionary options = new Dictionary(this.TestOptions); + using (Controller c = new Controller("file://" + this.TARGETFOLDER, options, null)) + { + IBackupResults backupResults = c.Backup(new[] {this.DATAFOLDER}); + Assert.AreEqual(0, backupResults.Errors.Count()); + Assert.AreEqual(0, backupResults.Warnings.Count()); + } + + Dictionary restoreOptions = new Dictionary(this.TestOptions) {["restore-path"] = this.RESTOREFOLDER}; + using (Controller c = new Controller("file://" + this.TARGETFOLDER, restoreOptions, null)) + { + IRestoreResults restoreResults = c.Restore(new[] {filePath}); + Assert.AreEqual(0, restoreResults.Errors.Count()); + Assert.AreEqual(0, restoreResults.Warnings.Count()); + } + + string restoreFilePath = SystemIO.IO_OS.PathCombine(this.RESTOREFOLDER, pathComponent); + Assert.IsTrue(SystemIO.IO_OS.FileExists(restoreFilePath)); + + MemoryStream restoredStream = new MemoryStream(); + using (FileStream fileStream = SystemIO.IO_OS.FileOpenRead(restoreFilePath)) + { + Utility.CopyStream(fileStream, restoredStream); + } + + Assert.AreEqual(fileBytes, restoredStream.ToArray()); + } + } +} \ No newline at end of file diff --git a/Duplicati/UnitTest/PurgeTesting.cs b/Duplicati/UnitTest/PurgeTesting.cs index cd6335ed5..d35d3dc1e 100644 --- a/Duplicati/UnitTest/PurgeTesting.cs +++ b/Duplicati/UnitTest/PurgeTesting.cs @@ -19,6 +19,8 @@ using System.Collections.Generic; using System.IO; using System.Linq; using Duplicati.Library.Common.IO; +using Duplicati.Library.Interface; +using Duplicati.Library.Main; using Duplicati.Library.Utility; using NUnit.Framework; @@ -45,6 +47,8 @@ namespace Duplicati.UnitTest using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null)) { var res = c.Backup(new string[] { DATAFOLDER }, new Library.Utility.FilterExpression(round1.Select(x => "*" + Path.DirectorySeparatorChar + x))); + Assert.AreEqual(0, res.Errors.Count()); + Assert.AreEqual(0, res.Warnings.Count()); Assert.AreEqual(res.AddedFiles, round1.Length); } @@ -52,6 +56,8 @@ namespace Duplicati.UnitTest using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null)) { var res = c.Backup(new string[] { DATAFOLDER }, new Library.Utility.FilterExpression(round2.Select(x => "*" + Path.DirectorySeparatorChar + x))); + Assert.AreEqual(0, res.Errors.Count()); + Assert.AreEqual(0, res.Warnings.Count()); Assert.AreEqual(res.AddedFiles, round2.Length - round1.Length); } @@ -59,6 +65,8 @@ namespace Duplicati.UnitTest using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null)) { var res = c.Backup(new string[] { DATAFOLDER }); + Assert.AreEqual(0, res.Errors.Count()); + Assert.AreEqual(0, res.Warnings.Count()); Assert.AreEqual(res.AddedFiles, filenames.Count - round2.Length); } @@ -68,13 +76,18 @@ namespace Duplicati.UnitTest using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts.Expand(new { list_sets_only = true }), null)) { var inf = c.List(); + Assert.AreEqual(0, inf.Errors.Count()); + Assert.AreEqual(0, inf.Warnings.Count()); var filesets = inf.Filesets.Count(); Assert.AreEqual(3, filesets, "Incorrect number of initial filesets"); } using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null)) { - var filecount = c.List("*").Files.Count(); + IListResults listResults = c.List("*"); + Assert.AreEqual(0, listResults.Errors.Count()); + Assert.AreEqual(0, listResults.Warnings.Count()); + var filecount = listResults.Files.Count(); Assert.AreEqual(filenames.Count + 1, filecount, "Incorrect number of initial files"); } @@ -84,6 +97,8 @@ namespace Duplicati.UnitTest using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null)) { var res = c.PurgeFiles(new Library.Utility.FilterExpression("*" + Path.DirectorySeparatorChar + allversion_candidate)); + Assert.AreEqual(0, res.Errors.Count()); + Assert.AreEqual(0, res.Warnings.Count()); Assert.AreEqual(3, res.RewrittenFileLists, "Incorrect number of rewritten filesets after all-versions purge"); Assert.AreEqual(3, res.RemovedFileCount, "Incorrect number of removed files after all-versions purge"); } @@ -93,6 +108,8 @@ namespace Duplicati.UnitTest using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts.Expand(new { version = i }), null)) { var res = c.PurgeFiles(new Library.Utility.FilterExpression(Path.Combine(this.DATAFOLDER, single_version_candidate))); + Assert.AreEqual(0, res.Errors.Count()); + Assert.AreEqual(0, res.Warnings.Count()); Assert.AreEqual(1, res.RewrittenFileLists, "Incorrect number of rewritten filesets after single-versions purge"); Assert.AreEqual(1, res.RemovedFileCount, "Incorrect number of removed files after single-versions purge"); } @@ -101,6 +118,8 @@ namespace Duplicati.UnitTest using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null)) { var res = c.PurgeFiles(new Library.Utility.FilterExpression(round2.Skip(round1.Length).Take(2).Select(x => "*" + Path.DirectorySeparatorChar + x))); + Assert.AreEqual(0, res.Errors.Count()); + Assert.AreEqual(0, res.Warnings.Count()); Assert.AreEqual(2, res.RewrittenFileLists, "Incorrect number of rewritten filesets after 2-versions purge"); Assert.AreEqual(4, res.RemovedFileCount, "Incorrect number of removed files after 2-versions purge"); } @@ -108,6 +127,8 @@ namespace Duplicati.UnitTest using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null)) { var res = c.PurgeFiles(new Library.Utility.FilterExpression(round3.Skip(round2.Length).Take(2).Select(x => "*" + Path.DirectorySeparatorChar + x))); + Assert.AreEqual(0, res.Errors.Count()); + Assert.AreEqual(0, res.Warnings.Count()); Assert.AreEqual(1, res.RewrittenFileLists, "Incorrect number of rewritten filesets after 1-versions purge"); Assert.AreEqual(2, res.RemovedFileCount, "Incorrect number of removed files after 1-versions purge"); } @@ -120,8 +141,12 @@ namespace Duplicati.UnitTest using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null)) { var listinfo = c.List("*"); + Assert.AreEqual(0, listinfo.Errors.Count()); + Assert.AreEqual(0, listinfo.Warnings.Count()); var filecount = listinfo.Files.Count(); listinfo = c.List(); + Assert.AreEqual(0, listinfo.Errors.Count()); + Assert.AreEqual(0, listinfo.Warnings.Count()); var filesets = listinfo.Filesets.Count(); Assert.AreEqual(3, filesets, "Incorrect number of filesets after purge"); @@ -129,14 +154,22 @@ namespace Duplicati.UnitTest } using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null)) - c.Backup(new string[] { DATAFOLDER }); + { + IBackupResults backupResults = c.Backup(new string[] {DATAFOLDER}); + Assert.AreEqual(0, backupResults.Errors.Count()); + Assert.AreEqual(0, backupResults.Warnings.Count()); + } using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null)) { var listinfo = c.List("*"); + Assert.AreEqual(0, listinfo.Errors.Count()); + Assert.AreEqual(0, listinfo.Warnings.Count()); var files = listinfo.Files.ToArray(); var filecount = files.Length; listinfo = c.List(); + Assert.AreEqual(0, listinfo.Errors.Count()); + Assert.AreEqual(0, listinfo.Warnings.Count()); var filesets = listinfo.Filesets.ToArray(); Console.WriteLine("Listing final version information"); @@ -169,6 +202,8 @@ namespace Duplicati.UnitTest using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null)) { var res = c.Backup(new string[] { DATAFOLDER }, new Library.Utility.FilterExpression(round1.Select(x => "*" + Path.DirectorySeparatorChar + x))); + Assert.AreEqual(0, res.Errors.Count()); + Assert.AreEqual(0, res.Warnings.Count()); Assert.AreEqual(res.AddedFiles, round1.Length); } @@ -183,6 +218,8 @@ namespace Duplicati.UnitTest using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null)) { var res = c.Backup(new string[] { DATAFOLDER }, new Library.Utility.FilterExpression(round2.Select(x => "*" + Path.DirectorySeparatorChar + x))); + Assert.AreEqual(0, res.Errors.Count()); + Assert.AreEqual(0, res.Warnings.Count()); Assert.AreEqual(round2.Length - round1.Length, res.AddedFiles); } @@ -190,6 +227,8 @@ namespace Duplicati.UnitTest using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null)) { var res = c.Backup(new string[] { DATAFOLDER }); + Assert.AreEqual(0, res.Errors.Count()); + Assert.AreEqual(0, res.Warnings.Count()); Assert.AreEqual(filenames.Count - round2.Length, res.AddedFiles); } @@ -200,6 +239,8 @@ namespace Duplicati.UnitTest using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null)) { var brk = c.ListBrokenFiles(null); + Assert.AreEqual(0, brk.Errors.Count()); + Assert.AreEqual(0, brk.Warnings.Count()); var sets = brk.BrokenFiles.Count(); var files = brk.BrokenFiles.Sum(x => x.Item3.Count()); Assert.AreEqual(3, sets); @@ -212,15 +253,28 @@ namespace Duplicati.UnitTest using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts.Expand(new { version = i }), null)) { var brk = c.ListBrokenFiles(null); + Assert.AreEqual(0, brk.Errors.Count()); + Assert.AreEqual(0, brk.Warnings.Count()); var sets = brk.BrokenFiles.Count(); var files = brk.BrokenFiles.Sum(x => x.Item3.Count()); Assert.AreEqual(1, sets); Assert.AreEqual(affectedfiles[i], files); } + // A dry-run should run without exceptions (see issue #4379). + Dictionary dryRunOptions = new Dictionary(testopts) {["dry-run"] = "true"}; + using (Controller c = new Controller("file://" + this.TARGETFOLDER, dryRunOptions, null)) + { + IPurgeBrokenFilesResults purgeResults = c.PurgeBrokenFiles(null); + Assert.AreEqual(0, purgeResults.Errors.Count()); + Assert.AreEqual(0, purgeResults.Warnings.Count()); + } + using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, testopts, null)) { var brk = c.PurgeBrokenFiles(null); + Assert.AreEqual(0, brk.Errors.Count()); + Assert.AreEqual(0, brk.Warnings.Count()); var modFilesets = 0L; if (brk.DeleteResults != null) @@ -230,7 +284,14 @@ namespace Duplicati.UnitTest Assert.AreEqual(3, modFilesets); } - } + // A subsequent backup should be successful. + using (Controller c = new Controller("file://" + this.TARGETFOLDER, testopts, null)) + { + IBackupResults backupResults = c.Backup(new[] {this.DATAFOLDER}); + Assert.AreEqual(0, backupResults.Errors.Count()); + Assert.AreEqual(0, backupResults.Warnings.Count()); + } + } } } diff --git a/Duplicati/UnitTest/RecoveryToolTests.cs b/Duplicati/UnitTest/RecoveryToolTests.cs index ea31d3ca0..b81d7b25a 100644 --- a/Duplicati/UnitTest/RecoveryToolTests.cs +++ b/Duplicati/UnitTest/RecoveryToolTests.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; +using Duplicati.Library.Interface; using Duplicati.Library.Main; using NUnit.Framework; @@ -63,7 +65,9 @@ namespace Duplicati.UnitTest string backendURL = "file://" + this.TARGETFOLDER; using (Controller c = new Controller(backendURL, options, null)) { - c.Backup(new[] {this.DATAFOLDER}); + IBackupResults backupResults = c.Backup(new[] {this.DATAFOLDER}); + Assert.AreEqual(0, backupResults.Errors.Count()); + Assert.AreEqual(0, backupResults.Warnings.Count()); } // Download the backend files. @@ -86,17 +90,7 @@ namespace Duplicati.UnitTest // last folder in the path. string baseFolder = Path.GetFileName(this.DATAFOLDER); - foreach (string filepath in Directory.EnumerateFiles(this.DATAFOLDER)) - { - string filename = Path.GetFileName(filepath); - Assert.IsTrue(TestUtils.CompareFiles(filepath, Path.Combine(restoreFolder, baseFolder, filename ?? String.Empty), filename, false)); - } - - foreach (string filepath in Directory.EnumerateFiles(subdirectoryPath)) - { - string filename = Path.GetFileName(filepath); - Assert.IsTrue(TestUtils.CompareFiles(filepath, Path.Combine(restoreFolder, baseFolder, subdirectoryName, filename ?? String.Empty), filename, false)); - } + TestUtils.AssertDirectoryTreesAreEquivalent(this.DATAFOLDER, Path.Combine(restoreFolder, baseFolder), false, "Verifying restore using RecoveryTool."); } } } \ No newline at end of file diff --git a/Duplicati/UnitTest/RepairHandlerTests.cs b/Duplicati/UnitTest/RepairHandlerTests.cs index 7c27bbee0..3cc63db78 100644 --- a/Duplicati/UnitTest/RepairHandlerTests.cs +++ b/Duplicati/UnitTest/RepairHandlerTests.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using Duplicati.Library.Interface; using Duplicati.Library.Main; using NUnit.Framework; @@ -24,7 +25,9 @@ namespace Duplicati.UnitTest Dictionary options = new Dictionary(this.TestOptions) {["no-encryption"] = noEncryption}; using (Controller c = new Controller("file://" + this.TARGETFOLDER, options, null)) { - c.Backup(new[] {this.DATAFOLDER}); + IBackupResults backupResults = c.Backup(new[] {this.DATAFOLDER}); + Assert.AreEqual(0, backupResults.Errors.Count()); + Assert.AreEqual(0, backupResults.Warnings.Count()); } string[] dindexFiles = Directory.EnumerateFiles(this.TARGETFOLDER, "*dindex*").ToArray(); @@ -36,7 +39,9 @@ namespace Duplicati.UnitTest using (Controller c = new Controller("file://" + this.TARGETFOLDER, options, null)) { - c.Repair(); + IRepairResults repairResults = c.Repair(); + Assert.AreEqual(0, repairResults.Errors.Count()); + Assert.AreEqual(0, repairResults.Warnings.Count()); } foreach (string file in dindexFiles) diff --git a/Duplicati/UnitTest/RestoreHandlerTests.cs b/Duplicati/UnitTest/RestoreHandlerTests.cs index e41ef37ef..39612fb59 100644 --- a/Duplicati/UnitTest/RestoreHandlerTests.cs +++ b/Duplicati/UnitTest/RestoreHandlerTests.cs @@ -1,15 +1,61 @@ -using Duplicati.Library.Common; -using Duplicati.Library.Main; -using NUnit.Framework; using System.Collections.Generic; using System.IO; -using System.Security.AccessControl; +using System.Linq; +using Duplicati.Library.Common; using Duplicati.Library.Common.IO; +using Duplicati.Library.Interface; +using Duplicati.Library.Main; +using NUnit.Framework; namespace Duplicati.UnitTest { public class RestoreHandlerTests : BasicSetupHelper { + [Test] + [Category("RestoreHandler")] + public void RestoreEmptyFile() + { + string folderPath = Path.Combine(this.DATAFOLDER, "folder"); + Directory.CreateDirectory(folderPath); + string filePath = Path.Combine(folderPath, "empty_file"); + File.WriteAllBytes(filePath, new byte[] { }); + + Dictionary options = new Dictionary(this.TestOptions); + using (Controller c = new Controller("file://" + this.TARGETFOLDER, options, null)) + { + IBackupResults backupResults = c.Backup(new[] {this.DATAFOLDER}); + Assert.AreEqual(0, backupResults.Errors.Count()); + Assert.AreEqual(0, backupResults.Warnings.Count()); + } + + // Issue #4148 described a situation where the folders containing the empty file were not recreated properly. + Dictionary restoreOptions = new Dictionary(this.TestOptions) + { + ["restore-path"] = this.RESTOREFOLDER, + ["dont-compress-restore-paths"] = "true" + }; + using (Controller c = new Controller("file://" + this.TARGETFOLDER, restoreOptions, null)) + { + IRestoreResults restoreResults = c.Restore(new[] {filePath}); + Assert.AreEqual(0, restoreResults.Errors.Count()); + Assert.AreEqual(0, restoreResults.Warnings.Count()); + } + + // We need to strip the root part of the path. Otherwise, Path.Combine will simply return the second argument + // if it's determined to be an absolute path. + string rootString = SystemIO.IO_OS.GetPathRoot(filePath); + string newPathPart = filePath.Substring(rootString.Length); + if (Platform.IsClientWindows) + { + // On Windows, the drive letter is included in the path when the dont-compress-restore-paths option is used. + // The drive letter is assumed to be the first character of the path root (e.g., C:\). + newPathPart = Path.Combine(rootString.Substring(0, 1), filePath.Substring(rootString.Length)); + } + + string restoredFilePath = Path.Combine(restoreOptions["restore-path"], newPathPart); + Assert.IsTrue(File.Exists(restoredFilePath)); + } + [Test] [Category("RestoreHandler")] public void RestoreInheritanceBreaks() @@ -32,19 +78,28 @@ namespace Duplicati.UnitTest Dictionary options = new Dictionary(this.TestOptions); using (Controller c = new Controller("file://" + this.TARGETFOLDER, options, null)) { - c.Backup(new[] {this.DATAFOLDER}); + IBackupResults backupResults = c.Backup(new[] {this.DATAFOLDER}); + Assert.AreEqual(0, backupResults.Errors.Count()); + Assert.AreEqual(0, backupResults.Warnings.Count()); } // First, restore without restoring permissions. Dictionary restoreOptions = new Dictionary(this.TestOptions) {["restore-path"] = this.RESTOREFOLDER}; using (Controller c = new Controller("file://" + this.TARGETFOLDER, restoreOptions, null)) { - c.Restore(new[] {filePath}); + IRestoreResults restoreResults = c.Restore(new[] {filePath}); + Assert.AreEqual(0, restoreResults.Errors.Count()); + Assert.AreEqual(0, restoreResults.Warnings.Count()); + string restoredFilePath = Path.Combine(this.RESTOREFOLDER, "file"); Assert.IsTrue(File.Exists(restoredFilePath)); FileSecurity restoredFileSecurity = File.GetAccessControl(restoredFilePath); Assert.IsFalse(restoredFileSecurity.AreAccessRulesProtected); + + // Remove the restored file so that the later restore avoids the "Restore completed + // without errors but no files were restored" warning. + File.Delete(restoredFilePath); } // Restore with restoring permissions. @@ -52,7 +107,10 @@ namespace Duplicati.UnitTest restoreOptions["restore-permissions"] = "true"; using (Controller c = new Controller("file://" + this.TARGETFOLDER, restoreOptions, null)) { - c.Restore(new[] {filePath}); + IRestoreResults restoreResults = c.Restore(new[] {filePath}); + Assert.AreEqual(0, restoreResults.Errors.Count()); + Assert.AreEqual(0, restoreResults.Warnings.Count()); + string restoredFilePath = Path.Combine(this.RESTOREFOLDER, "file"); Assert.IsTrue(File.Exists(restoredFilePath)); diff --git a/Duplicati/UnitTest/RunScriptTests.cs b/Duplicati/UnitTest/RunScriptTests.cs index 30a9eaabd..f6462b895 100644 --- a/Duplicati/UnitTest/RunScriptTests.cs +++ b/Duplicati/UnitTest/RunScriptTests.cs @@ -20,6 +20,7 @@ using System.IO; using System.Linq; using Duplicati.Library.Common; using Duplicati.Library.Interface; +using Duplicati.Library.Main; using NUnit.Framework; namespace Duplicati.UnitTest @@ -43,6 +44,8 @@ namespace Duplicati.UnitTest using (var c = new Library.Main.Controller("file://" + TARGETFOLDER, options, null)) { var res = c.Backup(new string[] { DATAFOLDER }); + Assert.AreEqual(0, res.Errors.Count()); + Assert.AreEqual(0, res.Warnings.Count()); if (res.ParsedResult != ParsedResultType.Success) throw new Exception("Unexpected result from base backup"); @@ -145,13 +148,42 @@ namespace Duplicati.UnitTest } } + [Test] + [Category("Border")] + public void CustomRemoteURL() + { + string customTargetFolder = Path.Combine(this.TARGETFOLDER, "destination"); + Directory.CreateDirectory(customTargetFolder); - private string CreateScript(int exitcode, string stderr = null, string stdout = null, int sleeptime = 0) + List customCommands = new List + { + $"echo --remoteurl = \"{customTargetFolder}\"" + }; + + Dictionary options = this.TestOptions; + options["run-script-before"] = CreateScript(0, null, null, 0, customCommands); + using (Controller c = new Library.Main.Controller("file://" + this.TARGETFOLDER, options, null)) + { + IBackupResults backupResults = c.Backup(new[] {this.DATAFOLDER}); + Assert.AreEqual(0, backupResults.Errors.Count()); + Assert.AreEqual(0, backupResults.Warnings.Count()); + } + + string[] targetEntries = Directory.EnumerateFileSystemEntries(this.TARGETFOLDER).ToArray(); + Assert.AreEqual(1, targetEntries.Length); + Assert.AreEqual(customTargetFolder, targetEntries[0]); + + // We expect a dblock, dlist, and dindex file. + IEnumerable customTargetEntries = Directory.EnumerateFileSystemEntries(customTargetFolder); + Assert.AreEqual(3, customTargetEntries.Count()); + } + + private string CreateScript(int exitcode, string stderr = null, string stdout = null, int sleeptime = 0, List customCommands = null) { var id = Guid.NewGuid().ToString("N").Substring(0, 6); if (Platform.IsClientWindows) { - var commands = new List(); + var commands = customCommands ?? new List(); if (!string.IsNullOrWhiteSpace(stdout)) commands.Add($@"echo {stdout}"); if (!string.IsNullOrWhiteSpace(stderr)) @@ -171,6 +203,11 @@ namespace Duplicati.UnitTest var commands = new List(); commands.Add("#!/bin/sh"); + if (customCommands != null) + { + commands.AddRange(customCommands); + } + if (!string.IsNullOrWhiteSpace(stdout)) commands.Add($@"echo {stdout}"); if (!string.IsNullOrWhiteSpace(stderr)) diff --git a/Duplicati/UnitTest/SVNCheckoutsTest.cs b/Duplicati/UnitTest/SVNCheckoutsTest.cs index 1a0df38c2..77480f291 100644 --- a/Duplicati/UnitTest/SVNCheckoutsTest.cs +++ b/Duplicati/UnitTest/SVNCheckoutsTest.cs @@ -421,11 +421,7 @@ namespace Duplicati.UnitTest throw new Exception("Unittest is broken"); } - if (!TestUtils.CompareFiles(sourcename, restoredname, s, verifymetadata)) - { - Log.WriteErrorMessage(LOGTAG, "PartialRestoreWrongFile", null, "Partial restore file differs: {0}", s); - BasicSetupHelper.ProgressWriteLine("Partial restore file differs: " + s); - } + TestUtils.AssertFilesAreEqual(sourcename, restoredname, verifymetadata, $"Partial restore file differs: {s}"); } } } @@ -435,7 +431,7 @@ namespace Duplicati.UnitTest using (new Timer(LOGTAG, "SourceVerification", "Verification of " + source)) { for (int j = 0; j < actualfolders.Length; j++) - TestUtils.VerifyDir(actualfolders[j], restorefoldernames[j], verifymetadata); + TestUtils.AssertDirectoryTreesAreEquivalent(actualfolders[j], restorefoldernames[j], verifymetadata, "VerifyFullRestore"); } } diff --git a/Duplicati/UnitTest/SymLinkTests.cs b/Duplicati/UnitTest/SymLinkTests.cs new file mode 100755 index 000000000..84d21a24d --- /dev/null +++ b/Duplicati/UnitTest/SymLinkTests.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Duplicati.Library.Interface; +using Duplicati.Library.Main; +using Duplicati.Library.Snapshots; +using NUnit.Framework; + +namespace Duplicati.UnitTest +{ + [TestFixture] + public class SymLinkTests : BasicSetupHelper + { + [Test] + [Category("SymLink")] + [TestCase(Options.SymlinkStrategy.Store)] + [TestCase(Options.SymlinkStrategy.Follow)] + [TestCase(Options.SymlinkStrategy.Ignore)] + public void SymLinkPolicy(Options.SymlinkStrategy symlinkPolicy) + { + // Create symlink target directory + const string targetDirName = "target"; + var targetDir = systemIO.PathCombine(this.DATAFOLDER, targetDirName); + systemIO.DirectoryCreate(targetDir); + // Create files in symlink target directory + var fileNames = new[] { "a.txt", "b.txt", "c.txt" }; + foreach (var file in fileNames) + { + var targetFile = systemIO.PathCombine(targetDir, file); + TestUtils.WriteFile(targetFile, Encoding.Default.GetBytes(file)); + } + + // Create actual symlink directory linking to the target directory + const string symlinkDirName = "symlink"; + var symlinkDir = systemIO.PathCombine(this.DATAFOLDER, symlinkDirName); + try + { + systemIO.CreateSymlink(symlinkDir, targetDir, asDir: true); + } + catch (Exception e) + { + // If client cannot create symlinks, mark test as ignored + Assert.Ignore($"Client could not create a symbolic link. Error reported: {e.Message}"); + } + + // Backup all files with given symlink policy + Dictionary restoreOptions = new Dictionary(this.TestOptions) { ["restore-path"] = this.RESTOREFOLDER }; + Dictionary backupOptions = new Dictionary(this.TestOptions) { ["symlink-policy"] = symlinkPolicy.ToString() }; + using (Controller c = new Controller("file://" + this.TARGETFOLDER, backupOptions, null)) + { + IBackupResults backupResults = c.Backup(new[] { this.DATAFOLDER }); + Assert.AreEqual(0, backupResults.Errors.Count()); + Assert.AreEqual(0, backupResults.Warnings.Count()); + } + // Restore all files + using (Controller c = new Controller("file://" + this.TARGETFOLDER, restoreOptions, null)) + { + IRestoreResults restoreResults = c.Restore(null); + Assert.AreEqual(0, restoreResults.Errors.Count()); + Assert.AreEqual(0, restoreResults.Warnings.Count()); + + // Verify that symlink policy was followed + var restoreSymlinkDir = systemIO.PathCombine(this.RESTOREFOLDER, symlinkDirName); + switch (symlinkPolicy) + { + case Options.SymlinkStrategy.Store: + // Restore should contain an actual symlink to the original target + Assert.That(systemIO.IsSymlink(restoreSymlinkDir), Is.True); + var restoredSymlinkFullPath = systemIO.PathGetFullPath(systemIO.GetSymlinkTarget(restoreSymlinkDir)); + var symlinkTargetFullPath = systemIO.PathGetFullPath(targetDir); + Assert.That(restoredSymlinkFullPath, Is.EqualTo(symlinkTargetFullPath)); + break; + case Options.SymlinkStrategy.Follow: + // Restore should contain a regular directory with copies of the files in the symlink target + Assert.That(systemIO.IsSymlink(restoreSymlinkDir), Is.False); + TestUtils.AssertDirectoryTreesAreEquivalent(targetDir, restoreSymlinkDir, true, "Restore"); + break; + case Options.SymlinkStrategy.Ignore: + // Restore should not contain the symlink or directory at all + Assert.That(systemIO.DirectoryExists(restoreSymlinkDir), Is.False); + Assert.That(systemIO.FileExists(restoreSymlinkDir), Is.False); + break; + default: + Assert.Fail($"Unexpected symlink policy"); + break; + } + } + } + } +} diff --git a/Duplicati/UnitTest/TestUtils.cs b/Duplicati/UnitTest/TestUtils.cs index 0c8732d65..36d7beaf1 100644 --- a/Duplicati/UnitTest/TestUtils.cs +++ b/Duplicati/UnitTest/TestUtils.cs @@ -24,7 +24,8 @@ using System.Reflection; using System.Threading; using System.Threading.Tasks; using Duplicati.Library.Common.IO; - +using NUnit.Framework; + namespace Duplicati.UnitTest { public static class TestUtils @@ -169,129 +170,90 @@ namespace Duplicati.UnitTest } /// - /// Verifies the existence of all files and folders, and ensures that all - /// files are binary equal. + /// Asserts that the two directory trees are equivalent; i.e., + /// that they they contain the same directories and files, recursively. /// - /// One folder - /// Another folder - public static void VerifyDir(string f1, string f2, bool verifymetadata) + /// The expected directory tree. + /// The actual directory tree. + /// True to also compare file metadata. + /// Context information to include in an assert message. + public static void AssertDirectoryTreesAreEquivalent(string expectedDir, string actualDir, bool verifymetadata, string contextMessage) { - var anymissing = false; - f1 = Util.AppendDirSeparator(f1); - f2 = Util.AppendDirSeparator(f2); - - var folders1 = Utility.EnumerateFolders(f1); - var folders2 = Utility.EnumerateFolders(f2).ToList(); - - foreach (string s in folders1) + var localMessage = $"{contextMessage}, in directories {expectedDir} and {actualDir}"; + // Assert that expectedDir and actualDir contain the same directories + var expectedSubdirs = SystemIO.IO_OS.EnumerateDirectories(expectedDir).OrderBy(SystemIO.IO_OS.PathGetFileName); + var actualSubdirs = SystemIO.IO_OS.EnumerateDirectories(actualDir).OrderBy(SystemIO.IO_OS.PathGetFileName); + Assert.That(expectedSubdirs.Select(SystemIO.IO_OS.PathGetFileName), Is.EquivalentTo(actualSubdirs.Select(SystemIO.IO_OS.PathGetFileName)), localMessage); + // Recursively compare the contained directories + var expectedSubdirsEnumerator = expectedSubdirs.GetEnumerator(); + var actualSubdirsEnumerator = actualSubdirs.GetEnumerator(); + while (expectedSubdirsEnumerator.MoveNext() && actualSubdirsEnumerator.MoveNext()) { - string relpath = s.Substring(f1.Length); - string target = System.IO.Path.Combine(f2, relpath); - int ix = IndexOf(folders2, target); - if (ix < 0) - { - Log.WriteErrorMessage(LOGTAG, "MissingFolder", null, "Missing folder: {0}", relpath); - Console.WriteLine("Missing folder: " + relpath); - anymissing = true; - } - else - folders2.RemoveAt(ix); + AssertDirectoryTreesAreEquivalent(expectedSubdirsEnumerator.Current, actualSubdirsEnumerator.Current, verifymetadata, contextMessage); } - - foreach (string s in folders2) + // Assert that expectedDir and actualDir contain the same files + var expectedFiles = SystemIO.IO_OS.EnumerateFiles(expectedDir).OrderBy(SystemIO.IO_OS.PathGetFileName); + var actualFiles = SystemIO.IO_OS.EnumerateFiles(actualDir).OrderBy(SystemIO.IO_OS.PathGetFileName); + Assert.That(expectedFiles.Select(SystemIO.IO_OS.PathGetFileName), Is.EquivalentTo(actualFiles.Select(SystemIO.IO_OS.PathGetFileName)), localMessage); + // Assert that the files are equal + var expectedFilesEnumerator = expectedFiles.GetEnumerator(); + var actualFilesEnumerator = actualFiles.GetEnumerator(); + while (expectedFilesEnumerator.MoveNext() && actualFilesEnumerator.MoveNext()) { - Log.WriteErrorMessage(LOGTAG, "ExtraFolder", null, "Extra folder: {0}", s.Substring(f2.Length)); - Console.WriteLine("Extra folder: " + s.Substring(f2.Length)); + AssertFilesAreEqual(expectedFilesEnumerator.Current, actualFilesEnumerator.Current, verifymetadata, contextMessage); } - - var files1 = Utility.EnumerateFiles(f1); - var files2 = Utility.EnumerateFiles(f2).ToList(); - foreach (string s in files1) - { - string relpath = s.Substring(f1.Length); - string target = System.IO.Path.Combine(f2, relpath); - int ix = IndexOf(files2, target); - if (ix < 0) - { - Log.WriteErrorMessage(LOGTAG, "MissingFile", null, "Missing file: {0}", relpath); - Console.WriteLine("Missing file: " + relpath); - anymissing = true; - } - else - { - files2.RemoveAt(ix); - if (!CompareFiles(s, target, relpath, verifymetadata)) - { - Log.WriteErrorMessage(LOGTAG, "FileDiffers", null, "File differs: {0}", relpath); - Console.WriteLine("File differs: " + relpath); - } - } - } - - foreach (string s in files2) - { - Log.WriteErrorMessage(LOGTAG, "ExtraFile", null, "Extra file: {0}", s.Substring(f2.Length)); - Console.WriteLine("Extra file: " + s.Substring(f2.Length)); - } - - if (anymissing) - throw new Exception("Verify failed, some items are missing"); } /// - /// Compares two files by reading all bytes, and comparing one by one + /// Asserts that two files are equal by comparing their length, contents, and, optionally, their metadata. /// - /// One file - /// Another file - /// File display name - /// True if they are equal, false otherwise - public static bool CompareFiles(string f1, string f2, string display, bool verifymetadata) + /// The expected file. + /// The actual file. + /// True to also compare file metadata. + /// Context information to include in an assert message. + public static void AssertFilesAreEqual(string expectedFile, string actualFile, bool verifymetadata, string contextMessage) { - using (System.IO.FileStream fs1 = System.IO.File.OpenRead(f1)) - using (System.IO.FileStream fs2 = System.IO.File.OpenRead(f2)) - if (fs1.Length != fs2.Length) + using (var expectedFileStream = SystemIO.IO_OS.FileOpenRead(expectedFile)) + using (var actualFileStream = SystemIO.IO_OS.FileOpenRead(actualFile)) + { + // Compare file lengths + var expectedFileStreamLength = expectedFileStream.Length; + var actualFileStreamLength = actualFileStream.Length; + Assert.That(actualFileStreamLength, Is.EqualTo(expectedFileStreamLength), $"{contextMessage}, file size mismatch for {expectedFile} and {actualFile}"); + // Compare file contents + // The byte-by-byte compare is dog-slow, so we use a fast(-er) check, and then report the first byte diff if required + if (!Utility.CompareStreams(expectedFileStream, actualFileStream, true)) { - Log.WriteErrorMessage(LOGTAG, "LengthsDiffer", null, "Lengths differ: {0}, {1} vs {2}", display, fs1.Length.ToString(), fs2.Length.ToString()); - Console.WriteLine("Lengths differ: " + display + ", " + fs1.Length.ToString() + " vs. " + fs2.Length.ToString()); - return false; - } - else - { - // The byte-by-byte compare is dog-slow, so we use a fast(-er) check, and then report the first byte diff if required - if (!Library.Utility.Utility.CompareStreams(fs1, fs2, true)) + // Reset stream positions + expectedFileStream.Position = 0; + actualFileStream.Position = 0; + for (long i = 0; i < expectedFileStreamLength; i++) { - fs1.Position = 0; - fs2.Position = 0; - long len = fs1.Length; - for(long l = 0; l < len; l++) - if (fs1.ReadByte() != fs2.ReadByte()) - { - Log.WriteErrorMessage(LOGTAG, "MismatchInFile", null, "Mismatch in byte {0} in file {1}", l.ToString(), display); - Console.WriteLine("Mismatch in byte " + l.ToString() + " in file " + display); - return false; - } + var expectedByte = expectedFileStream.ReadByte(); + var actualByte = actualFileStream.ReadByte(); + // For performance reasons, only exercise Assert mechanism and generate message if byte comparison fails + if (expectedByte != actualByte) + { + var message = $"{contextMessage}, file contents mismatch at position {i} for {expectedFile} and {actualFile}"; + Assert.That(actualByte, Is.EqualTo(expectedByte), message); + } } } - + } + // Compare file metadata if (verifymetadata) { - if (System.IO.File.GetLastWriteTime(f1) != System.IO.File.GetLastWriteTime(f2)) - { - Log.WriteWarningMessage(LOGTAG, "MismatchInLastModified", null, "Mismatch in lastmodified for {0}, {1} vs {2}", f2, System.IO.File.GetLastWriteTimeUtc(f1), System.IO.File.GetLastWriteTimeUtc(f2)); - Console.WriteLine("Mismatch in lastmodified for " + f2 + ", " + System.IO.File.GetLastWriteTimeUtc(f1) + " vs. " + System.IO.File.GetLastWriteTimeUtc(f2)); - } - - if (System.IO.File.GetCreationTimeUtc(f1) != System.IO.File.GetCreationTimeUtc(f2)) - { - Log.WriteWarningMessage(LOGTAG, "MismatchInCreateTime", null, "Mismatch in create-time for {0}, {1} vs {2}", f2, System.IO.File.GetCreationTimeUtc(f1), System.IO.File.GetCreationTimeUtc(f2)); - Console.WriteLine("Mismatch in create-time for " + f2 + ", " + System.IO.File.GetCreationTimeUtc(f1) + " vs. " + System.IO.File.GetCreationTimeUtc(f2)); - } + Assert.That( + SystemIO.IO_OS.GetLastWriteTimeUtc(actualFile), + Is.EqualTo(SystemIO.IO_OS.GetLastWriteTimeUtc(expectedFile)).Within(1).Milliseconds, + $"{contextMessage}, last write time mismatch for {expectedFile} and {actualFile}"); + Assert.That( + SystemIO.IO_OS.GetCreationTimeUtc(actualFile), + Is.EqualTo(SystemIO.IO_OS.GetCreationTimeUtc(expectedFile)).Within(1).Milliseconds, + $"{contextMessage}, creation time mismatch for {expectedFile} and {actualFile}"); } - - - return true; } - + public static Dictionary Expand(this Dictionary self, object extra) { var res = new Dictionary(self); @@ -312,6 +274,16 @@ namespace Duplicati.UnitTest return res; } + /// + /// Write file with . + /// + public static void WriteFile(string path, byte[] contents) + { + using (FileStream fileStream = SystemIO.IO_OS.FileOpenWrite(path)) + { + Utility.CopyStream(new MemoryStream(contents), fileStream); + } + } } } diff --git a/Installer/AssemblyRedirects.xml b/Installer/AssemblyRedirects.xml index 57f8304b0..ca39a1d2f 100644 --- a/Installer/AssemblyRedirects.xml +++ b/Installer/AssemblyRedirects.xml @@ -1,6 +1,7 @@ + @@ -39,6 +40,6 @@ - + - \ No newline at end of file + diff --git a/Installer/Docker/README.md b/Installer/Docker/README.md index 9c17e3633..3928c17d5 100644 --- a/Installer/Docker/README.md +++ b/Installer/Docker/README.md @@ -3,7 +3,7 @@ Duplicati is a free, open source, backup client that securely stores encrypted, *Amazon S3, OneDrive, Google Drive, Rackspace Cloud Files, HubiC, Backblaze (B2), Swift / OpenStack, WebDAV, SSH (SFTP), FTP, and more!* -Duplicati is licensed under LGPL and available for Windows, OSX and Linux (.NET 4.6.2+ or Mono 4.8.0+ required). +Duplicati is licensed under LGPL and available for Windows, OSX and Linux (.NET 4.7.1+ or Mono 4.8.0+ required). ## Available tags diff --git a/Installer/Docker/context/Dockerfile b/Installer/Docker/context/Dockerfile index c93e0d54a..ffcd6e3cc 100644 --- a/Installer/Docker/context/Dockerfile +++ b/Installer/Docker/context/Dockerfile @@ -3,6 +3,7 @@ FROM --platform=$TARGETPLATFORM mono:5-slim RUN apt-get update && \ apt-get install -y --no-install-recommends \ curl \ + mono-complete \ libmono-sqlite4.0-cil \ libmono-system-drawing4.0-cil \ libmono-system-net-http-webrequest4.0-cil \ diff --git a/Installer/OSX/make-dmg.sh b/Installer/OSX/make-dmg.sh index 0c8a8d724..0b090ab75 100644 --- a/Installer/OSX/make-dmg.sh +++ b/Installer/OSX/make-dmg.sh @@ -9,7 +9,7 @@ WC_DIR=wc TEMPLATE_DMG=template.dmg OUTPUT_DMG=Duplicati.dmg OUTPUT_PKG=Duplicati.pkg -UNWANTED_FILES="appindicator-sharp.dll SQLite win-tools control_dir Duplicati.sqlite Duplicati-server.sqlite run-script-example.bat lvm-scripts Duplicati.debug.log SVGIcons" +UNWANTED_FILES="appindicator-sharp.dll SQLite win-tools control_dir Duplicati.sqlite Duplicati-server.sqlite run-script-example.bat lvm-scripts Duplicati.debug.log SVGIcons storj_uplink.dll libstorj_uplink.so win-x64 win-x86" # These are set via the macos-gatekeeper file CODESIGN_IDENTITY= diff --git a/Installer/Synology/make-binary-package.sh b/Installer/Synology/make-binary-package.sh index 8e1bd75ca..e2cf0aac6 100644 --- a/Installer/Synology/make-binary-package.sh +++ b/Installer/Synology/make-binary-package.sh @@ -76,6 +76,10 @@ rm -rf ./OSX\ Icons rm -rf ./OSXTrayHost rm -rf ./licenses/MonoMac rm -rf ./licenses/gpg +rm -rf ./win-x64/storj_uplink.dll +rm -rf ./win-x86/storj_uplink.dll +rm -rf ./storj_uplink.dll +rm -rf ./libstorj_uplink.dylib # Install extra items for Synology cp -R ../web-extra/* webroot/ diff --git a/Installer/Windows/Duplicati.wxs b/Installer/Windows/Duplicati.wxs index 0fff03271..313626588 100644 --- a/Installer/Windows/Duplicati.wxs +++ b/Installer/Windows/Duplicati.wxs @@ -27,7 +27,7 @@ - + diff --git a/Installer/Windows/WixInstaller.wixproj b/Installer/Windows/WixInstaller.wixproj index 1a8374ec6..191140b6b 100644 --- a/Installer/Windows/WixInstaller.wixproj +++ b/Installer/Windows/WixInstaller.wixproj @@ -67,7 +67,7 @@ - + @@ -76,4 +76,4 @@ - \ No newline at end of file + diff --git a/Installer/debian/bin-rules.sh b/Installer/debian/bin-rules.sh index 7770b270a..3aaebf98b 100644 --- a/Installer/debian/bin-rules.sh +++ b/Installer/debian/bin-rules.sh @@ -56,6 +56,10 @@ override_dh_auto_install: rm -rf build/lib/duplicati/OSXTrayHost rm -rf build/lib/duplicati/licenses/MonoMac rm -rf build/lib/duplicati/licenses/gpg + rm -rf build/lib/duplicati/win-x64/storj_uplink.dll + rm -rf build/lib/duplicati/win-x86/storj_uplink.dll + rm -rf build/lib/duplicati/storj_uplink.dll + rm -rf build/lib/duplicati/libstorj_uplink.dylib find build/lib/duplicati/* -type f | xargs chmod 644 find build/lib/duplicati/* -type d | xargs chmod 755 find build/lib/duplicati/* -type f -name \*.exe | xargs chmod 755 diff --git a/Installer/debian/debian/control b/Installer/debian/debian/control index 0ba51f729..79c2c6b0a 100644 --- a/Installer/debian/debian/control +++ b/Installer/debian/debian/control @@ -9,7 +9,7 @@ Vcs-Git: https://github.com/duplicati/duplicati.git Package: duplicati Architecture: all -Depends: mono-runtime (>= 5.0), ca-certificates-mono, libmono-2.0-1, libmono-system-core4.0-cil, libmono-system-configuration4.0-cil, libmono-system-configuration-install4.0-cil, libmono-system-data4.0-cil, libmono-system-drawing4.0-cil, libmono-system-net4.0-cil, libmono-system-net-http4.0-cil, libmono-system-net-http-webrequest4.0-cil, libmono-system-numerics4.0-cil, libmono-system-runtime-serialization4.0-cil, libmono-system-servicemodel4.0a-cil, libmono-system-servicemodel-discovery4.0-cil, libmono-system-serviceprocess4.0-cil, libmono-system-transactions4.0-cil, libmono-system-web4.0-cil, libmono-system-web-services4.0-cil, libmono-system-xml4.0-cil, libmono-microsoft-csharp4.0-cil, libsqlite3-0 (>= 3.6.12), libappindicator0.1-cil | libappindicator3-0.1-cil, gtk-sharp2 +Depends: mono-runtime (>= 5.10.0), ca-certificates-mono, libmono-2.0-1, libmono-system-core4.0-cil, libmono-system-configuration4.0-cil, libmono-system-configuration-install4.0-cil, libmono-system-data4.0-cil, libmono-system-drawing4.0-cil, libmono-system-net4.0-cil, libmono-system-net-http4.0-cil, libmono-system-net-http-webrequest4.0-cil, libmono-system-numerics4.0-cil, libmono-system-runtime-serialization4.0-cil, libmono-system-servicemodel4.0a-cil, libmono-system-servicemodel-discovery4.0-cil, libmono-system-serviceprocess4.0-cil, libmono-system-transactions4.0-cil, libmono-system-web4.0-cil, libmono-system-web-services4.0-cil, libmono-system-xml4.0-cil, libmono-microsoft-csharp4.0-cil, libsqlite3-0 (>= 3.6.12), libappindicator0.1-cil | libappindicator3-0.1-cil, gtk-sharp2 Description: Backup client for encrypted online backups Duplicati is a free backup client that securely stores encrypted, incremental, compressed backups on cloud storage services and remote file servers. It diff --git a/Installer/fedora/duplicati-binary.spec b/Installer/fedora/duplicati-binary.spec index d771e40c2..9b4dc58cb 100644 --- a/Installer/fedora/duplicati-binary.spec +++ b/Installer/fedora/duplicati-binary.spec @@ -11,6 +11,9 @@ # Then load overrides %include %{_topdir}/SOURCES/%{namer}-buildinfo.spec +# Make sure it does not break because we have som arch-dependant libraries bundled +%define _binaries_in_noarch_packages_terminate_build 0 + Name: %{namer} Version: %{_buildversion} Release: %{_buildtag} @@ -94,6 +97,10 @@ rm -rf OSX\ Icons rm -rf OSXTrayHost rm -rf licenses/MonoMac rm -rf licenses/gpg +rm -rf win-x64\storj_uplink.dll +rm -rf win-x86\storj_uplink.dll +rm -rf storj_uplink.dll +rm -rf libstorj_uplink.dylib %install diff --git a/Installer/fedora/duplicati.spec b/Installer/fedora/duplicati.spec index 2f1d8322d..aa659b911 100755 --- a/Installer/fedora/duplicati.spec +++ b/Installer/fedora/duplicati.spec @@ -49,7 +49,7 @@ Requires: bash Requires: sqlite >= 3.6.12 Requires: mono(appindicator-sharp) Requires: libappindicator -Requires: mono-core >= 5.0 +Requires: mono-core >= 5.10.0 Requires: mono-data-sqlite Requires: mono(System) Requires: mono(System.Configuration) diff --git a/README.md b/README.md index be5ad2244..08e067a92 100644 --- a/README.md +++ b/README.md @@ -21,9 +21,9 @@ Removed Gitter Duplicati is a free, open source, backup client that securely stores encrypted, incremental, compressed backups on cloud storage services and remote file servers. It works with: -   *Amazon S3, [Backblaze (B2)](https://www.backblaze.com/blog/duplicati-backups-cloud-storage/ "Duplicati with Backblaze B2 Cloud Storage"), Box, Dropbox, FTP, Google Cloud and Drive, HubiC, MEGA, Microsoft Azure and OneDrive, Rackspace Cloud Files, OpenStack Storage (Swift), Sia, Tardigrade, SSH (SFTP), WebDAV, [and more!](https://duplicati.readthedocs.io/en/latest/01-introduction/#supported-backends)* +   *Amazon S3, [Backblaze (B2)](https://www.backblaze.com/blog/duplicati-backups-cloud-storage/ "Duplicati with Backblaze B2 Cloud Storage"), Box, Dropbox, FTP, Google Cloud and Drive, HubiC, MEGA, Microsoft Azure and OneDrive, Rackspace Cloud Files, OpenStack Storage (Swift), Sia, Tardigrade, SSH (SFTP), WebDAV, Tencent Cloud Object Storage (COS), Telegram, [and more!](https://duplicati.readthedocs.io/en/latest/01-introduction/#supported-backends)* -Duplicati is licensed under LGPL and available for Windows, OSX and Linux (.NET 4.6.2+ or Mono 4.8.0+ required). +Duplicati is licensed under LGPL and available for Windows, OSX and Linux (.NET 4.7.1+ or Mono 5.10.0+ required). Download ======== diff --git a/Updates/build_version.txt b/Updates/build_version.txt index e34885bbc..58c9bdf9d 100644 --- a/Updates/build_version.txt +++ b/Updates/build_version.txt @@ -1 +1 @@ -107 +111 diff --git a/build-release.sh b/build-release.sh index 4ce995016..70a9809e9 100644 --- a/build-release.sh +++ b/build-release.sh @@ -392,8 +392,8 @@ ${RELEASE_CHANGEINFO_NEWS} curl -X POST "https://forum.duplicati.com/posts" \ -H "Content-Type: multipart/form-data" \ -H "Accept: application/json" \ - -F "api_key=${DISCOURSE_APIKEY}" \ - -F "api_username=${DISCOURSE_USERNAME}" \ + -H "Api-Key: ${DISCOURSE_APIKEY}" \ + -H "Api-Username: ${DISCOURSE_USERNAME}" \ -F "category=10" \ -F "title=Release: ${RELEASE_VERSION} (${RELEASE_TYPE}) ${RELEASE_TIMESTAMP}" \ -F "raw=${body}" diff --git a/build.sh b/build.sh deleted file mode 100755 index 12008517c..000000000 --- a/build.sh +++ /dev/null @@ -1,53 +0,0 @@ -#!/bin/bash - -quit_on_error() { - echo "Error on line $1, stopping build." - exit 1 -} - -list_dir() { - echo "listing directory $1 :" - ls -al $1 -} - -trap 'quit_on_error $LINENO' ERR - -CATEGORY=$1 -TRAVIS_BUILD_DIR=${2:-.} - -if id travis &> /dev/null -then - TESTUSER=travis -else - TESTUSER=$(whoami) -fi - -echo "Build script starting with parameters TRAVIS_BUILD_DIR=$TRAVIS_BUILD_DIR and CATEGORY=$CATEGORY" - -# build duplicati - -list_dir "${TRAVIS_BUILD_DIR}"/packages/ -echo "travis_fold:start:build_duplicati" -msbuild /p:Configuration=Release Duplicati.sln -cp -r ./Duplicati/Server/webroot ./Duplicati/GUI/Duplicati.GUI.TrayIcon/bin/Release/webroot -echo "travis_fold:end:build_duplicati" - -rm -rf ~/duplicati_testdata && mkdir ~/duplicati_testdata -chown -R $TESTUSER ~/duplicati_testdata/ -chmod -R 755 ~/duplicati_testdata - -# run unit tests -echo "travis_fold:start:unit_test" -if [[ "$CATEGORY" != "GUI" && "$CATEGORY" != "" ]]; then - mono ./testrunner/NUnit.ConsoleRunner.3.10.0/tools/nunit3-console.exe \ - ./Duplicati/UnitTest/bin/Release/Duplicati.UnitTest.dll --where:cat==$CATEGORY --workers=1 -fi -echo "travis_fold:end:unit_test" - -# start server and run gui tests -echo "travis_fold:start:gui_unit_test" -if [[ "$CATEGORY" == "GUI" ]]; then - mono ./Duplicati/GUI/Duplicati.GUI.TrayIcon/bin/Release/Duplicati.Server.exe & - python guiTests/guiTest.py -fi -echo "travis_fold:end:gui_unit_test" diff --git a/changelog.txt b/changelog.txt index 77f596381..21b797425 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,47 @@ +2020-09-26 - 2.0.5.111_canary_2020-09-26 +========== +* Support for Windows paths starting with "\\?\", thanks @dferreyra +* Improved performance of USN, thanks @dgehri +* Improved performance for restoring, thanks @pcmind +* Added support for filters with literals that would normally be treated as wildcards, thanks @dferreyra +* Improved build and tests, thanks @warwickmm and @dferreyra +* Switched to the new built-in .Net support for long paths, thanks @dferreyra +* Fixed issues with the Docker images by including mono-complete, thanks @warwickmm +* Fixed an issue with exporting passwords with url-encoding characters, thanks @warwickmm +* Fixed an issue with displaying icons in the restore tree, thanks @warwickmm +* Fixed a type that prevented a warning box from showing, thanks @warwickmm +* Added Storadera as an S3 destination, thanks @warwickmm + +2020-08-10 - 2.0.5.110_canary_2020-08-10 +========== +* Fixed an issue with snapshot paths, thanks @dferreyra + +2020-08-07 - 2.0.5.109_canary_2020-08-07 +========== +* Fixed hiding compression errors, thanks @jpschewe +* Fixed Tardigrade backend issues, thanks @TopperDEL +* Fixed a case where the recovery tool would fail to recover files witth dashes in the name, thanks @dferreyra +* Improved handling of "problematic" filenames on Windows and increased test coverage, thanks @dferreyra +* Improved test coverage for invalid paths and removed unused code, thanks @warwickmm +* Improved code to remove remote files to better support remote folders that are not exclusively used by Duplicati, thanks @warwickmm +* Updated to use and require .Net 4.7.1, thanks @warwickmm +* Updated USN support to work with the newer Windows features for moving instead of deleting files, thanks @dgehri + +2020-07-09 - 2.0.5.108_canary_2020-07-09 +========== +* Removed unsupported compiler directives, thanks @kenkendk +* Removed incorrect missing SSL certificate warnings, thanks @kenkendk +* Upograded to retina icons, thanks @baumerdev +* Updates to Tardigrade support, thanks @TopperDEL +* Added Tencent Cloud Object Storage (COS) backend, thanks @trueai-org +* Improved error reporting for S3 backend, thanks @jpschewe +* Added the Milan EU region to AWS S3 regions, thanks @warwickmm +* Fixed incorrect documentation on OpenStack help, thanks @warwickmm +* Fixed an incorrect endpoint for AWS S3 Bahrain, thanks @warwickmm +* Code cleanup, thanks @warwickmm +* Fixed a case where restoring files could fail if the containing folder was not restored, thanks @warwickmm +* Improved unittests, thanks @warwickmm + 2020-05-26 - 2.0.5.107_canary_2020-05-26 ========== * Added a backend for Tardigrade.io, thanks @TopperDEL diff --git a/pipeline/stage_build/job.sh b/pipeline/stage_build/job.sh index 70dd17549..b61f3dd3a 100755 --- a/pipeline/stage_build/job.sh +++ b/pipeline/stage_build/job.sh @@ -3,31 +3,30 @@ . /pipeline/shared/duplicati.sh function build () { - nuget restore "${DUPLICATI_ROOT}"/Duplicati.sln + nuget restore "${DUPLICATI_ROOT}"/Duplicati.sln - if [ ! -d "${DUPLICATI_ROOT}"/packages/SharpCompress.0.23.0 ]; then - ln -s "${DUPLICATI_ROOT}"/packages/sharpcompress.0.23.0 "${DUPLICATI_ROOT}"/packages/SharpCompress.0.23.0 - fi + if [ ! -d "${DUPLICATI_ROOT}"/packages/SharpCompress.0.23.0 ]; then + ln -s "${DUPLICATI_ROOT}"/packages/sharpcompress.0.23.0 "${DUPLICATI_ROOT}"/packages/SharpCompress.0.23.0 + fi - # build version stamper - msbuild -p:Configuration=Release -v:minimal "${DUPLICATI_ROOT}/BuildTools/UpdateVersionStamp/UpdateVersionStamp.csproj" - mono "${DUPLICATI_ROOT}/BuildTools/UpdateVersionStamp/bin/Release/UpdateVersionStamp.exe" --version="${releaseversion}" + # build version stamper + msbuild -p:Configuration=Release -v:minimal "${DUPLICATI_ROOT}/BuildTools/UpdateVersionStamp/UpdateVersionStamp.csproj" + mono "${DUPLICATI_ROOT}/BuildTools/UpdateVersionStamp/bin/Release/UpdateVersionStamp.exe" --version="${releaseversion}" - # build autoupdate - nuget restore "${DUPLICATI_ROOT}/BuildTools/AutoUpdateBuilder/AutoUpdateBuilder.sln" - msbuild -p:Configuration=Release -v:minimal "${DUPLICATI_ROOT}/BuildTools/AutoUpdateBuilder/AutoUpdateBuilder.sln" + # build autoupdate + nuget restore "${DUPLICATI_ROOT}/BuildTools/AutoUpdateBuilder/AutoUpdateBuilder.sln" + msbuild -p:Configuration=Release -v:minimal "${DUPLICATI_ROOT}/BuildTools/AutoUpdateBuilder/AutoUpdateBuilder.sln" - # build gpg signing tool - nuget restore "${DUPLICATI_ROOT}/BuildTools/GnupgSigningTool/GnupgSigningTool.sln" - msbuild -p:Configuration=Release -v:minimal "${DUPLICATI_ROOT}/BuildTools/GnupgSigningTool/GnupgSigningTool.sln" + # build gpg signing tool + nuget restore "${DUPLICATI_ROOT}/BuildTools/GnupgSigningTool/GnupgSigningTool.sln" + msbuild -p:Configuration=Release -v:minimal "${DUPLICATI_ROOT}/BuildTools/GnupgSigningTool/GnupgSigningTool.sln" - # build duplicati - msbuild -p:DefineConstants=__MonoCS__ -p:DefineConstants=ENABLE_GTK /p:Configuration=Release "${DUPLICATI_ROOT}/Duplicati.sln" + # build duplicati + msbuild -p:DefineConstants=ENABLE_GTK /p:Configuration=Release "${DUPLICATI_ROOT}/Duplicati.sln" - msbuild -p:Configuration=Release -v:minimal "${DUPLICATI_ROOT}"/Duplicati.sln - cp -r "${DUPLICATI_ROOT}"/Duplicati/Server/webroot "${DUPLICATI_ROOT}"/Duplicati/GUI/Duplicati.GUI.TrayIcon/bin/Release/webroot + cp -r "${DUPLICATI_ROOT}"/Duplicati/Server/webroot "${DUPLICATI_ROOT}"/Duplicati/GUI/Duplicati.GUI.TrayIcon/bin/Release/webroot } travis_mark_begin "BUILDING BINARIES" build -travis_mark_end "BUILDING BINARIES" \ No newline at end of file +travis_mark_end "BUILDING BINARIES" diff --git a/pipeline/stage_integrationtests/job.sh b/pipeline/stage_integrationtests/job.sh index de0fed3e7..12f6762c9 100755 --- a/pipeline/stage_integrationtests/job.sh +++ b/pipeline/stage_integrationtests/job.sh @@ -2,6 +2,15 @@ . /pipeline/docker-run/markers.sh . /pipeline/shared/duplicati.sh +# The version of mono available within the selenium docker image is too old so +# we will install the latest version. +function update_mono () { + sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF + echo "deb https://download.mono-project.com/repo/ubuntu stable-bionic main" | sudo tee /etc/apt/sources.list.d/mono-official-stable.list + sudo apt update + sudo apt install -y mono-complete +} + function start_test () { pip install selenium pip install --upgrade urllib3 @@ -18,5 +27,6 @@ function start_test () { } travis_mark_begin "INTEGRATION TESTING" +update_mono start_test -travis_mark_end "INTEGRATION TESTING" \ No newline at end of file +travis_mark_end "INTEGRATION TESTING" diff --git a/pipeline/stage_integrationtests/trigger.sh b/pipeline/stage_integrationtests/trigger.sh index c747a5888..f3a404fb2 100755 --- a/pipeline/stage_integrationtests/trigger.sh +++ b/pipeline/stage_integrationtests/trigger.sh @@ -1,9 +1,9 @@ #!/bin/bash . error_handling.sh -PACKAGES="python-pip rsync mono-complete" +PACKAGES="python-pip rsync" docker-run --image selenium/standalone-firefox \ --packages "$PACKAGES" \ --asroot \ --sharedmem \ ---command "sudo -u seluser /pipeline/stage_integrationtests/job.sh" $@ \ No newline at end of file +--command "sudo -u seluser /pipeline/stage_integrationtests/job.sh" $@ diff --git a/pipeline/start.sh b/pipeline/start.sh index 9267229be..a208fd6b3 100755 --- a/pipeline/start.sh +++ b/pipeline/start.sh @@ -6,6 +6,6 @@ SCRIPT_DIR="$( cd "$(dirname "$0")" ; pwd -P )" ${ROOT_DIR}/pipeline/jobs/build_job.sh ${ROOT_DIR}/pipeline/jobs/unittest_job.sh --testcategories BulkNormal ${ROOT_DIR}/pipeline/jobs/unittest_job.sh --testcategories BulkNoSize -${ROOT_DIR}/pipeline/jobs/unittest_job.sh --testcategories SVNDataLong,SVNData,RecoveryTool +${ROOT_DIR}/pipeline/jobs/unittest_job.sh --testcategories SVNDataLong,SVNData,RecoveryTool,ProblematicPath,SymLink ${ROOT_DIR}/pipeline/jobs/unittest_job.sh --testcategories Border -${ROOT_DIR}/pipeline/jobs/unittest_job.sh --testcategories Filter,Targeted,Purge,Serialization,WebApi,Utility,UriUtility,IO,ImportExport,Disruption,RestoreHandler,RepairHandler,DeleteHandler +${ROOT_DIR}/pipeline/jobs/unittest_job.sh --testcategories Filter,Targeted,Purge,Serialization,WebApi,Utility,UriUtility,IO,ImportExport,Disruption,RestoreHandler,RepairHandler,DeleteHandler,Controller diff --git a/thirdparty/TLSharp/Homepage.txt b/thirdparty/TLSharp/Homepage.txt new file mode 100644 index 000000000..0d853f760 --- /dev/null +++ b/thirdparty/TLSharp/Homepage.txt @@ -0,0 +1 @@ +https://github.com/sochix/TLSharp \ No newline at end of file diff --git a/thirdparty/TLSharp/License.txt b/thirdparty/TLSharp/License.txt new file mode 100644 index 000000000..6025f791e --- /dev/null +++ b/thirdparty/TLSharp/License.txt @@ -0,0 +1,9 @@ +The MIT License + +Copyright (c) 2015 Ilya Pirozhenko http://www.sochix.ru/ + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/thirdparty/TLSharp/licensedata.json b/thirdparty/TLSharp/licensedata.json new file mode 100644 index 000000000..74c0f6366 --- /dev/null +++ b/thirdparty/TLSharp/licensedata.json @@ -0,0 +1,7 @@ +{ + "name": "TLSharp", + "description": "Unofficial Telegram (http://telegram.org) client library implemented in C#", + "link": "https://github.com/sochix/TLSharp", + "license": "MIT", + "notes": "" +} \ No newline at end of file diff --git a/thirdparty/Tencentyun/LICENSE.txt b/thirdparty/Tencentyun/LICENSE.txt new file mode 100644 index 000000000..ce659efdc --- /dev/null +++ b/thirdparty/Tencentyun/LICENSE.txt @@ -0,0 +1,20 @@ + +Copyright (c) 2018 腾讯云 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/thirdparty/Tencentyun/download.txt b/thirdparty/Tencentyun/download.txt new file mode 100644 index 000000000..92161963c --- /dev/null +++ b/thirdparty/Tencentyun/download.txt @@ -0,0 +1 @@ +https://github.com/tencentyun/qcloud-sdk-dotnet \ No newline at end of file diff --git a/thirdparty/Tencentyun/licensedata.json b/thirdparty/Tencentyun/licensedata.json new file mode 100644 index 000000000..d94ef9e78 --- /dev/null +++ b/thirdparty/Tencentyun/licensedata.json @@ -0,0 +1,7 @@ +{ + "name": "qcloud-sdk-dotnet", + "description": "", + "link": "https://github.com/tencentyun/qcloud-sdk-dotnet", + "license": "MIT", + "notes": "" +} diff --git a/thirdparty/TlsTest/app.config b/thirdparty/TlsTest/app.config index 2a0024f75..d7c60fd53 100644 --- a/thirdparty/TlsTest/app.config +++ b/thirdparty/TlsTest/app.config @@ -1,3 +1,3 @@ - + diff --git a/thirdparty/mozroots/app.config b/thirdparty/mozroots/app.config index 2a0024f75..d7c60fd53 100644 --- a/thirdparty/mozroots/app.config +++ b/thirdparty/mozroots/app.config @@ -1,3 +1,3 @@ - +