2024-02-28 15:45:30 +01:00
// Copyright (C) 2024, 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.
2016-04-27 14:48:11 +02:00
2019-08-19 19:54:29 -04:00
using Duplicati.Library.Common.IO ;
2019-02-22 21:58:40 -06:00
using Duplicati.Library.Interface ;
2023-05-23 02:25:54 +03:00
using Duplicati.Library.Utility ;
2019-02-17 09:52:15 -06:00
using FluentFTP ;
2023-05-18 01:27:29 +03:00
using FluentFTP.Client.BaseClient ;
using FluentFTP.Exceptions ;
2016-04-27 14:48:11 +02:00
using System ;
using System.Collections.Generic ;
2019-02-17 09:52:15 -06:00
using System.IO ;
2016-04-27 14:48:11 +02:00
using System.Linq ;
using System.Net.Security ;
using System.Security.Authentication ;
2019-02-22 21:58:40 -06:00
using System.Threading ;
using System.Threading.Tasks ;
2016-04-27 14:48:11 +02:00
using CoreUtility = Duplicati . Library . Utility . Utility ;
2021-06-12 11:30:08 -07:00
using Uri = System . Uri ;
2016-04-27 14:48:11 +02:00
namespace Duplicati.Library.Backend.AlternativeFTP
{
// ReSharper disable once RedundantExtendsListEntry
public class AlternativeFtpBackend : IBackend , IStreamingBackend
{
private System . Net . NetworkCredential _userInfo ;
private const string OPTION_ACCEPT_SPECIFIED_CERTIFICATE = "accept-specified-ssl-hash" ; // Global option
private const string OPTION_ACCEPT_ANY_CERTIFICATE = "accept-any-ssl-certificate" ; // Global option
private const FtpDataConnectionType DEFAULT_DATA_CONNECTION_TYPE = FtpDataConnectionType . AutoPassive ;
private const FtpEncryptionMode DEFAULT_ENCRYPTION_MODE = FtpEncryptionMode . None ;
private const SslProtocols DEFAULT_SSL_PROTOCOLS = SslProtocols . Default ;
private const string CONFIG_KEY_AFTP_ENCRYPTION_MODE = "aftp-encryption-mode" ;
private const string CONFIG_KEY_AFTP_DATA_CONNECTION_TYPE = "aftp-data-connection-type" ;
private const string CONFIG_KEY_AFTP_SSL_PROTOCOLS = "aftp-ssl-protocols" ;
2019-10-21 08:29:34 +02:00
private const string CONFIG_KEY_AFTP_UPLOAD_DELAY = "aftp-upload-delay" ;
2016-04-27 14:48:11 +02:00
2023-09-24 10:52:57 +02:00
private const string CONFIG_KEY_AFTP_LOGTOCONSOLE = "aftp-log-to-console" ;
private const string CONFIG_KEY_AFTP_LOGPRIVATEINFOTOCONSOLE = "aftp-log-privateinfo-to-console" ;
2017-05-24 01:39:30 +07:00
private const string TEST_FILE_NAME = "duplicati-access-privileges-test.tmp" ;
2019-10-21 08:29:34 +02:00
private const string TEST_FILE_CONTENT = "This file is used by Duplicati to test access permissions and can be safely deleted." ;
2017-05-24 01:39:30 +07:00
2016-04-27 14:48:11 +02:00
// ReSharper disable InconsistentNaming
private static readonly string DEFAULT_DATA_CONNECTION_TYPE_STRING = DEFAULT_DATA_CONNECTION_TYPE . ToString ();
private static readonly string DEFAULT_ENCRYPTION_MODE_STRING = DEFAULT_ENCRYPTION_MODE . ToString ();
private static readonly string DEFAULT_SSL_PROTOCOLS_STRING = DEFAULT_SSL_PROTOCOLS . ToString ();
2019-10-21 08:29:34 +02:00
private static readonly string DEFAULT_UPLOAD_DELAY_STRING = "0s" ;
2016-04-27 14:48:11 +02:00
// ReSharper restore InconsistentNaming
private readonly string _url ;
private readonly bool _listVerify = true ;
2023-05-18 01:27:29 +03:00
private readonly FtpConfig _ftpConfig ;
2019-10-21 08:29:34 +02:00
private readonly TimeSpan _uploadWaitTime ;
2016-04-27 14:48:11 +02:00
2023-09-24 10:52:57 +02:00
private readonly bool _logToConsole = false ;
private readonly bool _logPrivateInfoToConsole = false ;
2016-04-27 14:48:11 +02:00
private readonly byte [] _copybuffer = new byte [ CoreUtility . DEFAULT_BUFFER_SIZE ];
private readonly bool _accepAllCertificates ;
private readonly string [] _validHashes ;
/// <summary>
/// The localized name to display for this backend
/// </summary>
public string DisplayName
{
get { return Strings . DisplayName ; }
}
/// <summary>
/// The protocol key, eg. ftp, http or ssh
/// </summary>
public string ProtocolKey
{
get { return "aftp" ; }
}
2023-05-18 01:27:29 +03:00
private AsyncFtpClient Client
2016-12-29 15:00:19 +01:00
{ get ; set ; }
2016-04-27 14:48:11 +02:00
public IList < ICommandLineArgument > SupportedCommands
{
get
{
return new List < ICommandLineArgument >( new ICommandLineArgument [] {
2017-05-24 01:39:30 +07:00
new CommandLineArgument ( "auth-password" , CommandLineArgument . ArgumentType . Password , Strings . DescriptionAuthPasswordShort , Strings . DescriptionAuthPasswordLong ),
new CommandLineArgument ( "auth-username" , CommandLineArgument . ArgumentType . String , Strings . DescriptionAuthUsernameShort , Strings . DescriptionAuthUsernameLong ),
new CommandLineArgument ( "disable-upload-verify" , CommandLineArgument . ArgumentType . Boolean , Strings . DescriptionDisableUploadVerifyShort , Strings . DescriptionDisableUploadVerifyLong ),
new CommandLineArgument ( CONFIG_KEY_AFTP_DATA_CONNECTION_TYPE , CommandLineArgument . ArgumentType . Enumeration , Strings . DescriptionFtpDataConnectionTypeShort , Strings . DescriptionFtpDataConnectionTypeLong , DEFAULT_DATA_CONNECTION_TYPE_STRING , null , Enum . GetNames ( typeof ( FtpDataConnectionType ))),
new CommandLineArgument ( CONFIG_KEY_AFTP_ENCRYPTION_MODE , CommandLineArgument . ArgumentType . Enumeration , Strings . DescriptionFtpEncryptionModeShort , Strings . DescriptionFtpEncryptionModeLong , DEFAULT_ENCRYPTION_MODE_STRING , null , Enum . GetNames ( typeof ( FtpEncryptionMode ))),
new CommandLineArgument ( CONFIG_KEY_AFTP_SSL_PROTOCOLS , CommandLineArgument . ArgumentType . Flags , Strings . DescriptionSslProtocolsShort , Strings . DescriptionSslProtocolsLong , DEFAULT_SSL_PROTOCOLS_STRING , null , Enum . GetNames ( typeof ( SslProtocols ))),
2019-10-21 08:29:34 +02:00
new CommandLineArgument ( CONFIG_KEY_AFTP_UPLOAD_DELAY , CommandLineArgument . ArgumentType . Timespan , Strings . DescriptionUploadDelayShort , Strings . DescriptionUploadDelayLong , DEFAULT_UPLOAD_DELAY_STRING ),
2023-09-24 10:52:57 +02:00
new CommandLineArgument ( CONFIG_KEY_AFTP_LOGTOCONSOLE , CommandLineArgument . ArgumentType . Boolean , Strings . DescriptionLogToConsoleShort , Strings . DescriptionLogToConsoleLong ),
new CommandLineArgument ( CONFIG_KEY_AFTP_LOGPRIVATEINFOTOCONSOLE , CommandLineArgument . ArgumentType . Boolean , Strings . DescriptionLogPrivateInfoToConsoleShort , Strings . DescriptionLogPrivateInfoToConsoleLong , "false" ),
2017-05-24 01:39:30 +07:00
});
2016-04-27 14:48:11 +02:00
}
}
/// <summary>
/// Initialize a new instance.
/// </summary>
public AlternativeFtpBackend ()
{
}
/// <summary>
/// Initialize a new instance/
/// </summary>
/// <param name="url">Configured url.</param>
/// <param name="options">Configured options. cannot be null.</param>
public AlternativeFtpBackend ( string url , Dictionary < string , string > options )
{
_accepAllCertificates = CoreUtility . ParseBoolOption ( options , OPTION_ACCEPT_ANY_CERTIFICATE );
string certHash ;
options . TryGetValue ( OPTION_ACCEPT_SPECIFIED_CERTIFICATE , out certHash );
_validHashes = certHash == null ? null : certHash . Split ( new [] { "," , ";" }, StringSplitOptions . RemoveEmptyEntries );
var u = new Utility . Uri ( url );
u . RequireHost ();
if (! string . IsNullOrEmpty ( u . Username ))
{
_userInfo = new System . Net . NetworkCredential ();
_userInfo . UserName = u . Username ;
if (! string . IsNullOrEmpty ( u . Password ))
_userInfo . Password = u . Password ;
else if ( options . ContainsKey ( "auth-password" ))
_userInfo . Password = options [ "auth-password" ];
}
else
{
if ( options . ContainsKey ( "auth-username" ))
{
_userInfo = new System . Net . NetworkCredential ();
_userInfo . UserName = options [ "auth-username" ];
if ( options . ContainsKey ( "auth-password" ))
_userInfo . Password = options [ "auth-password" ];
}
}
//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
if ( _userInfo != null )
_userInfo . Domain = "" ;
_url = u . SetScheme ( "ftp" ). SetQuery ( null ). SetCredentials ( null , null ). ToString ();
2018-11-02 21:34:07 +01:00
_url = Common . IO . Util . AppendDirSeparator ( _url , "/" );
2016-04-27 14:48:11 +02:00
_listVerify = ! CoreUtility . ParseBoolOption ( options , "disable-upload-verify" );
2019-10-21 08:29:34 +02:00
if ( options . TryGetValue ( CONFIG_KEY_AFTP_UPLOAD_DELAY , out var uploadWaitTimeString ) && ! string . IsNullOrWhiteSpace ( uploadWaitTimeString ))
_uploadWaitTime = Duplicati . Library . Utility . Timeparser . ParseTimeSpan ( uploadWaitTimeString );
2016-04-27 14:48:11 +02:00
// Process the aftp-data-connection-type option
string dataConnectionTypeString ;
2023-05-18 01:27:29 +03:00
FtpDataConnectionType dataConnectionType ;
2016-04-27 14:48:11 +02:00
if (! options . TryGetValue ( CONFIG_KEY_AFTP_DATA_CONNECTION_TYPE , out dataConnectionTypeString ) || string . IsNullOrWhiteSpace ( dataConnectionTypeString ))
{
dataConnectionTypeString = null ;
}
2023-05-18 01:27:29 +03:00
if ( dataConnectionTypeString == null || ! Enum . TryParse ( dataConnectionTypeString , true , out dataConnectionType ))
2016-04-27 14:48:11 +02:00
{
2023-05-18 01:27:29 +03:00
dataConnectionType = DEFAULT_DATA_CONNECTION_TYPE ;
2016-04-27 14:48:11 +02:00
}
// Process the aftp-encryption-mode option
string encryptionModeString ;
2023-05-18 01:27:29 +03:00
FtpEncryptionMode encryptionMode ;
2016-04-27 14:48:11 +02:00
if (! options . TryGetValue ( CONFIG_KEY_AFTP_ENCRYPTION_MODE , out encryptionModeString ) || string . IsNullOrWhiteSpace ( encryptionModeString ))
{
encryptionModeString = null ;
}
2023-05-18 01:27:29 +03:00
if ( encryptionModeString == null || ! Enum . TryParse ( encryptionModeString , true , out encryptionMode ))
2016-04-27 14:48:11 +02:00
{
2023-05-18 01:27:29 +03:00
encryptionMode = DEFAULT_ENCRYPTION_MODE ;
2016-04-27 14:48:11 +02:00
}
// Process the aftp-ssl-protocols option
string sslProtocolsString ;
2023-05-18 01:27:29 +03:00
SslProtocols sslProtocols ;
2016-04-27 14:48:11 +02:00
2016-09-15 11:39:27 +02:00
if (! options . TryGetValue ( CONFIG_KEY_AFTP_SSL_PROTOCOLS , out sslProtocolsString ) || string . IsNullOrWhiteSpace ( sslProtocolsString ))
2016-04-27 14:48:11 +02:00
{
sslProtocolsString = null ;
}
2023-05-18 01:27:29 +03:00
if ( sslProtocolsString == null || ! Enum . TryParse ( sslProtocolsString , true , out sslProtocols ))
2016-04-27 14:48:11 +02:00
{
2023-05-18 01:27:29 +03:00
sslProtocols = DEFAULT_SSL_PROTOCOLS ;
2016-04-27 14:48:11 +02:00
}
2023-05-18 01:27:29 +03:00
2023-09-24 10:52:57 +02:00
if ( Utility . Utility . ParseBoolOption ( options , CONFIG_KEY_AFTP_LOGTOCONSOLE ))
{
_logToConsole = true ;
}
if ( Utility . Utility . ParseBoolOption ( options , CONFIG_KEY_AFTP_LOGPRIVATEINFOTOCONSOLE ))
{
_logPrivateInfoToConsole = true ;
}
2023-05-18 01:27:29 +03:00
_ftpConfig = new FtpConfig
{
DataConnectionType = dataConnectionType ,
EncryptionMode = encryptionMode ,
SslProtocols = sslProtocols ,
2023-09-24 10:52:57 +02:00
LogToConsole = _logToConsole ,
2023-05-18 01:27:29 +03:00
};
2023-09-24 10:52:57 +02:00
if ( _logPrivateInfoToConsole )
{
_ftpConfig . LogHost = _ftpConfig . LogPassword = _ftpConfig . LogUserName = true ;
}
2016-04-27 14:48:11 +02:00
}
2017-09-25 23:17:45 -06:00
public IEnumerable < IFileEntry > List ()
2016-04-27 14:48:11 +02:00
{
return List ( "" );
}
2017-09-25 23:17:45 -06:00
public IEnumerable < IFileEntry > List ( string filename )
2016-04-27 14:48:11 +02:00
{
return List ( filename , false );
}
2017-09-25 23:17:45 -06:00
private IEnumerable < IFileEntry > List ( string filename , bool stripFile )
2016-04-27 14:48:11 +02:00
{
var list = new List < IFileEntry >();
string remotePath = filename ;
2023-05-23 02:25:54 +03:00
var ftpClient = CreateClient ();
2016-12-29 21:20:12 +01:00
2023-05-23 02:25:54 +03:00
// Get the remote path
var url = new Uri ( this . _url );
remotePath = "/" + this . GetUnescapedAbsolutePath ( url );
2016-04-27 14:48:11 +02:00
2023-05-23 02:25:54 +03:00
if (! string . IsNullOrEmpty ( filename ))
{
if (! stripFile )
2016-12-29 21:20:12 +01:00
{
2023-05-23 02:25:54 +03:00
// Append the filename
remotePath += filename ;
2016-12-29 21:20:12 +01:00
}
2023-05-23 02:25:54 +03:00
else if ( filename . Contains ( "/" ))
{
remotePath += filename . Substring ( 0 , filename . LastIndexOf ( "/" , StringComparison . Ordinal ));
}
// else: stripping the filename in this case ignoring it
}
2016-04-27 14:48:11 +02:00
2023-05-23 02:25:54 +03:00
foreach ( FtpListItem item in ftpClient . GetListing ( remotePath , FtpListOption . Modify | FtpListOption . Size ). Await ())
{
switch ( item . Type )
2016-12-29 21:20:12 +01:00
{
2023-05-23 02:25:54 +03:00
case FtpObjectType . Directory :
{
if ( item . Name == "." || item . Name == ".." )
2016-04-27 14:48:11 +02:00
{
2023-05-23 02:25:54 +03:00
continue ;
2016-04-27 14:48:11 +02:00
}
2023-05-23 02:25:54 +03:00
list . Add ( new FileEntry ( item . Name , - 1 , new DateTime (), item . Modified )
2016-04-27 14:48:11 +02:00
{
2023-05-23 02:25:54 +03:00
IsFolder = true ,
});
break ;
}
case FtpObjectType . File :
{
list . Add ( new FileEntry ( item . Name , item . Size , new DateTime (), item . Modified ));
break ;
}
case FtpObjectType . Link :
{
if ( item . Name == "." || item . Name == ".." )
{
continue ;
}
2016-04-27 14:48:11 +02:00
2023-05-23 02:25:54 +03:00
if ( item . LinkObject != null )
{
switch ( item . LinkObject . Type )
2016-04-27 14:48:11 +02:00
{
2023-05-23 02:25:54 +03:00
case FtpObjectType . Directory :
{
if ( item . Name == "." || item . Name == ".." )
2016-04-27 14:48:11 +02:00
{
2023-05-23 02:25:54 +03:00
continue ;
2016-12-29 21:20:12 +01:00
}
2023-05-23 02:25:54 +03:00
list . Add ( new FileEntry ( item . Name , - 1 , new DateTime (), item . Modified )
2016-12-29 21:20:12 +01:00
{
2023-05-23 02:25:54 +03:00
IsFolder = true ,
});
2016-04-27 14:48:11 +02:00
2023-05-23 02:25:54 +03:00
break ;
}
case FtpObjectType . File :
{
list . Add ( new FileEntry ( item . Name , item . Size , new DateTime (), item . Modified ));
break ;
}
2016-04-27 14:48:11 +02:00
}
}
2023-05-23 02:25:54 +03:00
break ;
}
2016-12-29 21:20:12 +01:00
2016-04-27 14:48:11 +02:00
}
}
return list ;
}
2019-03-17 18:20:14 -05:00
public async Task PutAsync ( string remotename , Stream input , CancellationToken cancelToken )
2016-04-27 14:48:11 +02:00
{
string remotePath = remotename ;
2019-03-07 18:16:39 -06:00
long streamLen ;
2016-04-27 14:48:11 +02:00
2023-05-23 02:25:54 +03:00
var ftpClient = CreateClient ();
2016-04-27 14:48:11 +02:00
try
{
2023-05-23 02:25:54 +03:00
streamLen = input . Length ;
}
catch ( NotSupportedException ) { streamLen = - 1 ; }
2016-12-29 21:20:12 +01:00
2023-05-23 02:25:54 +03:00
// Get the remote path
remotePath = "" ;
2016-04-27 14:48:11 +02:00
2023-05-23 02:25:54 +03:00
if (! string . IsNullOrEmpty ( remotename ))
{
// Append the filename
remotePath += remotename ;
}
2016-04-27 14:48:11 +02:00
2023-05-23 02:25:54 +03:00
var status = await ftpClient . UploadStream ( input , remotePath , FtpRemoteExists . Overwrite , createRemoteDir : false , token : cancelToken , progress : null ). ConfigureAwait ( false );
if ( status != FtpStatus . Success )
{
throw new UserInformationException ( string . Format ( Strings . ErrorWriteFile , remotename ), "AftpPutFailure" );
}
2019-10-21 08:29:34 +02:00
2023-05-23 02:25:54 +03:00
// Wait for the upload, if required
if ( _uploadWaitTime . Ticks > 0 )
{
Thread . Sleep ( _uploadWaitTime );
2016-04-27 14:48:11 +02:00
}
2023-05-23 02:25:54 +03:00
if ( _listVerify )
2016-04-27 14:48:11 +02:00
{
2023-05-23 02:25:54 +03:00
// check remote file size; matching file size indicates completion
var remoteSize = await ftpClient . GetFileSize ( remotePath , - 1 , cancelToken );
if ( streamLen != remoteSize )
2016-04-27 14:48:11 +02:00
{
2023-05-23 02:25:54 +03:00
throw new UserInformationException ( Strings . ListVerifySizeFailure ( remotename , remoteSize , streamLen ), "AftpListVerifySizeFailure" );
2016-04-27 14:48:11 +02:00
}
}
}
2021-06-12 11:30:08 -07:00
public async Task PutAsync ( string remotename , string localname , CancellationToken cancelToken )
2016-04-27 14:48:11 +02:00
{
2019-02-17 09:52:15 -06:00
using ( FileStream fs = File . Open ( localname , FileMode . Open , FileAccess . Read , FileShare . Read ))
2016-04-27 14:48:11 +02:00
{
2021-06-12 11:30:08 -07:00
await PutAsync ( remotename , fs , cancelToken );
2016-04-27 14:48:11 +02:00
}
}
2019-02-17 09:52:15 -06:00
public void Get ( string remotename , Stream output )
2016-04-27 14:48:11 +02:00
{
2016-12-29 21:20:12 +01:00
var ftpClient = CreateClient ();
// Get the remote path
var remotePath = "" ;
if (! string . IsNullOrEmpty ( remotename ))
2016-04-27 14:48:11 +02:00
{
2016-12-29 21:20:12 +01:00
// Append the filename
remotePath += remotename ;
}
2016-04-27 14:48:11 +02:00
2023-05-23 02:25:54 +03:00
using ( var inputStream = ftpClient . OpenRead ( remotePath ). Await ())
2016-12-29 21:20:12 +01:00
{
try
2016-04-27 14:48:11 +02:00
{
2016-12-29 21:20:12 +01:00
CoreUtility . CopyStream ( inputStream , output , false , _copybuffer );
2016-04-27 14:48:11 +02:00
}
2016-12-29 21:20:12 +01:00
finally
2016-04-27 14:48:11 +02:00
{
2016-12-29 21:20:12 +01:00
inputStream . Close ();
2016-04-27 14:48:11 +02:00
}
}
2016-12-29 21:20:12 +01:00
2016-04-27 14:48:11 +02:00
}
public void Get ( string remotename , string localname )
{
2019-02-17 09:52:15 -06:00
using ( FileStream fs = File . Open ( localname , FileMode . Create , FileAccess . Write , FileShare . None ))
2016-04-27 14:48:11 +02:00
{
Get ( remotename , fs );
}
}
public void Delete ( string remotename )
{
2016-12-29 21:20:12 +01:00
var ftpClient = CreateClient ();
2016-04-27 14:48:11 +02:00
2016-12-29 21:20:12 +01:00
// Get the remote path
var remotePath = "" ;
2016-04-27 14:48:11 +02:00
2016-12-29 21:20:12 +01:00
if (! string . IsNullOrEmpty ( remotename ))
{
// Append the filename
remotePath += remotename ;
2016-04-27 14:48:11 +02:00
}
2016-12-29 21:20:12 +01:00
2023-05-23 02:25:54 +03:00
ftpClient . DeleteFile ( remotePath ). Await ();
2016-12-29 21:20:12 +01:00
2016-04-27 14:48:11 +02:00
}
/// <summary>
/// A localized description of the backend, for display in the usage information
/// </summary>
public string Description
{
get
{
return Strings . Description ;
}
}
2018-02-26 11:37:10 +01:00
public string [] DNSName
2018-02-21 22:38:01 +01:00
{
2018-02-26 11:37:10 +01:00
get { return new string [] { new Uri ( _url ). Host }; }
2018-02-21 22:38:01 +01:00
}
2018-02-18 00:18:44 +01:00
2017-05-24 01:39:30 +07:00
/// <summary>
/// Test FTP access permissions.
/// </summary>
2016-04-27 14:48:11 +02:00
public void Test ()
{
2017-05-24 01:39:30 +07:00
var list = List ();
// Delete test file if exists
if ( list . Any ( entry => entry . Name == TEST_FILE_NAME ))
{
try
{
Delete ( TEST_FILE_NAME );
}
catch ( Exception e )
{
2023-05-23 02:25:54 +03:00
if ( e . InnerException != null ) { e = e . InnerException ; }
2017-05-24 01:39:30 +07:00
throw new Exception ( string . Format ( Strings . ErrorDeleteFile , e . Message ), e );
}
}
// Test write permissions
2021-06-02 19:35:40 +10:00
using ( var testStream = new MemoryStream ( System . Text . Encoding . UTF8 . GetBytes ( TEST_FILE_CONTENT )))
2017-05-24 01:39:30 +07:00
{
try
{
2023-05-23 02:25:54 +03:00
PutAsync ( TEST_FILE_NAME , testStream , CancellationToken . None ). Await ();
2017-05-24 01:39:30 +07:00
}
catch ( Exception e )
{
2023-05-23 02:25:54 +03:00
if ( e . InnerException != null ) { e = e . InnerException ; }
2017-05-24 01:39:30 +07:00
throw new Exception ( string . Format ( Strings . ErrorWriteFile , e . Message ), e );
}
}
// Test read permissions
2021-06-01 02:51:42 +10:00
using ( var testStream = new MemoryStream ())
2017-05-24 01:39:30 +07:00
{
try
{
2021-06-01 02:51:42 +10:00
Get ( TEST_FILE_NAME , testStream );
2021-06-02 19:35:40 +10:00
var readValue = System . Text . Encoding . UTF8 . GetString ( testStream . ToArray ());
2021-06-01 02:51:42 +10:00
if ( readValue != TEST_FILE_CONTENT )
throw new Exception ( "Test file corrupted." );
2017-05-24 01:39:30 +07:00
}
catch ( Exception e )
{
2023-05-23 02:25:54 +03:00
if ( e . InnerException != null ) { e = e . InnerException ; }
2017-05-24 01:39:30 +07:00
throw new Exception ( string . Format ( Strings . ErrorReadFile , e . Message ), e );
}
}
// Cleanup
try
{
Delete ( TEST_FILE_NAME );
}
catch ( Exception e )
{
2023-05-23 02:25:54 +03:00
if ( e . InnerException != null ) { e = e . InnerException ; }
2017-05-24 01:39:30 +07:00
throw new Exception ( string . Format ( Strings . ErrorDeleteFile , e . Message ), e );
}
2016-04-27 14:48:11 +02:00
}
public void CreateFolder ()
{
2023-05-23 02:25:54 +03:00
var client = CreateClient ( false );
2016-04-27 14:48:11 +02:00
2016-12-29 21:20:12 +01:00
var url = new Uri ( _url );
// Get the remote path
2021-08-10 10:30:54 -07:00
var remotePath = this . GetUnescapedAbsolutePath ( url );
2016-12-29 21:20:12 +01:00
// Try to create the directory
2023-05-23 02:25:54 +03:00
client . CreateDirectory ( remotePath , true ). Await ();
2016-04-27 14:48:11 +02:00
}
public void Dispose ()
{
2017-05-24 01:39:30 +07:00
if ( Client != null )
2016-12-29 21:20:12 +01:00
Client . Dispose ();
Client = null ;
2016-04-27 14:48:11 +02:00
_userInfo = null ;
}
2023-05-23 02:25:54 +03:00
private AsyncFtpClient CreateClient ( bool setWorkingDirectory = true )
2016-04-27 14:48:11 +02:00
{
2019-02-17 09:52:15 -06:00
var uri = new Uri ( _url );
2016-12-29 15:00:19 +01:00
2019-01-12 23:32:34 +01:00
if ( this . Client == null ) // Create connection if it doesn't exist yet
{
2023-05-18 01:27:29 +03:00
var ftpClient = new AsyncFtpClient
2016-12-29 15:00:19 +01:00
{
Host = uri . Host ,
Port = uri . Port == - 1 ? 21 : uri . Port ,
Credentials = _userInfo ,
2023-05-18 01:27:29 +03:00
Config = _ftpConfig ,
2016-12-29 15:00:19 +01:00
};
ftpClient . ValidateCertificate += HandleValidateCertificate ;
this . Client = ftpClient ;
2023-09-24 10:52:57 +02:00
2016-12-29 15:00:19 +01:00
} // else reuse existing connection
2023-05-23 02:25:54 +03:00
if ( setWorkingDirectory )
{
// Change working directory to the remote path
// Do this every time to prevent issues when FtpClient silently reconnects after failure.
var remotePath = this . GetUnescapedAbsolutePath ( uri );
try
{
this . Client . SetWorkingDirectory ( remotePath ). Await ();
}
catch ( FtpCommandException ex )
{
if ( ex . CompletionCode == "550" )
{
throw new FolderMissingException ( Strings . MissingFolderError ( remotePath , ex . Message ), ex );
}
throw ;
}
}
2019-01-12 23:32:34 +01:00
2016-12-29 15:00:19 +01:00
return this . Client ;
2016-04-27 14:48:11 +02:00
}
2021-08-10 10:30:54 -07:00
private string GetUnescapedAbsolutePath ( Uri uri )
{
string absolutePath = Uri . UnescapeDataString ( uri . AbsolutePath );
return absolutePath . EndsWith ( "/" , StringComparison . Ordinal ) ? absolutePath . Substring ( 0 , absolutePath . Length - 1 ) : absolutePath ;
}
2023-05-18 01:27:29 +03:00
private void HandleValidateCertificate ( BaseFtpClient control , FtpSslValidationEventArgs e )
2016-04-27 14:48:11 +02:00
{
if ( e . PolicyErrors == SslPolicyErrors . None || _accepAllCertificates )
{
e . Accept = true ;
return ;
}
try
{
var certHash = ( _validHashes != null && _validHashes . Length > 0 ) ? CoreUtility . ByteArrayAsHexString ( e . Certificate . GetCertHash ()) : null ;
if ( certHash != null )
{
2017-09-18 23:23:45 -06:00
if ( _validHashes . Any ( hash => ! string . IsNullOrEmpty ( hash ) && certHash . Equals ( hash , StringComparison . OrdinalIgnoreCase )))
2016-04-27 14:48:11 +02:00
{
e . Accept = true ;
}
}
}
catch
{
e . Accept = false ;
}
}
}
}