2025-01-14 14:03:48 +01:00
// 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
2024-09-29 22:00:57 +02:00
// 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 ;
2015-01-20 21:07:24 +01:00
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 ;
2019-02-22 21:58:40 -06:00
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 ;
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>
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>
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 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>
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 ;
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>
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 />
2024-10-15 12:17:51 +02:00
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 ),
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 ),
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
2025-01-14 14:03:48 +01:00
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
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
2013-05-05 23:46:00 +02: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" ];
2009-07-16 20:36:49 +00:00
}
2024-10-11 11:21:54 +02:00
else
2024-09-29 22:00:57 +02:00
{
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 );
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 );
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 ,
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
2017-09-25 23:17:45 -06:00
{
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 )
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
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" );
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 )
{
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 />
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 );
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 ;
2024-11-19 06:52:17 +01:00
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 ;
2024-11-19 06:52:17 +01:00
throw new UserInformationException ( Strings . ErrorReadFile ( TEST_FILE_NAME , e . Message ), "TestReadError" );
2024-10-11 11:21:54 +02:00
}
}
2018-02-18 00:18:44 +01: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 ;
2024-11-19 06:52:17 +01:00
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 ();
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
{
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 ,
2024-12-31 16:47:28 +01:00
Config = _ftpConfig ,
Logger = _diagnosticsLog ? new DiagnosticsLogger () : null
2024-10-11 11:21:54 +02:00
};
2024-09-29 22:00:57 +02:00
2024-11-19 13:55:23 +01:00
client . ValidateCertificate += HandleValidateCertificate ;
2025-01-01 14:06:35 +01:00
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
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>
/// <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
}
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
}
}