diff --git a/Duplicati.sln b/Duplicati.sln index 8bb2a2767..ee7fd75de 100644 --- a/Duplicati.sln +++ b/Duplicati.sln @@ -141,6 +141,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.CommandLine", "Ex EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnixSupport", "Duplicati\UnixSupport\UnixSupport.csproj", "{2B605B81-353F-43CD-BF3A-29B3926A204F}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Duplicati.Library.Backend.Idrivee2", "Duplicati\Library\Backend\Idrivee2\Duplicati.Library.Backend.Idrivee2.csproj", "{6B594D23-B629-465C-B799-70EE9E56C218}" + ProjectSection(ProjectDependencies) = postProject + {C03F6DFD-805A-4BE0-9338-64870ADDB4A2} = {C03F6DFD-805A-4BE0-9338-64870ADDB4A2} + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -403,6 +408,10 @@ Global {2B605B81-353F-43CD-BF3A-29B3926A204F}.Debug|Any CPU.Build.0 = Debug|Any CPU {2B605B81-353F-43CD-BF3A-29B3926A204F}.Release|Any CPU.ActiveCfg = Release|Any CPU {2B605B81-353F-43CD-BF3A-29B3926A204F}.Release|Any CPU.Build.0 = Release|Any CPU + {6B594D23-B629-465C-B799-70EE9E56C218}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6B594D23-B629-465C-B799-70EE9E56C218}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6B594D23-B629-465C-B799-70EE9E56C218}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6B594D23-B629-465C-B799-70EE9E56C218}.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 fc20b9460..4d5d35b08 100644 --- a/Duplicati/CommandLine/BackendTester/Duplicati.CommandLine.BackendTester.csproj +++ b/Duplicati/CommandLine/BackendTester/Duplicati.CommandLine.BackendTester.csproj @@ -13,6 +13,8 @@ + + @@ -22,10 +24,8 @@ - - diff --git a/Duplicati/CommandLine/BackendTool/Duplicati.CommandLine.BackendTool.csproj b/Duplicati/CommandLine/BackendTool/Duplicati.CommandLine.BackendTool.csproj index ebaab8c7f..5b3be3b8e 100644 --- a/Duplicati/CommandLine/BackendTool/Duplicati.CommandLine.BackendTool.csproj +++ b/Duplicati/CommandLine/BackendTool/Duplicati.CommandLine.BackendTool.csproj @@ -16,6 +16,8 @@ + + @@ -31,10 +33,8 @@ - - diff --git a/Duplicati/CommandLine/RecoveryTool/Duplicati.CommandLine.RecoveryTool.csproj b/Duplicati/CommandLine/RecoveryTool/Duplicati.CommandLine.RecoveryTool.csproj index c1948aa2d..7078bc173 100644 --- a/Duplicati/CommandLine/RecoveryTool/Duplicati.CommandLine.RecoveryTool.csproj +++ b/Duplicati/CommandLine/RecoveryTool/Duplicati.CommandLine.RecoveryTool.csproj @@ -23,6 +23,8 @@ + + diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/Duplicati.GUI.TrayIcon.csproj b/Duplicati/GUI/Duplicati.GUI.TrayIcon/Duplicati.GUI.TrayIcon.csproj index 753497902..d2c5c8bd1 100644 --- a/Duplicati/GUI/Duplicati.GUI.TrayIcon/Duplicati.GUI.TrayIcon.csproj +++ b/Duplicati/GUI/Duplicati.GUI.TrayIcon/Duplicati.GUI.TrayIcon.csproj @@ -71,6 +71,8 @@ + + @@ -86,12 +88,10 @@ - - @@ -160,12 +160,4 @@ - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - diff --git a/Duplicati/GUI/Duplicati.GUI.TrayIcon/Program.cs b/Duplicati/GUI/Duplicati.GUI.TrayIcon/Program.cs index 266e25a01..a6ad29851 100644 --- a/Duplicati/GUI/Duplicati.GUI.TrayIcon/Program.cs +++ b/Duplicati/GUI/Duplicati.GUI.TrayIcon/Program.cs @@ -223,7 +223,16 @@ namespace Duplicati.GUI.TrayIcon { try { - System.Net.ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12; + ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12; + + try + { + //try TLS 1.3 (type not available on .NET < 4.8) + ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 | (SecurityProtocolType)12288; + } + catch (NotSupportedException) + { + } using (Connection = new HttpServerConnection(serverURL, password, saltedpassword, databaseConnection != null ? PasswordSource.Database : PasswordSource.HostedServer, disableTrayIconLogin, options)) { diff --git a/Duplicati/Library/AutoUpdater/UpdaterManager.cs b/Duplicati/Library/AutoUpdater/UpdaterManager.cs index c67f4934c..380597536 100644 --- a/Duplicati/Library/AutoUpdater/UpdaterManager.cs +++ b/Duplicati/Library/AutoUpdater/UpdaterManager.cs @@ -58,7 +58,7 @@ namespace Duplicati.Library.AutoUpdater : Environment.ExpandEnvironmentVariables(System.Environment.GetEnvironmentVariable(string.Format(BASEINSTALLDIR_ENVNAME_TEMPLATE, APPNAME))); //FIXME: Auto Update doesn't work with the .net5 version - private static readonly bool DISABLE_UPDATE_DOMAIN = true;//Utility.Utility.ParseBool(Environment.GetEnvironmentVariable(string.Format(SKIPUPDATE_ENVNAME_TEMPLATE, APPNAME)), false); + private static readonly bool DISABLE_UPDATE_DOMAIN = true;//Debugger.IsAttached || Utility.Utility.ParseBool(Environment.GetEnvironmentVariable(string.Format(SKIPUPDATE_ENVNAME_TEMPLATE, APPNAME)), false); public static bool RequiresRespawn { get; set; } diff --git a/Duplicati/Library/Backend/Backblaze/Strings.cs b/Duplicati/Library/Backend/Backblaze/Strings.cs index 168553757..b4d0b2429 100644 --- a/Duplicati/Library/Backend/Backblaze/Strings.cs +++ b/Duplicati/Library/Backend/Backblaze/Strings.cs @@ -14,9 +14,9 @@ namespace Duplicati.Library.Backend.Strings { public static string NoB2UserIDError { get { return LC.L(@"No ""B2 Cloud Storage Account ID"" given"); } } public static string Description { get { return LC.L(@"This backend can read and write data to the Backblaze B2 Cloud Storage. Allowed formats are: ""b2://bucketname/prefix"""); } } public static string B2createbuckettypeDescriptionLong { get { return LC.L(@"By default, a private bucket is created. Use this option to set the bucket type. Refer to the B2 documentation for allowed types "); } } - public static string B2createbuckettypeDescriptionShort { get { return LC.L(@"The bucket type used when creating a bucket"); } } - public static string B2pagesizeDescriptionLong { get { return LC.L(@"Use this option to set the page size for listing contents of B2 buckets. A lower number means less data, but can increase the number of Class C transaction on B2. Suggested values are between 100 and 1000"); } } - public static string B2pagesizeDescriptionShort { get { return LC.L(@"The size of file-listing pages"); } } + public static string B2createbuckettypeDescriptionShort { get { return LC.L(@"The bucket type used when creating a bucket"); } } + public static string B2pagesizeDescriptionLong { get { return LC.L(@"Use this option to set the page size for listing contents of B2 buckets. A lower number means less data, but can increase the number of Class C transaction on B2. Suggested values are between 100 and 1000"); } } + public static string B2pagesizeDescriptionShort { get { return LC.L(@"The size of file-listing pages"); } } public static string B2downloadurlDescriptionLong { get { return LC.L(@"Change this if you want to use your custom domain to download files, and uploading will not be affected. The default download url depends on your account and looks like ""https://f00X.backblazeb2.com"""); } } public static string B2downloadurlDescriptionShort { get { return LC.L(@"The base URL to use for downloading files"); } } public static string InvalidPageSizeError(string argname, string value) { return LC.L(@"The setting ""{0}"" is invalid for ""{1}"", it must be an integer larger than zero", value, argname); } diff --git a/Duplicati/Library/Backend/Dropbox/DropboxHelper.cs b/Duplicati/Library/Backend/Dropbox/DropboxHelper.cs index b277f6312..1e7dd9a34 100644 --- a/Duplicati/Library/Backend/Dropbox/DropboxHelper.cs +++ b/Duplicati/Library/Backend/Dropbox/DropboxHelper.cs @@ -18,7 +18,9 @@ namespace Duplicati.Library.Backend : base(accessToken, "dropbox") { base.AutoAuthHeader = true; - base.AccessTokenOnly = true; + // Pre 2022 tokens are direct Dropbox tokens (no ':') + // Post 2022-02-21 tokens are regular authid tokens (with a ':') + base.AccessTokenOnly = !accessToken.Contains(":"); } public ListFolderResult ListFiles(string path) diff --git a/Duplicati/Library/Backend/Dropbox/WebApi.cs b/Duplicati/Library/Backend/Dropbox/WebApi.cs index 1d0f2af86..376dd4ec9 100644 --- a/Duplicati/Library/Backend/Dropbox/WebApi.cs +++ b/Duplicati/Library/Backend/Dropbox/WebApi.cs @@ -41,7 +41,7 @@ namespace Duplicati.Library.Backend.WebApi return Uri.UriBuilder(Url.API, Path.DeleteFolder); } - public static string UploadSessionStartUrl() + public static string UploadSessionStartUrl() { return Uri.UriBuilder(Url.CONTENT_API_URL, Path.UploadSessionStart); } diff --git a/Duplicati/Library/Backend/File/FileBackend.cs b/Duplicati/Library/Backend/File/FileBackend.cs index 3b41484c9..a2b005458 100644 --- a/Duplicati/Library/Backend/File/FileBackend.cs +++ b/Duplicati/Library/Backend/File/FileBackend.cs @@ -22,6 +22,7 @@ using Duplicati.Library.Common.IO; using Duplicati.Library.Interface; using System; using System.Collections.Generic; +using System.IO; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; @@ -36,6 +37,7 @@ namespace Duplicati.Library.Backend private const string OPTION_ALTERNATE_PATHS = "alternate-target-paths"; private const string OPTION_MOVE_FILE = "use-move-for-put"; private const string OPTION_FORCE_REAUTH = "force-smb-authentication"; + private const string OPTION_DISABLE_LENGTH_VERIFICATION = "disable-length-verification"; private readonly string m_path; private string m_username; @@ -43,6 +45,7 @@ namespace Duplicati.Library.Backend private readonly bool m_moveFile; private bool m_hasAutenticated; private readonly bool m_forceReauth; + private readonly bool m_verifyDestinationLength; private readonly byte[] m_copybuffer = new byte[Utility.Utility.DEFAULT_BUFFER_SIZE]; @@ -56,7 +59,7 @@ namespace Duplicati.Library.Backend { var uri = new Utility.Uri(url); m_path = uri.HostAndPath; - + if (options.ContainsKey("auth-username")) m_username = options["auth-username"]; if (options.ContainsKey("auth-password")) @@ -65,7 +68,7 @@ namespace Duplicati.Library.Backend m_username = uri.Username; if (!string.IsNullOrEmpty(uri.Password)) m_password = uri.Password; - + if (!System.IO.Path.IsPathRooted(m_path)) m_path = systemIO.PathGetFullPath(m_path); @@ -81,7 +84,7 @@ namespace Duplicati.Library.Backend if (!Platform.IsClientPosix) { System.IO.DriveInfo[] drives = System.IO.DriveInfo.GetDrives(); - + for (int i = 0; i < paths.Count; i++) { if (paths[i].StartsWith("*:", StringComparison.Ordinal)) @@ -126,6 +129,7 @@ namespace Duplicati.Library.Backend m_moveFile = Utility.Utility.ParseBoolOption(options, OPTION_MOVE_FILE); m_forceReauth = Utility.Utility.ParseBoolOption(options, OPTION_FORCE_REAUTH); + m_verifyDestinationLength = Utility.Utility.ParseBoolOption(options, OPTION_DISABLE_LENGTH_VERIFICATION); m_hasAutenticated = false; } @@ -195,10 +199,14 @@ namespace Duplicati.Library.Backend } } #else - public async Task PutAsync(string remotename, System.IO.Stream stream, CancellationToken cancelToken) + public async Task PutAsync(string targetFilename, Stream sourceStream, CancellationToken cancelToken) { - using (System.IO.FileStream writestream = systemIO.FileCreate(GetRemoteName(remotename))) - await Utility.Utility.CopyStreamAsync(stream, writestream, true, cancelToken, m_copybuffer); + string targetFilePath = GetRemoteName(targetFilename); + long copiedBytes = 0; + using (var targetStream = systemIO.FileCreate(targetFilePath)) + copiedBytes = await Utility.Utility.CopyStreamAsync(sourceStream, targetStream, true, cancelToken, m_copybuffer); + + VerifyMatchingSize(targetFilePath, sourceStream, copiedBytes); } #endif @@ -209,18 +217,27 @@ namespace Duplicati.Library.Backend Utility.Utility.CopyStream(readstream, stream, true, m_copybuffer); } - public Task PutAsync(string remotename, string filename, CancellationToken cancelToken) + public Task PutAsync(string targetFilename, string sourceFilePath, CancellationToken cancelToken) { - string path = GetRemoteName(remotename); + string targetFilePath = GetRemoteName(targetFilename); if (m_moveFile) { - if (systemIO.FileExists(path)) - systemIO.FileDelete(path); - - systemIO.FileMove(filename, path); + if (systemIO.FileExists(targetFilePath)) + systemIO.FileDelete(targetFilePath); + + var sourceFileInfo = new FileInfo(sourceFilePath); + var sourceFileLength = sourceFileInfo.Exists ? (long?)sourceFileInfo.Length : null; + + systemIO.FileMove(sourceFilePath, targetFilePath); + if (m_verifyDestinationLength) + VerifyMatchingSize(targetFilePath, null, sourceFileLength); } else - systemIO.FileCopy(filename, path, true); + { + systemIO.FileCopy(sourceFilePath, targetFilePath, true); + if (m_verifyDestinationLength) + VerifyMatchingSize(targetFilePath, sourceFilePath); + } return Task.FromResult(true); } @@ -246,6 +263,8 @@ namespace Duplicati.Library.Backend new CommandLineArgument(OPTION_ALTERNATE_PATHS, CommandLineArgument.ArgumentType.Path, Strings.FileBackend.AlternateTargetPathsShort, Strings.FileBackend.AlternateTargetPathsLong(OPTION_DESTINATION_MARKER, System.IO.Path.PathSeparator)), new CommandLineArgument(OPTION_MOVE_FILE, CommandLineArgument.ArgumentType.Boolean, Strings.FileBackend.UseMoveForPutShort, Strings.FileBackend.UseMoveForPutLong), new CommandLineArgument(OPTION_FORCE_REAUTH, CommandLineArgument.ArgumentType.Boolean, Strings.FileBackend.ForceReauthShort, Strings.FileBackend.ForceReauthLong), + new CommandLineArgument(OPTION_DISABLE_LENGTH_VERIFICATION, CommandLineArgument.ArgumentType.Boolean, Strings.FileBackend.DisableLengthVerificationShort, Strings.FileBackend.DisableLengthVerificationShort), + }); } @@ -386,5 +405,51 @@ namespace Duplicati.Library.Backend out ulong lpTotalNumberOfBytes, out ulong lpTotalNumberOfFreeBytes); } + + private static void VerifyMatchingSize(string targetFilePath, string sourceFilePath) + { + try + { + var targetFileInfo = new FileInfo(targetFilePath); + if (!targetFileInfo.Exists) + throw new FileMissingException($"Target file does not exist. Target: {targetFilePath}"); + + var sourceFileInfo = new FileInfo(sourceFilePath); + if (!sourceFileInfo.Exists) + throw new FileMissingException($"Source file does not exist. Source: {sourceFilePath}"); + + if (targetFileInfo.Length != sourceFileInfo.Length) + throw new FileMissingException($"Target file size ({targetFileInfo.Length:n0}) is different from source file size ({sourceFileInfo.Length:n0}). Target: {targetFilePath}"); + } + catch + { + try { System.IO.File.Delete(targetFilePath); } catch { } + throw; + } + } + + private static void VerifyMatchingSize(string targetFilePath, Stream sourceStream, long? expectedLength) + { + try + { + var targetFileInfo = new FileInfo(targetFilePath); + if (!targetFileInfo.Exists) + throw new FileMissingException($"Target file does not exist. Target: {targetFilePath}"); + + bool isStreamPostion = false; + long? sourceStreamLength = sourceStream == null ? null : Utility.Utility.GetStreamLength(sourceStream, out isStreamPostion); + + if (sourceStreamLength.HasValue && targetFileInfo.Length != sourceStreamLength.Value) + throw new FileMissingException($"Target file size ({targetFileInfo.Length:n0}) is different from the source length ({sourceStreamLength.Value:n0}){(isStreamPostion ? " - ending stream position)" : "")}. Target: {targetFilePath}"); + + if (expectedLength.HasValue && targetFileInfo.Length != expectedLength.Value) + throw new FileMissingException($"Target file size ({targetFileInfo.Length:n0}) is different from the expected length ({expectedLength.Value:n0}). Target: {targetFilePath}"); + } + catch + { + try { System.IO.File.Delete(targetFilePath); } catch { } + throw; + } + } } } diff --git a/Duplicati/Library/Backend/File/Strings.cs b/Duplicati/Library/Backend/File/Strings.cs index 3bb2659b6..f5bb19aa7 100644 --- a/Duplicati/Library/Backend/File/Strings.cs +++ b/Duplicati/Library/Backend/File/Strings.cs @@ -17,5 +17,7 @@ namespace Duplicati.Library.Backend.Strings { public static string UseMoveForPutShort { get { return LC.L(@"Move the file instead of copying it"); } } public static string ForceReauthShort { get { return LC.L(@"Force authentication against remote share"); } } public static string ForceReauthLong { get { return LC.L(@"If this option is set, any existing authentication against the remote share is dropped before attempting to authenticate"); } } + public static string DisableLengthVerificationShort { get { return LC.L(@"Disable length verification"); } } + public static string DisableLengthVerificationLong { get { return LC.L(@"As an extra precaution the uploaded file length will be checked against the local source length."); } } } } diff --git a/Duplicati/Library/Backend/Idrivee2/Duplicati.Library.Backend.Idrivee2.csproj b/Duplicati/Library/Backend/Idrivee2/Duplicati.Library.Backend.Idrivee2.csproj new file mode 100644 index 000000000..b9d9ba9b9 --- /dev/null +++ b/Duplicati/Library/Backend/Idrivee2/Duplicati.Library.Backend.Idrivee2.csproj @@ -0,0 +1,23 @@ + + + + netstandard2.0 + LGPL, Copyright © Duplicati Team 2021 + iDrivee2 backend for Duplicati + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/Duplicati/Library/Backend/Idrivee2/Idrivee2Backend.cs b/Duplicati/Library/Backend/Idrivee2/Idrivee2Backend.cs new file mode 100644 index 000000000..aef41259b --- /dev/null +++ b/Duplicati/Library/Backend/Idrivee2/Idrivee2Backend.cs @@ -0,0 +1,249 @@ +#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 Duplicati.Library.Common.IO; +using Duplicati.Library.Interface; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Duplicati.Library.Backend +{ + public class Idrivee2Backend : IBackend, IStreamingBackend + { + private static readonly string LOGTAG = Logging.Log.LogTagFromType(); + + static Idrivee2Backend() + { + + } + + private readonly string m_prefix; + private readonly string m_bucket; + + private IS3Client m_s3Client; + + public Idrivee2Backend() + { + } + + public Idrivee2Backend(string url, Dictionary options) + { + var uri = new Utility.Uri(url); + m_bucket = uri.Host; + m_prefix = uri.Path; + m_prefix = m_prefix.Trim(); + if (m_prefix.Length != 0) + { + m_prefix = Util.AppendDirSeparator(m_prefix, "/"); + } + string accessKeyId = null; + string accessKeySecret = null; + + if (options.ContainsKey("auth-username")) + accessKeyId = options["auth-username"]; + if (options.ContainsKey("auth-password")) + accessKeySecret = options["auth-password"]; + + if (options.ContainsKey("access_key_id")) + accessKeyId = options["access_key_id"]; + if (options.ContainsKey("secret_access_key")) + accessKeySecret = options["secret_access_key"]; + + if (string.IsNullOrEmpty(accessKeyId)) + throw new UserInformationException(Strings.Idrivee2Backend.NoKeyIdError, "Idrivee2NoKeyId"); + if (string.IsNullOrEmpty(accessKeySecret)) + throw new UserInformationException(Strings.Idrivee2Backend.NoKeySecretError, "Idrivee2NoKeySecret"); + string host= GetRegionEndpoint("https://api.idrivee2.com/api/service/get_region_end_point/" + accessKeyId); + + + m_s3Client = new S3AwsClient(accessKeyId, accessKeySecret, null, host, null, true, options); + + } + + public string GetRegionEndpoint(string url) + { + try + { + System.Net.HttpWebRequest req = (System.Net.HttpWebRequest)System.Net.WebRequest.Create(url); + req.Method = System.Net.WebRequestMethods.Http.Get; + + Utility.AsyncHttpRequest areq = new Utility.AsyncHttpRequest(req); + + using (System.Net.HttpWebResponse resp = (System.Net.HttpWebResponse)areq.GetResponse()) + { + int code = (int)resp.StatusCode; + if (code < 200 || code >= 300) //For some reason Mono does not throw this automatically + throw new Exception("Failed to fetch region endpoint"); + using (var s = areq.GetResponseStream()) + { + using (var reader = new StreamReader(s)) + { + string endpoint = reader.ReadToEnd(); + return endpoint; + } + } + } + } + catch (System.Net.WebException wex) + { + //Convert to better exception + throw new Exception("Failed to fetch region endpoint"); + } + } + + #region IBackend Members + + public string DisplayName + { + get { return Strings.Idrivee2Backend.DisplayName; } + } + + public string ProtocolKey => "e2"; + + public bool SupportsStreaming => true; + + + public IEnumerable List() + { + foreach (IFileEntry file in Connection.ListBucket(m_bucket, m_prefix)) + { + ((FileEntry)file).Name = file.Name.Substring(m_prefix.Length); + if (file.Name.StartsWith("/", StringComparison.Ordinal) && !m_prefix.StartsWith("/", StringComparison.Ordinal)) + ((FileEntry)file).Name = file.Name.Substring(1); + + yield return file; + } + } + + public async Task PutAsync(string remotename, string localname, CancellationToken cancelToken) + { + using (FileStream fs = File.Open(localname, FileMode.Open, FileAccess.Read, FileShare.Read)) + await PutAsync(remotename, fs, cancelToken); + } + + public async Task PutAsync(string remotename, Stream input, CancellationToken cancelToken) + { + await Connection.AddFileStreamAsync(m_bucket, GetFullKey(remotename), input, cancelToken); + } + + public void Get(string remotename, string localname) + { + using (var fs = File.Open(localname, FileMode.Create, FileAccess.Write, FileShare.None)) + Get(remotename, fs); + } + + public void Get(string remotename, Stream output) + { + Connection.GetFileStream(m_bucket, GetFullKey(remotename), output); + } + + public void Delete(string remotename) + { + Connection.DeleteObject(m_bucket, GetFullKey(remotename)); + } + + public IList SupportedCommands + { + get + { + + var defaults = new Amazon.S3.AmazonS3Config(); + + var exts = + typeof(Amazon.S3.AmazonS3Config).GetProperties().Where(x => x.CanRead && x.CanWrite && (x.PropertyType == typeof(string) || x.PropertyType == typeof(bool) || x.PropertyType == typeof(int) || x.PropertyType == typeof(long) || x.PropertyType.IsEnum)) + .Select(x => (ICommandLineArgument)new CommandLineArgument( + "s3-ext-" + x.Name.ToLowerInvariant(), + x.PropertyType == typeof(bool) ? CommandLineArgument.ArgumentType.Boolean : x.PropertyType.IsEnum ? CommandLineArgument.ArgumentType.Enumeration : CommandLineArgument.ArgumentType.String, + x.Name, + string.Format("Extended option {0}", x.Name), + string.Format("{0}", x.GetValue(defaults)), + null, + x.PropertyType.IsEnum ? Enum.GetNames(x.PropertyType) : null)); + + + var normal = new ICommandLineArgument[] { + + new CommandLineArgument("access_key_secret", CommandLineArgument.ArgumentType.Password, Strings.Idrivee2Backend.KeySecretDescriptionShort, Strings.Idrivee2Backend.KeySecretDescriptionLong, null, new[]{"auth-password"}, null), + new CommandLineArgument("access_key_id", CommandLineArgument.ArgumentType.String, Strings.Idrivee2Backend.KeyIDDescriptionShort, Strings.Idrivee2Backend.KeyIDDescriptionLong,null, new[]{"auth-username"}, null) + + }; + + return normal.Union(exts).ToList(); + + } + } + + public string Description + { + get + { + return Strings.Idrivee2Backend.Description; + } + } + + public void Test() + { + this.TestList(); + } + + public void CreateFolder() + { + //S3 does not complain if the bucket already exists + Connection.AddBucket(m_bucket); + } + + #endregion + + #region IRenameEnabledBackend Members + + public void Rename(string source, string target) + { + Connection.RenameFile(m_bucket, GetFullKey(source), GetFullKey(target)); + } + + #endregion + + #region IDisposable Members + + public void Dispose() + { + m_s3Client?.Dispose(); + m_s3Client = null; + } + + #endregion + + private IS3Client Connection => m_s3Client; + + public string[] DNSName + { + get { return new[] { m_s3Client.GetDnsHost() }; } + } + + private string GetFullKey(string name) + { + //AWS SDK encodes the filenames correctly + return m_prefix + name; + } + } +} diff --git a/Duplicati/Library/Backend/Idrivee2/Strings.cs b/Duplicati/Library/Backend/Idrivee2/Strings.cs new file mode 100644 index 000000000..f80a5f159 --- /dev/null +++ b/Duplicati/Library/Backend/Idrivee2/Strings.cs @@ -0,0 +1,17 @@ +using Duplicati.Library.Localization.Short; +namespace Duplicati.Library.Backend.Strings { + internal static class Idrivee2Backend { + public static string KeySecretDescriptionLong { get { return LC.L(@"The ""Access Key Secret "" can be obtained after logging into your IDrive e2 account, this can also be supplied through the ""auth-password"" property."); } } + public static string KeySecretDescriptionShort { get { return LC.L(@"The ""Access Key Secret"""); } } + public static string KeyIDDescriptionLong { get { return LC.L(@"The ""Access Key ID"" can be obtained after logging into your IDrive e2 account., this can also be supplied through the ""auth-username"" property."); } } + public static string KeyIDDescriptionShort { get { return LC.L(@"The ""Access Key ID"""); } } + + public static string BucketNameOrPathDescriptionLong { get { return LC.L(@"The ""Bucket Name or Complete Path"" is name of target bucket or complete of a folder inside the bucket."); } } + public static string BucketNameOrPathDescriptionShort { get { return LC.L(@"The ""Bucket Name or Complete Path"""); } } + + public static string DisplayName { get { return LC.L(@"IDrive e2"); } } + public static string NoKeySecretError { get { return LC.L(@"No Access key secret given"); } } + public static string NoKeyIdError { get { return LC.L(@"No Access key Id given"); } } + public static string Description { get; set; } + } +} diff --git a/Duplicati/Library/Backend/Jottacloud/Duplicati.Library.Backend.Jottacloud.csproj b/Duplicati/Library/Backend/Jottacloud/Duplicati.Library.Backend.Jottacloud.csproj index a18810944..46afd1da5 100644 --- a/Duplicati/Library/Backend/Jottacloud/Duplicati.Library.Backend.Jottacloud.csproj +++ b/Duplicati/Library/Backend/Jottacloud/Duplicati.Library.Backend.Jottacloud.csproj @@ -18,6 +18,6 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + - - \ No newline at end of file + diff --git a/Duplicati/Library/Backend/Jottacloud/Jottacloud.cs b/Duplicati/Library/Backend/Jottacloud/Jottacloud.cs index 0bfb6ce83..7d80170b0 100644 --- a/Duplicati/Library/Backend/Jottacloud/Jottacloud.cs +++ b/Duplicati/Library/Backend/Jottacloud/Jottacloud.cs @@ -32,9 +32,9 @@ namespace Duplicati.Library.Backend // This class is instantiated dynamically in the BackendLoader. public class Jottacloud : IBackend, IStreamingBackend { + private const string AUTHID_OPTION = "authid"; private const string JFS_ROOT = "https://jfs.jottacloud.com/jfs"; private const string JFS_ROOT_UPLOAD = "https://up.jottacloud.com/jfs"; // Separate host for uploading files - private const string API_VERSION = "2.4"; // Hard coded per 09. March 2017. private const string JFS_BUILTIN_DEVICE = "Jotta"; // The built-in device used for the built-in Sync and Archive mount points. private static readonly string JFS_DEFAULT_BUILTIN_MOUNT_POINT = "Archive"; // When using the built-in device we pick this mount point as our default. private static readonly string JFS_DEFAULT_CUSTOM_MOUNT_POINT = "Duplicati"; // When custom device is specified then we pick this mount point as our default. @@ -52,7 +52,6 @@ namespace Duplicati.Library.Backend private readonly string m_url_device; private readonly string m_url; private readonly string m_url_upload; - private readonly System.Net.NetworkCredential m_userInfo; private readonly byte[] m_copybuffer = new byte[Duplicati.Library.Utility.Utility.DEFAULT_BUFFER_SIZE]; private static readonly string JFS_DEFAULT_CHUNKSIZE = "5mb"; @@ -60,6 +59,8 @@ namespace Duplicati.Library.Backend private readonly int m_threads; private readonly long m_chunksize; + private readonly JottacloudAuthHelper m_oauth; + /// /// The default maximum number of concurrent connections allowed by a ServicePoint object is 2. /// It should be increased to allow multiple download threads. @@ -133,40 +134,21 @@ namespace Duplicati.Library.Backend m_mountPoint = JFS_DEFAULT_CUSTOM_MOUNT_POINT; // Set a suitable default mount point for custom (backup) devices. } + string authid = null; + if (options.ContainsKey(AUTHID_OPTION)) + authid = options[AUTHID_OPTION]; + m_oauth = new JottacloudAuthHelper(authid); + // Build URL var u = new Utility.Uri(url); m_path = u.HostAndPath; // Host and path of "jottacloud://folder/subfolder" is "folder/subfolder", so the actual folder path within the mount point. if (string.IsNullOrEmpty(m_path)) // Require a folder. Actually it is possible to store files directly on the root level of the mount point, but that does not seem to be a good option. throw new UserInformationException(Strings.Jottacloud.NoPathError, "JottaNoPath"); m_path = Util.AppendDirSeparator(m_path, "/"); - if (!string.IsNullOrEmpty(u.Username)) - { - m_userInfo = new System.Net.NetworkCredential(); - m_userInfo.UserName = u.Username; - if (!string.IsNullOrEmpty(u.Password)) - m_userInfo.Password = u.Password; - else if (options.ContainsKey("auth-password")) - m_userInfo.Password = options["auth-password"]; - } - else - { - if (options.ContainsKey("auth-username")) - { - m_userInfo = new System.Net.NetworkCredential(); - m_userInfo.UserName = options["auth-username"]; - if (options.ContainsKey("auth-password")) - m_userInfo.Password = options["auth-password"]; - } - } - if (m_userInfo == null || string.IsNullOrEmpty(m_userInfo.UserName)) - throw new UserInformationException(Strings.Jottacloud.NoUsernameError, "JottaNoUsername"); - if (m_userInfo == null || string.IsNullOrEmpty(m_userInfo.Password)) - throw new UserInformationException(Strings.Jottacloud.NoPasswordError, "JottaNoPassword"); - if (m_userInfo != null) // Bugfix, see http://connect.microsoft.com/VisualStudio/feedback/details/695227/networkcredential-default-constructor-leaves-domain-null-leading-to-null-object-reference-exceptions-in-framework-code - m_userInfo.Domain = ""; - m_url_device = JFS_ROOT + "/" + m_userInfo.UserName + "/" + m_device; + + m_url_device = JFS_ROOT + "/" + m_oauth.Username + "/" + m_device; m_url = m_url_device + "/" + m_mountPoint + "/" + m_path; - m_url_upload = JFS_ROOT_UPLOAD + "/" + m_userInfo.UserName + "/" + m_device + "/" + m_mountPoint + "/" + m_path; // Different hostname, else identical to m_url. + m_url_upload = JFS_ROOT_UPLOAD + "/" + m_oauth.Username + "/" + m_device + "/" + m_mountPoint + "/" + m_path; // Different hostname, else identical to m_url. m_threads = int.Parse(options.ContainsKey(JFS_THREADS) ? options[JFS_THREADS] : JFS_DEFAULT_THREADS); @@ -187,7 +169,7 @@ namespace Duplicati.Library.Backend m_chunksize = chunksize; } - #region IBackend Members +#region IBackend Members public string DisplayName { @@ -330,8 +312,7 @@ namespace Duplicati.Library.Backend get { return new List(new ICommandLineArgument[] { - new CommandLineArgument("auth-password", CommandLineArgument.ArgumentType.Password, Strings.Jottacloud.DescriptionAuthPasswordShort, Strings.Jottacloud.DescriptionAuthPasswordLong), - new CommandLineArgument("auth-username", CommandLineArgument.ArgumentType.String, Strings.Jottacloud.DescriptionAuthUsernameShort, Strings.Jottacloud.DescriptionAuthUsernameLong), + new CommandLineArgument(AUTHID_OPTION, CommandLineArgument.ArgumentType.Password, Strings.Jottacloud.AuthidShort, Strings.Jottacloud.AuthidLong(OAuthHelper.OAUTH_LOGIN_URL("jottacloud"))), new CommandLineArgument(JFS_DEVICE_OPTION, CommandLineArgument.ArgumentType.String, Strings.Jottacloud.DescriptionDeviceShort, Strings.Jottacloud.DescriptionDeviceLong(JFS_MOUNT_POINT_OPTION)), new CommandLineArgument(JFS_MOUNT_POINT_OPTION, CommandLineArgument.ArgumentType.String, Strings.Jottacloud.DescriptionMountPointShort, Strings.Jottacloud.DescriptionMountPointLong(JFS_DEVICE_OPTION)), new CommandLineArgument(JFS_THREADS, CommandLineArgument.ArgumentType.Integer, Strings.Jottacloud.ThreadsShort, Strings.Jottacloud.ThreadsLong, JFS_DEFAULT_THREADS), @@ -369,26 +350,19 @@ namespace Duplicati.Library.Backend } } - #endregion +#endregion - #region IDisposable Members +#region IDisposable Members public void Dispose() { } - #endregion +#endregion private System.Net.HttpWebRequest CreateRequest(string method, string url, string queryparams) { - System.Net.HttpWebRequest req = (System.Net.HttpWebRequest)System.Net.HttpWebRequest.Create(url + (string.IsNullOrEmpty(queryparams) || queryparams.Trim().Length == 0 ? "" : "?" + queryparams)); - req.Method = method; - req.Credentials = m_userInfo; - req.PreAuthenticate = true; // We need this under Mono for some reason, and it appears some servers require this as well - req.KeepAlive = false; - req.UserAgent = "Duplicati Jottacloud Client v" + System.Reflection.Assembly.GetExecutingAssembly().GetName().Version; - req.Headers.Add("x-jftp-version", API_VERSION); - return req; + return m_oauth.CreateRequest(url + (string.IsNullOrEmpty(queryparams) || queryparams.Trim().Length == 0 ? "" : "?" + queryparams), method); } private System.Net.HttpWebRequest CreateRequest(string method, string remotename, string queryparams, bool upload) diff --git a/Duplicati/Library/Backend/Jottacloud/JottacloudAuthHelper.cs b/Duplicati/Library/Backend/Jottacloud/JottacloudAuthHelper.cs new file mode 100644 index 000000000..b6b4a146d --- /dev/null +++ b/Duplicati/Library/Backend/Jottacloud/JottacloudAuthHelper.cs @@ -0,0 +1,71 @@ +// 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +using Duplicati.Library.Interface; +using Newtonsoft.Json; + +namespace Duplicati.Library.Backend +{ + public class JottacloudAuthHelper : OAuthHelper + { + private const string USERINFO_URL = "https://id.jottacloud.com/auth/realms/jottacloud/protocol/openid-connect/userinfo"; + private string m_username; + + public JottacloudAuthHelper(string accessToken) + : base(accessToken, "jottacloud") + { + base.AutoAuthHeader = true; + base.AutoV2 = false; // Jottacloud is not v2 compatible because it generates a new refresh token with every access token refresh and invalidates the old. + + var userinfo = GetJSONData(USERINFO_URL); + if (userinfo == null || string.IsNullOrEmpty(userinfo.Username)) + throw new UserInformationException(Strings.Jottacloud.NoUsernameError, "JottaNoUsername"); + m_username = userinfo.Username; + } + + public string Username + { + get + { + return m_username; + } + } + + private class UserInfo + { + [JsonProperty("sub")] + public string Subject { get; set; } + [JsonProperty("email_verified")] + public bool EmailVerified { get; set; } + [JsonProperty("name")] + public string Name { get; set; } + [JsonProperty("realm")] + public string Realm { get; set; } + [JsonProperty("preferred_username")] + public string PreferredUsername { get; set; } // The numeric internal username, same as Username + [JsonProperty("given_name")] + public string GivenName { get; set; } + [JsonProperty("family_name")] + public string FamilyName { get; set; } + [JsonProperty("email")] + public string Email { get; set; } + [JsonProperty("username")] + public string Username { get; set; } // The numeric internal username, same as PreferredUsername + } + } + +} + diff --git a/Duplicati/Library/Backend/Jottacloud/Strings.cs b/Duplicati/Library/Backend/Jottacloud/Strings.cs index 08ba011b8..010050022 100644 --- a/Duplicati/Library/Backend/Jottacloud/Strings.cs +++ b/Duplicati/Library/Backend/Jottacloud/Strings.cs @@ -4,15 +4,12 @@ namespace Duplicati.Library.Backend.Strings { { public static string DisplayName { get { return LC.L(@"Jottacloud"); } } public static string Description { get { return LC.L(@"This backend can read and write data to Jottacloud using it's REST protocol. Allowed format is ""jottacloud://folder/subfolder""."); } } - public static string NoUsernameError { get { return LC.L(@"No username given"); } } - public static string NoPasswordError { get { return LC.L(@"No password given"); } } + public static string AuthidShort { get { return LC.L(@"The authorization code"); } } + public static string AuthidLong(string url) { return LC.L(@"The authorization token retrieved from {0}", url); } + public static string NoUsernameError { get { return LC.L(@"No username found"); } } public static string NoPathError { get { return LC.L(@"No path given, cannot upload files to the root folder"); } } public static string IllegalMountPoint { get { return LC.L(@"Illegal mount point given."); } } public static string FileUploadError { get { return LC.L(@"Failed to upload file"); } } - public static string DescriptionAuthUsernameShort { get { return LC.L(@"Supplies the username used to connect to the server"); } } - public static string DescriptionAuthUsernameLong { get { return LC.L(@"The username used to connect to the server. This may also be supplied as the environment variable ""AUTH_USERNAME""."); } } - public static string DescriptionAuthPasswordShort { get { return LC.L(@"Supplies the password used to connect to the server"); } } - public static string DescriptionAuthPasswordLong { get { return LC.L(@"The password used to connect to the server. This may also be supplied as the environment variable ""AUTH_PASSWORD""."); } } public static string DescriptionDeviceShort { get { return LC.L(@"Supplies the backup device to use"); } } public static string DescriptionDeviceLong(string mountPointOption) { return LC.L(@"The backup device to use. Will be created if not already exists. You can manage your devices from the backup panel in the Jottacloud web interface. When you specify a custom device you should also specify the mount point to use on this device with the ""{0}"" option.", mountPointOption); } public static string DescriptionMountPointShort { get { return LC.L(@"Supplies the mount point to use on the server"); } } diff --git a/Duplicati/Library/Backend/OAuthHelper/OAuthHelper.cs b/Duplicati/Library/Backend/OAuthHelper/OAuthHelper.cs index 007376cc0..ca9212e7b 100644 --- a/Duplicati/Library/Backend/OAuthHelper/OAuthHelper.cs +++ b/Duplicati/Library/Backend/OAuthHelper/OAuthHelper.cs @@ -83,6 +83,16 @@ namespace Duplicati.Library /// Set to true if the provider does not use refresh tokens, but only access tokens /// public bool AccessTokenOnly { get; set; } + /// + /// If true (the default), when a v1 authid is being used it will be swapped + /// with a v2 authid, when the OAuth service returns one (which it dypically + /// does after a provider token refresh has been performed). Some providers + /// are not compatible with v2 authid, tyically because they generate a new + /// refresh token with every access token refresh and invalidates the old. + /// If the oauth service still returns a v2 authid for such a provider, + /// set this property to false to make Duplicati ignore it. + /// + public bool AutoV2 { get; set; } = true; public OAuthHelper(string authid, string servicename, string useragent = null) : base(useragent) @@ -134,7 +144,7 @@ namespace Duplicati.Library var res = GetTokenResponse(); m_tokenExpires = DateTime.UtcNow.AddSeconds(res.expires - 30); - if (!string.IsNullOrWhiteSpace(res.v2_authid)) + if (AutoV2 && !string.IsNullOrWhiteSpace(res.v2_authid)) m_authid = res.v2_authid; return m_token = res.access_token; } diff --git a/Duplicati/Library/Backend/OpenStack/OpenStackStorage.cs b/Duplicati/Library/Backend/OpenStack/OpenStackStorage.cs index 935645c3b..f4afda942 100644 --- a/Duplicati/Library/Backend/OpenStack/OpenStackStorage.cs +++ b/Duplicati/Library/Backend/OpenStack/OpenStackStorage.cs @@ -68,6 +68,10 @@ namespace Duplicati.Library.Backend.OpenStack new KeyValuePair("OVH Cloud Storage", "https://auth.cloud.ovh.net/v3"), new KeyValuePair("Selectel Cloud Storage", "https://auth.selcdn.ru"), new KeyValuePair("Memset Cloud Storage", "https://auth.storage.memset.com"), + new KeyValuePair("Infomaniak Swiss Backup cluster 1", "https://swiss-backup.infomaniak.com/identity/v3"), + new KeyValuePair("Infomaniak Swiss Backup cluster 2", "https://swiss-backup02.infomaniak.com/identity/v3"), + new KeyValuePair("Infomaniak Swiss Backup cluster 3", "https://swiss-backup03.infomaniak.com/identity/v3"), + new KeyValuePair("Infomaniak Public Cloud 1", "https://api.pub1.infomaniak.cloud/identity/v3"), }; public static readonly KeyValuePair[] OPENSTACK_VERSIONS = { diff --git a/Duplicati/Library/Backend/S3/S3Backend.cs b/Duplicati/Library/Backend/S3/S3Backend.cs index 56b4da5e6..c1741b84c 100644 --- a/Duplicati/Library/Backend/S3/S3Backend.cs +++ b/Duplicati/Library/Backend/S3/S3Backend.cs @@ -56,6 +56,10 @@ namespace Duplicati.Library.Backend { "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" }, + { "Infomaniak Swiss Backup cluster 1", "s3.swiss-backup.infomaniak.com" }, + { "Infomaniak Swiss Backup cluster 2", "s3.swiss-backup02.infomaniak.com" }, + { "Infomaniak Swiss Backup cluster 3", "s3.swiss-backup03.infomaniak.com" }, + { "Infomaniak Public Cloud 1", "s3.pub1.infomaniak.cloud" }, }; //Updated list: http://docs.amazonwebservices.com/general/latest/gr/rande.html#s3_region diff --git a/Duplicati/Library/Common/IO/SystemIOWindows.cs b/Duplicati/Library/Common/IO/SystemIOWindows.cs index bd0ee5fed..dd228835e 100644 --- a/Duplicati/Library/Common/IO/SystemIOWindows.cs +++ b/Duplicati/Library/Common/IO/SystemIOWindows.cs @@ -508,7 +508,19 @@ namespace Duplicati.Library.Common.IO public IFileEntry FileEntry(string path) { var fileInfo = new FileInfo(AddExtendedDevicePathPrefix(path)); - return new FileEntry(fileInfo.Name, fileInfo.Length, fileInfo.LastAccessTime, fileInfo.LastWriteTime); + var lastAccess = new DateTime(); + try + { + // Internally this will convert the FILETIME value from Windows API to a + // DateTime. If the value represents a date after 12/31/9999 it will throw + // ArgumentOutOfRangeException, because this is not supported by DateTime. + // Some file systems seem to set strange access timestamps on files, which + // may lead to this exception being thrown. Since the last accessed + // timestamp is not important such exeptions are just silently ignored. + lastAccess = fileInfo.LastAccessTime; + } + catch { } + return new FileEntry(fileInfo.Name, fileInfo.Length, lastAccess, fileInfo.LastWriteTime); } public Dictionary GetMetadata(string path, bool isSymlink, bool followSymlink) diff --git a/Duplicati/Library/Main/AsyncDownloader.cs b/Duplicati/Library/Main/AsyncDownloader.cs index 52900326e..ea72f51cf 100644 --- a/Duplicati/Library/Main/AsyncDownloader.cs +++ b/Duplicati/Library/Main/AsyncDownloader.cs @@ -13,6 +13,8 @@ namespace Duplicati.Library.Main internal class AsyncDownloader : IEnumerable { + private static readonly string LOGTAG = Logging.Log.LogTagFromType(); + private class AsyncDownloaderEnumerator : IEnumerator { private class AsyncDownloadedFile : IAsyncDownloadedFile @@ -110,6 +112,7 @@ namespace Duplicati.Library.Main } catch (Exception ex) { + Logging.Log.WriteErrorMessage(LOGTAG, "FailedToRetrieveFile", ex, "Failed to retrieve file {0}", m_volumes[m_index].Name); exception = ex; } diff --git a/Duplicati/Library/Main/Database/Database schema/12. Performance Indexes.sql b/Duplicati/Library/Main/Database/Database schema/12. Performance Indexes.sql new file mode 100644 index 000000000..9301b637f --- /dev/null +++ b/Duplicati/Library/Main/Database/Database schema/12. Performance Indexes.sql @@ -0,0 +1,12 @@ +CREATE INDEX "nnc_Metadataset" ON Metadataset ("ID","BlocksetID"); +CREATE INDEX "nn_FilesetentryFile" on FilesetEntry ("FilesetID","FileID"); + +-- Line 602 & 603 LocalBackupDatabase.cs +-- CREATE INDEX "tmpName1" ON "{0}" ("Path"),tmpName1 +-- CREATE INDEX "tmpName2" ON "{0}" ("Path"),tmpName2 + +CREATE INDEX "nn_FileLookup_BlockMeta" ON FileLookup ("BlocksetID", "MetadataID"); + +CREATE INDEX "nnc_BlocksetEntry" ON "BlocksetEntry" ("Index", "BlocksetID", "BlockID"); + +UPDATE "Version" SET "Version" = 12; diff --git a/Duplicati/Library/Main/Database/Database schema/Schema.sql b/Duplicati/Library/Main/Database/Database schema/Schema.sql index 95e4529d3..2ee13404b 100644 --- a/Duplicati/Library/Main/Database/Database schema/Schema.sql +++ b/Duplicati/Library/Main/Database/Database schema/Schema.sql @@ -81,6 +81,8 @@ CREATE TABLE "FilesetEntry" ( /* Improved reverse lookup for joining Fileset and File table */ CREATE INDEX "FilesetentryFileIdIndex" on "FilesetEntry" ("FileID"); +CREATE INDEX "nn_FilesetentryFile" on FilesetEntry ("FilesetID","FileID"); + /* @@ -109,6 +111,9 @@ CREATE TABLE "FileLookup" ( /* Fast path based lookup, single properties are auto-indexed */ CREATE UNIQUE INDEX "FileLookupPath" ON "FileLookup" ("PrefixID", "Path", "BlocksetID", "MetadataID"); +CREATE INDEX "nn_FileLookup_BlockMeta" ON FileLookup ("BlocksetID", "MetadataID"); + + /* The File view contains an ID @@ -167,7 +172,7 @@ CREATE TABLE "BlocksetEntry" ( /* As this table is a cross table we need fast lookup */ CREATE INDEX "BlocksetEntry_IndexIdsBackwards" ON "BlocksetEntry" ("BlockID"); - +CREATE INDEX "nnc_BlocksetEntry" ON "BlocksetEntry" ("Index", "BlocksetID", "BlockID"); /* The individual block hashes, @@ -222,6 +227,8 @@ CREATE TABLE "Metadataset" ( ); CREATE INDEX "MetadatasetBlocksetID" ON "Metadataset" ("BlocksetID"); +CREATE INDEX "nnc_Metadataset" ON Metadataset ("ID","BlocksetID"); + /* Operations performed on the backend, @@ -280,4 +287,4 @@ CREATE TABLE "ChangeJournalData" ( "ConfigHash" TEXT NOT NULL ); -INSERT INTO "Version" ("Version") VALUES (11); +INSERT INTO "Version" ("Version") VALUES (12); diff --git a/Duplicati/Library/Main/Database/LocalBackupDatabase.cs b/Duplicati/Library/Main/Database/LocalBackupDatabase.cs index 10b8f737e..3f812d3ad 100644 --- a/Duplicati/Library/Main/Database/LocalBackupDatabase.cs +++ b/Duplicati/Library/Main/Database/LocalBackupDatabase.cs @@ -601,6 +601,9 @@ namespace Duplicati.Library.Main.Database cmd.ExecuteNonQuery(string.Format(@"CREATE TEMPORARY TABLE ""{0}"" AS " + subqueryFiles, tmpName1), lastFilesetId); cmd.ExecuteNonQuery(string.Format(@"CREATE TEMPORARY TABLE ""{0}"" AS " + subqueryFiles, tmpName2), m_filesetId); + cmd.ExecuteNonQuery(string.Format(@"CREATE INDEX ""nn_tmpName1"" ON ""{0}"" (""Path"")",tmpName1)); + cmd.ExecuteNonQuery(string.Format(@"CREATE INDEX ""nn_tmpName2"" ON ""{0}"" (""Path"")",tmpName2)); + results.AddedFiles = cmd.ExecuteScalarInt64(string.Format(@"SELECT COUNT(*) FROM ""File"" INNER JOIN ""FilesetEntry"" ON ""File"".""ID"" = ""FilesetEntry"".""FileID"" WHERE ""FilesetEntry"".""FilesetID"" = ? AND ""File"".""BlocksetID"" != ? AND ""File"".""BlocksetID"" != ? AND NOT ""File"".""Path"" IN (SELECT ""Path"" FROM ""{0}"")", tmpName1), 0, m_filesetId, FOLDER_BLOCKSET_ID, SYMLINK_BLOCKSET_ID); results.DeletedFiles = cmd.ExecuteScalarInt64(string.Format(@"SELECT COUNT(*) FROM ""{0}"" WHERE ""{0}"".""Path"" NOT IN (SELECT ""Path"" FROM ""File"" INNER JOIN ""FilesetEntry"" ON ""File"".""ID"" = ""FilesetEntry"".""FileID"" WHERE ""FilesetEntry"".""FilesetID"" = ?)", tmpName1), 0, m_filesetId); @@ -691,9 +694,9 @@ 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; } @@ -704,7 +707,7 @@ namespace Duplicati.Library.Main.Database long lastFilesetId = prevFileSetId < 0 ? GetPreviousFilesetID(cmd, timestamp, fileSetId) : prevFileSetId; // 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()); + 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); @@ -736,10 +739,10 @@ namespace Duplicati.Library.Main.Database cmd.ExecuteNonQuery($@"INSERT INTO ""FilesetEntry"" (""FilesetID"", ""FileID"", ""Lastmodified"") SELECT ?, ""FileID"", ""Lastmodified"" FROM ""{tempFileSetTable}""", fileSetId); - tr.Commit(); + tr.Commit(); } - } - + } + /// /// Creates a timestamped backup operation to correctly associate the fileset with the time it was created. /// @@ -782,23 +785,23 @@ namespace Duplicati.Library.Main.Database ); else return default(RemoteVolumeEntry); - } - - public IEnumerable GetTemporaryFilelistVolumeNames(bool latestOnly, IDbTransaction transaction = null) - { - var incompleteFilesetIDs = GetIncompleteFilesets(transaction).OrderBy(x => x.Value).Select(x => x.Key).ToArray(); - - if (!incompleteFilesetIDs.Any()) - return Enumerable.Empty(); - - if (latestOnly) - incompleteFilesetIDs = new long[] { incompleteFilesetIDs.Last() }; - - var volumeNames = new List(); - foreach (var filesetID in incompleteFilesetIDs) - volumeNames.Add(GetRemoteVolumeFromFilesetID(filesetID).Name); - - return volumeNames; + } + + public IEnumerable GetTemporaryFilelistVolumeNames(bool latestOnly, IDbTransaction transaction = null) + { + var incompleteFilesetIDs = GetIncompleteFilesets(transaction).OrderBy(x => x.Value).Select(x => x.Key).ToArray(); + + if (!incompleteFilesetIDs.Any()) + return Enumerable.Empty(); + + if (latestOnly) + incompleteFilesetIDs = new long[] { incompleteFilesetIDs.Last() }; + + var volumeNames = new List(); + foreach (var filesetID in incompleteFilesetIDs) + volumeNames.Add(GetRemoteVolumeFromFilesetID(filesetID).Name); + + return volumeNames; } public IEnumerable GetMissingIndexFiles(System.Data.IDbTransaction transaction) diff --git a/Duplicati/Library/Main/DatabaseLocator.cs b/Duplicati/Library/Main/DatabaseLocator.cs index b014cda91..c9658d476 100644 --- a/Duplicati/Library/Main/DatabaseLocator.cs +++ b/Duplicati/Library/Main/DatabaseLocator.cs @@ -73,9 +73,6 @@ namespace Duplicati.Library.Main folder = newlocation; } - if (!System.IO.Directory.Exists(folder)) - System.IO.Directory.CreateDirectory(folder); - var file = System.IO.Path.Combine(folder, "dbconfig.json"); List configs; if (!System.IO.File.Exists(file)) @@ -179,7 +176,10 @@ namespace Duplicati.Library.Main Databasepath = newpath, ParameterFile = null }); - + + if (!System.IO.Directory.Exists(folder)) + System.IO.Directory.CreateDirectory(folder); + var settings = new Newtonsoft.Json.JsonSerializerSettings(); settings.Formatting = Newtonsoft.Json.Formatting.Indented; System.IO.File.WriteAllText(file, Newtonsoft.Json.JsonConvert.SerializeObject(configs, settings), System.Text.Encoding.UTF8); diff --git a/Duplicati/Library/Main/Duplicati.Library.Main.csproj b/Duplicati/Library/Main/Duplicati.Library.Main.csproj index d6b2338b2..54047f3f1 100644 --- a/Duplicati/Library/Main/Duplicati.Library.Main.csproj +++ b/Duplicati/Library/Main/Duplicati.Library.Main.csproj @@ -46,6 +46,7 @@ + @@ -61,4 +62,4 @@ - \ No newline at end of file + diff --git a/Duplicati/Library/Main/Options.cs b/Duplicati/Library/Main/Options.cs index 4387d7549..3aeab6a5a 100644 --- a/Duplicati/Library/Main/Options.cs +++ b/Duplicati/Library/Main/Options.cs @@ -1646,7 +1646,7 @@ namespace Duplicati.Library.Main /// public bool PatchWithLocalBlocks { - get { return m_options.ContainsKey("patch-with-local-blocks"); } + get { return Library.Utility.Utility.ParseBoolOption(m_options, "patch-with-local-blocks"); } } /// diff --git a/Duplicati/Library/Main/default_compressed_extensions.txt b/Duplicati/Library/Main/default_compressed_extensions.txt index 54afe8a73..b41a7f3c5 100644 --- a/Duplicati/Library/Main/default_compressed_extensions.txt +++ b/Duplicati/Library/Main/default_compressed_extensions.txt @@ -171,8 +171,12 @@ # Other Compressed or High-Entropy Files .apk #Android Application Package .eftx #Microsoft Office Theme Effects +.mrimg #Macrium Reflect disk image format +.rvz #Dolphin compression storage format .sdg #OpenOffice.org Configuration File .thmx #Microsoft Office Theme +.tib #Acronis True Image disk image format +.tibx #Acronis True Image disk image format .vsix #Microsoft Visual Studio Extension .vsv #Microsoft Virtual Machine Saved State .wmz #Windows Media Player Skin Package diff --git a/Duplicati/Library/Snapshots/HyperVUtility.cs b/Duplicati/Library/Snapshots/HyperVUtility.cs index 338706c85..423b24911 100644 --- a/Duplicati/Library/Snapshots/HyperVUtility.cs +++ b/Duplicati/Library/Snapshots/HyperVUtility.cs @@ -302,7 +302,16 @@ namespace Duplicati.Library.Snapshots select ((string[])systemBaseObj["Connection"])[0]).ToList(); foreach (var vhd in tempvhd) - result.Add(vhd); + { + if (File.Exists(vhd)) + { + result.Add(vhd); + } + else + { + Logging.Log.WriteWarningMessage(LOGTAG, "HyperVInvalidVhd", null, "Invalid VHD file detected, file does not exist: {0}", vhd); + } + } } } @@ -319,11 +328,16 @@ namespace Duplicati.Library.Snapshots if (outParams != null) { var doc = new System.Xml.XmlDocument(); - doc.LoadXml((string)outParams[_wmiv2Namespace ? "SettingData" : "Info"]); - var node = doc.SelectSingleNode("//PROPERTY[@NAME = 'ParentPath']/VALUE/child::text()"); + var propertyValue = (string)outParams[_wmiv2Namespace ? "SettingData" : "Info"]; - if (node != null && File.Exists(node.Value)) - ParentPaths.Add(node.Value); + if (propertyValue != null) + { + doc.LoadXml(propertyValue); + var node = doc.SelectSingleNode("//PROPERTY[@NAME = 'ParentPath']/VALUE/child::text()"); + + if (node != null && File.Exists(node.Value)) + ParentPaths.Add(node.Value); + } } } } diff --git a/Duplicati/Library/Utility/Duplicati.Library.Utility.csproj b/Duplicati/Library/Utility/Duplicati.Library.Utility.csproj index ce326ad8f..d6d6203b8 100644 --- a/Duplicati/Library/Utility/Duplicati.Library.Utility.csproj +++ b/Duplicati/Library/Utility/Duplicati.Library.Utility.csproj @@ -24,4 +24,4 @@ - \ No newline at end of file + diff --git a/Duplicati/Library/Utility/KeyGenerator.cs b/Duplicati/Library/Utility/KeyGenerator.cs deleted file mode 100644 index 7a42715f3..000000000 --- a/Duplicati/Library/Utility/KeyGenerator.cs +++ /dev/null @@ -1,92 +0,0 @@ -#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; -using System.Collections.Generic; -using System.Text; - -namespace Duplicati.Library.Utility -{ - public static class KeyGenerator - { - public static readonly char[] ALPHA_LOWER = new char[] { 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z' }; - public static readonly char[] ALPHA_UPPER = new char[] { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'K', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z' }; - public static readonly char[] DIGITS = new char[] { '0', '2', '3', '4', '5', '6', '7', '8', '9' }; - public static readonly char[] PUNCTUATION = new char[] { '.', ',', ':', ';', '!', '?' }; - public static readonly char[] MISC = new char[] { '*', '@', '#', '$', '%', '&', '/', '(', ')', '=', '+', '>', '<', '-', '_' }; - - public static readonly char[] HEX_CHARS = new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; - - private static char[] _allowed = null; - private static Random _rnd = null; - - public static char[] AllowedChars - { - get - { - if (_allowed == null) - { - List l = new List(); - l.AddRange(ALPHA_LOWER); - l.AddRange(ALPHA_UPPER); - l.AddRange(DIGITS); - l.AddRange(PUNCTUATION); - l.AddRange(MISC); - _allowed = l.ToArray(); - } - - return _allowed; - } - } - - public static Random Rnd - { - get - { - if (_rnd == null) - _rnd = new Random(); - return _rnd; - } - } - - public static string GenerateKey(int minChars, int maxChars) - { - char[] tmp = new char[Rnd.Next(minChars, maxChars + 1)]; - for (int i = 0; i < tmp.Length; i++) - tmp[i] = AllowedChars[Rnd.Next(0, AllowedChars.Length)]; - - return new string(tmp); - } - - public static string GenerateSignKey() - { - byte[] tmp = new byte[4]; - Rnd.NextBytes(tmp); - - char[] res = new char[8]; - for (int i = 0; i < tmp.Length; i++) - { - res[i * 2] = HEX_CHARS[tmp[i] & 0xF]; - res[i * 2 + 1] = HEX_CHARS[(tmp[i] >> 4) & 0xF]; - } - - return new string(res); - } - } -} diff --git a/Duplicati/Library/Utility/Utility.cs b/Duplicati/Library/Utility/Utility.cs index e1c30f51d..3b31f4d3f 100644 --- a/Duplicati/Library/Utility/Utility.cs +++ b/Duplicati/Library/Utility/Utility.cs @@ -135,10 +135,49 @@ namespace Duplicati.Library.Utility await target.WriteAsync(buf, 0, read, cancelToken).ConfigureAwait(false); total += read; } - + return total; } + /// + /// Get the length of a stream. + /// Attempt to use the stream's Position property if allowPositionFallback is true (only valid if stream is at the end). + /// + /// Stream to get the length of. + /// Attempt to use the Position property if true and the Length property is not available (only valid if stream is at the end). + /// Returns the stream's length, if available, or null if not supported by the stream. + public static long? GetStreamLength(Stream stream, bool allowPositionFallback = true) + { + return GetStreamLength(stream, out bool _, allowPositionFallback); + } + + /// + /// Get the length of a stream. + /// Attempt to use the stream's Position property if allowPositionFallback is true (only valid if stream is at the end). + /// + /// Stream to get the length of. + /// Indicates if the Position value was used instead of Length. + /// Attempt to use the Position property if true and the Length property is not available (only valid if stream is at the end). + /// Returns the stream's length, if available, or null if not supported by the stream. + public static long? GetStreamLength(Stream stream, out bool isStreamPosition, bool allowPositionFallback = true) + { + isStreamPosition = false; + long? streamLength = null; + try { streamLength = stream.Length; } catch { } + if (!streamLength.HasValue && allowPositionFallback) + { + try + { + // Hack: This is a fall-back method to detect the source stream size, assuming the current position is the end of the stream. + streamLength = stream.Position; + isStreamPosition = true; + } + catch { } // + } + + return streamLength; + } + /// /// These are characters that must be escaped when using a globbing expression /// diff --git a/Duplicati/Server/Duplicati.Server.csproj b/Duplicati/Server/Duplicati.Server.csproj index 2248fb935..90ef9c2d4 100644 --- a/Duplicati/Server/Duplicati.Server.csproj +++ b/Duplicati/Server/Duplicati.Server.csproj @@ -35,6 +35,8 @@ + + @@ -50,7 +52,6 @@ - diff --git a/Duplicati/Server/WebServer/RESTMethods/ServerSettings.cs b/Duplicati/Server/WebServer/RESTMethods/ServerSettings.cs index 1df9d13cf..cb09037d8 100644 --- a/Duplicati/Server/WebServer/RESTMethods/ServerSettings.cs +++ b/Duplicati/Server/WebServer/RESTMethods/ServerSettings.cs @@ -75,10 +75,10 @@ namespace Duplicati.Server.WebServer.RESTMethods var serversettings = data.Where(x => !string.IsNullOrWhiteSpace(x.Key)).ToDictionary(x => x.Key, x => x.Key.StartsWith("--", StringComparison.Ordinal) ? null : x.Value); var globalsettings = data.Where(x => !string.IsNullOrWhiteSpace(x.Key) && x.Key.StartsWith("--", StringComparison.Ordinal)); - serversettings.Remove("server-ssl-certificate"); - serversettings.Remove("ServerSSLCertificate"); + serversettings.Remove("server-ssl-certificate"); + serversettings.Remove("ServerSSLCertificate"); - if (serversettings.Any()) + if (serversettings.Any()) Program.DataConnection.ApplicationSettings.UpdateSettings(serversettings, false); if (globalsettings.Any()) diff --git a/Duplicati/Server/WebServer/Server.cs b/Duplicati/Server/WebServer/Server.cs index 0ab45ba54..e9d5396e5 100644 --- a/Duplicati/Server/WebServer/Server.cs +++ b/Duplicati/Server/WebServer/Server.cs @@ -158,11 +158,23 @@ namespace Duplicati.Server.WebServer // so we create a new server for each attempt var server = CreateServer(options); - + if (!certValid) server.Start(listenInterface, p); else - server.Start(listenInterface, p, cert, System.Security.Authentication.SslProtocols.Tls11 | System.Security.Authentication.SslProtocols.Tls12, null, false); + { + var secProtocols = System.Security.Authentication.SslProtocols.Tls12; + + try + { + //try TLS 1.3 (type not available on .NET < 4.8) + secProtocols = System.Security.Authentication.SslProtocols.Tls12 | (System.Security.Authentication.SslProtocols)12288; + } + catch (NotSupportedException) + { + } + server.Start(listenInterface, p, cert, secProtocols, null, false); + } m_server = server; m_server.ServerName = string.Format("{0} v{1}", Library.AutoUpdater.AutoUpdateSettings.AppName, System.Reflection.Assembly.GetExecutingAssembly().GetName().Version); diff --git a/Duplicati/Server/webroot/ngax/index.html b/Duplicati/Server/webroot/ngax/index.html index 040957e69..42c588c28 100755 --- a/Duplicati/Server/webroot/ngax/index.html +++ b/Duplicati/Server/webroot/ngax/index.html @@ -283,9 +283,9 @@
-
+
-