2025-03-19 15:27:02 +01:00
// Copyright (C) 2025, The Duplicati Team
2025-01-14 14:03:48 +01:00
// 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
2024-09-29 22:00:57 +02:00
// DEALINGS IN THE SOFTWARE.
2024-02-28 15:45:30 +01:00
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 Duplicati.Library.Common.IO ;
using Duplicati.Library.Interface ;
2025-03-25 12:09:11 -03:00
using Duplicati.Library.Logging ;
2024-10-11 11:21:54 +02:00
using Duplicati.Library.Utility ;
2025-03-19 15:27:02 +01:00
using Duplicati.Library.Utility.Options ;
2024-10-11 11:21:54 +02:00
using FluentFTP ;
using FluentFTP.Client.BaseClient ;
using FluentFTP.Exceptions ;
using CoreUtility = Duplicati . Library . Utility . Utility ;
using Uri = System . Uri ;
2019-02-22 21:58:40 -06:00
2009-01-14 17:29:30 +00:00
namespace Duplicati.Library.Backend
{
2024-11-19 06:52:17 +01:00
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>
2026-01-27 13:08:20 +01:00
public class FTP : IStreamingBackend , IRenameEnabledBackend
2009-01-14 17:29:30 +00:00
{
2025-03-25 12:09:11 -03:00
private static readonly string LogTag = Log . LogTagFromType ( typeof ( FTP ));
2024-11-19 13:55:23 +01:00
/// <summary>
/// The credentials used to authenticate with the FTP server
/// </summary>
private readonly NetworkCredential ? _userInfo ;
/// <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
2025-03-25 12:09:11 -03:00
/// <summary>
/// Configuration key for the flag to ignore the PureFTPd limit issue, suppressing the exceptions.
///
/// Chosen not to have ftp prefix to be agnostic between aftp and ftp
/// </summary>
2025-04-01 15:49:14 +02:00
protected virtual string CONFIG_KEY_FTP_IGNORE_PUREFTP => "ignore-pureftpd-limit-issue" ;
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>
2024-12-31 16:47:28 +01:00
/// The configuration key for the FTP log to console
/// </summary>
protected virtual string CONFIG_KEY_FTP_LOGDIAGNOSTICS => "ftp-log-diagnostics" ;
/// <summary>
2024-11-21 14:37:25 +01:00
/// The configuration key for the FTP absolute paths option
2024-11-19 13:55:23 +01:00
/// </summary>
2024-11-21 13:13:44 +01:00
protected virtual string CONFIG_KEY_FTP_ABSOLUTE_PATH => "ftp-absolute-path" ;
2024-11-19 13:55:23 +01:00
/// <summary>
2024-11-21 14:37:25 +01:00
/// 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" ;
2024-10-15 12:17:51 +02:00
// 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>
2024-10-15 12:17:51 +02:00
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>
2024-10-15 12:17:51 +02:00
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>
2024-10-15 12:17:51 +02:00
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 default data connection type as a string
/// </summary>
2024-10-15 12:17:51 +02:00
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>
2024-10-15 12:17:51 +02:00
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>
2024-10-15 12:17:51 +02:00
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>
2024-10-15 12:17:51 +02:00
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>
2024-11-21 13:13:44 +01:00
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 ;
2025-03-25 12:09:11 -03:00
/// <summary>
/// Flag to ignore the PureFTPd limit issue, suppressing the exceptions.
/// </summary>
private readonly bool _IgnorePureFTPdLimitIssue ;
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>
2024-12-31 16:47:28 +01:00
/// The flag to indicate if diagnostics information should be logged
/// </summary>
private readonly bool _diagnosticsLog ;
/// <summary>
2025-03-19 15:27:02 +01:00
/// The ssl certificate options to use
2024-11-19 13:55:23 +01:00
/// </summary>
2025-03-19 15:27:02 +01:00
private readonly SslOptionsHelper . SslCertificateOptions _sslOptions ;
2024-11-19 13:55:23 +01:00
/// <summary>
2025-03-19 15:27:02 +01:00
/// The timeout options to use
2024-11-19 13:55:23 +01:00
/// </summary>
2025-03-19 15:27:02 +01:00
private readonly TimeoutOptionsHelper . Timeouts _timeouts ;
2024-10-11 11:21:54 +02:00
/// <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 />
2024-10-15 12:17:51 +02:00
public virtual IList < ICommandLineArgument > SupportedCommands =>
2025-03-19 15:27:02 +01:00
[
.. AuthOptionsHelper.GetOptions(),
2024-11-19 13:55:23 +01:00
new CommandLineArgument(CONFIG_KEY_DISABLE_UPLOAD_VERIFY, CommandLineArgument.ArgumentType.Boolean, Strings.DescriptionDisableUploadVerifyShort, Strings.DescriptionDisableUploadVerifyLong),
2024-11-21 13:13:44 +01:00
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"),
2024-12-31 16:47:28 +01:00
new CommandLineArgument(CONFIG_KEY_FTP_LOGDIAGNOSTICS, CommandLineArgument.ArgumentType.Boolean, Strings.DescriptionLogDiagnosticsShort, Strings.DescriptionLogDiagnosticsLong),
2024-10-15 12:17:51 +02:00
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),
2025-03-25 12:09:11 -03:00
new CommandLineArgument(CONFIG_KEY_FTP_IGNORE_PUREFTP, CommandLineArgument.ArgumentType.Boolean, Strings.DescriptionIgnorePureFTPShort, Strings.DescriptionIgnorePureFTPLong, "false"),
2025-03-19 15:27:02 +01:00
.. SslOptionsHelper.GetCertOnlyOptions(),
.. TimeoutOptionsHelper.GetOptions(),
] ;
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
2025-03-19 15:27:02 +01:00
_sslOptions = null !;
_timeouts = null !;
2024-11-19 13:55:23 +01:00
_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>
2025-01-14 14:03:48 +01:00
public FTP ( string url , Dictionary < string , string? > options )
2009-01-14 17:29:30 +00:00
{
2025-03-19 23:16:37 +01:00
_sslOptions = SslOptionsHelper . Parse ( options );
2011-09-24 11:39:50 +00:00
2013-05-05 23:46:00 +02: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
2025-03-20 10:15:27 +01:00
var auth = AuthOptionsHelper . Parse ( options , u );
2025-03-19 22:40:09 +01:00
if ( auth . HasUsername )
2009-01-15 12:57:52 +00:00
{
2025-03-19 22:40:09 +01:00
_userInfo = new NetworkCredential ()
2024-10-11 11:21:54 +02:00
{
2025-03-19 22:40:09 +01:00
UserName = auth . Username ,
Domain = ""
2024-10-11 11:21:54 +02:00
};
2018-06-28 21:07:23 +02:00
2025-03-19 22:40:09 +01:00
if ( auth . HasPassword )
_userInfo . Password = auth . Password ;
}
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 );
2024-11-21 14:37:25 +01:00
_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 );
2025-01-30 10:52:17 +01:00
var sslProtocols = CoreUtility . ParseFlagsOption ( options , CONFIG_KEY_FTP_SSL_PROTOCOLS , DEFAULT_SSL_PROTOCOLS );
2009-01-14 17:29:30 +00:00
2024-10-15 12:17:51 +02: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
2024-11-19 06:52:17 +01:00
2024-10-15 12:17:51 +02:00
// This flag takes precedence over ftp-data-connection-type
2024-11-19 06:52:17 +01:00
if ( CoreUtility . ParseBoolOption ( options , CONFIG_KEY_FTP_LEGACY_FTPPASSIVE ))
2024-10-15 12:17:51 +02:00
dataConnectionType = FtpDataConnectionType . AutoPassive ;
2024-11-19 06:52:17 +01:00
2024-10-15 12:58:37 +02:00
// This flag takes precedence over the ftp-passive flag
2024-11-19 06:52:17 +01:00
if ( CoreUtility . ParseBoolOption ( options , CONFIG_KEY_FTP_LEGACY_FTPREGULAR ))
2024-10-15 12:58:37 +02:00
dataConnectionType = FtpDataConnectionType . AutoActive ;
2024-11-19 06:52:17 +01:00
2024-10-15 12:17:51 +02:00
// 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 ;
2024-12-04 08:34:09 +01:00
encryptionMode = FtpEncryptionMode . Explicit ;
2024-10-15 12:17:51 +02:00
}
}
2024-11-19 06:52:17 +01:00
2024-10-11 11:21:54 +02:00
_logToConsole = CoreUtility . ParseBoolOption ( options , CONFIG_KEY_FTP_LOGTOCONSOLE );
_logPrivateInfoToConsole = CoreUtility . ParseBoolOption ( options , CONFIG_KEY_FTP_LOGPRIVATEINFOTOCONSOLE );
2024-12-31 16:47:28 +01:00
_diagnosticsLog = CoreUtility . ParseBoolOption ( options , CONFIG_KEY_FTP_LOGDIAGNOSTICS );
2025-03-19 23:16:37 +01:00
_timeouts = TimeoutOptionsHelper . Parse ( options );
2024-11-19 06:52:17 +01:00
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 ,
2025-03-19 15:27:02 +01:00
ValidateAnyCertificate = _sslOptions . AcceptAllCertificates ,
2024-12-26 10:41:06 +01:00
Noop = true
2024-10-11 11:21:54 +02:00
};
2009-01-14 17:29:30 +00:00
2025-03-25 12:09:11 -03:00
_IgnorePureFTPdLimitIssue = CoreUtility . ParseBoolOption ( options , CONFIG_KEY_FTP_IGNORE_PUREFTP );
2025-04-01 15:49:14 +02: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
2017-09-25 23:17:45 -06:00
{
2025-03-19 15:27:02 +01:00
var client = await CreateClient ( cancelToken ). ConfigureAwait ( false );
2024-11-19 13:55:23 +01:00
var remotePath = PreparePathForClient ( null );
2025-03-19 15:27:02 +01:00
2025-03-19 15:48:01 +01:00
items = await Utility . Utility . WithTimeout ( _timeouts . ListTimeout , cancelToken , ct =>
client . GetListing ( remotePath , FtpListOption . Modify | FtpListOption . Size , ct )
). ConfigureAwait ( false );
2025-03-25 12:09:11 -03:00
if ( client . ServerType == FtpServer . PureFTPd )
{
// If the list was truncated an exception has to be raised as the listing is incomplete and can lead to misleading backup/restore results
2025-04-01 15:49:14 +02:00
if ( client . LastReplies . Any ( x =>
2025-03-25 12:09:11 -03:00
x . Code == "226" &&
x . Message . Contains ( "truncated" , StringComparison . InvariantCultureIgnoreCase )))
throw new UserInformationException ( "PureFTPd server effectively truncated the listing due to LimitRecursion parameter - please check documentation for more information" , "PureFTPdTruncatedListing" );
2025-04-01 15:49:14 +02:00
2025-03-25 12:09:11 -03:00
// If no truncation occured and the ignore flag is not set, issue an advisory message
if (! _IgnorePureFTPdLimitIssue )
Log . WriteWarningMessage ( LogTag , "PureFTPdIssue" , null , Strings . DescriptionIgnorePureFTPLong );
2025-04-01 15:49:14 +02:00
}
2025-02-17 16:45:51 +01:00
}
catch ( Exception e )
{
2025-05-21 17:19:27 +02:00
TranslateException ( null , e );
2025-02-17 16:45:51 +01:00
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 )
2017-09-25 23:19:31 -06: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 ;
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
2017-09-25 23:19:31 -06:00
}
2024-11-19 13:55:23 +01:00
}
2017-09-25 23:17:45 -06: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-09-29 22:00:57 +02:00
2024-11-19 13:55:23 +01:00
try { streamLen = input . Length ; }
catch ( NotSupportedException ) { }
2019-02-22 21:58:40 -06:00
2025-03-19 15:27:02 +01:00
using var timeoutStream = input . ObserveReadTimeout ( _timeouts . ReadWriteTimeout , false );
var status = await client . UploadStream ( timeoutStream , clientRemoteName , createRemoteDir : false , token : cancelToken , progress : null ). ConfigureAwait ( false );
2024-11-19 13:55:23 +01:00
if ( status != FtpStatus . Success )
throw new UserInformationException ( Strings . ErrorWriteFile ( remotename , $"Status is {status}" ), "FtpPutFailure" );
2015-01-20 21:07:24 +01:00
2024-11-19 13:55:23 +01:00
// Wait for the upload, if required
if ( _uploadWaitTime . Ticks > 0 )
Thread . Sleep ( _uploadWaitTime );
2024-09-29 22:00:57 +02:00
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 )
{
2025-05-21 17:19:27 +02:00
TranslateException ( remotename , e );
2024-11-19 13:55:23 +01:00
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 );
2025-03-21 14:13:21 +01:00
await using var inputStream = await Utility . Utility . WithTimeout ( _timeouts . ShortTimeout , cancelToken , ct => client . OpenRead ( clientRemoteName , token : ct )). ConfigureAwait ( false );
2025-03-19 15:27:02 +01:00
await using var timeoutStream = inputStream . ObserveReadTimeout ( _timeouts . ReadWriteTimeout );
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
{
2025-05-21 17:19:27 +02:00
TranslateException ( remotename , e );
2024-11-19 13:55:23 +01:00
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 />
2024-09-29 22:00:57 +02:00
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 );
2025-03-19 15:48:01 +01:00
await Utility . Utility . WithTimeout ( _timeouts . ShortTimeout , cancelToken , ct =>
client . DeleteFile ( clientRemoteName , ct )
). ConfigureAwait ( false );
2009-03-21 12:46:54 +00:00
}
2024-11-19 13:55:23 +01:00
catch ( Exception e )
{
2025-05-21 17:19:27 +02:00
TranslateException ( remotename , e );
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
2025-11-03 12:48:37 +01:00
/// <inheritdoc/>
public bool SupportsStreaming => true ;
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
{
2025-03-25 12:09:11 -03:00
// Start with a simple list and pureFTP detection
try
{
var client = await CreateClient ( cancellationToken ). ConfigureAwait ( false );
await Utility . Utility . WithTimeout ( _timeouts . ShortTimeout , cancellationToken , async ct =>
{
await ListAsync ( cancellationToken ). AnyAsync ( cancellationToken ). ConfigureAwait ( false );
}). ConfigureAwait ( false );
2025-04-01 15:49:14 +02:00
2025-03-25 12:09:11 -03:00
if ( client . ServerType == FtpServer . PureFTPd && ! _IgnorePureFTPdLimitIssue )
throw new UserInformationException ( Strings . DescriptionIgnorePureFTPLong , "PureFTPdDetected" );
2025-04-01 15:49:14 +02:00
2025-03-25 12:09:11 -03:00
}
catch ( Exception e )
{
2025-05-21 17:19:27 +02:00
TranslateException ( null , e );
2025-03-25 12:09:11 -03:00
throw ;
}
2025-04-01 15:49:14 +02: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 );
2025-03-19 15:48:01 +01:00
await Utility . Utility . WithTimeout ( _timeouts . ShortTimeout , cancellationToken , async ct =>
2024-10-11 11:21:54 +02:00
{
2025-03-19 15:27:02 +01:00
if (! _useCwdNames )
{
var folderpath = PreparePathForClient ( null );
await client . SetWorkingDirectory ( folderpath , ct ). ConfigureAwait ( false );
}
2025-03-19 15:48:01 +01:00
}). ConfigureAwait ( false );
2024-10-11 11:21:54 +02:00
}
2024-11-19 13:55:23 +01:00
catch ( Exception e )
{
2025-05-21 17:19:27 +02:00
TranslateException ( null , e );
2024-11-19 13:55:23 +01:00
throw ;
}
2025-05-21 17:19:27 +02:00
// Test the read/write permissions in folder
await this . TestReadWritePermissionsAsync ( cancellationToken ). ConfigureAwait ( false );
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
{
var client = await CreateClient ( cancellationToken , false ). ConfigureAwait ( false );
var clientPath = PreparePathForClient ( null , false );
2025-03-19 15:27:02 +01:00
2025-03-19 15:48:01 +01:00
await Utility . Utility . WithTimeout ( _timeouts . ShortTimeout , cancellationToken , async ct =>
2024-11-19 13:55:23 +01:00
{
2025-03-19 15:27:02 +01:00
// Try to create the directory
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 , ct ). ConfigureAwait ( false );
await client . CreateDirectory ( folderName , true , ct ). ConfigureAwait ( false );
// Reset the client and check that it works
_client = null ;
client = await CreateClient ( ct ). ConfigureAwait ( false );
var cwd = await client . GetWorkingDirectory ( ct ). ConfigureAwait ( false );
if (! string . Equals ( cwd ?. TrimEnd ( '/' ), clientPath , StringComparison . OrdinalIgnoreCase ))
throw new UserInformationException ( Strings . ErrorCreateFolder ( clientPath , cwd ), "CreateFolderError" );
}
else
{
await client . CreateDirectory ( clientPath , true , ct );
}
2025-03-19 15:48:01 +01:00
}). ConfigureAwait ( false );
2024-11-19 13:55:23 +01:00
}
catch ( Exception ex )
{
2025-05-21 17:19:27 +02:00
TranslateException ( null , ex );
2024-11-19 13:55:23 +01:00
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 ();
2012-01-19 19:04:56 +00:00
}
2024-09-29 22:00:57 +02:00
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 )
2012-01-19 19:04:56 +00:00
{
2024-11-19 13:55:23 +01:00
if ( _client == null )
2024-10-11 11:21:54 +02:00
{
2025-03-19 15:48:01 +01:00
_client = await Utility . Utility . WithTimeout ( _timeouts . ShortTimeout , cancellationToken , async ct =>
2024-10-11 11:21:54 +02:00
{
2025-03-19 15:27:02 +01:00
var client = new AsyncFtpClient
{
Host = _url . Host ,
Port = _url . Port == - 1 ? 21 : _url . Port ,
Credentials = _userInfo ,
Config = _ftpConfig ,
Logger = _diagnosticsLog ? new DiagnosticsLogger () : null
};
client . ValidateCertificate += HandleValidateCertificate ;
await client . Connect ( ct ). ConfigureAwait ( false );
// Set up for relative paths
if ( _relativePaths )
{
_initialCwd = await client . GetWorkingDirectory ( ct ). 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 , ct ). ConfigureAwait ( false );
}
return client ;
2025-03-19 15:48:01 +01:00
}). ConfigureAwait ( false );
2024-11-19 13:55:23 +01:00
}
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
{
2025-03-19 15:27:02 +01:00
if ( e . PolicyErrors == SslPolicyErrors . None || _sslOptions . AcceptAllCertificates )
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
{
2025-03-19 15:27:02 +01:00
var certHash = ( _sslOptions . AcceptSpecificCertificateHashes != null && _sslOptions . AcceptSpecificCertificateHashes . Length > 0 ) ? e . Certificate ?. GetCertHashString () : null ;
if ( certHash != null && _sslOptions . AcceptSpecificCertificateHashes != null && _sslOptions . AcceptSpecificCertificateHashes . Any ( hash => ! string . IsNullOrEmpty ( hash ) && certHash . Equals ( hash , StringComparison . OrdinalIgnoreCase )))
2024-11-19 13:55:23 +01:00
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 )
2025-03-19 15:27:02 +01:00
throw new SslCertificateValidator . InvalidCertificateException ( e . Certificate ?. GetCertHashString () ?? "<unknown>" , e . PolicyErrors );
2024-11-19 13:55:23 +01:00
}
/// <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
2024-12-17 10:11:45 -03:00
: 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 ))
2024-12-17 10:11:45 -03:00
return Uri . UnescapeDataString ( remotePath );
2024-11-19 13:55:23 +01:00
if ( path . StartsWith ( "/" , StringComparison . Ordinal ))
2024-12-17 10:11:45 -03:00
return Uri . UnescapeDataString ( path );
2024-11-19 13:55:23 +01:00
2024-12-17 10:11:45 -03: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>
2025-05-21 17:19:27 +02:00
private void TranslateException ( string? filename , Exception ex )
2024-11-19 13:55:23 +01:00
{
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 );
2025-05-21 17:19:27 +02:00
throw ex ;
2024-11-19 13:55:23 +01:00
}
if ( ex is SslCertificateValidator . InvalidCertificateException )
2025-05-21 17:19:27 +02:00
throw ex ;
2010-05-27 19:37:14 +00:00
}
2024-12-31 16:47:28 +01:00
2026-01-27 13:08:20 +01:00
public async Task RenameAsync ( string oldname , string newname , CancellationToken cancellationToken )
{
try
{
var client = await CreateClient ( cancellationToken ). ConfigureAwait ( false );
await Utility . Utility . WithTimeout ( _timeouts . ShortTimeout , cancellationToken , async ct =>
{
var oldPath = PreparePathForClient ( oldname , false , client );
var newPath = PreparePathForClient ( newname , false , client );
await client . Rename ( oldPath , newPath , ct ). ConfigureAwait ( false );
}). ConfigureAwait ( false );
}
catch ( Exception e )
{
TranslateException ( oldname , e );
throw ;
}
}
2024-12-31 16:47:28 +01: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
}
}