From 40b811733282705caf5e6dcaf80c5d5c85c727eb Mon Sep 17 00:00:00 2001 From: Mauro van der Gun Date: Tue, 2 Jan 2024 09:46:32 -0400 Subject: [PATCH] add ftp adapter --- .../src/AmazonS3Adapter.cs | 3 +- .../src/AzureBlobStorageAdapter.cs | 3 +- .../src/AzureFileStorageAdapter.cs | 3 +- .../src/DropboxAdapter.cs | 3 +- .../FileSystem.Adapters.Ftp.csproj | 31 +++ FileSystem.Adapters.Ftp/src/FtpAdapter.cs | 253 ++++++++++++++++++ .../src/FtpAdapterConfiguration.cs | 8 + FileSystem.Adapters.Ftp/src/ModelFactory.cs | 33 +++ .../src/GoogleDriveAdapter.cs | 3 +- .../src/MicrosoftOneDriveAdapter.cs | 3 +- .../FileSystem.Adapters.Sftp.csproj | 2 +- FileSystem.Adapters.Sftp/src/SftpAdapter.cs | 6 +- FileSystem.sln | 6 + FileSystem/FileSystem.csproj | 2 +- FileSystem/src/Adapters/Adapter.cs | 7 +- FileSystem/src/Adapters/IAdapter.cs | 2 +- FileSystem/src/Adapters/LocalAdapter.cs | 7 +- FileSystem/src/Constants/AdapterConstants.cs | 7 - .../src/Constants/FileSystemConstants.cs | 10 + .../Exceptions/InvalidVirtualPathException.cs | 17 ++ FileSystem/src/FileSystem.cs | 82 ++++-- FileSystem/src/Utilities/PathUtilities.cs | 22 ++ FileSystem/src/Utilities/StreamUtilities.cs | 2 +- README.md | 16 ++ .../AmazonS3AdapterTest.cs | 6 +- .../AzureBlobStorageAdapterTest.cs | 6 +- .../AzureFileStorageAdapterTest.cs | 6 +- .../GoogleDriveAdapterTest.cs | 6 +- .../MicrosoftOneDriveAdapterTest.cs | 6 +- .../SftpAdapterTest.cs | 6 +- 30 files changed, 493 insertions(+), 74 deletions(-) create mode 100644 FileSystem.Adapters.Ftp/FileSystem.Adapters.Ftp.csproj create mode 100644 FileSystem.Adapters.Ftp/src/FtpAdapter.cs create mode 100644 FileSystem.Adapters.Ftp/src/FtpAdapterConfiguration.cs create mode 100644 FileSystem.Adapters.Ftp/src/ModelFactory.cs delete mode 100644 FileSystem/src/Constants/AdapterConstants.cs create mode 100644 FileSystem/src/Constants/FileSystemConstants.cs create mode 100644 FileSystem/src/Exceptions/InvalidVirtualPathException.cs diff --git a/FileSystem.Adapters.AmazonS3/src/AmazonS3Adapter.cs b/FileSystem.Adapters.AmazonS3/src/AmazonS3Adapter.cs index 8c18873..4e95be8 100644 --- a/FileSystem.Adapters.AmazonS3/src/AmazonS3Adapter.cs +++ b/FileSystem.Adapters.AmazonS3/src/AmazonS3Adapter.cs @@ -31,9 +31,10 @@ public override void Dispose() client.Dispose(); } - public override void Connect() + public override async Task ConnectAsync(CancellationToken cancellationToken = default) { Logger.LogStartConnectingAdapter(this); + await Task.CompletedTask; Logger.LogFinishedConnectingAdapter(this); } diff --git a/FileSystem.Adapters.AzureBlobStorage/src/AzureBlobStorageAdapter.cs b/FileSystem.Adapters.AzureBlobStorage/src/AzureBlobStorageAdapter.cs index 43d0dfe..6333eba 100644 --- a/FileSystem.Adapters.AzureBlobStorage/src/AzureBlobStorageAdapter.cs +++ b/FileSystem.Adapters.AzureBlobStorage/src/AzureBlobStorageAdapter.cs @@ -28,9 +28,10 @@ public override void Dispose() { } - public override void Connect() + public override async Task ConnectAsync(CancellationToken cancellationToken = default) { Logger.LogStartConnectingAdapter(this); + await Task.CompletedTask; Logger.LogFinishedConnectingAdapter(this); } diff --git a/FileSystem.Adapters.AzureFileStorage/src/AzureFileStorageAdapter.cs b/FileSystem.Adapters.AzureFileStorage/src/AzureFileStorageAdapter.cs index 24a9876..19378dd 100644 --- a/FileSystem.Adapters.AzureFileStorage/src/AzureFileStorageAdapter.cs +++ b/FileSystem.Adapters.AzureFileStorage/src/AzureFileStorageAdapter.cs @@ -27,9 +27,10 @@ public override void Dispose() { } - public override void Connect() + public override async Task ConnectAsync(CancellationToken cancellationToken = default) { Logger.LogStartConnectingAdapter(this); + await Task.CompletedTask; Logger.LogFinishedConnectingAdapter(this); } diff --git a/FileSystem.Adapters.Dropbox/src/DropboxAdapter.cs b/FileSystem.Adapters.Dropbox/src/DropboxAdapter.cs index 6ff9106..7f40622 100644 --- a/FileSystem.Adapters.Dropbox/src/DropboxAdapter.cs +++ b/FileSystem.Adapters.Dropbox/src/DropboxAdapter.cs @@ -29,9 +29,10 @@ public override void Dispose() client.Dispose(); } - public override void Connect() + public override async Task ConnectAsync(CancellationToken cancellationToken = default) { Logger.LogStartConnectingAdapter(this); + await Task.CompletedTask; Logger.LogFinishedConnectingAdapter(this); } diff --git a/FileSystem.Adapters.Ftp/FileSystem.Adapters.Ftp.csproj b/FileSystem.Adapters.Ftp/FileSystem.Adapters.Ftp.csproj new file mode 100644 index 0000000..6c8c872 --- /dev/null +++ b/FileSystem.Adapters.Ftp/FileSystem.Adapters.Ftp.csproj @@ -0,0 +1,31 @@ + + + + SharpGrip.FileSystem.Adapters.Ftp + + + + SharpGrip.FileSystem.Adapters.Ftp + SharpGrip.FileSystem.Adapters.Ftp + SharpGrip FileSystem FTP adapter + The SharpGrip FileSystem FTP adapter. + sharpgrip;file-system;ftp + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/FileSystem.Adapters.Ftp/src/FtpAdapter.cs b/FileSystem.Adapters.Ftp/src/FtpAdapter.cs new file mode 100644 index 0000000..d95e289 --- /dev/null +++ b/FileSystem.Adapters.Ftp/src/FtpAdapter.cs @@ -0,0 +1,253 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using FluentFTP; +using FluentFTP.Exceptions; +using SharpGrip.FileSystem.Constants; +using SharpGrip.FileSystem.Exceptions; +using SharpGrip.FileSystem.Extensions; +using SharpGrip.FileSystem.Models; +using SharpGrip.FileSystem.Utilities; +using DirectoryNotFoundException = SharpGrip.FileSystem.Exceptions.DirectoryNotFoundException; +using FileNotFoundException = SharpGrip.FileSystem.Exceptions.FileNotFoundException; + +namespace SharpGrip.FileSystem.Adapters.Ftp +{ + public class FtpAdapter : Adapter + { + private readonly IAsyncFtpClient client; + + public FtpAdapter(string prefix, string rootPath, IAsyncFtpClient client, Action? configuration = null) : base(prefix, rootPath, configuration) + { + this.client = client; + } + + public override void Dispose() + { + client.Dispose(); + } + + public override async Task ConnectAsync(CancellationToken cancellationToken = default) + { + if (client.IsConnected) + { + return; + } + + try + { + Logger.LogStartConnectingAdapter(this); + await client.Connect(cancellationToken); + Logger.LogFinishedConnectingAdapter(this); + } + catch (Exception exception) + { + throw Exception(exception); + } + } + + public override async Task GetFileAsync(string virtualPath, CancellationToken cancellationToken = default) + { + var path = GetPath(virtualPath); + + try + { + var file = await client.GetObjectInfo(path, token: cancellationToken); + + if (file == null || file.Type != FtpObjectType.File) + { + throw new FileNotFoundException(path, Prefix); + } + + return ModelFactory.CreateFile(file, virtualPath); + } + catch (Exception exception) + { + throw Exception(exception); + } + } + + public override async Task GetDirectoryAsync(string virtualPath, CancellationToken cancellationToken = default) + { + var path = GetPath(virtualPath); + + try + { + var directory = await client.GetObjectInfo(path, token: cancellationToken); + + if (directory == null || directory.Type != FtpObjectType.Directory) + { + throw new DirectoryNotFoundException(path, Prefix); + } + + return ModelFactory.CreateDirectory(directory, virtualPath); + } + catch (Exception exception) + { + throw Exception(exception); + } + } + + public override async Task> GetFilesAsync(string virtualPath = "", CancellationToken cancellationToken = default) + { + await GetDirectoryAsync(virtualPath, cancellationToken); + var path = GetPath(virtualPath); + + try + { + var ftpListItems = await client.GetListing(path, cancellationToken); + + return ftpListItems.Where(file => file.Type == FtpObjectType.File).Select(file => ModelFactory.CreateFile(file, GetVirtualPath(file.FullName))); + } + catch (Exception exception) + { + throw Exception(exception); + } + } + + public override async Task> GetDirectoriesAsync(string virtualPath = "", CancellationToken cancellationToken = default) + { + await GetDirectoryAsync(virtualPath, cancellationToken); + var path = GetPath(virtualPath); + + try + { + var ftpListItems = await client.GetListing(path, cancellationToken); + + return ftpListItems.Where(file => file.Type == FtpObjectType.Directory).Select(file => ModelFactory.CreateDirectory(file, GetVirtualPath(file.FullName))); + } + catch (Exception exception) + { + throw Exception(exception); + } + } + + public override async Task CreateDirectoryAsync(string virtualPath, CancellationToken cancellationToken = default) + { + if (await DirectoryExistsAsync(virtualPath, cancellationToken)) + { + throw new DirectoryExistsException(GetPath(virtualPath), Prefix); + } + + try + { + await client.CreateDirectory(GetPath(virtualPath), cancellationToken); + } + catch (Exception exception) + { + throw Exception(exception); + } + } + + public override async Task DeleteDirectoryAsync(string virtualPath, CancellationToken cancellationToken = default) + { + await GetDirectoryAsync(virtualPath, cancellationToken); + + try + { + await client.DeleteDirectory(GetPath(virtualPath), cancellationToken); + } + catch (Exception exception) + { + throw Exception(exception); + } + } + + public override async Task DeleteFileAsync(string virtualPath, CancellationToken cancellationToken = default) + { + await GetFileAsync(virtualPath, cancellationToken); + + try + { + await client.DeleteFile(GetPath(virtualPath), cancellationToken); + } + catch (Exception exception) + { + throw Exception(exception); + } + } + + public override async Task ReadFileStreamAsync(string virtualPath, CancellationToken cancellationToken = default) + { + await GetFileAsync(virtualPath, cancellationToken); + + try + { + var fileStream = await client.OpenRead(GetPath(virtualPath), token: cancellationToken); + + return await StreamUtilities.CopyContentsToMemoryStreamAsync(fileStream, true, cancellationToken); + } + catch (Exception exception) + { + throw Exception(exception); + } + } + + public override async Task WriteFileAsync(string virtualPath, Stream contents, bool overwrite = false, CancellationToken cancellationToken = default) + { + if (!overwrite && await FileExistsAsync(virtualPath, cancellationToken)) + { + throw new FileExistsException(GetPath(virtualPath), Prefix); + } + + try + { + contents.Seek(0, SeekOrigin.Begin); + + using var writeStream = await client.OpenWrite(GetPath(virtualPath), token: cancellationToken); + + await contents.CopyToAsync(writeStream, FileSystemConstants.Streaming.DefaultMemoryStreamBufferSize, cancellationToken); + await writeStream.FlushAsync(cancellationToken); + } + catch (Exception exception) + { + throw Exception(exception); + } + } + + public override async Task AppendFileAsync(string virtualPath, Stream contents, CancellationToken cancellationToken = default) + { + await GetFileAsync(virtualPath, cancellationToken); + + try + { + using var fileStream = await client.OpenAppend(GetPath(virtualPath), token: cancellationToken); + + await contents.CopyToAsync(fileStream); + } + catch (Exception exception) + { + throw new AdapterRuntimeException(exception); + } + } + + protected override Exception Exception(Exception exception) + { + if (exception is FileSystemException) + { + return exception; + } + + if (exception is SocketException socketException) + { + return new ConnectionException(socketException); + } + + if (exception is FtpAuthenticationException ftpAuthenticationException) + { + return new ConnectionException(ftpAuthenticationException); + } + + if (exception is FtpSecurityNotAvailableException ftpSecurityNotAvailableException) + { + return new ConnectionException(ftpSecurityNotAvailableException); + } + + return new AdapterRuntimeException(exception); + } + } +} \ No newline at end of file diff --git a/FileSystem.Adapters.Ftp/src/FtpAdapterConfiguration.cs b/FileSystem.Adapters.Ftp/src/FtpAdapterConfiguration.cs new file mode 100644 index 0000000..6d5764c --- /dev/null +++ b/FileSystem.Adapters.Ftp/src/FtpAdapterConfiguration.cs @@ -0,0 +1,8 @@ +using SharpGrip.FileSystem.Configuration; + +namespace SharpGrip.FileSystem.Adapters.Ftp +{ + public class FtpAdapterConfiguration : AdapterConfiguration + { + } +} \ No newline at end of file diff --git a/FileSystem.Adapters.Ftp/src/ModelFactory.cs b/FileSystem.Adapters.Ftp/src/ModelFactory.cs new file mode 100644 index 0000000..7bced46 --- /dev/null +++ b/FileSystem.Adapters.Ftp/src/ModelFactory.cs @@ -0,0 +1,33 @@ +using FluentFTP; +using SharpGrip.FileSystem.Models; + +namespace SharpGrip.FileSystem.Adapters.Ftp +{ + public static class ModelFactory + { + public static IFile CreateFile(FtpListItem file, string virtualPath) + { + return new FileModel + { + Name = file.Name, + Path = file.FullName, + VirtualPath = virtualPath, + Length = file.Size, + LastModifiedDateTime = file.Modified, + CreatedDateTime = file.Created + }; + } + + public static DirectoryModel CreateDirectory(FtpListItem directory, string virtualPath) + { + return new DirectoryModel + { + Name = directory.Name, + Path = directory.FullName, + VirtualPath = virtualPath, + LastModifiedDateTime = directory.Modified, + CreatedDateTime = directory.Created + }; + } + } +} \ No newline at end of file diff --git a/FileSystem.Adapters.GoogleDrive/src/GoogleDriveAdapter.cs b/FileSystem.Adapters.GoogleDrive/src/GoogleDriveAdapter.cs index c0f7f28..0b26edc 100644 --- a/FileSystem.Adapters.GoogleDrive/src/GoogleDriveAdapter.cs +++ b/FileSystem.Adapters.GoogleDrive/src/GoogleDriveAdapter.cs @@ -40,9 +40,10 @@ public override void Dispose() client.Dispose(); } - public override void Connect() + public override async Task ConnectAsync(CancellationToken cancellationToken = default) { Logger.LogStartConnectingAdapter(this); + await Task.CompletedTask; Logger.LogFinishedConnectingAdapter(this); } diff --git a/FileSystem.Adapters.MicrosoftOneDrive/src/MicrosoftOneDriveAdapter.cs b/FileSystem.Adapters.MicrosoftOneDrive/src/MicrosoftOneDriveAdapter.cs index 60d0ff5..15adbed 100644 --- a/FileSystem.Adapters.MicrosoftOneDrive/src/MicrosoftOneDriveAdapter.cs +++ b/FileSystem.Adapters.MicrosoftOneDrive/src/MicrosoftOneDriveAdapter.cs @@ -29,9 +29,10 @@ public override void Dispose() { } - public override void Connect() + public override async Task ConnectAsync(CancellationToken cancellationToken = default) { Logger.LogStartConnectingAdapter(this); + await Task.CompletedTask; Logger.LogFinishedConnectingAdapter(this); } diff --git a/FileSystem.Adapters.Sftp/FileSystem.Adapters.Sftp.csproj b/FileSystem.Adapters.Sftp/FileSystem.Adapters.Sftp.csproj index a8c6114..6fd1441 100644 --- a/FileSystem.Adapters.Sftp/FileSystem.Adapters.Sftp.csproj +++ b/FileSystem.Adapters.Sftp/FileSystem.Adapters.Sftp.csproj @@ -21,7 +21,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/FileSystem.Adapters.Sftp/src/SftpAdapter.cs b/FileSystem.Adapters.Sftp/src/SftpAdapter.cs index 8c0aad4..dc9e6ce 100644 --- a/FileSystem.Adapters.Sftp/src/SftpAdapter.cs +++ b/FileSystem.Adapters.Sftp/src/SftpAdapter.cs @@ -35,7 +35,7 @@ public override void Dispose() ((IBaseClient) client).Dispose(); } - public override void Connect() + public override async Task ConnectAsync(CancellationToken cancellationToken = default) { if (client.IsConnected) { @@ -45,7 +45,7 @@ public override void Connect() try { Logger.LogStartConnectingAdapter(this); - client.Connect(); + await client.ConnectAsync(cancellationToken); Logger.LogFinishedConnectingAdapter(this); } catch (Exception exception) @@ -224,7 +224,7 @@ public override async Task WriteFileAsync(string virtualPath, Stream contents, b using var writeStream = client.OpenWrite(GetPath(virtualPath)); - await contents.CopyToAsync(writeStream, AdapterConstants.DefaultMemoryStreamBufferSize, cancellationToken); + await contents.CopyToAsync(writeStream, FileSystemConstants.Streaming.DefaultMemoryStreamBufferSize, cancellationToken); await writeStream.FlushAsync(cancellationToken); } catch (Exception exception) diff --git a/FileSystem.sln b/FileSystem.sln index 39ed7fe..46362fa 100644 --- a/FileSystem.sln +++ b/FileSystem.sln @@ -10,6 +10,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileSystem.Adapters.AzureFi EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileSystem.Adapters.Dropbox", "FileSystem.Adapters.Dropbox\FileSystem.Adapters.Dropbox.csproj", "{FA70DD02-3D25-44C6-ACC8-D61BA55F6C6F}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileSystem.Adapters.Ftp", "FileSystem.Adapters.Ftp\FileSystem.Adapters.Ftp.csproj", "{ED38402A-3667-4701-A974-4D7710E1544A}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileSystem.Adapters.GoogleDrive", "FileSystem.Adapters.GoogleDrive\FileSystem.Adapters.GoogleDrive.csproj", "{CB1F411C-3FB5-4441-9541-423951C17BAF}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileSystem.Adapters.MicrosoftOneDrive", "FileSystem.Adapters.MicrosoftOneDrive\FileSystem.Adapters.MicrosoftOneDrive.csproj", "{9955422E-4A50-43CD-BC2B-91E6820F206C}" @@ -44,6 +46,10 @@ Global {FA70DD02-3D25-44C6-ACC8-D61BA55F6C6F}.Debug|Any CPU.Build.0 = Debug|Any CPU {FA70DD02-3D25-44C6-ACC8-D61BA55F6C6F}.Release|Any CPU.ActiveCfg = Release|Any CPU {FA70DD02-3D25-44C6-ACC8-D61BA55F6C6F}.Release|Any CPU.Build.0 = Release|Any CPU + {ED38402A-3667-4701-A974-4D7710E1544A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ED38402A-3667-4701-A974-4D7710E1544A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ED38402A-3667-4701-A974-4D7710E1544A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ED38402A-3667-4701-A974-4D7710E1544A}.Release|Any CPU.Build.0 = Release|Any CPU {CB1F411C-3FB5-4441-9541-423951C17BAF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CB1F411C-3FB5-4441-9541-423951C17BAF}.Debug|Any CPU.Build.0 = Debug|Any CPU {CB1F411C-3FB5-4441-9541-423951C17BAF}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/FileSystem/FileSystem.csproj b/FileSystem/FileSystem.csproj index 7c7fffc..2285baa 100644 --- a/FileSystem/FileSystem.csproj +++ b/FileSystem/FileSystem.csproj @@ -9,7 +9,7 @@ SharpGrip.FileSystem SharpGrip FileSystem SharpGrip FileSystem is a file system abstraction supporting multiple adapters. - sharpgrip;file-system;amazon-s3;azure-blob-storage;azure-file-storage;dropbox;microsoft-onedrive;sftp + sharpgrip;file-system;amazon-s3;azure-blob-storage;azure-file-storage;dropbox;ftp;google-drive;microsoft-onedrive;sftp diff --git a/FileSystem/src/Adapters/Adapter.cs b/FileSystem/src/Adapters/Adapter.cs index aa39db7..a977c21 100644 --- a/FileSystem/src/Adapters/Adapter.cs +++ b/FileSystem/src/Adapters/Adapter.cs @@ -20,7 +20,7 @@ namespace SharpGrip.FileSystem.Adapters { public string Prefix { get; } public string RootPath { get; } - public string Name => GetType().FullName!; + public string Name { get; } public IAdapterConfiguration AdapterConfiguration => Configuration; protected TAdapterConfiguration Configuration { get; } protected ILogger Logger { get; } = NullLogger>.Instance; @@ -32,6 +32,7 @@ protected Adapter(string prefix, string rootPath, Action? var adapterConfiguration = new TAdapterConfiguration(); configuration?.Invoke(adapterConfiguration); + Name = GetType().FullName!; Configuration = adapterConfiguration; if (Configuration.EnableLogging) @@ -96,7 +97,7 @@ public virtual async Task AppendFileAsync(string virtualPath, Stream contents, C using var memoryStream = await StreamUtilities.CopyContentsToMemoryStreamAsync(fileContents, false, cancellationToken); - await contents.CopyToAsync(memoryStream, AdapterConstants.DefaultMemoryStreamBufferSize, cancellationToken); + await contents.CopyToAsync(memoryStream, FileSystemConstants.Streaming.DefaultMemoryStreamBufferSize, cancellationToken); memoryStream.Seek(0, SeekOrigin.Begin); await DeleteFileAsync(virtualPath, cancellationToken); @@ -116,7 +117,7 @@ public virtual async Task AppendFileAsync(string virtualPath, Stream contents, C } public abstract void Dispose(); - public abstract void Connect(); + public abstract Task ConnectAsync(CancellationToken cancellationToken = default); public abstract Task GetFileAsync(string virtualPath, CancellationToken cancellationToken = default); public abstract Task GetDirectoryAsync(string virtualPath, CancellationToken cancellationToken = default); public abstract Task> GetFilesAsync(string virtualPath = "", CancellationToken cancellationToken = default); diff --git a/FileSystem/src/Adapters/IAdapter.cs b/FileSystem/src/Adapters/IAdapter.cs index 3839204..496959f 100644 --- a/FileSystem/src/Adapters/IAdapter.cs +++ b/FileSystem/src/Adapters/IAdapter.cs @@ -14,7 +14,7 @@ public interface IAdapter : IDisposable public string RootPath { get; } public string Name { get; } public IAdapterConfiguration AdapterConfiguration { get; } - public void Connect(); + public Task ConnectAsync(CancellationToken cancellationToken = default); public void ClearCache(); public Task GetFileAsync(string virtualPath, CancellationToken cancellationToken = default); public Task GetDirectoryAsync(string virtualPath, CancellationToken cancellationToken = default); diff --git a/FileSystem/src/Adapters/LocalAdapter.cs b/FileSystem/src/Adapters/LocalAdapter.cs index 1837a92..3a52558 100644 --- a/FileSystem/src/Adapters/LocalAdapter.cs +++ b/FileSystem/src/Adapters/LocalAdapter.cs @@ -22,9 +22,10 @@ public override void Dispose() { } - public override void Connect() + public override async Task ConnectAsync(CancellationToken cancellationToken = default) { Logger.LogStartConnectingAdapter(this); + await Task.CompletedTask; Logger.LogFinishedConnectingAdapter(this); } @@ -196,7 +197,7 @@ public override async Task WriteFileAsync(string virtualPath, Stream contents, b using var fileStream = new FileStream(GetPath(virtualPath), FileMode.Create); contents.Seek(0, SeekOrigin.Begin); - await contents.CopyToAsync(fileStream, AdapterConstants.DefaultMemoryStreamBufferSize, cancellationToken); + await contents.CopyToAsync(fileStream, FileSystemConstants.Streaming.DefaultMemoryStreamBufferSize, cancellationToken); } catch (Exception exception) { @@ -213,7 +214,7 @@ public override async Task AppendFileAsync(string virtualPath, Stream contents, using var fileStream = new FileStream(GetPath(virtualPath), FileMode.Append); contents.Seek(0, SeekOrigin.Begin); - await contents.CopyToAsync(fileStream, AdapterConstants.DefaultMemoryStreamBufferSize, cancellationToken); + await contents.CopyToAsync(fileStream, FileSystemConstants.Streaming.DefaultMemoryStreamBufferSize, cancellationToken); } catch (Exception exception) { diff --git a/FileSystem/src/Constants/AdapterConstants.cs b/FileSystem/src/Constants/AdapterConstants.cs deleted file mode 100644 index 6b18777..0000000 --- a/FileSystem/src/Constants/AdapterConstants.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace SharpGrip.FileSystem.Constants -{ - public static class AdapterConstants - { - public const int DefaultMemoryStreamBufferSize = 81920; - } -} \ No newline at end of file diff --git a/FileSystem/src/Constants/FileSystemConstants.cs b/FileSystem/src/Constants/FileSystemConstants.cs new file mode 100644 index 0000000..2c14b4c --- /dev/null +++ b/FileSystem/src/Constants/FileSystemConstants.cs @@ -0,0 +1,10 @@ +namespace SharpGrip.FileSystem.Constants +{ + public static class FileSystemConstants + { + public static class Streaming + { + public const int DefaultMemoryStreamBufferSize = 81920; + } + } +} \ No newline at end of file diff --git a/FileSystem/src/Exceptions/InvalidVirtualPathException.cs b/FileSystem/src/Exceptions/InvalidVirtualPathException.cs new file mode 100644 index 0000000..71d26b5 --- /dev/null +++ b/FileSystem/src/Exceptions/InvalidVirtualPathException.cs @@ -0,0 +1,17 @@ +namespace SharpGrip.FileSystem.Exceptions +{ + public class InvalidVirtualPathException : FileSystemException + { + public string VirtualPath { get; } + + public InvalidVirtualPathException(string virtualPath) : base(GetMessage(virtualPath)) + { + VirtualPath = virtualPath; + } + + private static string GetMessage(string virtualPath) + { + return $"Invalid virtual path '{virtualPath}'."; + } + } +} \ No newline at end of file diff --git a/FileSystem/src/FileSystem.cs b/FileSystem/src/FileSystem.cs index 983b7c2..28d62ce 100644 --- a/FileSystem/src/FileSystem.cs +++ b/FileSystem/src/FileSystem.cs @@ -154,23 +154,25 @@ public IFile GetFile(string virtualPath) /// Thrown when an adapter could not be found via the provided prefix. /// Thrown when a prefix in the provided path could not be found. /// Thrown if the file does not exists at the given path. - public Task GetFileAsync(string virtualPath, CancellationToken cancellationToken = default) + public async Task GetFileAsync(string virtualPath, CancellationToken cancellationToken = default) { try { Logger.LogStartExecutingMethod(nameof(GetFileAsync)); + virtualPath = PathUtilities.NormalizeVirtualPath(virtualPath); + var prefix = PathUtilities.GetPrefix(virtualPath); var adapter = GetAdapter(prefix); - adapter.Connect(); + await adapter.ConnectAsync(cancellationToken); if (!adapter.AdapterConfiguration.EnableCache) { adapter.ClearCache(); } - return adapter.GetFileAsync(virtualPath, cancellationToken); + return await adapter.GetFileAsync(virtualPath, cancellationToken); } catch (FileSystemException fileSystemException) { @@ -213,23 +215,25 @@ public IDirectory GetDirectory(string virtualPath) /// Thrown when an adapter could not be found via the provided prefix. /// Thrown when a prefix in the provided path could not be found. /// Thrown if the directory does not exists at the given path. - public Task GetDirectoryAsync(string virtualPath, CancellationToken cancellationToken = default) + public async Task GetDirectoryAsync(string virtualPath, CancellationToken cancellationToken = default) { try { Logger.LogStartExecutingMethod(nameof(GetDirectoryAsync)); + virtualPath = PathUtilities.NormalizeVirtualPath(virtualPath); + var prefix = PathUtilities.GetPrefix(virtualPath); var adapter = GetAdapter(prefix); - adapter.Connect(); + await adapter.ConnectAsync(cancellationToken); if (!adapter.AdapterConfiguration.EnableCache) { adapter.ClearCache(); } - return adapter.GetDirectoryAsync(virtualPath, cancellationToken); + return await adapter.GetDirectoryAsync(virtualPath, cancellationToken); } catch (FileSystemException fileSystemException) { @@ -272,23 +276,25 @@ public IEnumerable GetFiles(string virtualPath = "") /// Thrown when an adapter could not be found via the provided prefix. /// Thrown when a prefix in the provided path could not be found. /// Thrown if the directory does not exists at the given path. - public Task> GetFilesAsync(string virtualPath = "", CancellationToken cancellationToken = default) + public async Task> GetFilesAsync(string virtualPath = "", CancellationToken cancellationToken = default) { try { Logger.LogStartExecutingMethod(nameof(GetFilesAsync)); + virtualPath = PathUtilities.NormalizeVirtualPath(virtualPath); + var prefix = PathUtilities.GetPrefix(virtualPath); var adapter = GetAdapter(prefix); - adapter.Connect(); + await adapter.ConnectAsync(cancellationToken); if (!adapter.AdapterConfiguration.EnableCache) { adapter.ClearCache(); } - return adapter.GetFilesAsync(virtualPath, cancellationToken); + return await adapter.GetFilesAsync(virtualPath, cancellationToken); } catch (FileSystemException fileSystemException) { @@ -331,23 +337,25 @@ public IEnumerable GetDirectories(string virtualPath = "") /// Thrown when an adapter could not be found via the provided prefix. /// Thrown when a prefix in the provided path could not be found. /// Thrown if the directory does not exists at the given path. - public Task> GetDirectoriesAsync(string virtualPath = "", CancellationToken cancellationToken = default) + public async Task> GetDirectoriesAsync(string virtualPath = "", CancellationToken cancellationToken = default) { try { Logger.LogStartExecutingMethod(nameof(GetDirectoriesAsync)); + virtualPath = PathUtilities.NormalizeVirtualPath(virtualPath); + var prefix = PathUtilities.GetPrefix(virtualPath); var adapter = GetAdapter(prefix); - adapter.Connect(); + await adapter.ConnectAsync(cancellationToken); if (!adapter.AdapterConfiguration.EnableCache) { adapter.ClearCache(); } - return adapter.GetDirectoriesAsync(virtualPath, cancellationToken); + return await adapter.GetDirectoriesAsync(virtualPath, cancellationToken); } catch (FileSystemException fileSystemException) { @@ -394,10 +402,12 @@ public async Task FileExistsAsync(string virtualPath, CancellationToken ca { Logger.LogStartExecutingMethod(nameof(FileExistsAsync)); + virtualPath = PathUtilities.NormalizeVirtualPath(virtualPath); + var prefix = PathUtilities.GetPrefix(virtualPath); var adapter = GetAdapter(prefix); - adapter.Connect(); + await adapter.ConnectAsync(cancellationToken); if (!adapter.AdapterConfiguration.EnableCache) { @@ -451,10 +461,12 @@ public async Task DirectoryExistsAsync(string virtualPath, CancellationTok { Logger.LogStartExecutingMethod(nameof(DirectoryExistsAsync)); + virtualPath = PathUtilities.NormalizeVirtualPath(virtualPath); + var prefix = PathUtilities.GetPrefix(virtualPath); var adapter = GetAdapter(prefix); - adapter.Connect(); + await adapter.ConnectAsync(cancellationToken); if (!adapter.AdapterConfiguration.EnableCache) { @@ -508,10 +520,12 @@ public async Task CreateDirectoryAsync(string virtualPath, CancellationToken can { Logger.LogStartExecutingMethod(nameof(CreateDirectoryAsync)); + virtualPath = PathUtilities.NormalizeVirtualPath(virtualPath); + var prefix = PathUtilities.GetPrefix(virtualPath); var adapter = GetAdapter(prefix); - adapter.Connect(); + await adapter.ConnectAsync(cancellationToken); if (!adapter.AdapterConfiguration.EnableCache) { @@ -565,10 +579,12 @@ public async Task DeleteFileAsync(string virtualPath, CancellationToken cancella { Logger.LogStartExecutingMethod(nameof(DeleteFileAsync)); + virtualPath = PathUtilities.NormalizeVirtualPath(virtualPath); + var prefix = PathUtilities.GetPrefix(virtualPath); var adapter = GetAdapter(prefix); - adapter.Connect(); + await adapter.ConnectAsync(cancellationToken); if (!adapter.AdapterConfiguration.EnableCache) { @@ -622,10 +638,12 @@ public async Task DeleteDirectoryAsync(string virtualPath, CancellationToken can { Logger.LogStartExecutingMethod(nameof(DeleteDirectoryAsync)); + virtualPath = PathUtilities.NormalizeVirtualPath(virtualPath); + var prefix = PathUtilities.GetPrefix(virtualPath); var adapter = GetAdapter(prefix); - adapter.Connect(); + await adapter.ConnectAsync(cancellationToken); if (!adapter.AdapterConfiguration.EnableCache) { @@ -663,10 +681,12 @@ public async Task ReadFileStreamAsync(string virtualPath, CancellationTo { Logger.LogStartExecutingMethod(nameof(ReadFileStreamAsync)); + virtualPath = PathUtilities.NormalizeVirtualPath(virtualPath); + var prefix = PathUtilities.GetPrefix(virtualPath); var adapter = GetAdapter(prefix); - adapter.Connect(); + await adapter.ConnectAsync(cancellationToken); if (!adapter.AdapterConfiguration.EnableCache) { @@ -722,6 +742,8 @@ public async Task ReadFileAsync(string virtualPath, CancellationToken ca { Logger.LogStartExecutingMethod(nameof(ReadFileAsync)); + virtualPath = PathUtilities.NormalizeVirtualPath(virtualPath); + var stream = await ReadFileStreamAsync(virtualPath, cancellationToken); using var memoryStream = await StreamUtilities.CopyContentsToMemoryStreamAsync(stream, true, cancellationToken); @@ -774,6 +796,8 @@ public async Task ReadTextFileAsync(string virtualPath, CancellationToke { Logger.LogStartExecutingMethod(nameof(ReadTextFileAsync)); + virtualPath = PathUtilities.NormalizeVirtualPath(virtualPath); + using var stream = await ReadFileStreamAsync(virtualPath, cancellationToken); using var streamReader = new StreamReader(stream); @@ -830,20 +854,23 @@ public async Task CopyFileAsync(string virtualSourcePath, string virtualDestinat { Logger.LogStartExecutingMethod(nameof(CopyFileAsync)); + virtualSourcePath = PathUtilities.NormalizeVirtualPath(virtualSourcePath); + virtualDestinationPath = PathUtilities.NormalizeVirtualPath(virtualDestinationPath); + var sourcePrefix = PathUtilities.GetPrefix(virtualSourcePath); var sourceAdapter = GetAdapter(sourcePrefix); var destinationPrefix = PathUtilities.GetPrefix(virtualDestinationPath); var destinationAdapter = GetAdapter(destinationPrefix); - sourceAdapter.Connect(); + await sourceAdapter.ConnectAsync(cancellationToken); if (!sourceAdapter.AdapterConfiguration.EnableCache) { sourceAdapter.ClearCache(); } - destinationAdapter.Connect(); + await destinationAdapter.ConnectAsync(cancellationToken); if (!destinationAdapter.AdapterConfiguration.EnableCache) { @@ -904,20 +931,23 @@ public async Task MoveFileAsync(string virtualSourcePath, string virtualDestinat { Logger.LogStartExecutingMethod(nameof(MoveFileAsync)); + virtualSourcePath = PathUtilities.NormalizeVirtualPath(virtualSourcePath); + virtualDestinationPath = PathUtilities.NormalizeVirtualPath(virtualDestinationPath); + var sourcePrefix = PathUtilities.GetPrefix(virtualSourcePath); var sourceAdapter = GetAdapter(sourcePrefix); var destinationPrefix = PathUtilities.GetPrefix(virtualDestinationPath); var destinationAdapter = GetAdapter(destinationPrefix); - sourceAdapter.Connect(); + await sourceAdapter.ConnectAsync(cancellationToken); if (!sourceAdapter.AdapterConfiguration.EnableCache) { sourceAdapter.ClearCache(); } - destinationAdapter.Connect(); + await destinationAdapter.ConnectAsync(cancellationToken); if (!destinationAdapter.AdapterConfiguration.EnableCache) { @@ -961,10 +991,12 @@ public async Task WriteFileAsync(string virtualPath, Stream contents, bool overw { Logger.LogStartExecutingMethod(nameof(WriteFileAsync)); + virtualPath = PathUtilities.NormalizeVirtualPath(virtualPath); + var prefix = PathUtilities.GetPrefix(virtualPath); var adapter = GetAdapter(prefix); - adapter.Connect(); + await adapter.ConnectAsync(cancellationToken); if (!adapter.AdapterConfiguration.EnableCache) { @@ -1078,10 +1110,12 @@ public async Task AppendFileAsync(string virtualPath, Stream contents, Cancellat { Logger.LogStartExecutingMethod(nameof(AppendFileAsync)); + virtualPath = PathUtilities.NormalizeVirtualPath(virtualPath); + var prefix = PathUtilities.GetPrefix(virtualPath); var adapter = GetAdapter(prefix); - adapter.Connect(); + await adapter.ConnectAsync(cancellationToken); if (!adapter.AdapterConfiguration.EnableCache) { diff --git a/FileSystem/src/Utilities/PathUtilities.cs b/FileSystem/src/Utilities/PathUtilities.cs index 2077fb8..61a008e 100644 --- a/FileSystem/src/Utilities/PathUtilities.cs +++ b/FileSystem/src/Utilities/PathUtilities.cs @@ -36,6 +36,28 @@ public static string NormalizeRootPath(string rootPath) return rootPath.Replace(InvalidPathSeparator, PathSeparator).RemoveTrailingForwardSlash(); } + /// + /// Normalizes a virtual path. + /// + /// The virtual path. + /// The normalized virtual path. + public static string NormalizeVirtualPath(string virtualPath) + { + if (!virtualPath.Contains(AdapterPrefixSeparator)) + { + throw new InvalidVirtualPathException(virtualPath); + } + + var prefixAndPath = ResolvePrefixAndPath(virtualPath); + + if (prefixAndPath.Length == 1) + { + return virtualPath; + } + + return prefixAndPath[0] + AdapterPrefixSeparator + prefixAndPath[1].RemoveLeadingForwardSlash(); + } + /// /// Returns the path from a prefixed path. /// diff --git a/FileSystem/src/Utilities/StreamUtilities.cs b/FileSystem/src/Utilities/StreamUtilities.cs index 4462951..aba6178 100644 --- a/FileSystem/src/Utilities/StreamUtilities.cs +++ b/FileSystem/src/Utilities/StreamUtilities.cs @@ -10,7 +10,7 @@ public static class StreamUtilities public static async Task CopyContentsToMemoryStreamAsync(Stream sourceStream, bool setPositionToStart, CancellationToken cancellationToken = default) { var memoryStream = new MemoryStream(); - await sourceStream.CopyToAsync(memoryStream, AdapterConstants.DefaultMemoryStreamBufferSize, cancellationToken); + await sourceStream.CopyToAsync(memoryStream, FileSystemConstants.Streaming.DefaultMemoryStreamBufferSize, cancellationToken); if (setPositionToStart) { diff --git a/README.md b/README.md index 7eadfd1..aeb6ee4 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ For adapters other than the local file system (included in the `SharpGrip.FileSy | [AzureBlobStorage](#azureblobstorage-adapter) | `SharpGrip.FileSystem.Adapters.AzureBlobStorage` | [![NuGet](https://img.shields.io/nuget/v/SharpGrip.FileSystem.Adapters.AzureBlobStorage)](https://www.nuget.org/packages/SharpGrip.FileSystem.Adapters.AzureBlobStorage) | | [AzureFileStorage](#azurefilestorage-adapter) | `SharpGrip.FileSystem.Adapters.AzureFileStorage` | [![NuGet](https://img.shields.io/nuget/v/SharpGrip.FileSystem.Adapters.AzureFileStorage)](https://www.nuget.org/packages/SharpGrip.FileSystem.Adapters.AzureFileStorage) | | [Dropbox](#dropbox-adapter) | `SharpGrip.FileSystem.Adapters.Dropbox` | [![NuGet](https://img.shields.io/nuget/v/SharpGrip.FileSystem.Adapters.Dropbox)](https://www.nuget.org/packages/SharpGrip.FileSystem.Adapters.Dropbox) | +| [FTP](#ftp-adapter) | `SharpGrip.FileSystem.Adapters.Ftp` | [![NuGet](https://img.shields.io/nuget/v/SharpGrip.FileSystem.Adapters.Ftp)](https://www.nuget.org/packages/SharpGrip.FileSystem.Adapters.Ftp) | | [GoogleDrive](#googledrive-adapter) | `SharpGrip.FileSystem.Adapters.GoogleDrive` | [![NuGet](https://img.shields.io/nuget/v/SharpGrip.FileSystem.Adapters.GoogleDrive)](https://www.nuget.org/packages/SharpGrip.FileSystem.Adapters.GoogleDrive) | | [MicrosoftOneDrive](#microsoftonedrive-adapter) | `SharpGrip.FileSystem.Adapters.MicrosoftOneDrive` | [![NuGet](https://img.shields.io/nuget/v/SharpGrip.FileSystem.Adapters.MicrosoftOneDrive)](https://www.nuget.org/packages/SharpGrip.FileSystem.Adapters.MicrosoftOneDrive) | | [SFTP](#sftp-adapter) | `SharpGrip.FileSystem.Adapters.Sftp` | [![NuGet](https://img.shields.io/nuget/v/SharpGrip.FileSystem.Adapters.Sftp)](https://www.nuget.org/packages/SharpGrip.FileSystem.Adapters.Sftp) | @@ -128,6 +129,21 @@ var adapters = new List var fileSystem = new FileSystem(adapters); ``` +### FTP adapter + +``` +// FTP connection. +var ftpClient = new AsyncFtpClient("hostname", "username", "password"); + +var adapters = new List +{ + new LocalAdapter("local", "/var/files"), + new FtpAdapter("ftp", "/var/files", ftpClient) +}; + +var fileSystem = new FileSystem(adapters); +``` + ### GoogleDrive adapter ``` diff --git a/Tests/src/FileSystem.Adapters.AmazonS3/AmazonS3AdapterTest.cs b/Tests/src/FileSystem.Adapters.AmazonS3/AmazonS3AdapterTest.cs index 0342332..45f1a59 100644 --- a/Tests/src/FileSystem.Adapters.AmazonS3/AmazonS3AdapterTest.cs +++ b/Tests/src/FileSystem.Adapters.AmazonS3/AmazonS3AdapterTest.cs @@ -36,14 +36,12 @@ public void Test_Instantiation() } [Fact] - public Task Test_Connect() + public async Task Test_Connect() { var amazonS3Client = Substitute.For(); var amazonS3Adapter = new AmazonS3Adapter("prefix", "/root-path", amazonS3Client, "bucket"); - amazonS3Adapter.Connect(); - - return Task.CompletedTask; + await amazonS3Adapter.ConnectAsync(); } [Fact] diff --git a/Tests/src/FileSystem.Adapters.AzureBlobStorage/AzureBlobStorageAdapterTest.cs b/Tests/src/FileSystem.Adapters.AzureBlobStorage/AzureBlobStorageAdapterTest.cs index 38e18a5..b57bc9a 100644 --- a/Tests/src/FileSystem.Adapters.AzureBlobStorage/AzureBlobStorageAdapterTest.cs +++ b/Tests/src/FileSystem.Adapters.AzureBlobStorage/AzureBlobStorageAdapterTest.cs @@ -19,14 +19,12 @@ public void Test_Instantiation() } [Fact] - public Task Test_Connect() + public async Task Test_Connect() { var blobContainerClient = Substitute.For(); var azureBlobStorageAdapter = new AzureBlobStorageAdapter("prefix", "/root-path", blobContainerClient); - azureBlobStorageAdapter.Connect(); - - return Task.CompletedTask; + await azureBlobStorageAdapter.ConnectAsync(); } [Fact] diff --git a/Tests/src/FileSystem.Adapters.AzureFileStorage/AzureFileStorageAdapterTest.cs b/Tests/src/FileSystem.Adapters.AzureFileStorage/AzureFileStorageAdapterTest.cs index 3dedba0..9ada96b 100644 --- a/Tests/src/FileSystem.Adapters.AzureFileStorage/AzureFileStorageAdapterTest.cs +++ b/Tests/src/FileSystem.Adapters.AzureFileStorage/AzureFileStorageAdapterTest.cs @@ -19,14 +19,12 @@ public void Test_Instantiation() } [Fact] - public Task Test_Connect() + public async Task Test_Connect() { var shareClient = Substitute.For(); var azureFileStorageAdapter = new AzureFileStorageAdapter("prefix", "/root-path", shareClient); - azureFileStorageAdapter.Connect(); - - return Task.CompletedTask; + await azureFileStorageAdapter.ConnectAsync(); } [Fact] diff --git a/Tests/src/FileSystem.Adapters.GoogleDrive/GoogleDriveAdapterTest.cs b/Tests/src/FileSystem.Adapters.GoogleDrive/GoogleDriveAdapterTest.cs index c19ea3a..ed76597 100644 --- a/Tests/src/FileSystem.Adapters.GoogleDrive/GoogleDriveAdapterTest.cs +++ b/Tests/src/FileSystem.Adapters.GoogleDrive/GoogleDriveAdapterTest.cs @@ -19,14 +19,12 @@ public void Test_Instantiation() } [Fact] - public Task Test_Connect() + public async Task Test_Connect() { var googleDriveClient = Substitute.For(); var googleDriveAdapter = new GoogleDriveAdapter("prefix", "/root-path", googleDriveClient); - googleDriveAdapter.Connect(); - - return Task.CompletedTask; + await googleDriveAdapter.ConnectAsync(); } [Fact] diff --git a/Tests/src/FileSystem.Adapters.MicrosoftOneDrive/MicrosoftOneDriveAdapterTest.cs b/Tests/src/FileSystem.Adapters.MicrosoftOneDrive/MicrosoftOneDriveAdapterTest.cs index e6cc23d..78072aa 100644 --- a/Tests/src/FileSystem.Adapters.MicrosoftOneDrive/MicrosoftOneDriveAdapterTest.cs +++ b/Tests/src/FileSystem.Adapters.MicrosoftOneDrive/MicrosoftOneDriveAdapterTest.cs @@ -21,15 +21,13 @@ public void Test_Instantiation() } [Fact] - public Task Test_Connect() + public async Task Test_Connect() { var delegateAuthenticationProvider = new DelegateAuthenticationProvider(message => Task.FromResult(message.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "12345"))); var graphServiceClient = Substitute.For(delegateAuthenticationProvider, null); var microsoftOneDriveAdapter = new MicrosoftOneDriveAdapter("prefix", "/root-path", graphServiceClient, "driveId"); - microsoftOneDriveAdapter.Connect(); - - return Task.CompletedTask; + await microsoftOneDriveAdapter.ConnectAsync(); } [Fact] diff --git a/Tests/src/FileSystem.Adapters.Sftp/SftpAdapterTest.cs b/Tests/src/FileSystem.Adapters.Sftp/SftpAdapterTest.cs index d410219..df4b1a4 100644 --- a/Tests/src/FileSystem.Adapters.Sftp/SftpAdapterTest.cs +++ b/Tests/src/FileSystem.Adapters.Sftp/SftpAdapterTest.cs @@ -31,14 +31,12 @@ public void Test_Instantiation() } [Fact] - public Task Test_Connect() + public async Task Test_Connect() { var sftpClient = Substitute.For(); var sftpAdapter = new SftpAdapter("prefix", "/root-path", sftpClient); - sftpAdapter.Connect(); - - return Task.CompletedTask; + await sftpAdapter.ConnectAsync(); } [Fact]