Files
duplicati/Duplicati/Library/Backend/FTP/FTPBackend.cs
T

805 lines
37 KiB
C#
Raw Normal View History

// Copyright (C) 2025, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
2024-02-28 15:45:30 +01:00
2024-11-19 13:55:23 +01:00
#nullable enable
2009-01-14 17:29:30 +00:00
using System;
using System.Collections.Generic;
2024-10-11 11:21:54 +02:00
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
2024-10-11 11:21:54 +02:00
using System.Net;
using System.Net.Security;
2025-02-17 16:45:51 +01:00
using System.Runtime.CompilerServices;
2024-10-11 11:21:54 +02:00
using System.Security.Authentication;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
2024-10-11 11:21:54 +02:00
using Duplicati.Library.Common.IO;
using Duplicati.Library.Interface;
using Duplicati.Library.Utility;
using FluentFTP;
using FluentFTP.Client.BaseClient;
using FluentFTP.Exceptions;
using CoreUtility = Duplicati.Library.Utility.Utility;
using Uri = System.Uri;
2009-01-14 17:29:30 +00:00
namespace Duplicati.Library.Backend
{
2024-10-11 11:21:54 +02:00
/// <summary>
/// The unified FTP backend which uses the FluentFTP library.
///
/// In previous versions, this was being exposed as AlternateFTPBackend, whist the FTP backend was
/// using the System.Net.FtpWebRequest class which is now deprecated.
///
/// To provide a transparent upgrade path, the AlternateFTPBackend now inherits from this class,
/// but overides the default configuration values to match the old names(prefixed with a) and the backedn
/// name being "aftp" rather than ftp.
/// </summary>
public class FTP : IStreamingBackend
2009-01-14 17:29:30 +00:00
{
2024-11-19 13:55:23 +01:00
/// <summary>
/// The credentials used to authenticate with the FTP server
/// </summary>
private readonly NetworkCredential? _userInfo;
/// <summary>
/// Th option used to accept a specific SSL certificate hash
/// </summary>
2024-10-11 11:21:54 +02:00
private const string OPTION_ACCEPT_SPECIFIED_CERTIFICATE = "accept-specified-ssl-hash"; // Global option
2024-11-19 13:55:23 +01:00
/// <summary>
/// The option used to accept any SSL certificate
/// </summary>
2024-10-11 11:21:54 +02:00
private const string OPTION_ACCEPT_ANY_CERTIFICATE = "accept-any-ssl-certificate"; // Global option
2024-11-19 13:55:23 +01:00
/// <summary>
/// The default data connection type
/// </summary>
2024-10-11 11:21:54 +02:00
private const FtpDataConnectionType DEFAULT_DATA_CONNECTION_TYPE = FtpDataConnectionType.AutoPassive;
2024-11-19 13:55:23 +01:00
/// <summary>
/// The default encryption mode
/// </summary>
2024-10-11 11:21:54 +02:00
private const FtpEncryptionMode DEFAULT_ENCRYPTION_MODE = FtpEncryptionMode.None;
2024-11-19 13:55:23 +01:00
/// <summary>
/// The default SSL protocols
/// </summary>
private static readonly SslProtocols DEFAULT_SSL_PROTOCOLS = SslProtocols.None; // NOTE: None means "use system default"
2024-10-11 11:21:54 +02:00
2024-11-19 13:55:23 +01:00
/// <summary>
/// The configuration key for the FTP encryption mode
/// </summary>
2024-10-11 11:21:54 +02:00
protected virtual string CONFIG_KEY_FTP_ENCRYPTION_MODE => "ftp-encryption-mode";
2024-11-19 13:55:23 +01:00
/// <summary>
/// The configuration key for the FTP data connection type
/// </summary>
2024-10-11 11:21:54 +02:00
protected virtual string CONFIG_KEY_FTP_DATA_CONNECTION_TYPE => "ftp-data-connection-type";
2024-11-19 13:55:23 +01:00
/// <summary>
/// The configuration key for the FTP SSL protocols
/// </summary>
2024-10-11 11:21:54 +02:00
protected virtual string CONFIG_KEY_FTP_SSL_PROTOCOLS => "ftp-ssl-protocols";
2024-11-19 13:55:23 +01:00
/// <summary>
/// The configuration key for the FTP upload delay
/// </summary>
2024-10-11 11:21:54 +02:00
protected virtual string CONFIG_KEY_FTP_UPLOAD_DELAY => "ftp-upload-delay";
2024-11-19 13:55:23 +01:00
/// <summary>
/// The configuration key for the FTP log to console
/// </summary>
2024-10-11 11:21:54 +02:00
protected virtual string CONFIG_KEY_FTP_LOGTOCONSOLE => "ftp-log-to-console";
2024-11-19 13:55:23 +01:00
/// <summary>
/// The configuration key for the FTP log private info to console
/// </summary>
2024-10-11 11:21:54 +02:00
protected virtual string CONFIG_KEY_FTP_LOGPRIVATEINFOTOCONSOLE => "ftp-log-privateinfo-to-console";
2024-11-19 13:55:23 +01:00
/// <summary>
/// The configuration key for the FTP log to console
/// </summary>
protected virtual string CONFIG_KEY_FTP_LOGDIAGNOSTICS => "ftp-log-diagnostics";
/// <summary>
/// The configuration key for the FTP absolute paths option
2024-11-19 13:55:23 +01:00
/// </summary>
protected virtual string CONFIG_KEY_FTP_ABSOLUTE_PATH => "ftp-absolute-path";
2024-11-19 13:55:23 +01:00
/// <summary>
/// The configuration key for the FTP relative path option
/// </summary>
protected virtual string CONFIG_KEY_FTP_RELATIVE_PATH => "ftp-relative-path";
/// <summary>
2024-11-19 13:55:23 +01:00
/// The configuration key for the FTP use CWD names option
/// </summary>
protected virtual string CONFIG_KEY_FTP_USE_CWD_NAMES => "ftp-use-cwd-names";
/// <summary>
/// The configuration key for the disable upload verify option
/// </summary>
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
2024-11-19 13:55:23 +01:00
/// <summary>
/// The configuration key for the legacy FTP passive mode
/// </summary>
private static string CONFIG_KEY_FTP_LEGACY_FTPPASSIVE => "ftp-passive";
2024-11-19 13:55:23 +01:00
/// <summary>
/// The configuration key for the legacy FTP active mode
/// </summary>
private static string CONFIG_KEY_FTP_LEGACY_FTPREGULAR => "ftp-regular";
2024-11-19 13:55:23 +01:00
/// <summary>
/// The configuration key for the legacy use SSL option
/// </summary>
private static string CONFIG_KEY_FTP_LEGACY_USESSL => "use-ssl";
2024-10-11 11:21:54 +02:00
2024-11-19 13:55:23 +01:00
/// <summary>
/// The test file name used to test access permissions
/// </summary>
2024-10-11 11:21:54 +02:00
private const string TEST_FILE_NAME = "duplicati-access-privileges-test.tmp";
2024-11-19 13:55:23 +01:00
/// <summary>
/// The test file content used to test access permissions
/// </summary>
2024-10-11 11:21:54 +02:00
private const string TEST_FILE_CONTENT = "This file is used by Duplicati to test access permissions and can be safely deleted.";
2024-11-19 13:55:23 +01:00
/// <summary>
/// The default data connection type as a string
/// </summary>
protected static readonly string DEFAULT_DATA_CONNECTION_TYPE_STRING = DEFAULT_DATA_CONNECTION_TYPE.ToString();
2024-11-19 13:55:23 +01:00
/// <summary>
/// The default encryption mode as a string
/// </summary>
protected static readonly string DEFAULT_ENCRYPTION_MODE_STRING = DEFAULT_ENCRYPTION_MODE.ToString();
2024-11-19 13:55:23 +01:00
/// <summary>
/// The default SSL protocols as a string
/// </summary>
protected static readonly string DEFAULT_SSL_PROTOCOLS_STRING = DEFAULT_SSL_PROTOCOLS.ToString();
2024-11-19 13:55:23 +01:00
/// <summary>
/// The default upload delay as a string
/// </summary>
protected static readonly string DEFAULT_UPLOAD_DELAY_STRING = "0s";
2024-10-11 11:21:54 +02:00
2024-11-19 13:55:23 +01:00
/// <summary>
/// The URL of the FTP server
/// </summary>
private readonly Uri _url;
/// <summary>
/// The flag to indicate if the list verify option is enabled
/// </summary>
2024-10-11 11:21:54 +02:00
private readonly bool _listVerify = true;
2024-11-19 13:55:23 +01:00
/// <summary>
/// The flag to indicate if relative paths are used
/// </summary>
private readonly bool _relativePaths = true;
2024-11-19 13:55:23 +01:00
/// <summary>
/// The flag to indicate if the CWD strategy is used
/// </summary>
private readonly bool _useCwdNames = false;
/// <summary>
/// The FTP configuration
/// </summary>
2024-10-11 11:21:54 +02:00
private readonly FtpConfig _ftpConfig;
2024-11-19 13:55:23 +01:00
/// <summary>
/// The wait time after each upload before checking the file size
/// </summary>
2024-10-11 11:21:54 +02:00
private readonly TimeSpan _uploadWaitTime;
2024-11-19 13:55:23 +01:00
/// <summary>
/// The flag to indicate if the dialog should be logged to the console
/// </summary>
2024-10-11 11:21:54 +02:00
private readonly bool _logToConsole;
2024-11-19 13:55:23 +01:00
/// <summary>
/// The flag to indicate if private information should be logged to the console
/// </summary>
2024-10-11 11:21:54 +02:00
private readonly bool _logPrivateInfoToConsole;
2024-11-19 13:55:23 +01:00
/// <summary>
/// The flag to indicate if diagnostics information should be logged
/// </summary>
private readonly bool _diagnosticsLog;
/// <summary>
2024-11-19 13:55:23 +01:00
/// The flag to indicate if all certificates should be accepted
/// </summary>
2024-10-11 11:21:54 +02:00
private readonly bool _accepAllCertificates;
2024-11-19 13:55:23 +01:00
/// <summary>
/// The valid certificate hashes
/// </summary>
2024-10-11 11:21:54 +02:00
private readonly string[] _validHashes;
/// <summary>
/// The localized name to display for this backend
/// </summary>
public virtual string DisplayName => Strings.DisplayName;
2009-01-14 17:29:30 +00:00
2024-10-11 11:21:54 +02:00
/// <summary>
/// The protocol key, e.g. ftp, http or ssh
/// </summary>
public virtual string ProtocolKey => "ftp";
2024-11-19 13:55:23 +01:00
/// <summary>
/// The client instance
/// </summary>
private AsyncFtpClient? _client;
2024-10-11 11:21:54 +02:00
2024-11-19 13:55:23 +01:00
/// <summary>
/// The server initial working directory
/// </summary>
private string? _initialCwd;
/// <inheritdoc />
public virtual IList<ICommandLineArgument> SupportedCommands =>
2024-10-11 11:21:54 +02:00
new List<ICommandLineArgument>([
new CommandLineArgument("auth-password", CommandLineArgument.ArgumentType.Password, Strings.DescriptionAuthPasswordShort, Strings.DescriptionAuthPasswordLong),
new CommandLineArgument("auth-username", CommandLineArgument.ArgumentType.String, Strings.DescriptionAuthUsernameShort, Strings.DescriptionAuthUsernameLong),
2024-11-19 13:55:23 +01:00
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),
2024-11-19 13:55:23 +01:00
new CommandLineArgument(CONFIG_KEY_FTP_USE_CWD_NAMES, CommandLineArgument.ArgumentType.Boolean, Strings.DescriptionUseCwdNamesShort, Strings.DescriptionUseCwdNamesLong),
2024-10-11 11:21:54 +02:00
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))),
new CommandLineArgument(CONFIG_KEY_FTP_UPLOAD_DELAY, CommandLineArgument.ArgumentType.Timespan, Strings.DescriptionUploadDelayShort, Strings.DescriptionUploadDelayLong, DEFAULT_UPLOAD_DELAY_STRING),
new CommandLineArgument(CONFIG_KEY_FTP_LOGTOCONSOLE, CommandLineArgument.ArgumentType.Boolean, Strings.DescriptionLogToConsoleShort, Strings.DescriptionLogToConsoleLong),
new CommandLineArgument(CONFIG_KEY_FTP_LOGPRIVATEINFOTOCONSOLE, CommandLineArgument.ArgumentType.Boolean, Strings.DescriptionLogPrivateInfoToConsoleShort, Strings.DescriptionLogPrivateInfoToConsoleLong, "false"),
new CommandLineArgument(CONFIG_KEY_FTP_LOGDIAGNOSTICS, CommandLineArgument.ArgumentType.Boolean, Strings.DescriptionLogDiagnosticsShort, Strings.DescriptionLogDiagnosticsLong),
new CommandLineArgument(CONFIG_KEY_FTP_LEGACY_FTPPASSIVE, CommandLineArgument.ArgumentType.Boolean, Strings.DescriptionFTPPassiveShort, Strings.DescriptionFTPPassiveLong, "false", null, null, Strings.FtpPassiveDeprecated),
new CommandLineArgument(CONFIG_KEY_FTP_LEGACY_FTPREGULAR, CommandLineArgument.ArgumentType.Boolean, Strings.DescriptionFTPActiveShort, Strings.DescriptionFTPActiveLong, "true", null, null, Strings.FtpActiveDeprecated),
new CommandLineArgument(CONFIG_KEY_FTP_LEGACY_USESSL, CommandLineArgument.ArgumentType.Boolean, Strings.DescriptionUseSSLShort, Strings.DescriptionUseSSLLong, "false", null, null, Strings.UseSslDeprecated),
2024-10-11 11:21:54 +02:00
]);
2010-02-10 20:44:48 +00:00
2024-10-11 11:21:54 +02:00
/// <summary>
/// Initialize a new instance.
/// </summary>
2009-01-14 17:29:30 +00:00
public FTP()
{
2024-11-19 13:55:23 +01:00
// TODO: Remove this constructor once static properties are introduced on IBackend
_validHashes = null!;
_ftpConfig = null!;
_url = null!;
_client = null!;
2009-01-14 17:29:30 +00:00
}
2024-10-11 11:21:54 +02:00
/// <summary>
/// Initialize a new instance/
/// </summary>
/// <param name="url">Configured url.</param>
/// <param name="options">Configured options. cannot be null.</param>
[SuppressMessage("ReSharper", "VirtualMemberCallInConstructor")] // The behavior of accessing the virtual properties is as expected
public FTP(string url, Dictionary<string, string?> options)
2009-01-14 17:29:30 +00:00
{
2024-10-11 11:21:54 +02:00
_accepAllCertificates = CoreUtility.ParseBoolOption(options, OPTION_ACCEPT_ANY_CERTIFICATE);
2024-11-19 13:55:23 +01:00
options.TryGetValue(OPTION_ACCEPT_SPECIFIED_CERTIFICATE, out var certHash);
2024-10-11 11:21:54 +02:00
2024-11-19 13:55:23 +01:00
_validHashes = certHash?.Split([",", ";"], StringSplitOptions.RemoveEmptyEntries) ?? [];
2011-09-24 11:39:50 +00:00
var u = new Utility.Uri(url);
2013-05-06 20:36:26 +02:00
u.RequireHost();
2009-01-15 12:57:52 +00:00
if (!string.IsNullOrEmpty(u.Username))
2009-01-15 12:57:52 +00:00
{
2024-10-11 11:21:54 +02:00
_userInfo = new NetworkCredential
{
UserName = u.Username
};
if (!string.IsNullOrEmpty(u.Password))
_userInfo.Password = u.Password;
else if (options.ContainsKey("auth-password"))
_userInfo.Password = options["auth-password"];
}
2024-10-11 11:21:54 +02:00
else
{
2024-10-11 11:21:54 +02:00
if (options.ContainsKey("auth-username"))
{
_userInfo = new NetworkCredential();
_userInfo.UserName = options["auth-username"];
if (options.ContainsKey("auth-password"))
_userInfo.Password = options["auth-password"];
}
2018-06-28 21:07:23 +02:00
}
2012-04-19 18:49:37 +00:00
//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
2024-10-11 11:21:54 +02:00
if (_userInfo != null)
_userInfo.Domain = "";
2012-04-19 18:49:37 +00:00
2024-11-19 13:55:23 +01:00
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 = ProtocolKey == "ftp"
? !CoreUtility.ParseBoolOption(options, CONFIG_KEY_FTP_ABSOLUTE_PATH)
: CoreUtility.ParseBoolOption(options, CONFIG_KEY_FTP_RELATIVE_PATH);
2024-11-19 13:55:23 +01:00
_useCwdNames = CoreUtility.ParseBoolOption(options, CONFIG_KEY_FTP_USE_CWD_NAMES);
2024-10-11 11:21:54 +02:00
if (options.TryGetValue(CONFIG_KEY_FTP_UPLOAD_DELAY, out var uploadWaitTimeString) && !string.IsNullOrWhiteSpace(uploadWaitTimeString))
_uploadWaitTime = Timeparser.ParseTimeSpan(uploadWaitTimeString);
2019-05-26 19:46:47 -07:00
2024-11-19 13:55:23 +01:00
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.ParseFlagsOption(options, CONFIG_KEY_FTP_SSL_PROTOCOLS, DEFAULT_SSL_PROTOCOLS);
2009-01-14 17:29:30 +00:00
// Process options of the legacy FTP 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))
dataConnectionType = FtpDataConnectionType.AutoPassive;
// This flag takes precedence over the ftp-passive flag
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))
{
sslProtocols = SslProtocols.None;
encryptionMode = FtpEncryptionMode.Explicit;
}
}
2024-10-11 11:21:54 +02:00
_logToConsole = CoreUtility.ParseBoolOption(options, CONFIG_KEY_FTP_LOGTOCONSOLE);
_logPrivateInfoToConsole = CoreUtility.ParseBoolOption(options, CONFIG_KEY_FTP_LOGPRIVATEINFOTOCONSOLE);
_diagnosticsLog = CoreUtility.ParseBoolOption(options, CONFIG_KEY_FTP_LOGDIAGNOSTICS);
2024-10-11 11:21:54 +02:00
_ftpConfig = new FtpConfig
2019-05-26 19:46:47 -07:00
{
2024-10-11 11:21:54 +02:00
DataConnectionType = dataConnectionType,
EncryptionMode = encryptionMode,
SslProtocols = sslProtocols,
LogToConsole = _logToConsole,
2024-12-26 10:41:06 +01:00
ValidateAnyCertificate = _accepAllCertificates,
Noop = true
2024-10-11 11:21:54 +02:00
};
2009-01-14 17:29:30 +00:00
2024-10-11 11:21:54 +02:00
if (_logPrivateInfoToConsole) _ftpConfig.LogHost = _ftpConfig.LogPassword = _ftpConfig.LogUserName = true;
2009-01-14 17:29:30 +00:00
}
2024-11-19 13:55:23 +01:00
/// <inheritdoc />
2025-02-17 16:45:51 +01:00
public async IAsyncEnumerable<IFileEntry> ListAsync([EnumeratorCancellation] CancellationToken cancelToken)
2009-01-14 17:29:30 +00:00
{
2025-02-17 16:45:51 +01:00
FtpListItem[] items;
2024-11-19 13:55:23 +01:00
try
{
2024-11-19 13:55:23 +01:00
var client = CreateClient(CancellationToken.None).Await();
var remotePath = PreparePathForClient(null);
2025-02-17 16:45:51 +01:00
items = await client.GetListing(remotePath, FtpListOption.Modify | FtpListOption.Size).ConfigureAwait(false);
}
catch (Exception e)
{
if (TranslateException(null, ref e))
throw e;
throw;
}
2024-10-11 11:21:54 +02:00
2025-02-17 16:45:51 +01:00
foreach (var item in items)
{
switch (item.Type)
{
2025-02-17 16:45:51 +01:00
case FtpObjectType.Directory:
if (item.Name == "." || item.Name == "..")
continue;
yield return new FileEntry(item.Name, -1, new DateTime(), item.Modified)
{
IsFolder = true,
};
break;
case FtpObjectType.File:
yield return new FileEntry(item.Name, item.Size, new DateTime(), item.Modified);
break;
case FtpObjectType.Link:
{
2024-10-11 11:21:54 +02:00
if (item.Name == "." || item.Name == "..")
continue;
2025-02-17 16:45:51 +01:00
if (item.LinkObject != null)
2024-10-11 11:21:54 +02:00
{
2025-02-17 16:45:51 +01:00
switch (item.LinkObject.Type)
2024-10-11 11:21:54 +02:00
{
2025-02-17 16:45:51 +01:00
case FtpObjectType.Directory:
if (item.Name == "." || item.Name == "..")
continue;
yield return new FileEntry(item.Name, -1, new DateTime(), item.Modified)
{
IsFolder = true,
};
break;
case FtpObjectType.File:
yield return new FileEntry(item.Name, item.Size, new DateTime(), item.Modified);
break;
2024-10-11 11:21:54 +02:00
}
}
2025-02-17 16:45:51 +01:00
break;
}
2024-10-11 11:21:54 +02:00
}
2024-11-19 13:55:23 +01:00
}
}
2024-11-19 13:55:23 +01:00
/// <inheritdoc />
2024-10-11 11:21:54 +02:00
public async Task PutAsync(string remotename, Stream input, CancellationToken cancelToken)
2009-01-14 17:29:30 +00:00
{
2024-10-11 11:21:54 +02:00
try
{
2024-11-19 13:55:23 +01:00
var streamLen = -1L;
var client = await CreateClient(cancelToken).ConfigureAwait(false);
var clientRemoteName = PreparePathForClient(remotename);
2024-11-19 13:55:23 +01:00
try { streamLen = input.Length; }
catch (NotSupportedException) { }
2024-11-19 13:55:23 +01:00
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");
2024-11-19 13:55:23 +01:00
// Wait for the upload, if required
if (_uploadWaitTime.Ticks > 0)
Thread.Sleep(_uploadWaitTime);
2024-11-19 13:55:23 +01:00
if (_listVerify)
2024-10-11 11:21:54 +02:00
{
2024-11-19 13:55:23 +01:00
// 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");
2024-10-11 11:21:54 +02:00
}
2010-02-10 20:44:48 +00:00
}
2024-11-19 13:55:23 +01:00
catch (Exception e)
{
if (TranslateException(remotename, ref e))
throw e;
throw;
}
2009-01-14 17:29:30 +00:00
}
2024-11-19 13:55:23 +01:00
/// <inheritdoc />
2021-06-12 11:30:08 -07:00
public async Task PutAsync(string remotename, string localname, CancellationToken cancelToken)
2009-01-25 19:33:36 +00:00
{
2024-11-19 13:55:23 +01:00
await using var fs = File.Open(localname, FileMode.Open, FileAccess.Read, FileShare.Read);
2024-10-11 11:21:54 +02:00
await PutAsync(remotename, fs, cancelToken);
2009-01-25 19:33:36 +00:00
}
2024-11-19 13:55:23 +01:00
/// <inheritdoc />
2024-10-11 11:21:54 +02:00
public async Task GetAsync(string remotename, Stream output, CancellationToken cancelToken)
2009-01-14 17:29:30 +00:00
{
2024-10-11 11:21:54 +02:00
try
{
2024-11-19 13:55:23 +01:00
var client = await CreateClient(cancelToken).ConfigureAwait(false);
var clientRemoteName = PreparePathForClient(remotename);
await using var inputStream = await client.OpenRead(clientRemoteName, token: cancelToken);
2024-10-11 11:21:54 +02:00
await CoreUtility.CopyStreamAsync(inputStream, output, false, cancelToken).ConfigureAwait(false);
}
2024-11-19 13:55:23 +01:00
catch (Exception e)
2024-10-11 11:21:54 +02:00
{
2024-11-19 13:55:23 +01:00
if (TranslateException(remotename, ref e))
throw e;
throw;
2024-10-11 11:21:54 +02:00
}
2009-01-25 19:33:36 +00:00
}
2024-11-19 13:55:23 +01:00
/// <inheritdoc />
public async Task GetAsync(string remotename, string localname, CancellationToken cancelToken)
2009-01-25 19:33:36 +00:00
{
2024-10-11 11:21:54 +02:00
await using FileStream fs = File.Open(localname, FileMode.Create, FileAccess.Write, FileShare.None);
await GetAsync(remotename, fs, cancelToken).ConfigureAwait(false);
2009-01-14 17:29:30 +00:00
}
2024-11-19 13:55:23 +01:00
/// <inheritdoc />
2024-10-11 11:21:54 +02:00
public async Task DeleteAsync(string remotename, CancellationToken cancelToken)
2009-01-14 17:29:30 +00:00
{
2024-11-19 13:55:23 +01:00
try
2009-03-21 12:46:54 +00:00
{
2024-11-19 13:55:23 +01:00
var client = await CreateClient(cancelToken).ConfigureAwait(false);
var clientRemoteName = PreparePathForClient(remotename);
await client.DeleteFile(clientRemoteName, cancelToken);
2009-03-21 12:46:54 +00:00
}
2024-11-19 13:55:23 +01:00
catch (Exception e)
{
if (TranslateException(remotename, ref e))
throw e;
2024-10-11 11:21:54 +02:00
2024-11-19 13:55:23 +01:00
throw;
}
2024-10-11 11:21:54 +02:00
2009-03-21 12:46:54 +00:00
}
2024-11-19 13:55:23 +01:00
/// <inheritdoc />
2024-10-11 11:21:54 +02:00
public virtual string Description => Strings.Description;
2024-11-19 13:55:23 +01:00
/// <inheritdoc />
public Task<string[]> GetDNSNamesAsync(CancellationToken cancelToken) => Task.FromResult(new[] { _url.Host });
2024-10-11 11:21:54 +02:00
2024-11-19 13:55:23 +01:00
/// <inheritdoc />
2024-10-11 11:21:54 +02:00
public async Task TestAsync(CancellationToken cancellationToken)
2009-03-21 12:46:54 +00:00
{
2024-11-19 13:55:23 +01:00
// Try to set the working directory to trigger a folder-not-found exception
try
2009-03-21 12:46:54 +00:00
{
2024-11-19 13:55:23 +01:00
var client = await CreateClient(cancellationToken).ConfigureAwait(false);
if (!_useCwdNames)
2024-10-11 11:21:54 +02:00
{
2024-11-19 13:55:23 +01:00
var folderpath = PreparePathForClient(null);
await client.SetWorkingDirectory(folderpath, cancellationToken).ConfigureAwait(false);
2024-10-11 11:21:54 +02:00
}
}
2024-11-19 13:55:23 +01:00
catch (Exception e)
{
if (TranslateException(null, ref e))
throw e;
throw;
}
// Remove the file if it exists
try
{
2025-02-17 16:45:51 +01:00
if (await ListAsync(cancellationToken).AnyAsync(entry => entry.Name == TEST_FILE_NAME).ConfigureAwait(false))
2024-11-19 13:55:23 +01:00
await DeleteAsync(TEST_FILE_NAME, cancellationToken).ConfigureAwait(false);
}
catch (Exception e)
{
if (TranslateException(TEST_FILE_NAME, ref e))
throw e;
throw new UserInformationException(Strings.ErrorDeleteFile(TEST_FILE_NAME, e.Message), "TestPreparationError");
}
2024-10-11 11:21:54 +02:00
// Test write permissions
using (var testStream = new MemoryStream(Encoding.UTF8.GetBytes(TEST_FILE_CONTENT)))
{
try
{
await PutAsync(TEST_FILE_NAME, testStream, cancellationToken).ConfigureAwait(false);
}
catch (Exception e)
{
2024-11-19 13:55:23 +01:00
// 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");
2024-10-11 11:21:54 +02:00
}
2009-03-21 12:46:54 +00:00
}
2024-10-11 11:21:54 +02:00
// Test read permissions
using (var testStream = new MemoryStream())
{
try
{
await GetAsync(TEST_FILE_NAME, testStream, cancellationToken).ConfigureAwait(false);
var readValue = Encoding.UTF8.GetString(testStream.ToArray());
if (readValue != TEST_FILE_CONTENT)
throw new Exception("Test file corrupted.");
}
catch (Exception e)
{
2024-11-19 13:55:23 +01:00
if (TranslateException(TEST_FILE_NAME, ref e))
throw e;
throw new UserInformationException(Strings.ErrorReadFile(TEST_FILE_NAME, e.Message), "TestReadError");
2024-10-11 11:21:54 +02:00
}
}
2024-10-11 11:21:54 +02:00
// Cleanup
try
{
await DeleteAsync(TEST_FILE_NAME, cancellationToken).ConfigureAwait(false);
}
catch (Exception e)
{
2024-11-19 13:55:23 +01:00
if (TranslateException(TEST_FILE_NAME, ref e))
throw e;
throw new UserInformationException(Strings.ErrorDeleteFile(TEST_FILE_NAME, e.Message), "TestCleanupError");
2024-10-11 11:21:54 +02:00
}
2013-05-05 17:07:24 +02:00
}
2024-11-19 13:55:23 +01:00
/// <inheritdoc />
public async Task CreateFolderAsync(CancellationToken cancellationToken)
2013-05-05 17:07:24 +02:00
{
2024-11-19 13:55:23 +01:00
try
{
// Try to create the directory
var client = await CreateClient(cancellationToken, false).ConfigureAwait(false);
var clientPath = PreparePathForClient(null, false);
if (_useCwdNames && clientPath.Contains('/') && clientPath != "/")
{
// 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);
// 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");
}
else
{
await client.CreateDirectory(clientPath, true, cancellationToken);
}
}
catch (Exception ex)
{
if (TranslateException(null, ref ex))
throw ex;
throw;
}
2009-01-25 19:33:36 +00:00
}
2024-11-19 13:55:23 +01:00
/// <inheritdoc />
2024-10-11 11:21:54 +02:00
public void Dispose()
2009-01-14 17:29:30 +00:00
{
2024-11-19 13:55:23 +01:00
_client?.Dispose();
}
2024-11-19 13:55:23 +01:00
/// <summary>
/// Create a new FTP client, or return the existing one if it already exists.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <param name="cwdFlag">A flag to override the CWD strategy.</param>
/// <returns>The FTP client.</returns>
private async Task<AsyncFtpClient> CreateClient(CancellationToken cancellationToken, bool? cwdFlag = null)
{
2024-11-19 13:55:23 +01:00
if (_client == null)
2024-10-11 11:21:54 +02:00
{
2024-11-19 13:55:23 +01:00
var client = new AsyncFtpClient
2024-10-11 11:21:54 +02:00
{
2024-11-19 13:55:23 +01:00
Host = _url.Host,
Port = _url.Port == -1 ? 21 : _url.Port,
2024-10-11 11:21:54 +02:00
Credentials = _userInfo,
Config = _ftpConfig,
Logger = _diagnosticsLog ? new DiagnosticsLogger() : null
2024-10-11 11:21:54 +02:00
};
2024-11-19 13:55:23 +01:00
client.ValidateCertificate += HandleValidateCertificate;
await client.Connect(cancellationToken).ConfigureAwait(false);
2024-10-11 11:21:54 +02:00
2024-11-19 13:55:23 +01:00
// Set up for relative paths
if (_relativePaths)
2024-10-11 11:21:54 +02:00
{
2024-11-19 13:55:23 +01:00
_initialCwd = await client.GetWorkingDirectory(cancellationToken).ConfigureAwait(false);
_initialCwd = _initialCwd?.TrimEnd('/');
2024-10-11 11:21:54 +02:00
}
2019-05-26 19:46:47 -07:00
2024-11-19 13:55:23 +01:00
// Setup the initial working directory, if needed
if (cwdFlag ?? _useCwdNames)
{
var clientPath = PreparePathForClient(null, false, client);
await client.SetWorkingDirectory(clientPath, cancellationToken).ConfigureAwait(false);
2024-10-11 11:21:54 +02:00
}
2009-01-15 12:57:52 +00:00
2024-11-19 13:55:23 +01:00
_client = client;
}
return _client;
2009-01-14 17:29:30 +00:00
}
2024-11-19 13:55:23 +01:00
/// <summary>
/// Handle the certificate validation event.
/// </summary>
/// <param name="control">The FTP client.</param>
/// <param name="e">The event arguments.</param>
2024-10-11 11:21:54 +02:00
private void HandleValidateCertificate(BaseFtpClient control, FtpSslValidationEventArgs e)
2010-05-27 19:37:14 +00:00
{
2024-10-11 11:21:54 +02:00
if (e.PolicyErrors == SslPolicyErrors.None || _accepAllCertificates)
2010-05-27 19:37:14 +00:00
{
2024-10-11 11:21:54 +02:00
e.Accept = true;
return;
2010-05-27 19:37:14 +00:00
}
2024-11-19 13:55:23 +01:00
e.Accept = false;
2024-10-11 11:21:54 +02:00
try
2010-05-27 19:37:14 +00:00
{
2024-11-19 13:55:23 +01:00
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;
2024-10-11 11:21:54 +02:00
}
catch
{
2010-05-27 19:37:14 +00:00
}
2024-11-19 13:55:23 +01:00
if (e.Accept == false && e.Certificate != null)
throw new SslCertificateValidator.InvalidCertificateException(e.Certificate?.GetCertHashString(), e.PolicyErrors);
}
/// <summary>
/// Prepare the path for the client.
/// </summary>
/// <param name="path">The path to prepare.</param>
/// <param name="cwdFlag">A flag to override the CWD strategy.</param>
/// <param name="client">The FTP client, if not using the class instance.</param>
/// <returns>The prepared path.</returns>
private string PreparePathForClient(string? path, bool? cwdFlag = null, AsyncFtpClient? client = null)
{
client = client ?? _client;
if (cwdFlag ?? _useCwdNames)
return string.IsNullOrWhiteSpace(path)
? string.Empty
: Uri.UnescapeDataString(path);
2024-11-19 13:55:23 +01:00
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 Uri.UnescapeDataString(remotePath);
2024-11-19 13:55:23 +01:00
if (path.StartsWith("/", StringComparison.Ordinal))
return Uri.UnescapeDataString(path);
2024-11-19 13:55:23 +01:00
return Uri.UnescapeDataString(remotePath + "/" + path);
2024-11-19 13:55:23 +01:00
}
/// <summary>
/// Translate an exception to a more user-friendly exception.
/// </summary>
/// <param name="filename">The filename provided.</param>
/// <param name="ex">The exception to translate.</param>
/// <returns>True if the exception was translated, otherwise false.</returns>
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;
2010-05-27 19:37:14 +00:00
}
private sealed class DiagnosticsLogger : IFtpLogger
{
private static readonly string LOGTAG = Logging.Log.LogTagFromType<DiagnosticsLogger>();
public void Log(FtpLogEntry entry)
{
var type = entry.Severity switch
{
FtpTraceLevel.Verbose => Logging.LogMessageType.Verbose,
FtpTraceLevel.Info => Logging.LogMessageType.Information,
FtpTraceLevel.Warn => Logging.LogMessageType.Warning,
FtpTraceLevel.Error => Logging.LogMessageType.Error,
_ => Logging.LogMessageType.Information
};
Logging.Log.WriteMessage(type, LOGTAG, "FtpLogMessage", entry.Exception, entry.Message);
}
}
2009-01-14 17:29:30 +00:00
}
}