From 1c12ad455fa33d2c4e8a43dbbfac3320a8736a58 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Fri, 5 Jan 2024 08:23:43 -0600 Subject: [PATCH] dotnet format --- .editorconfig | 205 ++++++++++++++---- .../Extensions/TaskExtensions.cs | 19 +- .../Storage/SshNetFileStorage.cs | 204 +++++++++++------ .../Storage/SshNetFileStorageOptions.cs | 40 ++-- .../Properties/AssemblyInfo.cs | 2 +- .../Storage/RootedSshNetStorageTests.cs | 79 ++++--- .../Storage/SshNetStorageTests.cs | 79 ++++--- 7 files changed, 433 insertions(+), 195 deletions(-) diff --git a/.editorconfig b/.editorconfig index 07ff554..9d3abe5 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,78 +1,189 @@ -# EditorConfig is awesome:http://EditorConfig.org +# editorconfig.org (https://github.com/dotnet/runtime/blob/main/.editorconfig) # top-most EditorConfig file root = true -# Don't use tabs for indentation. +# Default settings: +# A newline ending every file +# Use 4 spaces as indentation [*] +insert_final_newline = true indent_style = space -# (Please don't specify an indent_size here; that has too many unintended consequences.) - -# Code files -[*.{cs,csx,vb,vbx}] indent_size = 4 +trim_trailing_whitespace = true -# Xml project files -[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] -indent_size = 2 +[*.json] +insert_final_newline = false -# Xml config files -[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] -indent_size = 2 +# Generated code +[*{_AssemblyInfo.cs,.notsupported.cs,AsmOffsets.cs}] +generated_code = true -# JSON files -[*.json] -indent_size = 2 +# C# files +[*.cs] +# New line preferences +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_between_query_expression_clauses = true -# Dotnet code style settings: -[*.{cs, vb}] -# Sort using and Import directives with System.* appearing first -dotnet_sort_system_directives_first = true -# Avoid "this." and "Me." if not necessary +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_switch_labels = true +csharp_indent_labels = one_less_than_current + +# Modifier preferences +csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async:suggestion + +# avoid this. unless absolutely necessary dotnet_style_qualification_for_field = false:suggestion dotnet_style_qualification_for_property = false:suggestion dotnet_style_qualification_for_method = false:suggestion dotnet_style_qualification_for_event = false:suggestion -# Use language keywords instead of framework type names for type references +# Types: use keywords instead of BCL types, and permit var only when the type is clear +csharp_style_var_for_built_in_types = false:suggestion +csharp_style_var_when_type_is_apparent = false:none +csharp_style_var_elsewhere = true:suggestion dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion dotnet_style_predefined_type_for_member_access = false:suggestion -# Suggest more modern language features when available +# name all constant fields using PascalCase +dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields +dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style +dotnet_naming_symbols.constant_fields.applicable_kinds = field +dotnet_naming_symbols.constant_fields.required_modifiers = const +dotnet_naming_style.pascal_case_style.capitalization = pascal_case + +# static fields should have s_ prefix +dotnet_naming_rule.static_fields_should_have_prefix.severity = false:none +dotnet_naming_rule.static_fields_should_have_prefix.symbols = static_fields +dotnet_naming_rule.static_fields_should_have_prefix.style = static_prefix_style +dotnet_naming_symbols.static_fields.applicable_kinds = field +dotnet_naming_symbols.static_fields.required_modifiers = static +dotnet_naming_symbols.static_fields.applicable_accessibilities = private, internal, private_protected +dotnet_naming_style.static_prefix_style.required_prefix = s_ +dotnet_naming_style.static_prefix_style.capitalization = camel_case + +# internal and private fields should be _camelCase +dotnet_naming_rule.camel_case_for_private_internal_fields.severity = suggestion +dotnet_naming_rule.camel_case_for_private_internal_fields.symbols = private_internal_fields +dotnet_naming_rule.camel_case_for_private_internal_fields.style = camel_case_underscore_style +dotnet_naming_symbols.private_internal_fields.applicable_kinds = field +dotnet_naming_symbols.private_internal_fields.applicable_accessibilities = private, internal +dotnet_naming_style.camel_case_underscore_style.required_prefix = _ +dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case + +# Code style defaults +csharp_using_directive_placement = outside_namespace:suggestion +dotnet_sort_system_directives_first = true +csharp_prefer_braces = true:silent +csharp_preserve_single_line_blocks = true:none +csharp_preserve_single_line_statements = false:none +csharp_prefer_static_local_function = true:suggestion +csharp_prefer_simple_using_statement = false:none +csharp_style_prefer_switch_expression = true:suggestion +dotnet_style_readonly_field = true:suggestion + +# Expression-level preferences dotnet_style_object_initializer = true:suggestion dotnet_style_collection_initializer = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion dotnet_style_coalesce_expression = true:suggestion dotnet_style_null_propagation = true:suggestion -dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_auto_properties = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +csharp_prefer_simple_default_expression = true:suggestion -# CSharp code style settings: -[*.cs] -# Prefer "var" everywhere -csharp_style_var_for_built_in_types = false:suggestion -csharp_style_var_when_type_is_apparent = true:suggestion -csharp_style_var_elsewhere = true:suggestion - -# Prefer method-like constructs to have a block body -csharp_style_expression_bodied_methods = false:none -csharp_style_expression_bodied_constructors = false:none -csharp_style_expression_bodied_operators = false:none +# Expression-bodied members +csharp_style_expression_bodied_methods = true:silent +csharp_style_expression_bodied_constructors = true:silent +csharp_style_expression_bodied_operators = true:silent +csharp_style_expression_bodied_properties = true:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = true:silent -# Prefer property-like constructs to have an expression-body -csharp_style_expression_bodied_properties = true:none -csharp_style_expression_bodied_indexers = true:none -csharp_style_expression_bodied_accessors = true:none - -# Suggest more modern language features when available +# Pattern matching csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion csharp_style_pattern_matching_over_as_with_null_check = true:suggestion csharp_style_inlined_variable_declaration = true:suggestion + +# Null checking preferences csharp_style_throw_expression = true:suggestion csharp_style_conditional_delegate_call = true:suggestion -# Newline settings -csharp_new_line_before_open_brace = false:error -csharp_new_line_before_else = false:error -csharp_new_line_before_catch = false:error -csharp_new_line_before_finally = false:error -csharp_new_line_before_members_in_object_initializers = false:error -csharp_new_line_before_members_in_anonymous_types = false:error \ No newline at end of file +# Other features +csharp_style_prefer_index_operator = false:none +csharp_style_prefer_range_operator = false:none +csharp_style_pattern_local_over_anonymous_function = false:none + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = do_not_ignore +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# C++ Files +[*.{cpp,h,in}] +curly_bracket_next_line = true +indent_brace_style = Allman + +# Xml project files +[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,nativeproj,locproj}] +indent_size = 2 + +[*.{csproj,vbproj,proj,nativeproj,locproj}] +charset = utf-8 + +# Xml build files +[*.builds] +indent_size = 2 + +# Xml files +[*.{xml,stylecop,resx,ruleset}] +indent_size = 2 + +# Xml config files +[*.{props,targets,config,nuspec}] +indent_size = 2 + +# YAML config files +[*.{yml,yaml}] +indent_size = 2 + +# Shell scripts +[*.sh] +end_of_line = lf +[*.{cmd,bat}] +end_of_line = crlf diff --git a/src/Foundatio.Storage.SshNet/Extensions/TaskExtensions.cs b/src/Foundatio.Storage.SshNet/Extensions/TaskExtensions.cs index 380eb7f..7d3b577 100644 --- a/src/Foundatio.Storage.SshNet/Extensions/TaskExtensions.cs +++ b/src/Foundatio.Storage.SshNet/Extensions/TaskExtensions.cs @@ -5,26 +5,31 @@ using System.Threading.Tasks; using Foundatio.AsyncEx; -namespace Foundatio.Extensions; +namespace Foundatio.Extensions; -internal static class TaskExtensions { +internal static class TaskExtensions +{ [DebuggerStepThrough] - public static ConfiguredTaskAwaitable AnyContext(this Task task) { + public static ConfiguredTaskAwaitable AnyContext(this Task task) + { return task.ConfigureAwait(continueOnCapturedContext: false); } [DebuggerStepThrough] - public static ConfiguredTaskAwaitable AnyContext(this Task task) { + public static ConfiguredTaskAwaitable AnyContext(this Task task) + { return task.ConfigureAwait(continueOnCapturedContext: false); } [DebuggerStepThrough] - public static ConfiguredTaskAwaitable AnyContext(this AwaitableDisposable task) where TResult : IDisposable { + public static ConfiguredTaskAwaitable AnyContext(this AwaitableDisposable task) where TResult : IDisposable + { return task.ConfigureAwait(continueOnCapturedContext: false); } [DebuggerStepThrough] - public static ConfiguredCancelableAsyncEnumerable AnyContext(this IAsyncEnumerable task) { + public static ConfiguredCancelableAsyncEnumerable AnyContext(this IAsyncEnumerable task) + { return task.ConfigureAwait(continueOnCapturedContext: false); } -} \ No newline at end of file +} diff --git a/src/Foundatio.Storage.SshNet/Storage/SshNetFileStorage.cs b/src/Foundatio.Storage.SshNet/Storage/SshNetFileStorage.cs index 2dcd806..7dede8e 100644 --- a/src/Foundatio.Storage.SshNet/Storage/SshNetFileStorage.cs +++ b/src/Foundatio.Storage.SshNet/Storage/SshNetFileStorage.cs @@ -15,18 +15,20 @@ namespace Foundatio.Storage; -public class SshNetFileStorage : IFileStorage { +public class SshNetFileStorage : IFileStorage +{ private readonly ISftpClient _client; private readonly ISerializer _serializer; protected readonly ILogger _logger; - public SshNetFileStorage(SshNetFileStorageOptions options) { + public SshNetFileStorage(SshNetFileStorageOptions options) + { if (options == null) throw new ArgumentNullException(nameof(options)); _serializer = options.Serializer ?? DefaultSerializer.Instance; _logger = options.LoggerFactory?.CreateLogger(GetType()) ?? NullLogger.Instance; - + var connectionInfo = CreateConnectionInfo(options); _client = new SftpClient(connectionInfo); } @@ -35,7 +37,8 @@ public SshNetFileStorage(Builder _serializer; - public ISftpClient GetClient() { + public ISftpClient GetClient() + { EnsureClientConnected(); return _client; } @@ -56,16 +59,20 @@ public async Task GetFileStreamAsync(string path, StreamMode streamMode, string normalizedPath = NormalizePath(path); _logger.LogTrace("Getting file stream for {Path}", normalizedPath); - - try { + + try + { return await _client.OpenAsync(normalizedPath, FileMode.Open, FileAccess.Read, cancellationToken).AnyContext(); - } catch (SftpPathNotFoundException ex) { + } + catch (SftpPathNotFoundException ex) + { _logger.LogError(ex, "Unable to get file stream for {Path}: File Not Found", normalizedPath); return null; } } - public Task GetFileInfoAsync(string path) { + public Task GetFileInfoAsync(string path) + { if (String.IsNullOrEmpty(path)) throw new ArgumentNullException(nameof(path)); @@ -73,22 +80,27 @@ public Task GetFileInfoAsync(string path) { string normalizedPath = NormalizePath(path); _logger.LogTrace("Getting file info for {Path}", normalizedPath); - - try { + + try + { var file = _client.Get(normalizedPath); - return Task.FromResult(new FileSpec { + return Task.FromResult(new FileSpec + { Path = normalizedPath, Created = file.LastWriteTimeUtc, Modified = file.LastWriteTimeUtc, Size = file.Length }); - } catch (SftpPathNotFoundException ex) { + } + catch (SftpPathNotFoundException ex) + { _logger.LogError(ex, "Unable to get file info for {Path}: File Not Found", normalizedPath); return Task.FromResult(null); } } - public Task ExistsAsync(string path) { + public Task ExistsAsync(string path) + { if (String.IsNullOrEmpty(path)) throw new ArgumentNullException(nameof(path)); @@ -99,7 +111,8 @@ public Task ExistsAsync(string path) { return Task.FromResult(_client.Exists(normalizedPath)); } - public async Task SaveFileAsync(string path, Stream stream, CancellationToken cancellationToken = default) { + public async Task SaveFileAsync(string path, Stream stream, CancellationToken cancellationToken = default) + { if (String.IsNullOrEmpty(path)) throw new ArgumentNullException(nameof(path)); if (stream == null) @@ -110,10 +123,13 @@ public async Task SaveFileAsync(string path, Stream stream, CancellationTo EnsureClientConnected(); - try { + try + { await using var sftpFileStream = await _client.OpenAsync(normalizedPath, FileMode.OpenOrCreate, FileAccess.Write, cancellationToken).AnyContext(); await stream.CopyToAsync(sftpFileStream, cancellationToken).AnyContext(); - } catch (SftpPathNotFoundException ex) { + } + catch (SftpPathNotFoundException ex) + { _logger.LogDebug(ex, "Error saving {Path}: Attempting to create directory", normalizedPath); CreateDirectory(normalizedPath); @@ -125,7 +141,8 @@ public async Task SaveFileAsync(string path, Stream stream, CancellationTo return true; } - public async Task RenameFileAsync(string path, string newPath, CancellationToken cancellationToken = default) { + public async Task RenameFileAsync(string path, string newPath, CancellationToken cancellationToken = default) + { if (String.IsNullOrEmpty(path)) throw new ArgumentNullException(nameof(path)); if (String.IsNullOrEmpty(newPath)) @@ -136,21 +153,27 @@ public async Task RenameFileAsync(string path, string newPath, Cancellatio _logger.LogInformation("Renaming {Path} to {NewPath}", normalizedPath, normalizedNewPath); EnsureClientConnected(); - if (await ExistsAsync(normalizedNewPath).AnyContext()) { + 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 { + try + { await _client.RenameFileAsync(normalizedPath, normalizedNewPath, cancellationToken).AnyContext(); - } catch (SftpPathNotFoundException ex) { + } + 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); await _client.RenameFileAsync(normalizedPath, normalizedNewPath, cancellationToken).AnyContext(); - } catch (Exception ex) { + } + catch (Exception ex) + { _logger.LogError(ex, "Error renaming {Path} to {NewPath}: {Message}", normalizedPath, normalizedNewPath, ex.Message); return false; } @@ -158,7 +181,8 @@ public async Task RenameFileAsync(string path, string newPath, Cancellatio return true; } - public async Task CopyFileAsync(string path, string targetPath, CancellationToken cancellationToken = default) { + public async Task CopyFileAsync(string path, string targetPath, CancellationToken cancellationToken = default) + { if (String.IsNullOrEmpty(path)) throw new ArgumentNullException(nameof(path)); if (String.IsNullOrEmpty(targetPath)) @@ -167,20 +191,24 @@ public async Task CopyFileAsync(string path, string targetPath, Cancellati string normalizedPath = NormalizePath(path); string normalizedTargetPath = NormalizePath(targetPath); _logger.LogInformation("Copying {Path} to {TargetPath}", normalizedPath, normalizedTargetPath); - - try { + + try + { await using var stream = await GetFileStreamAsync(normalizedPath, StreamMode.Read, cancellationToken).AnyContext(); if (stream == null) return false; return await SaveFileAsync(normalizedTargetPath, stream, cancellationToken).AnyContext(); - } catch (Exception ex) { + } + catch (Exception ex) + { _logger.LogError(ex, "Error copying {Path} to {TargetPath}: {Message}", normalizedPath, normalizedTargetPath, ex.Message); return false; } } - public async Task DeleteFileAsync(string path, CancellationToken cancellationToken = default) { + public async Task DeleteFileAsync(string path, CancellationToken cancellationToken = default) + { if (String.IsNullOrEmpty(path)) throw new ArgumentNullException(nameof(path)); @@ -188,10 +216,13 @@ public async Task DeleteFileAsync(string path, CancellationToken cancellat string normalizedPath = NormalizePath(path); _logger.LogTrace("Deleting {Path}", normalizedPath); - - try { + + try + { await _client.DeleteFileAsync(normalizedPath, cancellationToken).AnyContext(); - } catch (SftpPathNotFoundException ex) { + } + catch (SftpPathNotFoundException ex) + { _logger.LogError(ex, "Unable to delete {Path}: File not found", normalizedPath); return false; } @@ -199,7 +230,8 @@ public async Task DeleteFileAsync(string path, CancellationToken cancellat return true; } - public async Task DeleteFilesAsync(string searchPattern = null, CancellationToken cancellationToken = default) { + public async Task DeleteFilesAsync(string searchPattern = null, CancellationToken cancellationToken = default) + { EnsureClientConnected(); if (searchPattern == null) @@ -213,7 +245,8 @@ public async Task DeleteFilesAsync(string searchPattern = null, Cancellatio // TODO: We could batch this, but we should ensure the batch isn't thousands of files. _logger.LogInformation("Deleting {FileCount} files matching {SearchPattern}", files.Count, searchPattern); - foreach (var file in files) { + foreach (var file in files) + { await DeleteFileAsync(file.Path, cancellationToken).AnyContext(); count++; } @@ -222,14 +255,16 @@ public async Task DeleteFilesAsync(string searchPattern = null, Cancellatio return count; } - private void CreateDirectory(string path) { + private void CreateDirectory(string path) + { string directory = NormalizePath(Path.GetDirectoryName(path)); _logger.LogTrace("Ensuring {Directory} directory exists", directory); - + string[] folderSegments = directory?.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries) ?? Array.Empty(); string currentDirectory = String.Empty; - foreach (string segment in folderSegments) { + foreach (string segment in folderSegments) + { // If current directory is empty, use current working directory instead of a rooted path. currentDirectory = String.IsNullOrEmpty(currentDirectory) ? segment @@ -243,19 +278,24 @@ private void CreateDirectory(string path) { } } - private async Task DeleteDirectoryAsync(string path, bool includeSelf, CancellationToken cancellationToken = default) { + private async Task DeleteDirectoryAsync(string path, bool includeSelf, CancellationToken cancellationToken = default) + { int count = 0; string directory = NormalizePath(path); _logger.LogInformation("Deleting {Directory} directory", directory); - await foreach (var file in _client.ListDirectoryAsync(directory, cancellationToken).AnyContext()) { - if (file.Name is "." or "..") + await foreach (var file in _client.ListDirectoryAsync(directory, cancellationToken).AnyContext()) + { + if (file.Name is "." or "..") continue; - if (file.IsDirectory) { + if (file.IsDirectory) + { count += await DeleteDirectoryAsync(file.FullName, true, cancellationToken); - } else { + } + else + { _logger.LogTrace("Deleting file {Path}", file.FullName); await _client.DeleteFileAsync(file.FullName, cancellationToken).AnyContext(); count++; @@ -269,7 +309,8 @@ private async Task DeleteDirectoryAsync(string path, bool includeSelf, Canc return count; } - public async Task GetPagedFileListAsync(int pageSize = 100, string searchPattern = null, CancellationToken cancellationToken = default) { + public async Task GetPagedFileListAsync(int pageSize = 100, string searchPattern = null, CancellationToken cancellationToken = default) + { if (pageSize <= 0) return PagedFileListResult.Empty; @@ -278,7 +319,8 @@ public async Task GetPagedFileListAsync(int pageSize = 100, return result; } - private async Task GetFiles(string searchPattern, int page, int pageSize, CancellationToken cancellationToken) { + private async Task GetFiles(string searchPattern, int page, int pageSize, CancellationToken cancellationToken) + { int pagingLimit = pageSize; int skip = (page - 1) * pagingLimit; if (pagingLimit < Int32.MaxValue) @@ -286,12 +328,14 @@ private async Task GetFiles(string searchPattern, int page, int var list = await GetFileListAsync(searchPattern, pagingLimit, skip, cancellationToken).AnyContext(); bool hasMore = false; - if (list.Count == pagingLimit) { + if (list.Count == pagingLimit) + { hasMore = true; list.RemoveAt(pagingLimit - 1); } - return new NextPageResult { + return new NextPageResult + { Success = true, HasMore = hasMore, Files = list, @@ -299,7 +343,8 @@ private async Task GetFiles(string searchPattern, int page, int }; } - private async Task> GetFileListAsync(string searchPattern = null, int? limit = null, int? skip = null, CancellationToken cancellationToken = default) { + private async Task> GetFileListAsync(string searchPattern = null, int? limit = null, int? skip = null, CancellationToken cancellationToken = default) + { if (limit is <= 0) return new List(); @@ -327,24 +372,32 @@ private async Task> GetFileListAsync(string searchPattern = null, return list; } - private async Task GetFileListRecursivelyAsync(string prefix, Regex pattern, ICollection list, int? recordsToReturn = null, CancellationToken cancellationToken = default) { - if (cancellationToken.IsCancellationRequested) { + private async Task GetFileListRecursivelyAsync(string prefix, Regex pattern, ICollection list, int? recordsToReturn = null, CancellationToken cancellationToken = default) + { + if (cancellationToken.IsCancellationRequested) + { _logger.LogDebug("Cancellation requested"); return; } var files = new List(); - try { - await foreach (var file in _client.ListDirectoryAsync(prefix, cancellationToken).AnyContext()) { + try + { + await foreach (var file in _client.ListDirectoryAsync(prefix, cancellationToken).AnyContext()) + { files.Add(file); } - } catch (SftpPathNotFoundException) { + } + catch (SftpPathNotFoundException) + { _logger.LogDebug("Directory not found with {Prefix}", prefix); return; } - foreach (var file in files.Where(f => f.IsRegularFile || f.IsDirectory).OrderByDescending(f => f.IsRegularFile).ThenBy(f => f.Name)) { - if (cancellationToken.IsCancellationRequested) { + foreach (var file in files.Where(f => f.IsRegularFile || f.IsDirectory).OrderByDescending(f => f.IsRegularFile).ThenBy(f => f.Name)) + { + if (cancellationToken.IsCancellationRequested) + { _logger.LogDebug("Cancellation requested"); return; } @@ -357,7 +410,8 @@ private async Task GetFileListRecursivelyAsync(string prefix, Regex pattern, ICo ? file.Name : String.Concat(prefix, "/", file.Name); - if (file.IsDirectory) { + if (file.IsDirectory) + { if (file.Name is "." or "..") continue; @@ -368,12 +422,14 @@ private async Task GetFileListRecursivelyAsync(string prefix, Regex pattern, ICo if (!file.IsRegularFile) continue; - if (pattern != null && !pattern.IsMatch(path)) { + if (pattern != null && !pattern.IsMatch(path)) + { _logger.LogTrace("Skipping {Path}: Doesn't match pattern", path); continue; } - list.Add(new FileSpec { + list.Add(new FileSpec + { Path = path, Created = file.LastWriteTimeUtc, Modified = file.LastWriteTimeUtc, @@ -382,14 +438,15 @@ private async Task GetFileListRecursivelyAsync(string prefix, Regex pattern, ICo } } - private ConnectionInfo CreateConnectionInfo(SshNetFileStorageOptions options) { + private ConnectionInfo CreateConnectionInfo(SshNetFileStorageOptions options) + { if (String.IsNullOrEmpty(options.ConnectionString)) throw new ArgumentNullException(nameof(options.ConnectionString)); if (!Uri.TryCreate(options.ConnectionString, UriKind.Absolute, out var uri) || String.IsNullOrEmpty(uri?.UserInfo)) throw new ArgumentException("Unable to parse connection string uri", nameof(options.ConnectionString)); - string[] userParts = uri.UserInfo.Split(new [] { ':' }, StringSplitOptions.RemoveEmptyEntries); + string[] userParts = uri.UserInfo.Split(new[] { ':' }, StringSplitOptions.RemoveEmptyEntries); string username = Uri.UnescapeDataString(userParts.First()); string password = Uri.UnescapeDataString(userParts.Length > 1 ? userParts[1] : String.Empty); int port = uri.Port > 0 ? uri.Port : 22; @@ -404,11 +461,12 @@ private ConnectionInfo CreateConnectionInfo(SshNetFileStorageOptions options) { if (authenticationMethods.Count == 0) authenticationMethods.Add(new NoneAuthenticationMethod(username)); - if (!String.IsNullOrEmpty(options.Proxy)) { + if (!String.IsNullOrEmpty(options.Proxy)) + { if (!Uri.TryCreate(options.Proxy, UriKind.Absolute, out var proxyUri) || String.IsNullOrEmpty(proxyUri?.UserInfo)) throw new ArgumentException("Unable to parse proxy uri", nameof(options.Proxy)); - string[] proxyParts = proxyUri.UserInfo.Split(new [] { ':' }, StringSplitOptions.RemoveEmptyEntries); + string[] proxyParts = proxyUri.UserInfo.Split(new[] { ':' }, StringSplitOptions.RemoveEmptyEntries); string proxyUsername = proxyParts.First(); string proxyPassword = proxyParts.Length > 1 ? proxyParts[1] : null; @@ -422,7 +480,8 @@ private ConnectionInfo CreateConnectionInfo(SshNetFileStorageOptions options) { return new ConnectionInfo(uri.Host, port, username, authenticationMethods.ToArray()); } - private void EnsureClientConnected() { + private void EnsureClientConnected() + { if (_client.IsConnected) return; @@ -431,16 +490,19 @@ private void EnsureClientConnected() { _logger.LogTrace("Connected to {Host}:{Port} in {WorkingDirectory}", _client.ConnectionInfo.Host, _client.ConnectionInfo.Port, _client.WorkingDirectory); } - private string NormalizePath(string path) { + private string NormalizePath(string path) + { return path?.Replace('\\', '/'); } - private class SearchCriteria { + private class SearchCriteria + { public string Prefix { get; set; } public Regex Pattern { get; set; } } - private SearchCriteria GetRequestCriteria(string searchPattern) { + private SearchCriteria GetRequestCriteria(string searchPattern) + { if (String.IsNullOrEmpty(searchPattern)) return new SearchCriteria { Prefix = String.Empty }; @@ -451,25 +513,31 @@ private SearchCriteria GetRequestCriteria(string searchPattern) { string prefix; Regex patternRegex; - if (hasWildcard) { + if (hasWildcard) + { patternRegex = new Regex($"^{Regex.Escape(normalizedSearchPattern).Replace("\\*", ".*?")}$"); string beforeWildcard = normalizedSearchPattern[..wildcardPos]; int slashPos = beforeWildcard.LastIndexOf('/'); prefix = slashPos >= 0 ? normalizedSearchPattern[..slashPos] : String.Empty; - } else { + } + else + { patternRegex = new Regex($"^{normalizedSearchPattern}$"); int slashPos = normalizedSearchPattern.LastIndexOf('/'); prefix = slashPos >= 0 ? normalizedSearchPattern[..slashPos] : String.Empty; } - return new SearchCriteria { + return new SearchCriteria + { Prefix = prefix, Pattern = patternRegex }; } - public void Dispose() { - if (_client.IsConnected) { + public void Dispose() + { + if (_client.IsConnected) + { _logger.LogTrace("Disconnecting from {Host}:{Port}", _client.ConnectionInfo.Host, _client.ConnectionInfo.Port); _client.Disconnect(); _logger.LogTrace("Disconnected from {Host}:{Port}", _client.ConnectionInfo.Host, _client.ConnectionInfo.Port); @@ -477,4 +545,4 @@ public void Dispose() { ((IBaseClient)_client).Dispose(); } -} \ No newline at end of file +} diff --git a/src/Foundatio.Storage.SshNet/Storage/SshNetFileStorageOptions.cs b/src/Foundatio.Storage.SshNet/Storage/SshNetFileStorageOptions.cs index b9c6e80..20123bd 100644 --- a/src/Foundatio.Storage.SshNet/Storage/SshNetFileStorageOptions.cs +++ b/src/Foundatio.Storage.SshNet/Storage/SshNetFileStorageOptions.cs @@ -3,9 +3,10 @@ using System.Text; using Renci.SshNet; -namespace Foundatio.Storage; +namespace Foundatio.Storage; -public class SshNetFileStorageOptions : SharedOptions { +public class SshNetFileStorageOptions : SharedOptions +{ public string ConnectionString { get; set; } public string Proxy { get; set; } public ProxyTypes ProxyType { get; set; } @@ -13,37 +14,44 @@ public class SshNetFileStorageOptions : SharedOptions { public string PrivateKeyPassPhrase { get; set; } } -public class SshNetFileStorageOptionsBuilder : SharedOptionsBuilder { - public SshNetFileStorageOptionsBuilder ConnectionString(string connectionString) { +public class SshNetFileStorageOptionsBuilder : SharedOptionsBuilder +{ + public SshNetFileStorageOptionsBuilder ConnectionString(string connectionString) + { Target.ConnectionString = connectionString ?? throw new ArgumentNullException(nameof(connectionString)); return this; } - - public SshNetFileStorageOptionsBuilder Proxy(string proxy) { + + public SshNetFileStorageOptionsBuilder Proxy(string proxy) + { Target.Proxy = proxy ?? throw new ArgumentNullException(nameof(proxy)); return this; } - - public SshNetFileStorageOptionsBuilder ProxyType(ProxyTypes proxyType) { + + public SshNetFileStorageOptionsBuilder ProxyType(ProxyTypes proxyType) + { Target.ProxyType = proxyType; return this; } - - public SshNetFileStorageOptionsBuilder PrivateKey(string privateKey) { - if (String.IsNullOrEmpty(privateKey)) + + public SshNetFileStorageOptionsBuilder PrivateKey(string privateKey) + { + if (String.IsNullOrEmpty(privateKey)) throw new ArgumentNullException(nameof(privateKey)); Target.PrivateKey = new MemoryStream(Encoding.UTF8.GetBytes(privateKey)); return this; } - - public SshNetFileStorageOptionsBuilder PrivateKey(Stream privateKey) { + + public SshNetFileStorageOptionsBuilder PrivateKey(Stream privateKey) + { Target.PrivateKey = privateKey ?? throw new ArgumentNullException(nameof(privateKey)); return this; } - - public SshNetFileStorageOptionsBuilder PrivateKeyPassPhrase(string privateKeyPassPhrase) { + + public SshNetFileStorageOptionsBuilder PrivateKeyPassPhrase(string privateKeyPassPhrase) + { Target.PrivateKeyPassPhrase = privateKeyPassPhrase ?? throw new ArgumentNullException(nameof(privateKeyPassPhrase)); return this; } -} \ No newline at end of file +} diff --git a/tests/Foundatio.Storage.SshNet.Tests/Properties/AssemblyInfo.cs b/tests/Foundatio.Storage.SshNet.Tests/Properties/AssemblyInfo.cs index b946c3e..3142569 100644 --- a/tests/Foundatio.Storage.SshNet.Tests/Properties/AssemblyInfo.cs +++ b/tests/Foundatio.Storage.SshNet.Tests/Properties/AssemblyInfo.cs @@ -1 +1 @@ -[assembly: Xunit.CollectionBehaviorAttribute(DisableTestParallelization = true, MaxParallelThreads = 1)] \ No newline at end of file +[assembly: Xunit.CollectionBehaviorAttribute(DisableTestParallelization = true, MaxParallelThreads = 1)] diff --git a/tests/Foundatio.Storage.SshNet.Tests/Storage/RootedSshNetStorageTests.cs b/tests/Foundatio.Storage.SshNet.Tests/Storage/RootedSshNetStorageTests.cs index 5f71945..9266c8f 100644 --- a/tests/Foundatio.Storage.SshNet.Tests/Storage/RootedSshNetStorageTests.cs +++ b/tests/Foundatio.Storage.SshNet.Tests/Storage/RootedSshNetStorageTests.cs @@ -6,13 +6,15 @@ using Xunit; using Xunit.Abstractions; -namespace Foundatio.SshNet.Tests.Storage; +namespace Foundatio.SshNet.Tests.Storage; [Collection("SshNetStorageIntegrationTests")] -public class RootedSshNetStorageTests : FileStorageTestsBase { - public RootedSshNetStorageTests(ITestOutputHelper output) : base(output) {} +public class RootedSshNetStorageTests : FileStorageTestsBase +{ + public RootedSshNetStorageTests(ITestOutputHelper output) : base(output) { } - protected override IFileStorage GetStorage() { + protected override IFileStorage GetStorage() + { string connectionString = Configuration.GetConnectionString("SshNetStorageConnectionString"); if (String.IsNullOrEmpty(connectionString)) return null; @@ -21,18 +23,20 @@ protected override IFileStorage GetStorage() { } [Fact] - public void CanCreateSshNetFileStorageWithoutConnectionStringPassword() { + public void CanCreateSshNetFileStorageWithoutConnectionStringPassword() + { //Arrange var options = new SshNetFileStorageOptionsBuilder() .ConnectionString("sftp://username@host") .Build(); - + //Act var storage = new SshNetFileStorage(options); } - + [Fact] - public void CanCreateSshNetFileStorageWithoutProxyPassword() { + public void CanCreateSshNetFileStorageWithoutProxyPassword() + { //Arrange var options = new SshNetFileStorageOptionsBuilder() .ConnectionString("sftp://username@host") @@ -44,97 +48,116 @@ public void CanCreateSshNetFileStorageWithoutProxyPassword() { } [Fact] - public override Task CanGetEmptyFileListOnMissingDirectoryAsync() { + public override Task CanGetEmptyFileListOnMissingDirectoryAsync() + { return base.CanGetEmptyFileListOnMissingDirectoryAsync(); } [Fact] - public override Task CanGetFileListForSingleFolderAsync() { + public override Task CanGetFileListForSingleFolderAsync() + { return base.CanGetFileListForSingleFolderAsync(); } [Fact] - public override Task CanGetFileListForSingleFileAsync() { + public override Task CanGetFileListForSingleFileAsync() + { return base.CanGetFileListForSingleFileAsync(); } [Fact] - public override Task CanGetPagedFileListForSingleFolderAsync() { + public override Task CanGetPagedFileListForSingleFolderAsync() + { return base.CanGetPagedFileListForSingleFolderAsync(); } [Fact] - public override Task CanGetFileInfoAsync() { + public override Task CanGetFileInfoAsync() + { return base.CanGetFileInfoAsync(); } [Fact] - public override Task CanGetNonExistentFileInfoAsync() { + public override Task CanGetNonExistentFileInfoAsync() + { return base.CanGetNonExistentFileInfoAsync(); } [Fact] - public override Task CanSaveFilesAsync() { + public override Task CanSaveFilesAsync() + { return base.CanSaveFilesAsync(); } [Fact] - public override Task CanManageFilesAsync() { + public override Task CanManageFilesAsync() + { return base.CanManageFilesAsync(); } [Fact] - public override Task CanRenameFilesAsync() { + public override Task CanRenameFilesAsync() + { return base.CanRenameFilesAsync(); } [Fact(Skip = "Doesn't work well with SFTP")] - public override Task CanConcurrentlyManageFilesAsync() { + public override Task CanConcurrentlyManageFilesAsync() + { return base.CanConcurrentlyManageFilesAsync(); } [Fact] - public override void CanUseDataDirectory() { + public override void CanUseDataDirectory() + { base.CanUseDataDirectory(); } [Fact] - public override Task CanDeleteEntireFolderAsync() { + public override Task CanDeleteEntireFolderAsync() + { return base.CanDeleteEntireFolderAsync(); } [Fact] - public override Task CanDeleteEntireFolderWithWildcardAsync() { + public override Task CanDeleteEntireFolderWithWildcardAsync() + { return base.CanDeleteEntireFolderWithWildcardAsync(); } [Fact] - public override Task CanDeleteFolderWithMultiFolderWildcardsAsync() { + public override Task CanDeleteFolderWithMultiFolderWildcardsAsync() + { return base.CanDeleteFolderWithMultiFolderWildcardsAsync(); } [Fact] - public override Task CanDeleteSpecificFilesAsync() { + public override Task CanDeleteSpecificFilesAsync() + { return base.CanDeleteSpecificFilesAsync(); } [Fact] - public override Task CanDeleteNestedFolderAsync() { + public override Task CanDeleteNestedFolderAsync() + { return base.CanDeleteNestedFolderAsync(); } [Fact] - public override Task CanDeleteSpecificFilesInNestedFolderAsync() { + public override Task CanDeleteSpecificFilesInNestedFolderAsync() + { return base.CanDeleteSpecificFilesInNestedFolderAsync(); } [Fact] - public override Task CanRoundTripSeekableStreamAsync() { + public override Task CanRoundTripSeekableStreamAsync() + { return base.CanRoundTripSeekableStreamAsync(); } [Fact] - public override Task WillRespectStreamOffsetAsync() { + public override Task WillRespectStreamOffsetAsync() + { return base.WillRespectStreamOffsetAsync(); } -} \ No newline at end of file +} diff --git a/tests/Foundatio.Storage.SshNet.Tests/Storage/SshNetStorageTests.cs b/tests/Foundatio.Storage.SshNet.Tests/Storage/SshNetStorageTests.cs index fc8d7ff..cea1613 100644 --- a/tests/Foundatio.Storage.SshNet.Tests/Storage/SshNetStorageTests.cs +++ b/tests/Foundatio.Storage.SshNet.Tests/Storage/SshNetStorageTests.cs @@ -6,13 +6,15 @@ using Xunit; using Xunit.Abstractions; -namespace Foundatio.SshNet.Tests.Storage; +namespace Foundatio.SshNet.Tests.Storage; [Collection("SshNetStorageIntegrationTests")] -public class SshNetStorageTests : FileStorageTestsBase { - public SshNetStorageTests(ITestOutputHelper output) : base(output) {} +public class SshNetStorageTests : FileStorageTestsBase +{ + public SshNetStorageTests(ITestOutputHelper output) : base(output) { } - protected override IFileStorage GetStorage() { + protected override IFileStorage GetStorage() + { string connectionString = Configuration.GetConnectionString("SshNetStorageConnectionString"); if (String.IsNullOrEmpty(connectionString)) return null; @@ -21,18 +23,20 @@ protected override IFileStorage GetStorage() { } [Fact] - public void CanCreateSshNetFileStorageWithoutConnectionStringPassword() { + public void CanCreateSshNetFileStorageWithoutConnectionStringPassword() + { //Arrange var options = new SshNetFileStorageOptionsBuilder() .ConnectionString("sftp://username@host") .Build(); - + //Act var storage = new SshNetFileStorage(options); } - + [Fact] - public void CanCreateSshNetFileStorageWithoutProxyPassword() { + public void CanCreateSshNetFileStorageWithoutProxyPassword() + { //Arrange var options = new SshNetFileStorageOptionsBuilder() .ConnectionString("sftp://username@host") @@ -44,97 +48,116 @@ public void CanCreateSshNetFileStorageWithoutProxyPassword() { } [Fact] - public override Task CanGetEmptyFileListOnMissingDirectoryAsync() { + public override Task CanGetEmptyFileListOnMissingDirectoryAsync() + { return base.CanGetEmptyFileListOnMissingDirectoryAsync(); } [Fact] - public override Task CanGetFileListForSingleFolderAsync() { + public override Task CanGetFileListForSingleFolderAsync() + { return base.CanGetFileListForSingleFolderAsync(); } [Fact] - public override Task CanGetFileListForSingleFileAsync() { + public override Task CanGetFileListForSingleFileAsync() + { return base.CanGetFileListForSingleFileAsync(); } [Fact] - public override Task CanGetPagedFileListForSingleFolderAsync() { + public override Task CanGetPagedFileListForSingleFolderAsync() + { return base.CanGetPagedFileListForSingleFolderAsync(); } [Fact] - public override Task CanGetFileInfoAsync() { + public override Task CanGetFileInfoAsync() + { return base.CanGetFileInfoAsync(); } [Fact] - public override Task CanGetNonExistentFileInfoAsync() { + public override Task CanGetNonExistentFileInfoAsync() + { return base.CanGetNonExistentFileInfoAsync(); } [Fact] - public override Task CanSaveFilesAsync() { + public override Task CanSaveFilesAsync() + { return base.CanSaveFilesAsync(); } [Fact] - public override Task CanManageFilesAsync() { + public override Task CanManageFilesAsync() + { return base.CanManageFilesAsync(); } [Fact] - public override Task CanRenameFilesAsync() { + public override Task CanRenameFilesAsync() + { return base.CanRenameFilesAsync(); } [Fact(Skip = "Doesn't work well with SFTP")] - public override Task CanConcurrentlyManageFilesAsync() { + public override Task CanConcurrentlyManageFilesAsync() + { return base.CanConcurrentlyManageFilesAsync(); } [Fact] - public override void CanUseDataDirectory() { + public override void CanUseDataDirectory() + { base.CanUseDataDirectory(); } [Fact] - public override Task CanDeleteEntireFolderAsync() { + public override Task CanDeleteEntireFolderAsync() + { return base.CanDeleteEntireFolderAsync(); } [Fact] - public override Task CanDeleteEntireFolderWithWildcardAsync() { + public override Task CanDeleteEntireFolderWithWildcardAsync() + { return base.CanDeleteEntireFolderWithWildcardAsync(); } [Fact] - public override Task CanDeleteFolderWithMultiFolderWildcardsAsync() { + public override Task CanDeleteFolderWithMultiFolderWildcardsAsync() + { return base.CanDeleteFolderWithMultiFolderWildcardsAsync(); } [Fact] - public override Task CanDeleteSpecificFilesAsync() { + public override Task CanDeleteSpecificFilesAsync() + { return base.CanDeleteSpecificFilesAsync(); } [Fact] - public override Task CanDeleteNestedFolderAsync() { + public override Task CanDeleteNestedFolderAsync() + { return base.CanDeleteNestedFolderAsync(); } [Fact] - public override Task CanDeleteSpecificFilesInNestedFolderAsync() { + public override Task CanDeleteSpecificFilesInNestedFolderAsync() + { return base.CanDeleteSpecificFilesInNestedFolderAsync(); } [Fact] - public override Task CanRoundTripSeekableStreamAsync() { + public override Task CanRoundTripSeekableStreamAsync() + { return base.CanRoundTripSeekableStreamAsync(); } [Fact] - public override Task WillRespectStreamOffsetAsync() { + public override Task WillRespectStreamOffsetAsync() + { return base.WillRespectStreamOffsetAsync(); } -} \ No newline at end of file +}