Compare commits

...
Author SHA1 Message Date
taz-ilandGitHub d5ff73cf4e FluentFTP version update - Fix handling of missing base folder (#4946)
* Improve errors specificity by:
a. Avoiding unnecesary wrapping in AggregateException - Using Library.Utility.Await instead of Wait / Result
b. Using messages from InnerException during TestConnection

* Fix detection of missing base directory:
Depend on error code while trying to SetWorkingDirectory, rather than very wide try/catch and a hardcoded error string (Different FTP servers return different error strings. E.g. hardcoded here was "Directory not found." while Filezilla returns "Can't open file or directory").

* Fix indentation

Intentionally seperated from the previous commit for easier reviewing (otherwise Github text diff display is confused by the removal of the large try / catch blocks)

* Fix creation of missing base directory:
(previously CreateClient would fail since it would try to SetWorkingDirectory to the not-yet-existing directory)
2023-05-23 01:25:54 +02:00
taz-ilandGitHub 056307961b Update FluentFTP to latest version (46.0.2) (#4935)
* Upgrade FluentFTP to v42 in an attempt to resolve TLS session issues:
duplicati/duplicati#4831

Note v40 introduced breaking changes, specifically seperating between sync and async clients.

Using a different approach this time, single async client.

* Bump FluentFTP to latest (42.0.0 --> 46.0.2)
2023-05-18 00:27:29 +02:00
3 changed files with 167 additions and 162 deletions
@@ -20,7 +20,10 @@
using Duplicati.Library.Common.IO;
using Duplicati.Library.Interface;
using Duplicati.Library.Utility;
using FluentFTP;
using FluentFTP.Client.BaseClient;
using FluentFTP.Exceptions;
using System;
using System.Collections.Generic;
using System.IO;
@@ -61,9 +64,7 @@ namespace Duplicati.Library.Backend.AlternativeFTP
private readonly string _url;
private readonly bool _listVerify = true;
private readonly FtpEncryptionMode _encryptionMode;
private readonly FtpDataConnectionType _dataConnectionType;
private readonly SslProtocols _sslProtocols;
private readonly FtpConfig _ftpConfig;
private readonly TimeSpan _uploadWaitTime;
private readonly byte[] _copybuffer = new byte[CoreUtility.DEFAULT_BUFFER_SIZE];
@@ -86,7 +87,7 @@ namespace Duplicati.Library.Backend.AlternativeFTP
get { return "aftp"; }
}
private FtpClient Client
private AsyncFtpClient Client
{ get; set; }
public IList<ICommandLineArgument> SupportedCommands
@@ -162,42 +163,52 @@ namespace Duplicati.Library.Backend.AlternativeFTP
// Process the aftp-data-connection-type option
string dataConnectionTypeString;
FtpDataConnectionType dataConnectionType;
if (!options.TryGetValue(CONFIG_KEY_AFTP_DATA_CONNECTION_TYPE, out dataConnectionTypeString) || string.IsNullOrWhiteSpace(dataConnectionTypeString))
{
dataConnectionTypeString = null;
}
if (dataConnectionTypeString == null || !Enum.TryParse(dataConnectionTypeString, true, out _dataConnectionType))
if (dataConnectionTypeString == null || !Enum.TryParse(dataConnectionTypeString, true, out dataConnectionType))
{
_dataConnectionType = DEFAULT_DATA_CONNECTION_TYPE;
dataConnectionType = DEFAULT_DATA_CONNECTION_TYPE;
}
// Process the aftp-encryption-mode option
string encryptionModeString;
FtpEncryptionMode encryptionMode;
if (!options.TryGetValue(CONFIG_KEY_AFTP_ENCRYPTION_MODE, out encryptionModeString) || string.IsNullOrWhiteSpace(encryptionModeString))
{
encryptionModeString = null;
}
if (encryptionModeString == null || !Enum.TryParse(encryptionModeString, true, out _encryptionMode))
if (encryptionModeString == null || !Enum.TryParse(encryptionModeString, true, out encryptionMode))
{
_encryptionMode = DEFAULT_ENCRYPTION_MODE;
encryptionMode = DEFAULT_ENCRYPTION_MODE;
}
// Process the aftp-ssl-protocols option
string sslProtocolsString;
SslProtocols sslProtocols;
if (!options.TryGetValue(CONFIG_KEY_AFTP_SSL_PROTOCOLS, out sslProtocolsString) || string.IsNullOrWhiteSpace(sslProtocolsString))
{
sslProtocolsString = null;
}
if (sslProtocolsString == null || !Enum.TryParse(sslProtocolsString, true, out _sslProtocols))
if (sslProtocolsString == null || !Enum.TryParse(sslProtocolsString, true, out sslProtocols))
{
_sslProtocols = DEFAULT_SSL_PROTOCOLS;
sslProtocols = DEFAULT_SSL_PROTOCOLS;
}
_ftpConfig = new FtpConfig
{
DataConnectionType = dataConnectionType,
EncryptionMode = encryptionMode,
SslProtocols = sslProtocols,
};
}
public IEnumerable<IFileEntry> List()
@@ -215,99 +226,87 @@ namespace Duplicati.Library.Backend.AlternativeFTP
var list = new List<IFileEntry>();
string remotePath = filename;
try
var ftpClient = CreateClient();
// Get the remote path
var url = new Uri(this._url);
remotePath = "/" + this.GetUnescapedAbsolutePath(url);
if (!string.IsNullOrEmpty(filename))
{
var ftpClient = CreateClient();
// Get the remote path
var url = new Uri(this._url);
remotePath = "/" + this.GetUnescapedAbsolutePath(url);
if (!string.IsNullOrEmpty(filename))
if (!stripFile)
{
if (!stripFile)
{
// Append the filename
remotePath += filename;
}
else if (filename.Contains("/"))
{
remotePath += filename.Substring(0, filename.LastIndexOf("/", StringComparison.Ordinal));
}
// else: stripping the filename in this case ignoring it
// Append the filename
remotePath += filename;
}
foreach (FtpListItem item in ftpClient.GetListing(remotePath, FtpListOption.Modify | FtpListOption.Size | FtpListOption.DerefLinks))
else if (filename.Contains("/"))
{
switch (item.Type)
{
case FtpFileSystemObjectType.Directory:
{
if (item.Name == "." || item.Name == "..")
{
continue;
}
list.Add(new FileEntry(item.Name, -1, new DateTime(), item.Modified)
{
IsFolder = true,
});
break;
}
case FtpFileSystemObjectType.File:
{
list.Add(new FileEntry(item.Name, item.Size, new DateTime(), item.Modified));
break;
}
case FtpFileSystemObjectType.Link:
{
if (item.Name == "." || item.Name == "..")
{
continue;
}
if (item.LinkObject != null)
{
switch (item.LinkObject.Type)
{
case FtpFileSystemObjectType.Directory:
{
if (item.Name == "." || item.Name == "..")
{
continue;
}
list.Add(new FileEntry(item.Name, -1, new DateTime(), item.Modified)
{
IsFolder = true,
});
break;
}
case FtpFileSystemObjectType.File:
{
list.Add(new FileEntry(item.Name, item.Size, new DateTime(), item.Modified));
break;
}
}
}
break;
}
}
remotePath += filename.Substring(0, filename.LastIndexOf("/", StringComparison.Ordinal));
}
}// Message "Directory not found." string
catch (FtpCommandException ex)
// else: stripping the filename in this case ignoring it
}
foreach (FtpListItem item in ftpClient.GetListing(remotePath, FtpListOption.Modify | FtpListOption.Size).Await())
{
if (ex.Message == "Directory not found.")
switch (item.Type)
{
throw new FolderMissingException(Strings.MissingFolderError(remotePath, ex.Message), ex);
}
case FtpObjectType.Directory:
{
if (item.Name == "." || item.Name == "..")
{
continue;
}
throw;
list.Add(new FileEntry(item.Name, -1, new DateTime(), item.Modified)
{
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;
}
if (item.LinkObject != null)
{
switch (item.LinkObject.Type)
{
case FtpObjectType.Directory:
{
if (item.Name == "." || item.Name == "..")
{
continue;
}
list.Add(new FileEntry(item.Name, -1, new DateTime(), item.Modified)
{
IsFolder = true,
});
break;
}
case FtpObjectType.File:
{
list.Add(new FileEntry(item.Name, item.Size, new DateTime(), item.Modified));
break;
}
}
}
break;
}
}
}
return list;
@@ -318,55 +317,43 @@ namespace Duplicati.Library.Backend.AlternativeFTP
string remotePath = remotename;
long streamLen;
var ftpClient = CreateClient();
try
{
var ftpClient = CreateClient();
try
{
streamLen = input.Length;
}
catch (NotSupportedException) { streamLen = -1; }
// Get the remote path
remotePath = "";
if (!string.IsNullOrEmpty(remotename))
{
// Append the filename
remotePath += remotename;
}
var success = await ftpClient.UploadAsync(input, remotePath, FtpExists.Overwrite, createRemoteDir: false, token: cancelToken, progress: null).ConfigureAwait(false);
if (!success)
{
throw new UserInformationException(string.Format(Strings.ErrorWriteFile, remotename), "AftpPutFailure");
}
// Wait for the upload, if required
if (_uploadWaitTime.Ticks > 0)
{
Thread.Sleep(_uploadWaitTime);
}
if (_listVerify)
{
// check remote file size; matching file size indicates completion
var remoteSize = await ftpClient.GetFileSizeAsync(remotePath, cancelToken);
if (streamLen != remoteSize)
{
throw new UserInformationException(Strings.ListVerifySizeFailure(remotename, remoteSize, streamLen), "AftpListVerifySizeFailure");
}
}
streamLen = input.Length;
}
catch (FtpCommandException ex)
{
if (ex.Message == "Directory not found.")
{
throw new FolderMissingException(Strings.MissingFolderError(remotePath, ex.Message), ex);
}
catch (NotSupportedException) { streamLen = -1; }
throw;
// Get the remote path
remotePath = "";
if (!string.IsNullOrEmpty(remotename))
{
// Append the filename
remotePath += remotename;
}
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");
}
// Wait for the upload, if required
if (_uploadWaitTime.Ticks > 0)
{
Thread.Sleep(_uploadWaitTime);
}
if (_listVerify)
{
// check remote file size; matching file size indicates completion
var remoteSize = await ftpClient.GetFileSize(remotePath, -1, cancelToken);
if (streamLen != remoteSize)
{
throw new UserInformationException(Strings.ListVerifySizeFailure(remotename, remoteSize, streamLen), "AftpListVerifySizeFailure");
}
}
}
@@ -391,7 +378,7 @@ namespace Duplicati.Library.Backend.AlternativeFTP
remotePath += remotename;
}
using (var inputStream = ftpClient.OpenRead(remotePath))
using (var inputStream = ftpClient.OpenRead(remotePath).Await())
{
try
{
@@ -426,7 +413,7 @@ namespace Duplicati.Library.Backend.AlternativeFTP
remotePath += remotename;
}
ftpClient.DeleteFile(remotePath);
ftpClient.DeleteFile(remotePath).Await();
}
@@ -462,6 +449,7 @@ namespace Duplicati.Library.Backend.AlternativeFTP
}
catch (Exception e)
{
if (e.InnerException != null) { e = e.InnerException; }
throw new Exception(string.Format(Strings.ErrorDeleteFile, e.Message), e);
}
}
@@ -471,10 +459,11 @@ namespace Duplicati.Library.Backend.AlternativeFTP
{
try
{
PutAsync(TEST_FILE_NAME, testStream, CancellationToken.None).Wait();
PutAsync(TEST_FILE_NAME, testStream, CancellationToken.None).Await();
}
catch (Exception e)
{
if (e.InnerException != null) { e = e.InnerException; }
throw new Exception(string.Format(Strings.ErrorWriteFile, e.Message), e);
}
}
@@ -491,6 +480,7 @@ namespace Duplicati.Library.Backend.AlternativeFTP
}
catch (Exception e)
{
if (e.InnerException != null) { e = e.InnerException; }
throw new Exception(string.Format(Strings.ErrorReadFile, e.Message), e);
}
}
@@ -502,13 +492,14 @@ namespace Duplicati.Library.Backend.AlternativeFTP
}
catch (Exception e)
{
if (e.InnerException != null) { e = e.InnerException; }
throw new Exception(string.Format(Strings.ErrorDeleteFile, e.Message), e);
}
}
public void CreateFolder()
{
var client = CreateClient();
var client = CreateClient(false);
var url = new Uri(_url);
@@ -516,7 +507,7 @@ namespace Duplicati.Library.Backend.AlternativeFTP
var remotePath = this.GetUnescapedAbsolutePath(url);
// Try to create the directory
client.CreateDirectory(remotePath, true);
client.CreateDirectory(remotePath, true).Await();
}
@@ -529,23 +520,18 @@ namespace Duplicati.Library.Backend.AlternativeFTP
_userInfo = null;
}
private FtpClient CreateClient()
private AsyncFtpClient CreateClient(bool setWorkingDirectory = true)
{
var uri = new Uri(_url);
if (this.Client == null) // Create connection if it doesn't exist yet
{
var ftpClient = new FtpClient
var ftpClient = new AsyncFtpClient
{
Host = uri.Host,
Port = uri.Port == -1 ? 21 : uri.Port,
Credentials = _userInfo,
EncryptionMode = _encryptionMode,
DataConnectionType = _dataConnectionType,
SslProtocols = _sslProtocols,
// We do not support parallel uploads, and the feature is buggy
EnableThreadSafeDataConnections = false,
Config = _ftpConfig,
};
ftpClient.ValidateCertificate += HandleValidateCertificate;
@@ -553,10 +539,25 @@ namespace Duplicati.Library.Backend.AlternativeFTP
this.Client = ftpClient;
} // else reuse existing connection
// 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);
this.Client.SetWorkingDirectory(remotePath);
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;
}
}
return this.Client;
}
@@ -567,7 +568,7 @@ namespace Duplicati.Library.Backend.AlternativeFTP
return absolutePath.EndsWith("/", StringComparison.Ordinal) ? absolutePath.Substring(0, absolutePath.Length - 1) : absolutePath;
}
private void HandleValidateCertificate(FtpClient control, FtpSslValidationEventArgs e)
private void HandleValidateCertificate(BaseFtpClient control, FtpSslValidationEventArgs e)
{
if (e.PolicyErrors == SslPolicyErrors.None || _accepAllCertificates)
{
@@ -35,8 +35,11 @@
<AssemblyOriginatorKeyFile>Duplicati.snk</AssemblyOriginatorKeyFile>
</PropertyGroup>
<ItemGroup>
<Reference Include="FluentFTP, Version=27.1.1.0, Culture=neutral, PublicKeyToken=f4af092b1d8df44f, processorArchitecture=MSIL">
<HintPath>..\..\..\..\packages\FluentFTP.27.1.1\lib\net45\FluentFTP.dll</HintPath>
<Reference Include="FluentFTP, Version=46.0.2.0, Culture=neutral, PublicKeyToken=f4af092b1d8df44f, processorArchitecture=MSIL">
<HintPath>..\..\..\..\packages\FluentFTP.46.0.2\lib\net462\FluentFTP.dll</HintPath>
</Reference>
<Reference Include="Microsoft.Extensions.Logging.Abstractions, Version=2.1.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60, processorArchitecture=MSIL">
<HintPath>..\..\..\..\packages\Microsoft.Extensions.Logging.Abstractions.2.1.0\lib\netstandard2.0\Microsoft.Extensions.Logging.Abstractions.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
@@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="FluentFTP" version="27.1.1" targetFramework="net471" />
</packages>
<package id="FluentFTP" version="46.0.2" targetFramework="net471" />
<package id="Microsoft.Extensions.Logging.Abstractions" version="2.1.0" targetFramework="net471" />
</packages>