From 2828729a594178badf22cab01b661e9b82ab55cb Mon Sep 17 00:00:00 2001 From: Kenneth Skovhede Date: Tue, 19 Nov 2024 06:52:17 +0100 Subject: [PATCH 1/4] Changed logic for how error messages are constructed --- Duplicati/Library/Backend/FTP/FTPBackend.cs | 30 ++++++++++----------- Duplicati/Library/Backend/FTP/Strings.cs | 7 +++-- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/Duplicati/Library/Backend/FTP/FTPBackend.cs b/Duplicati/Library/Backend/FTP/FTPBackend.cs index c4bfb03da..fbdf1c63a 100644 --- a/Duplicati/Library/Backend/FTP/FTPBackend.cs +++ b/Duplicati/Library/Backend/FTP/FTPBackend.cs @@ -41,7 +41,7 @@ using Uri = System.Uri; namespace Duplicati.Library.Backend { - + /// /// The unified FTP backend which uses the FluentFTP library. /// @@ -68,7 +68,7 @@ namespace Duplicati.Library.Backend protected virtual string CONFIG_KEY_FTP_UPLOAD_DELAY => "ftp-upload-delay"; protected virtual string CONFIG_KEY_FTP_LOGTOCONSOLE => "ftp-log-to-console"; protected virtual string CONFIG_KEY_FTP_LOGPRIVATEINFOTOCONSOLE => "ftp-log-privateinfo-to-console"; - + // The following keys are private because they are irrelevant for inheritors and are here for backwards compatibility private static string CONFIG_KEY_FTP_LEGACY_FTPPASSIVE => "ftp-passive"; private static string CONFIG_KEY_FTP_LEGACY_FTPREGULAR => "ftp-regular"; @@ -196,7 +196,7 @@ namespace Duplicati.Library.Backend if (encryptionModeString == null || !Enum.TryParse(encryptionModeString, true, out encryptionMode)) encryptionMode = DEFAULT_ENCRYPTION_MODE; - + // Process the aftp-ssl-protocols option SslProtocols sslProtocols; if (!options.TryGetValue(CONFIG_KEY_FTP_SSL_PROTOCOLS, out var sslProtocolsString) || string.IsNullOrWhiteSpace(sslProtocolsString)) @@ -208,15 +208,15 @@ namespace Duplicati.Library.Backend if (ProtocolKey == "ftp") { // To mirror the behavior of existing backups, we need to check the legacy options - + // This flag takes precedence over ftp-data-connection-type - if (CoreUtility.ParseBoolOption(options, CONFIG_KEY_FTP_LEGACY_FTPPASSIVE)) + if (CoreUtility.ParseBoolOption(options, CONFIG_KEY_FTP_LEGACY_FTPPASSIVE)) dataConnectionType = FtpDataConnectionType.AutoPassive; - + // This flag takes precedence over the ftp-passive flag - if (CoreUtility.ParseBoolOption(options, CONFIG_KEY_FTP_LEGACY_FTPREGULAR)) + if (CoreUtility.ParseBoolOption(options, CONFIG_KEY_FTP_LEGACY_FTPREGULAR)) dataConnectionType = FtpDataConnectionType.AutoActive; - + // When using legacy useSSL option, the encryption is set to automatic and the SSL protocols are set to none // (None meaning the OS will choose the appropriate protocol) if (CoreUtility.ParseBoolOption(options, CONFIG_KEY_FTP_LEGACY_USESSL)) @@ -225,10 +225,10 @@ namespace Duplicati.Library.Backend encryptionMode = FtpEncryptionMode.Auto; } } - + _logToConsole = CoreUtility.ParseBoolOption(options, CONFIG_KEY_FTP_LOGTOCONSOLE); _logPrivateInfoToConsole = CoreUtility.ParseBoolOption(options, CONFIG_KEY_FTP_LOGPRIVATEINFOTOCONSOLE); - + _ftpConfig = new FtpConfig { DataConnectionType = dataConnectionType, @@ -366,7 +366,7 @@ namespace Duplicati.Library.Backend var status = await ftpClient.UploadStream(input, remotePath, createRemoteDir: false, token: cancelToken, progress: null).ConfigureAwait(false); if (status != FtpStatus.Success) { - throw new UserInformationException(string.Format(Strings.ErrorWriteFile, remotename), "AftpPutFailure"); + throw new UserInformationException(Strings.ErrorWriteFile(remotename, $"Status is {status}"), "AftpPutFailure"); } // Wait for the upload, if required @@ -463,7 +463,7 @@ namespace Duplicati.Library.Backend catch (Exception e) { if (e.InnerException != null) { e = e.InnerException; } - throw new Exception(string.Format(Strings.ErrorDeleteFile, e.Message), e); + throw new UserInformationException(Strings.ErrorDeleteFile(TEST_FILE_NAME, e.Message), "TestPreparationError"); } } @@ -477,7 +477,7 @@ namespace Duplicati.Library.Backend catch (Exception e) { if (e.InnerException != null) { e = e.InnerException; } - throw new Exception(string.Format(Strings.ErrorWriteFile, e.Message), e); + throw new UserInformationException(Strings.ErrorWriteFile(TEST_FILE_NAME, e.Message), "TestWriteError"); } } @@ -494,7 +494,7 @@ namespace Duplicati.Library.Backend catch (Exception e) { if (e.InnerException != null) { e = e.InnerException; } - throw new Exception(string.Format(Strings.ErrorReadFile, e.Message), e); + throw new UserInformationException(Strings.ErrorReadFile(TEST_FILE_NAME, e.Message), "TestReadError"); } } @@ -506,7 +506,7 @@ namespace Duplicati.Library.Backend catch (Exception e) { if (e.InnerException != null) { e = e.InnerException; } - throw new Exception(string.Format(Strings.ErrorDeleteFile, e.Message), e); + throw new UserInformationException(Strings.ErrorDeleteFile(TEST_FILE_NAME, e.Message), "TestCleanupError"); } } diff --git a/Duplicati/Library/Backend/FTP/Strings.cs b/Duplicati/Library/Backend/FTP/Strings.cs index 95b36b82a..46ef6f8d2 100644 --- a/Duplicati/Library/Backend/FTP/Strings.cs +++ b/Duplicati/Library/Backend/FTP/Strings.cs @@ -52,9 +52,9 @@ namespace Duplicati.Library.Backend public static string DescriptionSslProtocolsShort { get { return LC.L(@"Configure the SSL policy to use when encryption is enabled"); } } public static string DescriptionUploadDelayLong { get { return LC.L(@"Some FTP servers need a small delay before reporting the correct file size. The required delay depends on network topology. If you experience errors related to the upload size not matching, try adding a few seconds delay."); } } public static string DescriptionUploadDelayShort { get { return LC.L(@"Add a delay after uploading a file"); } } - public static string ErrorDeleteFile { get { return LC.L(@"Error on deleting file: {0}"); } } - public static string ErrorReadFile { get { return LC.L(@"Error reading file: {0}"); } } - public static string ErrorWriteFile { get { return LC.L(@"Error writing file: {0}"); } } + public static string ErrorDeleteFile(string filename, string message) { return LC.L(@"Error on deleting file: {0}, error: {1}", filename, message); } + public static string ErrorReadFile(string filename, string message) { return LC.L(@"Error reading file: {0}, error: {1}", filename, message); } + public static string ErrorWriteFile(string filename, string message) { return LC.L(@"Error writing file: {0}, error: {1}", filename, message); } public static string DescriptionUseSSLLong { get { return LC.L(@"Use this option to communicate using Secure Socket Layer (SSL) over ftp (ftps)."); } } public static string DescriptionUseSSLShort { get { return LC.L(@"Instruct Duplicati to use an SSL (ftps) connection"); } } public static string DescriptionFTPActiveLong { get { return LC.L(@"Activate this option to make the FTP connection in active mode. Even if the option --{0} is also set, the connection will be made in active mode.", "ftp-passive"); } } @@ -64,6 +64,5 @@ namespace Duplicati.Library.Backend public static string FtpPassiveDeprecated { get { return LC.L(@"The option ftp-passive is deprecated, use ftp-data-connection-type instead."); } } public static string FtpActiveDeprecated { get { return LC.L(@"ftp-regular is deprecated, use ftp-data-connection-type instead."); } } public static string UseSslDeprecated { get { return LC.L(@"use-ssl is deprecated, use ftp-ssl-protocols instead."); } } - } } From 5712264e6a4a919bd9590deee0dab2d8db92858a Mon Sep 17 00:00:00 2001 From: Kenneth Skovhede Date: Tue, 19 Nov 2024 13:55:23 +0100 Subject: [PATCH 2/4] Updated some parsing to be simpler. Changed the strategy to use absolute paths for all operations, to avoid cases where a silent reconnect would end up in a different folder. Add backwards compatible option to use CWD style relative operations. Added support for detecting the SSL certificate hash when there is a certificate error. --- .../Backend/FTP/AlternateFTPBackend.cs | 16 +- Duplicati/Library/Backend/FTP/FTPBackend.cs | 654 +++++++++++------- Duplicati/Library/Backend/FTP/Strings.cs | 6 + 3 files changed, 422 insertions(+), 254 deletions(-) diff --git a/Duplicati/Library/Backend/FTP/AlternateFTPBackend.cs b/Duplicati/Library/Backend/FTP/AlternateFTPBackend.cs index a021dcf4f..9cd072d4f 100644 --- a/Duplicati/Library/Backend/FTP/AlternateFTPBackend.cs +++ b/Duplicati/Library/Backend/FTP/AlternateFTPBackend.cs @@ -13,7 +13,7 @@ namespace Duplicati.Library.Backend; /// /// The functionality is identical to the FTP backend /// -public class AlternateFTPBackend: FTP +public class AlternateFTPBackend : FTP { /* These constants are overriden to provide transparent access to the alternate configuration keys @@ -24,9 +24,9 @@ public class AlternateFTPBackend: FTP protected override string CONFIG_KEY_FTP_UPLOAD_DELAY => "aftp-upload-delay"; protected override string CONFIG_KEY_FTP_LOGTOCONSOLE => "aftp-log-to-console"; protected override string CONFIG_KEY_FTP_LOGPRIVATEINFOTOCONSOLE => "aftp-log-privateinfo-to-console"; - + public override string Description => Strings.DescriptionAlternate; - + public override string DisplayName => Strings.DisplayNameAlternate; /* @@ -36,12 +36,12 @@ public class AlternateFTPBackend: FTP public AlternateFTPBackend() { - + } - public AlternateFTPBackend(string url, Dictionary options): base(url, options) + public AlternateFTPBackend(string url, Dictionary options) : base(url, options) { } - + /// /// Overriden so that for the alternate FTP backend, we need to support the alternate config keys starting with "aftp" /// @@ -49,7 +49,9 @@ public class AlternateFTPBackend: FTP new List([ new CommandLineArgument("auth-password", CommandLineArgument.ArgumentType.Password, Strings.DescriptionAuthPasswordShort, Strings.DescriptionAuthPasswordLong), new CommandLineArgument("auth-username", CommandLineArgument.ArgumentType.String, Strings.DescriptionAuthUsernameShort, Strings.DescriptionAuthUsernameLong), - new CommandLineArgument("disable-upload-verify", CommandLineArgument.ArgumentType.Boolean, Strings.DescriptionDisableUploadVerifyShort, Strings.DescriptionDisableUploadVerifyLong), + new CommandLineArgument(CONFIG_KEY_DISABLE_UPLOAD_VERIFY, CommandLineArgument.ArgumentType.Boolean, Strings.DescriptionDisableUploadVerifyShort, Strings.DescriptionDisableUploadVerifyLong), + new CommandLineArgument(CONFIG_KEY_FTP_RELATIVE_PATHS, CommandLineArgument.ArgumentType.Boolean, Strings.DescriptionRelativePathsShort, Strings.DescriptionRelativePathsLong), + new CommandLineArgument(CONFIG_KEY_FTP_USE_CWD_NAMES, CommandLineArgument.ArgumentType.Boolean, Strings.DescriptionUseCwdNamesShort, Strings.DescriptionUseCwdNamesLong), new CommandLineArgument(CONFIG_KEY_FTP_DATA_CONNECTION_TYPE, CommandLineArgument.ArgumentType.Enumeration, Strings.DescriptionFtpDataConnectionTypeShort, Strings.DescriptionFtpDataConnectionTypeLong, DEFAULT_DATA_CONNECTION_TYPE_STRING, null, Enum.GetNames(typeof(FtpDataConnectionType))), new CommandLineArgument(CONFIG_KEY_FTP_ENCRYPTION_MODE, CommandLineArgument.ArgumentType.Enumeration, Strings.DescriptionFtpEncryptionModeShort, Strings.DescriptionFtpEncryptionModeLong, DEFAULT_ENCRYPTION_MODE_STRING, null, Enum.GetNames(typeof(FtpEncryptionMode))), new CommandLineArgument(CONFIG_KEY_FTP_SSL_PROTOCOLS, CommandLineArgument.ArgumentType.Flags, Strings.DescriptionSslProtocolsShort, Strings.DescriptionSslProtocolsLong, DEFAULT_SSL_PROTOCOLS_STRING, null, Enum.GetNames(typeof(SslProtocols))), diff --git a/Duplicati/Library/Backend/FTP/FTPBackend.cs b/Duplicati/Library/Backend/FTP/FTPBackend.cs index fbdf1c63a..8bede87ec 100644 --- a/Duplicati/Library/Backend/FTP/FTPBackend.cs +++ b/Duplicati/Library/Backend/FTP/FTPBackend.cs @@ -19,6 +19,8 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. +#nullable enable + using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; @@ -54,42 +56,151 @@ namespace Duplicati.Library.Backend /// public class FTP : IStreamingBackend { - private NetworkCredential _userInfo; + /// + /// The credentials used to authenticate with the FTP server + /// + private readonly NetworkCredential? _userInfo; + /// + /// Th option used to accept a specific SSL certificate hash + /// private const string OPTION_ACCEPT_SPECIFIED_CERTIFICATE = "accept-specified-ssl-hash"; // Global option + /// + /// The option used to accept any SSL certificate + /// private const string OPTION_ACCEPT_ANY_CERTIFICATE = "accept-any-ssl-certificate"; // Global option + /// + /// The default data connection type + /// private const FtpDataConnectionType DEFAULT_DATA_CONNECTION_TYPE = FtpDataConnectionType.AutoPassive; + /// + /// The default encryption mode + /// private const FtpEncryptionMode DEFAULT_ENCRYPTION_MODE = FtpEncryptionMode.None; - private static readonly SslProtocols DEFAULT_SSL_PROTOCOLS = SslProtocols.None; // NOTE: None means "use system default" + /// + /// The default SSL protocols + /// + private static readonly SslProtocols DEFAULT_SSL_PROTOCOLS = SslProtocols.None; // NOTE: None means "use system default" + /// + /// The configuration key for the FTP encryption mode + /// protected virtual string CONFIG_KEY_FTP_ENCRYPTION_MODE => "ftp-encryption-mode"; + /// + /// The configuration key for the FTP data connection type + /// protected virtual string CONFIG_KEY_FTP_DATA_CONNECTION_TYPE => "ftp-data-connection-type"; + /// + /// The configuration key for the FTP SSL protocols + /// protected virtual string CONFIG_KEY_FTP_SSL_PROTOCOLS => "ftp-ssl-protocols"; + /// + /// The configuration key for the FTP upload delay + /// protected virtual string CONFIG_KEY_FTP_UPLOAD_DELAY => "ftp-upload-delay"; + /// + /// The configuration key for the FTP log to console + /// protected virtual string CONFIG_KEY_FTP_LOGTOCONSOLE => "ftp-log-to-console"; + /// + /// The configuration key for the FTP log private info to console + /// protected virtual string CONFIG_KEY_FTP_LOGPRIVATEINFOTOCONSOLE => "ftp-log-privateinfo-to-console"; + /// + /// The configuration key for the FTP relative paths option + /// + protected virtual string CONFIG_KEY_FTP_RELATIVE_PATHS => "ftp-relative-paths"; + /// + /// The configuration key for the FTP use CWD names option + /// + protected virtual string CONFIG_KEY_FTP_USE_CWD_NAMES => "ftp-use-cwd-names"; + + /// + /// The configuration key for the disable upload verify option + /// + protected virtual string CONFIG_KEY_DISABLE_UPLOAD_VERIFY => "disable-upload-verify"; + // The following keys are private because they are irrelevant for inheritors and are here for backwards compatibility + /// + /// The configuration key for the legacy FTP passive mode + /// private static string CONFIG_KEY_FTP_LEGACY_FTPPASSIVE => "ftp-passive"; + /// + /// The configuration key for the legacy FTP active mode + /// private static string CONFIG_KEY_FTP_LEGACY_FTPREGULAR => "ftp-regular"; + /// + /// The configuration key for the legacy use SSL option + /// private static string CONFIG_KEY_FTP_LEGACY_USESSL => "use-ssl"; + /// + /// The test file name used to test access permissions + /// private const string TEST_FILE_NAME = "duplicati-access-privileges-test.tmp"; + /// + /// The test file content used to test access permissions + /// private const string TEST_FILE_CONTENT = "This file is used by Duplicati to test access permissions and can be safely deleted."; + /// + /// The default data connection type as a string + /// protected static readonly string DEFAULT_DATA_CONNECTION_TYPE_STRING = DEFAULT_DATA_CONNECTION_TYPE.ToString(); + /// + /// The default encryption mode as a string + /// protected static readonly string DEFAULT_ENCRYPTION_MODE_STRING = DEFAULT_ENCRYPTION_MODE.ToString(); + /// + /// The default SSL protocols as a string + /// protected static readonly string DEFAULT_SSL_PROTOCOLS_STRING = DEFAULT_SSL_PROTOCOLS.ToString(); + /// + /// The default upload delay as a string + /// protected static readonly string DEFAULT_UPLOAD_DELAY_STRING = "0s"; - private readonly string _url; + /// + /// The URL of the FTP server + /// + private readonly Uri _url; + /// + /// The flag to indicate if the list verify option is enabled + /// private readonly bool _listVerify = true; + /// + /// The flag to indicate if relative paths are used + /// + private readonly bool _relativePaths = false; + /// + /// The flag to indicate if the CWD strategy is used + /// + private readonly bool _useCwdNames = false; + /// + /// The FTP configuration + /// private readonly FtpConfig _ftpConfig; + /// + /// The wait time after each upload before checking the file size + /// private readonly TimeSpan _uploadWaitTime; + /// + /// The flag to indicate if the dialog should be logged to the console + /// private readonly bool _logToConsole; + /// + /// The flag to indicate if private information should be logged to the console + /// private readonly bool _logPrivateInfoToConsole; + /// + /// The flag to indicate if all certificates should be accepted + /// private readonly bool _accepAllCertificates; + /// + /// The valid certificate hashes + /// private readonly string[] _validHashes; /// @@ -102,14 +213,24 @@ namespace Duplicati.Library.Backend /// public virtual string ProtocolKey => "ftp"; - private AsyncFtpClient Client - { get; set; } + /// + /// The client instance + /// + private AsyncFtpClient? _client; + /// + /// The server initial working directory + /// + private string? _initialCwd; + + /// public virtual IList SupportedCommands => new List([ new CommandLineArgument("auth-password", CommandLineArgument.ArgumentType.Password, Strings.DescriptionAuthPasswordShort, Strings.DescriptionAuthPasswordLong), new CommandLineArgument("auth-username", CommandLineArgument.ArgumentType.String, Strings.DescriptionAuthUsernameShort, Strings.DescriptionAuthUsernameLong), - new CommandLineArgument("disable-upload-verify", CommandLineArgument.ArgumentType.Boolean, Strings.DescriptionDisableUploadVerifyShort, Strings.DescriptionDisableUploadVerifyLong), + new CommandLineArgument(CONFIG_KEY_DISABLE_UPLOAD_VERIFY, CommandLineArgument.ArgumentType.Boolean, Strings.DescriptionDisableUploadVerifyShort, Strings.DescriptionDisableUploadVerifyLong), + new CommandLineArgument(CONFIG_KEY_FTP_RELATIVE_PATHS, CommandLineArgument.ArgumentType.Boolean, Strings.DescriptionRelativePathsShort, Strings.DescriptionRelativePathsLong), + new CommandLineArgument(CONFIG_KEY_FTP_USE_CWD_NAMES, CommandLineArgument.ArgumentType.Boolean, Strings.DescriptionUseCwdNamesShort, Strings.DescriptionUseCwdNamesLong), new CommandLineArgument(CONFIG_KEY_FTP_DATA_CONNECTION_TYPE, CommandLineArgument.ArgumentType.Enumeration, Strings.DescriptionFtpDataConnectionTypeShort, Strings.DescriptionFtpDataConnectionTypeLong, DEFAULT_DATA_CONNECTION_TYPE_STRING, null, Enum.GetNames(typeof(FtpDataConnectionType))), new CommandLineArgument(CONFIG_KEY_FTP_ENCRYPTION_MODE, CommandLineArgument.ArgumentType.Enumeration, Strings.DescriptionFtpEncryptionModeShort, Strings.DescriptionFtpEncryptionModeLong, DEFAULT_ENCRYPTION_MODE_STRING, null, Enum.GetNames(typeof(FtpEncryptionMode))), new CommandLineArgument(CONFIG_KEY_FTP_SSL_PROTOCOLS, CommandLineArgument.ArgumentType.Flags, Strings.DescriptionSslProtocolsShort, Strings.DescriptionSslProtocolsLong, DEFAULT_SSL_PROTOCOLS_STRING, null, Enum.GetNames(typeof(SslProtocols))), @@ -126,7 +247,11 @@ namespace Duplicati.Library.Backend /// public FTP() { - + // TODO: Remove this constructor once static properties are introduced on IBackend + _validHashes = null!; + _ftpConfig = null!; + _url = null!; + _client = null!; } /// @@ -139,10 +264,9 @@ namespace Duplicati.Library.Backend { _accepAllCertificates = CoreUtility.ParseBoolOption(options, OPTION_ACCEPT_ANY_CERTIFICATE); - string certHash; - options.TryGetValue(OPTION_ACCEPT_SPECIFIED_CERTIFICATE, out certHash); + options.TryGetValue(OPTION_ACCEPT_SPECIFIED_CERTIFICATE, out var certHash); - _validHashes = certHash?.Split([",", ";"], StringSplitOptions.RemoveEmptyEntries); + _validHashes = certHash?.Split([",", ";"], StringSplitOptions.RemoveEmptyEntries) ?? []; var u = new Utility.Uri(url); u.RequireHost(); @@ -173,36 +297,19 @@ namespace Duplicati.Library.Backend if (_userInfo != null) _userInfo.Domain = ""; - _url = u.SetScheme("ftp").SetQuery(null).SetCredentials(null, null).ToString(); - _url = Util.AppendDirSeparator(_url, "/"); - _listVerify = !CoreUtility.ParseBoolOption(options, "disable-upload-verify"); + var parsedurl = u.SetScheme("ftp").SetQuery(null).SetCredentials(null, null).ToString(); + parsedurl = Util.AppendDirSeparator(parsedurl, "/"); + _url = new Uri(parsedurl); + + _listVerify = !CoreUtility.ParseBoolOption(options, CONFIG_KEY_DISABLE_UPLOAD_VERIFY); + _relativePaths = CoreUtility.ParseBoolOption(options, CONFIG_KEY_FTP_RELATIVE_PATHS); + _useCwdNames = CoreUtility.ParseBoolOption(options, CONFIG_KEY_FTP_USE_CWD_NAMES); if (options.TryGetValue(CONFIG_KEY_FTP_UPLOAD_DELAY, out var uploadWaitTimeString) && !string.IsNullOrWhiteSpace(uploadWaitTimeString)) _uploadWaitTime = Timeparser.ParseTimeSpan(uploadWaitTimeString); - // Process the aftp-data-connection-type option - string dataConnectionTypeString; - FtpDataConnectionType dataConnectionType; - - if (!options.TryGetValue(CONFIG_KEY_FTP_DATA_CONNECTION_TYPE, out dataConnectionTypeString) || string.IsNullOrWhiteSpace(dataConnectionTypeString)) - dataConnectionTypeString = null; - - if (dataConnectionTypeString == null || !Enum.TryParse(dataConnectionTypeString, true, out dataConnectionType)) - dataConnectionType = DEFAULT_DATA_CONNECTION_TYPE; - - // Process the aftp-encryption-mode option - FtpEncryptionMode encryptionMode; - if (!options.TryGetValue(CONFIG_KEY_FTP_ENCRYPTION_MODE, out var encryptionModeString) || string.IsNullOrWhiteSpace(encryptionModeString)) - encryptionModeString = null; - - if (encryptionModeString == null || !Enum.TryParse(encryptionModeString, true, out encryptionMode)) - encryptionMode = DEFAULT_ENCRYPTION_MODE; - - // Process the aftp-ssl-protocols option - SslProtocols sslProtocols; - if (!options.TryGetValue(CONFIG_KEY_FTP_SSL_PROTOCOLS, out var sslProtocolsString) || string.IsNullOrWhiteSpace(sslProtocolsString)) - sslProtocolsString = null; - - if (sslProtocolsString == null || !Enum.TryParse(sslProtocolsString, true, out sslProtocols)) sslProtocols = DEFAULT_SSL_PROTOCOLS; + var dataConnectionType = CoreUtility.ParseEnumOption(options, CONFIG_KEY_FTP_DATA_CONNECTION_TYPE, DEFAULT_DATA_CONNECTION_TYPE); + var encryptionMode = CoreUtility.ParseEnumOption(options, CONFIG_KEY_FTP_ENCRYPTION_MODE, DEFAULT_ENCRYPTION_MODE); + var sslProtocols = CoreUtility.ParseEnumOption(options, CONFIG_KEY_FTP_SSL_PROTOCOLS, DEFAULT_SSL_PROTOCOLS); // Process options of the legacy FTP backend if (ProtocolKey == "ftp") @@ -235,236 +342,209 @@ namespace Duplicati.Library.Backend EncryptionMode = encryptionMode, SslProtocols = sslProtocols, LogToConsole = _logToConsole, + ValidateAnyCertificate = _accepAllCertificates }; if (_logPrivateInfoToConsole) _ftpConfig.LogHost = _ftpConfig.LogPassword = _ftpConfig.LogUserName = true; } + /// public IEnumerable List() { - return List(string.Empty); - } - - public IEnumerable List(string filename) - { - return List(filename, false); - } - - private IEnumerable List(string filename, bool stripFile) - { - var list = new List(); - string remotePath = filename; - - var ftpClient = CreateClient(); - - // Get the remote path - var url = new Uri(_url); - remotePath = "/" + GetUnescapedAbsolutePath(url); - - if (!string.IsNullOrEmpty(filename)) + try { - if (!stripFile) - { - // Append the filename - remotePath += filename; - } - else if (filename.Contains("/")) - { - remotePath += filename.Substring(0, filename.LastIndexOf("/", StringComparison.Ordinal)); - } - // else: stripping the filename in this case ignoring it - } + var list = new List(); + var client = CreateClient(CancellationToken.None).Await(); + var remotePath = PreparePathForClient(null); - foreach (FtpListItem item in ftpClient.GetListing(remotePath, FtpListOption.Modify | FtpListOption.Size).Await()) - { - switch (item.Type) + foreach (var item in client.GetListing(remotePath, FtpListOption.Modify | FtpListOption.Size).Await()) { - case FtpObjectType.Directory: - { + switch (item.Type) + { + case FtpObjectType.Directory: if (item.Name == "." || item.Name == "..") - { continue; - } list.Add(new FileEntry(item.Name, -1, new DateTime(), item.Modified) { IsFolder = true, }); - break; - } - case FtpObjectType.File: - { + + case FtpObjectType.File: list.Add(new FileEntry(item.Name, item.Size, new DateTime(), item.Modified)); - break; - } - case FtpObjectType.Link: - { - if (item.Name == "." || item.Name == "..") - { - continue; - } - if (item.LinkObject != null) + case FtpObjectType.Link: { - switch (item.LinkObject.Type) + if (item.Name == "." || item.Name == "..") + continue; + + if (item.LinkObject != null) { - case FtpObjectType.Directory: - { + switch (item.LinkObject.Type) + { + case FtpObjectType.Directory: if (item.Name == "." || item.Name == "..") - { continue; - } list.Add(new FileEntry(item.Name, -1, new DateTime(), item.Modified) { IsFolder = true, }); - break; - } - case FtpObjectType.File: - { + + case FtpObjectType.File: list.Add(new FileEntry(item.Name, item.Size, new DateTime(), item.Modified)); - break; - } + } } + break; } - break; - } + } } - } - return list; + return list; + } + catch (Exception e) + { + if (TranslateException(null, ref e)) + throw e; + + throw; + } } + /// public async Task PutAsync(string remotename, Stream input, CancellationToken cancelToken) { - string remotePath = remotename; - long streamLen; - - var ftpClient = CreateClient(); - try { - streamLen = input.Length; - } - catch (NotSupportedException) { streamLen = -1; } + var streamLen = -1L; + var client = await CreateClient(cancelToken).ConfigureAwait(false); + var clientRemoteName = PreparePathForClient(remotename); - // Get the remote path - remotePath = ""; + try { streamLen = input.Length; } + catch (NotSupportedException) { } - if (!string.IsNullOrEmpty(remotename)) - { - // Append the filename - remotePath += remotename; - } + var status = await client.UploadStream(input, clientRemoteName, createRemoteDir: false, token: cancelToken, progress: null).ConfigureAwait(false); + if (status != FtpStatus.Success) + throw new UserInformationException(Strings.ErrorWriteFile(remotename, $"Status is {status}"), "FtpPutFailure"); - var status = await ftpClient.UploadStream(input, remotePath, createRemoteDir: false, token: cancelToken, progress: null).ConfigureAwait(false); - if (status != FtpStatus.Success) - { - throw new UserInformationException(Strings.ErrorWriteFile(remotename, $"Status is {status}"), "AftpPutFailure"); - } + // Wait for the upload, if required + if (_uploadWaitTime.Ticks > 0) + Thread.Sleep(_uploadWaitTime); - // Wait for the upload, if required - if (_uploadWaitTime.Ticks > 0) - { - Thread.Sleep(_uploadWaitTime); - } - - if (_listVerify) - { - // check remote file size; matching file size indicates completion - var remoteSize = await ftpClient.GetFileSize(remotePath, -1, cancelToken); - if (streamLen != remoteSize) + if (_listVerify) { - throw new UserInformationException(Strings.ListVerifySizeFailure(remotename, remoteSize, streamLen), "AftpListVerifySizeFailure"); + // check remote file size; matching file size indicates completion + var remoteSize = await client.GetFileSize(clientRemoteName, -1, cancelToken); + if (streamLen != remoteSize) + throw new UserInformationException(Strings.ListVerifySizeFailure(remotename, remoteSize, streamLen), "FtpListVerifySizeFailure"); } } + catch (Exception e) + { + if (TranslateException(remotename, ref e)) + throw e; + + throw; + } } + /// public async Task PutAsync(string remotename, string localname, CancellationToken cancelToken) { - await using FileStream fs = File.Open(localname, FileMode.Open, FileAccess.Read, FileShare.Read); + await using var fs = File.Open(localname, FileMode.Open, FileAccess.Read, FileShare.Read); await PutAsync(remotename, fs, cancelToken); } + /// public async Task GetAsync(string remotename, Stream output, CancellationToken cancelToken) { - var ftpClient = CreateClient(); - - // Get the remote path - var remotePath = ""; - - if (!string.IsNullOrEmpty(remotename)) - { - // Append the filename - remotePath += remotename; - } - - await using var inputStream = await ftpClient.OpenRead(remotePath, token: cancelToken); try { + var client = await CreateClient(cancelToken).ConfigureAwait(false); + var clientRemoteName = PreparePathForClient(remotename); + await using var inputStream = await client.OpenRead(clientRemoteName, token: cancelToken); await CoreUtility.CopyStreamAsync(inputStream, output, false, cancelToken).ConfigureAwait(false); } - finally + catch (Exception e) { - inputStream.Close(); + if (TranslateException(remotename, ref e)) + throw e; + + throw; } } + /// public async Task GetAsync(string remotename, string localname, CancellationToken cancelToken) { await using FileStream fs = File.Open(localname, FileMode.Create, FileAccess.Write, FileShare.None); await GetAsync(remotename, fs, cancelToken).ConfigureAwait(false); } + /// public async Task DeleteAsync(string remotename, CancellationToken cancelToken) { - var ftpClient = CreateClient(); - - // Get the remote path - var remotePath = ""; - - if (!string.IsNullOrEmpty(remotename)) + try { - // Append the filename - remotePath += remotename; + var client = await CreateClient(cancelToken).ConfigureAwait(false); + var clientRemoteName = PreparePathForClient(remotename); + await client.DeleteFile(clientRemoteName, cancelToken); } + catch (Exception e) + { + if (TranslateException(remotename, ref e)) + throw e; - await ftpClient.DeleteFile(remotePath, cancelToken); + throw; + } } - /// - /// A localized description of the backend, for display in the usage information - /// + /// public virtual string Description => Strings.Description; - public Task GetDNSNamesAsync(CancellationToken cancelToken) => Task.FromResult(new[] { new Uri(_url).Host }); + /// + public Task GetDNSNamesAsync(CancellationToken cancelToken) => Task.FromResult(new[] { _url.Host }); - /// - /// Test FTP access permissions. - /// + /// public async Task TestAsync(CancellationToken cancellationToken) { - var list = List(); - - // Delete test file if exists - if (list.Any(entry => entry.Name == TEST_FILE_NAME)) + // Try to set the working directory to trigger a folder-not-found exception + try { - try + var client = await CreateClient(cancellationToken).ConfigureAwait(false); + if (!_useCwdNames) { + var folderpath = PreparePathForClient(null); + await client.SetWorkingDirectory(folderpath, cancellationToken).ConfigureAwait(false); + } + } + catch (Exception e) + { + if (TranslateException(null, ref e)) + throw e; + + throw; + } + + // Remove the file if it exists + try + { + var list = List(); + if (list.Any(entry => entry.Name == TEST_FILE_NAME)) await DeleteAsync(TEST_FILE_NAME, cancellationToken).ConfigureAwait(false); - } - catch (Exception e) - { - if (e.InnerException != null) { e = e.InnerException; } - throw new UserInformationException(Strings.ErrorDeleteFile(TEST_FILE_NAME, e.Message), "TestPreparationError"); - } + } + catch (Exception e) + { + if (TranslateException(TEST_FILE_NAME, ref e)) + throw e; + + throw new UserInformationException(Strings.ErrorDeleteFile(TEST_FILE_NAME, e.Message), "TestPreparationError"); } // Test write permissions @@ -476,7 +556,10 @@ namespace Duplicati.Library.Backend } catch (Exception e) { - if (e.InnerException != null) { e = e.InnerException; } + // Do not pass the filename here because a not-found should be treated as folder-not-found + if (TranslateException(null, ref e)) + throw e; + throw new UserInformationException(Strings.ErrorWriteFile(TEST_FILE_NAME, e.Message), "TestWriteError"); } } @@ -493,7 +576,9 @@ namespace Duplicati.Library.Backend } catch (Exception e) { - if (e.InnerException != null) { e = e.InnerException; } + if (TranslateException(TEST_FILE_NAME, ref e)) + throw e; + throw new UserInformationException(Strings.ErrorReadFile(TEST_FILE_NAME, e.Message), "TestReadError"); } } @@ -505,82 +590,99 @@ namespace Duplicati.Library.Backend } catch (Exception e) { - if (e.InnerException != null) { e = e.InnerException; } + if (TranslateException(TEST_FILE_NAME, ref e)) + throw e; + throw new UserInformationException(Strings.ErrorDeleteFile(TEST_FILE_NAME, e.Message), "TestCleanupError"); } } - public Task CreateFolderAsync(CancellationToken cancellationToken) + /// + public async Task CreateFolderAsync(CancellationToken cancellationToken) { - var client = CreateClient(false); - - var url = new Uri(_url); - - // Get the remote path - var remotePath = GetUnescapedAbsolutePath(url); - - // Try to create the directory - return client.CreateDirectory(remotePath, true, cancellationToken); - } - - public void Dispose() - { - if (Client != null) - Client.Dispose(); - - Client = null; - _userInfo = null; - } - - private AsyncFtpClient CreateClient(bool setWorkingDirectory = true) - { - var uri = new Uri(_url); - - if (Client == null) // Create connection if it doesn't exist yet + try { - var ftpClient = new AsyncFtpClient + // Try to create the directory + var client = await CreateClient(cancellationToken, false).ConfigureAwait(false); + var clientPath = PreparePathForClient(null, false); + if (_useCwdNames && clientPath.Contains('/') && clientPath != "/") { - Host = uri.Host, - Port = uri.Port == -1 ? 21 : uri.Port, - Credentials = _userInfo, - Config = _ftpConfig, - }; + // Go to the parent folder and create the folder + var parentPath = clientPath.Substring(0, clientPath.LastIndexOf('/')); + var folderName = clientPath.Substring(clientPath.LastIndexOf('/') + 1); + await client.SetWorkingDirectory(parentPath, cancellationToken).ConfigureAwait(false); + await client.CreateDirectory(folderName, true, cancellationToken).ConfigureAwait(false); - ftpClient.ValidateCertificate += HandleValidateCertificate; - - Client = ftpClient; - - } // else reuse existing connection - - if (setWorkingDirectory) - { - // Change working directory to the remote path - // Do this every time to prevent issues when FtpClient silently reconnects after failure. - var remotePath = GetUnescapedAbsolutePath(uri); - try - { - Client.SetWorkingDirectory(remotePath).Await(); + // Reset the client and check that it works + _client = null; + client = await CreateClient(cancellationToken).ConfigureAwait(false); + var cwd = await client.GetWorkingDirectory(cancellationToken).ConfigureAwait(false); + if (!string.Equals(cwd?.TrimEnd('/'), clientPath, StringComparison.OrdinalIgnoreCase)) + throw new UserInformationException(Strings.ErrorCreateFolder(clientPath, cwd), "CreateFolderError"); } - catch (FtpCommandException ex) + else { - if (ex.CompletionCode == "550") - { - throw new FolderMissingException(Strings.MissingFolderError(remotePath, ex.Message), ex); - } - - throw; + await client.CreateDirectory(clientPath, true, cancellationToken); } } - - return Client; + catch (Exception ex) + { + if (TranslateException(null, ref ex)) + throw ex; + throw; + } } - private string GetUnescapedAbsolutePath(Uri uri) + /// + public void Dispose() { - string absolutePath = Uri.UnescapeDataString(uri.AbsolutePath); - return absolutePath.EndsWith("/", StringComparison.Ordinal) ? absolutePath.Substring(0, absolutePath.Length - 1) : absolutePath; + _client?.Dispose(); } + /// + /// Create a new FTP client, or return the existing one if it already exists. + /// + /// The cancellation token. + /// A flag to override the CWD strategy. + /// The FTP client. + private async Task CreateClient(CancellationToken cancellationToken, bool? cwdFlag = null) + { + if (_client == null) + { + var client = new AsyncFtpClient + { + Host = _url.Host, + Port = _url.Port == -1 ? 21 : _url.Port, + Credentials = _userInfo, + Config = _ftpConfig + }; + + client.ValidateCertificate += HandleValidateCertificate; + + // Set up for relative paths + if (_relativePaths) + { + _initialCwd = await client.GetWorkingDirectory(cancellationToken).ConfigureAwait(false); + _initialCwd = _initialCwd?.TrimEnd('/'); + } + + // Setup the initial working directory, if needed + if (cwdFlag ?? _useCwdNames) + { + var clientPath = PreparePathForClient(null, false, client); + await client.SetWorkingDirectory(clientPath, cancellationToken).ConfigureAwait(false); + } + + _client = client; + } + return _client; + } + + /// + /// Handle the certificate validation event. + /// + /// The FTP client. + /// The event arguments. private void HandleValidateCertificate(BaseFtpClient control, FtpSslValidationEventArgs e) { if (e.PolicyErrors == SslPolicyErrors.None || _accepAllCertificates) @@ -589,21 +691,79 @@ namespace Duplicati.Library.Backend return; } + e.Accept = false; try { - var certHash = (_validHashes != null && _validHashes.Length > 0) ? CoreUtility.ByteArrayAsHexString(e.Certificate.GetCertHash()) : null; - if (certHash != null) - { - if (_validHashes.Any(hash => !string.IsNullOrEmpty(hash) && certHash.Equals(hash, StringComparison.OrdinalIgnoreCase))) - { - e.Accept = true; - } - } + var certHash = (_validHashes != null && _validHashes.Length > 0) ? e.Certificate?.GetCertHashString() : null; + if (certHash != null && _validHashes != null && _validHashes.Any(hash => !string.IsNullOrEmpty(hash) && certHash.Equals(hash, StringComparison.OrdinalIgnoreCase))) + e.Accept = true; } catch { - e.Accept = false; } + + if (e.Accept == false && e.Certificate != null) + throw new SslCertificateValidator.InvalidCertificateException(e.Certificate?.GetCertHashString(), e.PolicyErrors); + } + + /// + /// Prepare the path for the client. + /// + /// The path to prepare. + /// A flag to override the CWD strategy. + /// The FTP client, if not using the class instance. + /// The prepared path. + private string PreparePathForClient(string? path, bool? cwdFlag = null, AsyncFtpClient? client = null) + { + client = client ?? _client; + if (cwdFlag ?? _useCwdNames) + return string.IsNullOrWhiteSpace(path) + ? string.Empty + : path; + + var remotePath = _url.AbsolutePath.TrimEnd('/'); + if (_relativePaths) + { + if (client == null) + throw new InvalidOperationException("Client not initialized"); + + if (!string.IsNullOrWhiteSpace(_initialCwd)) + remotePath = _initialCwd + "/" + remotePath.TrimStart('/'); + } + + if (string.IsNullOrEmpty(path)) + return remotePath; + + if (path.StartsWith("/", StringComparison.Ordinal)) + return path; + + return remotePath + "/" + path; + } + + /// + /// Translate an exception to a more user-friendly exception. + /// + /// The filename provided. + /// The exception to translate. + /// True if the exception was translated, otherwise false. + private bool TranslateException(string? filename, ref Exception ex) + { + if (ex.InnerException != null && (ex.InnerException is FtpCommandException || ex.InnerException is SslCertificateValidator.InvalidCertificateException)) + ex = ex.InnerException; + + if (ex is FtpCommandException ftpEx && (ftpEx.CompletionCode == "550" || ftpEx.CompletionCode == "450")) + { + ex = string.IsNullOrWhiteSpace(filename) + ? new FolderMissingException(Strings.MissingFolderError(_url.AbsolutePath, ftpEx.Message), ftpEx) + : new FileMissingException(Strings.FileMissingError(filename, ftpEx.Message), ftpEx); + + return true; + } + + if (ex is SslCertificateValidator.InvalidCertificateException) + return true; + + return false; } } } diff --git a/Duplicati/Library/Backend/FTP/Strings.cs b/Duplicati/Library/Backend/FTP/Strings.cs index 46ef6f8d2..e4dbec0a2 100644 --- a/Duplicati/Library/Backend/FTP/Strings.cs +++ b/Duplicati/Library/Backend/FTP/Strings.cs @@ -64,5 +64,11 @@ namespace Duplicati.Library.Backend public static string FtpPassiveDeprecated { get { return LC.L(@"The option ftp-passive is deprecated, use ftp-data-connection-type instead."); } } public static string FtpActiveDeprecated { get { return LC.L(@"ftp-regular is deprecated, use ftp-data-connection-type instead."); } } public static string UseSslDeprecated { get { return LC.L(@"use-ssl is deprecated, use ftp-ssl-protocols instead."); } } + public static string FileMissingError(string filename, string message) { return LC.L(@"The file {0} was not found. Message: {1}", filename, message); } + public static string DescriptionRelativePathsShort { get { return LC.L(@"Use relative paths for FTP connections"); } } + public static string DescriptionRelativePathsLong { get { return LC.L(@"Use this option to use relative paths for FTP connections. This option only has an effect if the initial starting folder in the FTP server is not the root folder."); } } + public static string DescriptionUseCwdNamesShort { get { return LC.L(@"Use CWD instead of absolute paths"); } } + public static string DescriptionUseCwdNamesLong { get { return LC.L(@"Use this option to start the connection with a CWD command instead of an absolute path. This can be useful if the FTP server does not support absolute paths."); } } + public static string ErrorCreateFolder(string targetFolderName, string resultingFolder) { return LC.L(@"Error creating folder {0}, gave folder: {1}", targetFolderName, resultingFolder); } } } From 9dadd514bf7c78fcab3e081fd9986d4e3f5e9320 Mon Sep 17 00:00:00 2001 From: Kenneth Skovhede Date: Thu, 21 Nov 2024 13:13:44 +0100 Subject: [PATCH 3/4] Inverted the flag for relative paths, as that is more compatible with the previous library. --- Duplicati/Library/Backend/FTP/AlternateFTPBackend.cs | 2 +- Duplicati/Library/Backend/FTP/FTPBackend.cs | 10 +++++----- Duplicati/Library/Backend/FTP/Strings.cs | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Duplicati/Library/Backend/FTP/AlternateFTPBackend.cs b/Duplicati/Library/Backend/FTP/AlternateFTPBackend.cs index 9cd072d4f..a3983a170 100644 --- a/Duplicati/Library/Backend/FTP/AlternateFTPBackend.cs +++ b/Duplicati/Library/Backend/FTP/AlternateFTPBackend.cs @@ -50,7 +50,7 @@ public class AlternateFTPBackend : FTP new CommandLineArgument("auth-password", CommandLineArgument.ArgumentType.Password, Strings.DescriptionAuthPasswordShort, Strings.DescriptionAuthPasswordLong), new CommandLineArgument("auth-username", CommandLineArgument.ArgumentType.String, Strings.DescriptionAuthUsernameShort, Strings.DescriptionAuthUsernameLong), new CommandLineArgument(CONFIG_KEY_DISABLE_UPLOAD_VERIFY, CommandLineArgument.ArgumentType.Boolean, Strings.DescriptionDisableUploadVerifyShort, Strings.DescriptionDisableUploadVerifyLong), - new CommandLineArgument(CONFIG_KEY_FTP_RELATIVE_PATHS, CommandLineArgument.ArgumentType.Boolean, Strings.DescriptionRelativePathsShort, Strings.DescriptionRelativePathsLong), + new CommandLineArgument(CONFIG_KEY_FTP_ABSOLUTE_PATH, CommandLineArgument.ArgumentType.Boolean, Strings.DescriptionAbsolutePathShort, Strings.DescriptionAbsolutePathLong), new CommandLineArgument(CONFIG_KEY_FTP_USE_CWD_NAMES, CommandLineArgument.ArgumentType.Boolean, Strings.DescriptionUseCwdNamesShort, Strings.DescriptionUseCwdNamesLong), new CommandLineArgument(CONFIG_KEY_FTP_DATA_CONNECTION_TYPE, CommandLineArgument.ArgumentType.Enumeration, Strings.DescriptionFtpDataConnectionTypeShort, Strings.DescriptionFtpDataConnectionTypeLong, DEFAULT_DATA_CONNECTION_TYPE_STRING, null, Enum.GetNames(typeof(FtpDataConnectionType))), new CommandLineArgument(CONFIG_KEY_FTP_ENCRYPTION_MODE, CommandLineArgument.ArgumentType.Enumeration, Strings.DescriptionFtpEncryptionModeShort, Strings.DescriptionFtpEncryptionModeLong, DEFAULT_ENCRYPTION_MODE_STRING, null, Enum.GetNames(typeof(FtpEncryptionMode))), diff --git a/Duplicati/Library/Backend/FTP/FTPBackend.cs b/Duplicati/Library/Backend/FTP/FTPBackend.cs index 8bede87ec..caa3ea5f8 100644 --- a/Duplicati/Library/Backend/FTP/FTPBackend.cs +++ b/Duplicati/Library/Backend/FTP/FTPBackend.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2024, The Duplicati Team +// Copyright (C) 2024, The Duplicati Team // https://duplicati.com, hello@duplicati.com // // Permission is hereby granted, free of charge, to any person obtaining a @@ -110,7 +110,7 @@ namespace Duplicati.Library.Backend /// /// The configuration key for the FTP relative paths option /// - protected virtual string CONFIG_KEY_FTP_RELATIVE_PATHS => "ftp-relative-paths"; + protected virtual string CONFIG_KEY_FTP_ABSOLUTE_PATH => "ftp-absolute-path"; /// /// The configuration key for the FTP use CWD names option /// @@ -172,7 +172,7 @@ namespace Duplicati.Library.Backend /// /// The flag to indicate if relative paths are used /// - private readonly bool _relativePaths = false; + private readonly bool _relativePaths = true; /// /// The flag to indicate if the CWD strategy is used /// @@ -229,7 +229,7 @@ namespace Duplicati.Library.Backend new CommandLineArgument("auth-password", CommandLineArgument.ArgumentType.Password, Strings.DescriptionAuthPasswordShort, Strings.DescriptionAuthPasswordLong), new CommandLineArgument("auth-username", CommandLineArgument.ArgumentType.String, Strings.DescriptionAuthUsernameShort, Strings.DescriptionAuthUsernameLong), new CommandLineArgument(CONFIG_KEY_DISABLE_UPLOAD_VERIFY, CommandLineArgument.ArgumentType.Boolean, Strings.DescriptionDisableUploadVerifyShort, Strings.DescriptionDisableUploadVerifyLong), - new CommandLineArgument(CONFIG_KEY_FTP_RELATIVE_PATHS, CommandLineArgument.ArgumentType.Boolean, Strings.DescriptionRelativePathsShort, Strings.DescriptionRelativePathsLong), + new CommandLineArgument(CONFIG_KEY_FTP_ABSOLUTE_PATH, CommandLineArgument.ArgumentType.Boolean, Strings.DescriptionAbsolutePathShort, Strings.DescriptionAbsolutePathLong), new CommandLineArgument(CONFIG_KEY_FTP_USE_CWD_NAMES, CommandLineArgument.ArgumentType.Boolean, Strings.DescriptionUseCwdNamesShort, Strings.DescriptionUseCwdNamesLong), new CommandLineArgument(CONFIG_KEY_FTP_DATA_CONNECTION_TYPE, CommandLineArgument.ArgumentType.Enumeration, Strings.DescriptionFtpDataConnectionTypeShort, Strings.DescriptionFtpDataConnectionTypeLong, DEFAULT_DATA_CONNECTION_TYPE_STRING, null, Enum.GetNames(typeof(FtpDataConnectionType))), new CommandLineArgument(CONFIG_KEY_FTP_ENCRYPTION_MODE, CommandLineArgument.ArgumentType.Enumeration, Strings.DescriptionFtpEncryptionModeShort, Strings.DescriptionFtpEncryptionModeLong, DEFAULT_ENCRYPTION_MODE_STRING, null, Enum.GetNames(typeof(FtpEncryptionMode))), @@ -302,7 +302,7 @@ namespace Duplicati.Library.Backend _url = new Uri(parsedurl); _listVerify = !CoreUtility.ParseBoolOption(options, CONFIG_KEY_DISABLE_UPLOAD_VERIFY); - _relativePaths = CoreUtility.ParseBoolOption(options, CONFIG_KEY_FTP_RELATIVE_PATHS); + _relativePaths = !CoreUtility.ParseBoolOption(options, CONFIG_KEY_FTP_ABSOLUTE_PATH); _useCwdNames = CoreUtility.ParseBoolOption(options, CONFIG_KEY_FTP_USE_CWD_NAMES); if (options.TryGetValue(CONFIG_KEY_FTP_UPLOAD_DELAY, out var uploadWaitTimeString) && !string.IsNullOrWhiteSpace(uploadWaitTimeString)) _uploadWaitTime = Timeparser.ParseTimeSpan(uploadWaitTimeString); diff --git a/Duplicati/Library/Backend/FTP/Strings.cs b/Duplicati/Library/Backend/FTP/Strings.cs index e4dbec0a2..9990e9a1d 100644 --- a/Duplicati/Library/Backend/FTP/Strings.cs +++ b/Duplicati/Library/Backend/FTP/Strings.cs @@ -65,8 +65,8 @@ namespace Duplicati.Library.Backend public static string FtpActiveDeprecated { get { return LC.L(@"ftp-regular is deprecated, use ftp-data-connection-type instead."); } } public static string UseSslDeprecated { get { return LC.L(@"use-ssl is deprecated, use ftp-ssl-protocols instead."); } } public static string FileMissingError(string filename, string message) { return LC.L(@"The file {0} was not found. Message: {1}", filename, message); } - public static string DescriptionRelativePathsShort { get { return LC.L(@"Use relative paths for FTP connections"); } } - public static string DescriptionRelativePathsLong { get { return LC.L(@"Use this option to use relative paths for FTP connections. This option only has an effect if the initial starting folder in the FTP server is not the root folder."); } } + public static string DescriptionAbsolutePathShort { get { return LC.L(@"Treat the url path as absolute"); } } + public static string DescriptionAbsolutePathLong { get { return LC.L(@"Use this option to interpret the url path as an absolute path. This option only has an effect if the initial starting folder in the FTP server is not the (virtual) root folder. If not set, the path in the url is treated as relative to the initial login folder."); } } public static string DescriptionUseCwdNamesShort { get { return LC.L(@"Use CWD instead of absolute paths"); } } public static string DescriptionUseCwdNamesLong { get { return LC.L(@"Use this option to start the connection with a CWD command instead of an absolute path. This can be useful if the FTP server does not support absolute paths."); } } public static string ErrorCreateFolder(string targetFolderName, string resultingFolder) { return LC.L(@"Error creating folder {0}, gave folder: {1}", targetFolderName, resultingFolder); } From c915b70697285222b71cf5f71b904e5e0aa97795 Mon Sep 17 00:00:00 2001 From: Kenneth Skovhede Date: Thu, 21 Nov 2024 14:37:25 +0100 Subject: [PATCH 4/4] Added support for ftp/aftp treating url paths as before the update --- Duplicati/Library/Backend/FTP/AlternateFTPBackend.cs | 4 +++- Duplicati/Library/Backend/FTP/FTPBackend.cs | 12 +++++++++--- Duplicati/Library/Backend/FTP/Strings.cs | 2 ++ 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/Duplicati/Library/Backend/FTP/AlternateFTPBackend.cs b/Duplicati/Library/Backend/FTP/AlternateFTPBackend.cs index a3983a170..bca7fc510 100644 --- a/Duplicati/Library/Backend/FTP/AlternateFTPBackend.cs +++ b/Duplicati/Library/Backend/FTP/AlternateFTPBackend.cs @@ -24,6 +24,8 @@ public class AlternateFTPBackend : FTP protected override string CONFIG_KEY_FTP_UPLOAD_DELAY => "aftp-upload-delay"; protected override string CONFIG_KEY_FTP_LOGTOCONSOLE => "aftp-log-to-console"; protected override string CONFIG_KEY_FTP_LOGPRIVATEINFOTOCONSOLE => "aftp-log-privateinfo-to-console"; + protected override string CONFIG_KEY_FTP_RELATIVE_PATH => "aftp-relative-path"; + protected override string CONFIG_KEY_FTP_USE_CWD_NAMES => "aftp-use-cwd-names"; public override string Description => Strings.DescriptionAlternate; @@ -50,7 +52,7 @@ public class AlternateFTPBackend : FTP new CommandLineArgument("auth-password", CommandLineArgument.ArgumentType.Password, Strings.DescriptionAuthPasswordShort, Strings.DescriptionAuthPasswordLong), new CommandLineArgument("auth-username", CommandLineArgument.ArgumentType.String, Strings.DescriptionAuthUsernameShort, Strings.DescriptionAuthUsernameLong), new CommandLineArgument(CONFIG_KEY_DISABLE_UPLOAD_VERIFY, CommandLineArgument.ArgumentType.Boolean, Strings.DescriptionDisableUploadVerifyShort, Strings.DescriptionDisableUploadVerifyLong), - new CommandLineArgument(CONFIG_KEY_FTP_ABSOLUTE_PATH, CommandLineArgument.ArgumentType.Boolean, Strings.DescriptionAbsolutePathShort, Strings.DescriptionAbsolutePathLong), + new CommandLineArgument(CONFIG_KEY_FTP_RELATIVE_PATH, CommandLineArgument.ArgumentType.Boolean, Strings.DescriptionAbsolutePathShort, Strings.DescriptionAbsolutePathLong), new CommandLineArgument(CONFIG_KEY_FTP_USE_CWD_NAMES, CommandLineArgument.ArgumentType.Boolean, Strings.DescriptionUseCwdNamesShort, Strings.DescriptionUseCwdNamesLong), new CommandLineArgument(CONFIG_KEY_FTP_DATA_CONNECTION_TYPE, CommandLineArgument.ArgumentType.Enumeration, Strings.DescriptionFtpDataConnectionTypeShort, Strings.DescriptionFtpDataConnectionTypeLong, DEFAULT_DATA_CONNECTION_TYPE_STRING, null, Enum.GetNames(typeof(FtpDataConnectionType))), new CommandLineArgument(CONFIG_KEY_FTP_ENCRYPTION_MODE, CommandLineArgument.ArgumentType.Enumeration, Strings.DescriptionFtpEncryptionModeShort, Strings.DescriptionFtpEncryptionModeLong, DEFAULT_ENCRYPTION_MODE_STRING, null, Enum.GetNames(typeof(FtpEncryptionMode))), diff --git a/Duplicati/Library/Backend/FTP/FTPBackend.cs b/Duplicati/Library/Backend/FTP/FTPBackend.cs index caa3ea5f8..8971f7a0d 100644 --- a/Duplicati/Library/Backend/FTP/FTPBackend.cs +++ b/Duplicati/Library/Backend/FTP/FTPBackend.cs @@ -106,12 +106,15 @@ namespace Duplicati.Library.Backend /// The configuration key for the FTP log private info to console /// protected virtual string CONFIG_KEY_FTP_LOGPRIVATEINFOTOCONSOLE => "ftp-log-privateinfo-to-console"; - /// - /// The configuration key for the FTP relative paths option + /// The configuration key for the FTP absolute paths option /// protected virtual string CONFIG_KEY_FTP_ABSOLUTE_PATH => "ftp-absolute-path"; /// + /// The configuration key for the FTP relative path option + /// + protected virtual string CONFIG_KEY_FTP_RELATIVE_PATH => "ftp-relative-path"; + /// /// The configuration key for the FTP use CWD names option /// protected virtual string CONFIG_KEY_FTP_USE_CWD_NAMES => "ftp-use-cwd-names"; @@ -302,7 +305,10 @@ namespace Duplicati.Library.Backend _url = new Uri(parsedurl); _listVerify = !CoreUtility.ParseBoolOption(options, CONFIG_KEY_DISABLE_UPLOAD_VERIFY); - _relativePaths = !CoreUtility.ParseBoolOption(options, CONFIG_KEY_FTP_ABSOLUTE_PATH); + _relativePaths = ProtocolKey == "ftp" + ? !CoreUtility.ParseBoolOption(options, CONFIG_KEY_FTP_ABSOLUTE_PATH) + : CoreUtility.ParseBoolOption(options, CONFIG_KEY_FTP_RELATIVE_PATH); + _useCwdNames = CoreUtility.ParseBoolOption(options, CONFIG_KEY_FTP_USE_CWD_NAMES); if (options.TryGetValue(CONFIG_KEY_FTP_UPLOAD_DELAY, out var uploadWaitTimeString) && !string.IsNullOrWhiteSpace(uploadWaitTimeString)) _uploadWaitTime = Timeparser.ParseTimeSpan(uploadWaitTimeString); diff --git a/Duplicati/Library/Backend/FTP/Strings.cs b/Duplicati/Library/Backend/FTP/Strings.cs index 9990e9a1d..cd72bda65 100644 --- a/Duplicati/Library/Backend/FTP/Strings.cs +++ b/Duplicati/Library/Backend/FTP/Strings.cs @@ -67,6 +67,8 @@ namespace Duplicati.Library.Backend public static string FileMissingError(string filename, string message) { return LC.L(@"The file {0} was not found. Message: {1}", filename, message); } public static string DescriptionAbsolutePathShort { get { return LC.L(@"Treat the url path as absolute"); } } public static string DescriptionAbsolutePathLong { get { return LC.L(@"Use this option to interpret the url path as an absolute path. This option only has an effect if the initial starting folder in the FTP server is not the (virtual) root folder. If not set, the path in the url is treated as relative to the initial login folder."); } } + public static string DescriptionRelativePathShort { get { return LC.L(@"Treat the url path as relative"); } } + public static string DescriptionRelativePathLong { get { return LC.L(@"Use this option to interpret the url path as a path that is relative to the initial login folder. This option only has an effect if the initial starting folder in the FTP server is not the (virtual) root folder. If not set, the path in the url is treated as absolute, ignoring the initial login folder."); } } public static string DescriptionUseCwdNamesShort { get { return LC.L(@"Use CWD instead of absolute paths"); } } public static string DescriptionUseCwdNamesLong { get { return LC.L(@"Use this option to start the connection with a CWD command instead of an absolute path. This can be useful if the FTP server does not support absolute paths."); } } public static string ErrorCreateFolder(string targetFolderName, string resultingFolder) { return LC.L(@"Error creating folder {0}, gave folder: {1}", targetFolderName, resultingFolder); }