Skip to content

Commit

Permalink
Upgraded to SSH.NET 2023.0.0 With Async support (#27)
Browse files Browse the repository at this point in the history
* Upgraded to SSH.NET 2023.0.0 With Async support

* Apply suggestions from code review

* Added error logging

* Had to do an exists check so rename doesn't error when file exists as it's no longer a sync posix rename operation

* Renamed async method
  • Loading branch information
niemyjski authored Oct 12, 2023
1 parent 80dad59 commit 4539154
Show file tree
Hide file tree
Showing 6 changed files with 56 additions and 55 deletions.
2 changes: 1 addition & 1 deletion build/common.props
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project ToolsVersion="15.0">

<PropertyGroup>
<TargetFrameworks>netstandard2.0</TargetFrameworks>
<TargetFrameworks>netstandard2.1</TargetFrameworks>
<Product>Foundatio</Product>
<Description>Pluggable foundation blocks for building distributed apps.</Description>
<PackageProjectUrl>https://github.com/FoundatioFx/Foundatio.Storage.SshNet</PackageProjectUrl>
Expand Down
13 changes: 0 additions & 13 deletions src/Foundatio.Storage.SshNet/Extensions/SftpExtensions.cs

This file was deleted.

6 changes: 6 additions & 0 deletions src/Foundatio.Storage.SshNet/Extensions/TaskExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
Expand All @@ -21,4 +22,9 @@ public static ConfiguredTaskAwaitable AnyContext(this Task task) {
public static ConfiguredTaskAwaitable<TResult> AnyContext<TResult>(this AwaitableDisposable<TResult> task) where TResult : IDisposable {
return task.ConfigureAwait(continueOnCapturedContext: false);
}

[DebuggerStepThrough]
public static ConfiguredCancelableAsyncEnumerable<TResult> AnyContext<TResult>(this IAsyncEnumerable<TResult> task) {
return task.ConfigureAwait(continueOnCapturedContext: false);
}
}
4 changes: 2 additions & 2 deletions src/Foundatio.Storage.SshNet/Foundatio.Storage.SshNet.csproj
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\build\common.props" />
<PropertyGroup>
<TargetFrameworks>netstandard2.0</TargetFrameworks>
<TargetFrameworks>netstandard2.1</TargetFrameworks>
<PackageTags>;File;Distributed;Storage;SFTP;SshNet</PackageTags>
</PropertyGroup>
<ItemGroup>
<None Include="..\..\docker-compose.yml" Link="docker-compose.yml" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="SSH.NET" Version="2020.0.2" />
<PackageReference Include="SSH.NET" Version="2023.0.0" />
<PackageReference Include="Foundatio" Version="10.6.1" Condition="'$(ReferenceFoundatioSource)' == '' OR '$(ReferenceFoundatioSource)' == 'false'" />
<ProjectReference Include="..\..\..\Foundatio\src\Foundatio\Foundatio.csproj" Condition="'$(ReferenceFoundatioSource)' == 'true'" />
</ItemGroup>
Expand Down
78 changes: 43 additions & 35 deletions src/Foundatio.Storage.SshNet/Storage/SshNetFileStorage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@
namespace Foundatio.Storage;

public class SshNetFileStorage : IFileStorage {
private readonly ConnectionInfo _connectionInfo;
private readonly SftpClient _client;
private readonly ISftpClient _client;
private readonly ISerializer _serializer;
protected readonly ILogger _logger;

Expand All @@ -28,15 +27,15 @@ public SshNetFileStorage(SshNetFileStorageOptions options) {
_serializer = options.Serializer ?? DefaultSerializer.Instance;
_logger = options.LoggerFactory?.CreateLogger(GetType()) ?? NullLogger.Instance;

_connectionInfo = CreateConnectionInfo(options);
_client = new SftpClient(_connectionInfo);
var connectionInfo = CreateConnectionInfo(options);
_client = new SftpClient(connectionInfo);
}

public SshNetFileStorage(Builder<SshNetFileStorageOptionsBuilder, SshNetFileStorageOptions> config)
: this(config(new SshNetFileStorageOptionsBuilder()).Build()) { }

ISerializer IHaveSerializer.Serializer => _serializer;
public SftpClient GetClient() {
public ISftpClient GetClient() {
EnsureClientConnected();
return _client;
}
Expand All @@ -51,11 +50,7 @@ public async Task<Stream> GetFileStreamAsync(string path, CancellationToken canc
_logger.LogTrace("Getting file stream for {Path}", normalizedPath);

try {
var stream = new MemoryStream();
await Task.Factory.FromAsync(_client.BeginDownloadFile(normalizedPath, stream, null, null), _client.EndDownloadFile).AnyContext();
stream.Seek(0, SeekOrigin.Begin);

return stream;
return await _client.OpenAsync(normalizedPath, FileMode.Open, FileAccess.Read, cancellationToken).AnyContext();
} catch (SftpPathNotFoundException ex) {
_logger.LogError(ex, "Unable to get file stream for {Path}: File Not Found", normalizedPath);
return null;
Expand Down Expand Up @@ -108,19 +103,21 @@ public async Task<bool> SaveFileAsync(string path, Stream stream, CancellationTo
EnsureClientConnected();

try {
await Task.Factory.FromAsync(_client.BeginUploadFile(stream, normalizedPath, null, null), _client.EndUploadFile).AnyContext();
await using var sftpFileStream = await _client.OpenAsync(normalizedPath, FileMode.OpenOrCreate, FileAccess.Write, cancellationToken).AnyContext();
await stream.CopyToAsync(sftpFileStream, cancellationToken).AnyContext();
} catch (SftpPathNotFoundException ex) {
_logger.LogDebug(ex, "Error saving {Path}: Attempting to create directory", normalizedPath);
CreateDirectory(normalizedPath);

_logger.LogTrace("Saving {Path}", normalizedPath);
await Task.Factory.FromAsync(_client.BeginUploadFile(stream, normalizedPath, null, null), _client.EndUploadFile).AnyContext();
await using var sftpFileStream = await _client.OpenAsync(normalizedPath, FileMode.OpenOrCreate, FileAccess.Write, cancellationToken).AnyContext();
await stream.CopyToAsync(sftpFileStream, cancellationToken).AnyContext();
}

return true;
}

public Task<bool> RenameFileAsync(string path, string newPath, CancellationToken cancellationToken = default) {
public async Task<bool> RenameFileAsync(string path, string newPath, CancellationToken cancellationToken = default) {
if (String.IsNullOrEmpty(path))
throw new ArgumentNullException(nameof(path));
if (String.IsNullOrEmpty(newPath))
Expand All @@ -131,17 +128,26 @@ public Task<bool> RenameFileAsync(string path, string newPath, CancellationToken
_logger.LogInformation("Renaming {Path} to {NewPath}", normalizedPath, normalizedNewPath);
EnsureClientConnected();

if (await ExistsAsync(normalizedNewPath).AnyContext()) {
_logger.LogDebug("Removing existing {NewPath} path for rename operation", normalizedNewPath);
await DeleteFileAsync(normalizedNewPath, cancellationToken).AnyContext();
_logger.LogDebug("Removed existing {NewPath} path for rename operation", normalizedNewPath);
}

try {
_client.RenameFile(normalizedPath, normalizedNewPath, true);
await _client.RenameFileAsync(normalizedPath, normalizedNewPath, cancellationToken).AnyContext();
} catch (SftpPathNotFoundException ex) {
_logger.LogDebug(ex, "Error renaming {Path} to {NewPath}: Attempting to create directory", normalizedPath, normalizedNewPath);
CreateDirectory(normalizedNewPath);

_logger.LogTrace("Renaming {Path} to {NewPath}", normalizedPath, normalizedNewPath);
_client.RenameFile(normalizedPath, normalizedNewPath, true);
await _client.RenameFileAsync(normalizedPath, normalizedNewPath, cancellationToken).AnyContext();
} catch (Exception ex) {
_logger.LogError(ex, "Error renaming {Path} to {NewPath}: {Message}", normalizedPath, normalizedNewPath, ex.Message);
return false;
}

return Task.FromResult(true);
return true;
}

public async Task<bool> CopyFileAsync(string path, string targetPath, CancellationToken cancellationToken = default) {
Expand All @@ -155,7 +161,7 @@ public async Task<bool> CopyFileAsync(string path, string targetPath, Cancellati
_logger.LogInformation("Copying {Path} to {TargetPath}", normalizedPath, normalizedTargetPath);

try {
using var stream = await GetFileStreamAsync(normalizedPath, cancellationToken).AnyContext();
await using var stream = await GetFileStreamAsync(normalizedPath, cancellationToken).AnyContext();
if (stream == null)
return false;

Expand All @@ -166,7 +172,7 @@ public async Task<bool> CopyFileAsync(string path, string targetPath, Cancellati
}
}

public Task<bool> DeleteFileAsync(string path, CancellationToken cancellationToken = default) {
public async Task<bool> DeleteFileAsync(string path, CancellationToken cancellationToken = default) {
if (String.IsNullOrEmpty(path))
throw new ArgumentNullException(nameof(path));

Expand All @@ -176,23 +182,23 @@ public Task<bool> DeleteFileAsync(string path, CancellationToken cancellationTok
_logger.LogTrace("Deleting {Path}", normalizedPath);

try {
_client.DeleteFile(normalizedPath);
await _client.DeleteFileAsync(normalizedPath, cancellationToken).AnyContext();
} catch (SftpPathNotFoundException ex) {
_logger.LogError(ex, "Unable to delete {Path}: File not found", normalizedPath);
return Task.FromResult(false);
return false;
}

return Task.FromResult(true);
return true;
}

public async Task<int> DeleteFilesAsync(string searchPattern = null, CancellationToken cancellationToken = default) {
EnsureClientConnected();

if (searchPattern == null)
return await DeleteDirectory(_client.WorkingDirectory, false);
return await DeleteDirectoryAsync(_client.WorkingDirectory, false, cancellationToken);

if (searchPattern.EndsWith("/*"))
return await DeleteDirectory(searchPattern.Substring(0, searchPattern.Length - 2), false);
return await DeleteDirectoryAsync(searchPattern[..^2], false, cancellationToken);

var files = await GetFileListAsync(searchPattern, cancellationToken: cancellationToken).AnyContext();
int count = 0;
Expand All @@ -209,10 +215,10 @@ public async Task<int> DeleteFilesAsync(string searchPattern = null, Cancellatio
}

private void CreateDirectory(string path) {
string directory = Path.GetDirectoryName(path);
string directory = NormalizePath(Path.GetDirectoryName(path));
_logger.LogTrace("Ensuring {Directory} directory exists", directory);

string[] folderSegments = directory.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
string[] folderSegments = directory?.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries) ?? Array.Empty<string>();
string currentDirectory = String.Empty;

foreach (string segment in folderSegments) {
Expand All @@ -229,21 +235,21 @@ private void CreateDirectory(string path) {
}
}

private async Task<int> DeleteDirectory(string path, bool includeSelf) {
private async Task<int> DeleteDirectoryAsync(string path, bool includeSelf, CancellationToken cancellationToken = default) {
int count = 0;

string directory = NormalizePath(path);
_logger.LogInformation("Deleting {Directory} directory", directory);

foreach (var file in await _client.ListDirectoryAsync(directory)) {
await foreach (var file in _client.ListDirectoryAsync(directory, cancellationToken).AnyContext()) {
if (file.Name is "." or "..")
continue;

if (file.IsDirectory) {
count += await DeleteDirectory(file.FullName, true);
count += await DeleteDirectoryAsync(file.FullName, true, cancellationToken);
} else {
_logger.LogTrace("Deleting file {Path}", file.FullName);
_client.DeleteFile(file.FullName);
await _client.DeleteFileAsync(file.FullName, cancellationToken).AnyContext();
count++;
}
}
Expand Down Expand Up @@ -319,9 +325,11 @@ private async Task GetFileListRecursivelyAsync(string prefix, Regex pattern, ICo
return;
}

var files = new List<SftpFile>();
var files = new List<ISftpFile>();
try {
files.AddRange(await _client.ListDirectoryAsync(prefix).AnyContext());
await foreach (var file in _client.ListDirectoryAsync(prefix, cancellationToken).AnyContext()) {
files.Add(file);
}
} catch (SftpPathNotFoundException) {
_logger.LogDebug("Directory not found with {Prefix}", prefix);
return;
Expand Down Expand Up @@ -437,13 +445,13 @@ private SearchCriteria GetRequestCriteria(string searchPattern) {

if (hasWildcard) {
patternRegex = new Regex($"^{Regex.Escape(normalizedSearchPattern).Replace("\\*", ".*?")}$");
string beforeWildcard = normalizedSearchPattern.Substring(0, wildcardPos);
string beforeWildcard = normalizedSearchPattern[..wildcardPos];
int slashPos = beforeWildcard.LastIndexOf('/');
prefix = slashPos >= 0 ? normalizedSearchPattern.Substring(0, slashPos) : String.Empty;
prefix = slashPos >= 0 ? normalizedSearchPattern[..slashPos] : String.Empty;
} else {
patternRegex = new Regex($"^{normalizedSearchPattern}$");
int slashPos = normalizedSearchPattern.LastIndexOf('/');
prefix = slashPos >= 0 ? normalizedSearchPattern.Substring(0, slashPos) : String.Empty;
prefix = slashPos >= 0 ? normalizedSearchPattern[..slashPos] : String.Empty;
}

return new SearchCriteria {
Expand All @@ -459,6 +467,6 @@ public void Dispose() {
_logger.LogTrace("Disconnected from {Host}:{Port}", _client.ConnectionInfo.Host, _client.ConnectionInfo.Port);
}

_client.Dispose();
((IBaseClient)_client).Dispose();
}
}
8 changes: 4 additions & 4 deletions tests/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
<NoWarn>$(NoWarn);CS1591;NU1701</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.2" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5" />
<PackageReference Include="GitHubActionsTestLogger" Version="2.3.2" PrivateAssets="All" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
<PackageReference Include="xunit" Version="2.5.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.1" />
<PackageReference Include="GitHubActionsTestLogger" Version="2.3.3" PrivateAssets="All" />

<PackageReference Include="Foundatio.TestHarness" Version="10.6.1" Condition="'$(ReferenceFoundatioSource)' == '' OR '$(ReferenceFoundatioSource)' == 'false'" />
<ProjectReference Include="..\..\..\Foundatio\src\Foundatio.TestHarness\Foundatio.TestHarness.csproj" Condition="'$(ReferenceFoundatioSource)' == 'true'" />
Expand Down

0 comments on commit 4539154

Please sign in to comment.