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}}
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