diff --git a/.editorconfig b/.editorconfig index 5faff29..9d3abe5 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,162 +1,189 @@ -############################### -# Core EditorConfig Options # -############################### +# editorconfig.org (https://github.com/dotnet/runtime/blob/main/.editorconfig) +# top-most EditorConfig file root = true -# All files +# Default settings: +# A newline ending every file +# Use 4 spaces as indentation [*] +insert_final_newline = true indent_style = space - -# Code files -[*.{cs,csx,vb,vbx}] indent_size = 4 +trim_trailing_whitespace = true + +[*.json] insert_final_newline = false -charset = utf-8-bom -# Xml project files -[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] -indent_size = 2 +# Generated code +[*{_AssemblyInfo.cs,.notsupported.cs,AsmOffsets.cs}] +generated_code = true -# Xml config files -[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] -indent_size = 2 - -# 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 -############################### -# .NET Coding Conventions # -############################### +# 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 -[*.{cs,vb}] -# Organize usings -dotnet_sort_system_directives_first = true +# Modifier preferences +csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async:suggestion -# this. preferences +# 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 -# Language keywords vs BCL types preferences +# 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 -# Parentheses preferences -dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent -dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent -dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent -dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent - -# Modifier preferences -dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion +# 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_null_propagation = true:suggestion dotnet_style_coalesce_expression = true:suggestion -dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent +dotnet_style_null_propagation = 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:suggestion -dotnet_style_prefer_conditional_expression_over_return = true:suggestion - -############################### -# Naming Conventions # -############################### - -# Style Definitions -dotnet_naming_style.pascal_case_style.capitalization = pascal_case - -# Use PascalCase for constant fields -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.applicable_accessibilities = * -dotnet_naming_symbols.constant_fields.required_modifiers = const - -############################### -# C# Coding Conventions # -############################### - -[*.cs] -# var preferences -csharp_style_var_for_built_in_types = false:suggestion -csharp_style_var_when_type_is_apparent = true:suggestion -csharp_style_var_elsewhere = 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 # Expression-bodied members -csharp_style_expression_bodied_methods = false:none -csharp_style_expression_bodied_constructors = false:none -csharp_style_expression_bodied_operators = false:none -csharp_style_expression_bodied_properties = true:suggestion -csharp_style_expression_bodied_indexers = true:suggestion -csharp_style_expression_bodied_accessors = true:suggestion - -# Pattern matching preferences +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 + +# 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 +# Null checking preferences csharp_style_throw_expression = true:suggestion csharp_style_conditional_delegate_call = true:suggestion -# Modifier preferences -csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion - -# Expression-level preferences -csharp_prefer_braces = false:suggestion -csharp_style_deconstructed_variable_declaration = true:suggestion -csharp_prefer_simple_default_expression = true:suggestion -csharp_style_pattern_local_over_anonymous_function = true:suggestion -csharp_style_inlined_variable_declaration = true:suggestion - -############################### -# C# Formatting Rules # -############################### - -# New line preferences -csharp_new_line_before_open_brace = none -csharp_new_line_before_else = false -csharp_new_line_before_catch = false -csharp_new_line_before_finally = false -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 - -# Indentation preferences -csharp_indent_case_contents = true -csharp_indent_switch_labels = true -csharp_indent_labels = flush_left +# 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_before_colon_in_inheritance_clause = true -csharp_space_after_colon_in_inheritance_clause = true -csharp_space_around_binary_operators = before_and_after -csharp_space_between_method_declaration_empty_parameter_list_parentheses = false -csharp_space_between_method_call_name_and_opening_parenthesis = false -csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_square_brackets = false -# Wrapping preferences -csharp_preserve_single_line_statements = true -csharp_preserve_single_line_blocks = true +# C++ Files +[*.{cpp,h,in}] +curly_bracket_next_line = true +indent_brace_style = Allman -############################### -# VB Coding Conventions # -############################### +# Xml project files +[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,nativeproj,locproj}] +indent_size = 2 -[*.vb] -# Modifier preferences -visual_basic_preferred_modifier_order = Partial,Default,Private,Protected,Public,Friend,NotOverridable,Overridable,MustOverride,Overloads,Overrides,MustInherit,NotInheritable,Static,Shared,Shadows,ReadOnly,WriteOnly,Dim,Const,WithEvents,Widening,Narrowing,Custom,Async:suggestion \ No newline at end of file +[*.{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.Minio/Extensions/TaskExtensions.cs b/src/Foundatio.Minio/Extensions/TaskExtensions.cs index 2b48580..da9c237 100644 --- a/src/Foundatio.Minio/Extensions/TaskExtensions.cs +++ b/src/Foundatio.Minio/Extensions/TaskExtensions.cs @@ -3,16 +3,20 @@ using System.Runtime.CompilerServices; using System.Threading.Tasks; -namespace Foundatio.Extensions { - internal static class TaskExtensions { +namespace Foundatio.Extensions +{ + internal static class TaskExtensions + { [DebuggerStepThrough] - public static ConfiguredTaskAwaitable AnyContext(this Task task) { + public static ConfiguredTaskAwaitable AnyContext(this Task task) + { return task.ConfigureAwait(false); } [DebuggerStepThrough] - public static ConfiguredTaskAwaitable AnyContext(this Task task) { + public static ConfiguredTaskAwaitable AnyContext(this Task task) + { return task.ConfigureAwait(false); } } -} \ No newline at end of file +} diff --git a/src/Foundatio.Minio/MinioConnectionStringBuilder.cs b/src/Foundatio.Minio/MinioConnectionStringBuilder.cs index 56a204c..fd86c7d 100644 --- a/src/Foundatio.Minio/MinioConnectionStringBuilder.cs +++ b/src/Foundatio.Minio/MinioConnectionStringBuilder.cs @@ -1,8 +1,10 @@ using System; using System.Linq; -namespace Foundatio { - public class MinioConnectionStringBuilder { +namespace Foundatio +{ + public class MinioConnectionStringBuilder + { public string AccessKey { get; set; } public string SecretKey { get; set; } @@ -13,31 +15,37 @@ public class MinioConnectionStringBuilder { protected MinioConnectionStringBuilder() { } - protected MinioConnectionStringBuilder(string connectionString) { + protected MinioConnectionStringBuilder(string connectionString) + { if (String.IsNullOrEmpty(connectionString)) throw new ArgumentNullException(nameof(connectionString)); Parse(connectionString); } - private void Parse(string connectionString) { + private void Parse(string connectionString) + { foreach (var option in connectionString .Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries) .Where(kvp => kvp.Contains('=')) - .Select(kvp => kvp.Split(new[] { '=' }, 2))) { + .Select(kvp => kvp.Split(new[] { '=' }, 2))) + { var optionKey = option[0].Trim(); var optionValue = option[1].Trim(); - if (!ParseItem(optionKey, optionValue)) { + if (!ParseItem(optionKey, optionValue)) + { throw new ArgumentException($"The option '{optionKey}' cannot be recognized in connection string.", nameof(connectionString)); } } } - protected virtual bool ParseItem(string key, string value) { + protected virtual bool ParseItem(string key, string value) + { if (String.Equals(key, "AccessKey", StringComparison.OrdinalIgnoreCase) || String.Equals(key, "Access Key", StringComparison.OrdinalIgnoreCase) || String.Equals(key, "AccessKeyId", StringComparison.OrdinalIgnoreCase) || String.Equals(key, "Access Key Id", StringComparison.OrdinalIgnoreCase) || - String.Equals(key, "Id", StringComparison.OrdinalIgnoreCase)) { + String.Equals(key, "Id", StringComparison.OrdinalIgnoreCase)) + { AccessKey = value; return true; } @@ -45,23 +53,27 @@ protected virtual bool ParseItem(string key, string value) { String.Equals(key, "Secret Key", StringComparison.OrdinalIgnoreCase) || String.Equals(key, "SecretAccessKey", StringComparison.OrdinalIgnoreCase) || String.Equals(key, "Secret Access Key", StringComparison.OrdinalIgnoreCase) || - String.Equals(key, "Secret", StringComparison.OrdinalIgnoreCase)) { + String.Equals(key, "Secret", StringComparison.OrdinalIgnoreCase)) + { SecretKey = value; return true; } - if (String.Equals(key, "Region", StringComparison.OrdinalIgnoreCase)) { + if (String.Equals(key, "Region", StringComparison.OrdinalIgnoreCase)) + { Region = value; return true; } if (String.Equals(key, "EndPoint", StringComparison.OrdinalIgnoreCase) || - String.Equals(key, "End Point", StringComparison.OrdinalIgnoreCase)) { + String.Equals(key, "End Point", StringComparison.OrdinalIgnoreCase)) + { EndPoint = value; return true; } return false; } - public override string ToString() { + public override string ToString() + { var connectionString = string.Empty; if (!string.IsNullOrEmpty(AccessKey)) connectionString += "AccessKey=" + AccessKey + ";"; diff --git a/src/Foundatio.Minio/Storage/MinioFileStorage.cs b/src/Foundatio.Minio/Storage/MinioFileStorage.cs index 9f37e42..9cf39f2 100644 --- a/src/Foundatio.Minio/Storage/MinioFileStorage.cs +++ b/src/Foundatio.Minio/Storage/MinioFileStorage.cs @@ -16,8 +16,10 @@ using Minio.DataModel.Args; using Minio.Exceptions; -namespace Foundatio.Storage { - public class MinioFileStorage : IFileStorage { +namespace Foundatio.Storage +{ + public class MinioFileStorage : IFileStorage + { private readonly string _bucket; private readonly bool _shouldAutoCreateBucket; private bool _bucketExistsChecked; @@ -25,7 +27,8 @@ public class MinioFileStorage : IFileStorage { private readonly ISerializer _serializer; private readonly ILogger _logger; - public MinioFileStorage(MinioFileStorageOptions options) { + public MinioFileStorage(MinioFileStorageOptions options) + { if (options == null) throw new ArgumentNullException(nameof(options)); @@ -44,13 +47,15 @@ public MinioFileStorage(Builder _serializer; public IMinioClient Client => _client; - private async Task EnsureBucketExists() { + private async Task EnsureBucketExists() + { if (!_shouldAutoCreateBucket || _bucketExistsChecked) return; _logger.LogTrace("Checking if bucket {Bucket} exists", _bucket); bool found = await _client.BucketExistsAsync(new BucketExistsArgs().WithBucket(_bucket)).AnyContext(); - if (!found) { + if (!found) + { _logger.LogInformation("Creating {Bucket}", _bucket); await _client.MakeBucketAsync(new MakeBucketArgs().WithBucket(_bucket)).AnyContext(); _logger.LogInformation("Created {Bucket}", _bucket); @@ -76,18 +81,22 @@ public async Task GetFileStreamAsync(string path, StreamMode streamMode, string normalizedPath = NormalizePath(path); _logger.LogTrace("Getting file stream for {Path}", normalizedPath); - try { + try + { Stream result = new MemoryStream(); await _client.GetObjectAsync(new GetObjectArgs().WithBucket(_bucket).WithObject(normalizedPath).WithCallbackStream(async (stream, _) => await stream.CopyToAsync(result).AnyContext()), cancellationToken).AnyContext(); result.Seek(0, SeekOrigin.Begin); return result; - } catch (Exception ex) { + } + catch (Exception ex) + { _logger.LogError(ex, "Unable to get file stream for {Path}: {Message}", normalizedPath, ex.Message); return null; } } - public async Task GetFileInfoAsync(string path) { + public async Task GetFileInfoAsync(string path) + { if (String.IsNullOrEmpty(path)) throw new ArgumentNullException(nameof(path)); @@ -95,22 +104,27 @@ public async Task GetFileInfoAsync(string path) { string normalizedPath = NormalizePath(path); _logger.LogTrace("Getting file info for {Path}", normalizedPath); - - try { + + try + { var metadata = await _client.StatObjectAsync(new StatObjectArgs().WithBucket(_bucket).WithObject(normalizedPath)).AnyContext(); - return new FileSpec { + return new FileSpec + { Path = normalizedPath, Size = metadata.Size, Created = metadata.LastModified.ToUniversalTime(), Modified = metadata.LastModified.ToUniversalTime() }; - } catch (Exception ex) { + } + catch (Exception ex) + { _logger.LogError(ex, "Unable to get file info for {Path}: {Message}", normalizedPath, ex.Message); return null; } } - public async Task ExistsAsync(string path) { + public async Task ExistsAsync(string path) + { if (String.IsNullOrEmpty(path)) throw new ArgumentNullException(nameof(path)); @@ -119,15 +133,19 @@ public async Task ExistsAsync(string path) { string normalizedPath = NormalizePath(path); _logger.LogTrace("Checking if {Path} exists", normalizedPath); - try { + try + { return await _client.StatObjectAsync(new StatObjectArgs().WithBucket(_bucket).WithObject(normalizedPath)).AnyContext() != null; - } catch (Exception ex) when(ex is ObjectNotFoundException or BucketNotFoundException) { + } + catch (Exception ex) when (ex is ObjectNotFoundException or BucketNotFoundException) + { _logger.LogDebug(ex, "Unable to check if {Path} exists: {Message}", normalizedPath, ex.Message); return false; } } - 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) @@ -139,24 +157,31 @@ public async Task SaveFileAsync(string path, Stream stream, CancellationTo _logger.LogTrace("Saving {Path}", normalizedPath); var seekableStream = stream.CanSeek ? stream : new MemoryStream(); - if (!stream.CanSeek) { + if (!stream.CanSeek) + { await stream.CopyToAsync(seekableStream).AnyContext(); seekableStream.Seek(0, SeekOrigin.Begin); } - try { + try + { await _client.PutObjectAsync(new PutObjectArgs().WithBucket(_bucket).WithObject(normalizedPath).WithStreamData(seekableStream).WithObjectSize(seekableStream.Length - seekableStream.Position), cancellationToken).AnyContext(); return true; - } catch (Exception ex) { + } + catch (Exception ex) + { _logger.LogError(ex, "Error saving {Path}: {Message}", normalizedPath, ex.Message); return false; - } finally { + } + finally + { if (!stream.CanSeek) seekableStream.Dispose(); } } - 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)) @@ -172,7 +197,8 @@ public async Task RenameFileAsync(string path, string newPath, Cancellatio await DeleteFileAsync(normalizedPath, cancellationToken).AnyContext(); } - 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)) @@ -183,22 +209,26 @@ 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 + { var copySourceArgs = new CopySourceObjectArgs().WithBucket(_bucket).WithObject(normalizedPath); - + await _client.CopyObjectAsync(new CopyObjectArgs() .WithBucket(_bucket) .WithObject(normalizedTargetPath) .WithCopyObjectSource(copySourceArgs), cancellationToken).AnyContext(); return true; - } 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)); @@ -207,28 +237,34 @@ public async Task DeleteFileAsync(string path, CancellationToken cancellat string normalizedPath = NormalizePath(path); _logger.LogTrace("Deleting {Path}", normalizedPath); - try { + try + { await _client.RemoveObjectAsync(new RemoveObjectArgs().WithBucket(_bucket).WithObject(normalizedPath), cancellationToken).AnyContext(); return true; - } catch (Exception ex) { + } + catch (Exception ex) + { _logger.LogError(ex, "Unable to delete {Path}: {Message}", normalizedPath, ex.Message); return false; } } - public async Task DeleteFilesAsync(string searchPattern = null, CancellationToken cancellation = default) { + public async Task DeleteFilesAsync(string searchPattern = null, CancellationToken cancellation = default) + { await EnsureBucketExists().AnyContext(); var files = await GetFileListAsync(searchPattern, cancellationToken: cancellation).AnyContext(); _logger.LogInformation("Deleting {FileCount} files matching {SearchPattern}", files.Count, searchPattern); - if (files.Count == 0) { + if (files.Count == 0) + { _logger.LogTrace("Finished deleting {FileCount} files matching {SearchPattern}", files.Count, searchPattern); return 0; } var result = await _client.RemoveObjectsAsync(new RemoveObjectsArgs().WithBucket(_bucket).WithObjects(files.Select(spec => NormalizePath(spec.Path)).ToList()), cancellation).AnyContext(); var resetEvent = new AutoResetEvent(false); - result.Subscribe(error => { + result.Subscribe(error => + { _logger.LogError("Error deleting {Path}: {Message}", error.Key, error.Message); resetEvent.Set(); }, () => resetEvent.Set()); @@ -239,7 +275,8 @@ public async Task DeleteFilesAsync(string searchPattern = null, Cancellatio 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; @@ -250,7 +287,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) @@ -258,12 +296,14 @@ private async Task GetFiles(string searchPattern, int page, int var list = (await GetFileListAsync(searchPattern, pagingLimit, skip, cancellationToken).AnyContext()).ToList(); 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, @@ -271,7 +311,8 @@ private async Task GetFiles(string searchPattern, int page, int }; } - private Task> GetFileListAsync(string searchPattern = null, int? limit = null, int? skip = null, CancellationToken cancellationToken = default) { + private Task> GetFileListAsync(string searchPattern = null, int? limit = null, int? skip = null, CancellationToken cancellationToken = default) + { if (limit is <= 0) return Task.FromResult(new List()); @@ -286,23 +327,27 @@ private Task> GetFileListAsync(string searchPattern = null, int? ExceptionDispatchInfo exception = null; var resetEvent = new AutoResetEvent(false); var observable = _client.ListObjectsAsync(new ListObjectsArgs().WithBucket(_bucket).WithPrefix(criteria.Prefix).WithRecursive(true), cancellationToken); - observable.Subscribe(item => { - if (item.IsDir) - return; + observable.Subscribe(item => + { + if (item.IsDir) + return; - if (criteria.Pattern != null && !criteria.Pattern.IsMatch(item.Key)) { - _logger.LogTrace("Skipping {Path}: Doesn't match pattern", item.Key); - return; - } + if (criteria.Pattern != null && !criteria.Pattern.IsMatch(item.Key)) + { + _logger.LogTrace("Skipping {Path}: Doesn't match pattern", item.Key); + return; + } list.Add(item); - }, error => { - if (error.GetType().ToString() != "Minio.EmptyBucketOperation") { - _logger.LogError(error, "Error getting file list: {Message}", error.Message); - exception = ExceptionDispatchInfo.Capture(error); - } - resetEvent.Set(); - }, + }, error => + { + if (error.GetType().ToString() != "Minio.EmptyBucketOperation") + { + _logger.LogError(error, "Error getting file list: {Message}", error.Message); + exception = ExceptionDispatchInfo.Capture(error); + } + resetEvent.Set(); + }, () => resetEvent.Set() ); resetEvent.WaitOne(); @@ -314,7 +359,8 @@ private Task> GetFileListAsync(string searchPattern = null, int? if (limit.HasValue) list = list.Take(limit.Value).ToList(); - return Task.FromResult(list.Select(blob => new FileSpec { + return Task.FromResult(list.Select(blob => new FileSpec + { Path = blob.Key, Size = (long)blob.Size, Modified = DateTime.Parse(blob.LastModified), @@ -322,16 +368,19 @@ private Task> GetFileListAsync(string searchPattern = null, int? }).ToList()); } - 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 }; @@ -342,13 +391,15 @@ private SearchCriteria GetRequestCriteria(string searchPattern) { string prefix = normalizedSearchPattern; Regex patternRegex = null; - if (hasWildcard) { + if (hasWildcard) + { patternRegex = new Regex($"^{Regex.Escape(normalizedSearchPattern).Replace("\\*", ".*?")}$"); int slashPos = normalizedSearchPattern.LastIndexOf('/'); prefix = slashPos >= 0 ? normalizedSearchPattern.Substring(0, slashPos) : String.Empty; } - return new SearchCriteria { + return new SearchCriteria + { Prefix = prefix, Pattern = patternRegex }; @@ -360,10 +411,13 @@ private SearchCriteria GetRequestCriteria(string searchPattern) { string endpoint; bool secure; - if (connectionString.EndPoint.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) { + if (connectionString.EndPoint.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) + { endpoint = connectionString.EndPoint.Substring(8); secure = true; - } else { + } + else + { endpoint = connectionString.EndPoint.StartsWith("http://", StringComparison.OrdinalIgnoreCase) ? connectionString.EndPoint.Substring(7) : connectionString.EndPoint; diff --git a/src/Foundatio.Minio/Storage/MinioFileStorageConnectionStringBuilder.cs b/src/Foundatio.Minio/Storage/MinioFileStorageConnectionStringBuilder.cs index d0143b9..17680fa 100644 --- a/src/Foundatio.Minio/Storage/MinioFileStorageConnectionStringBuilder.cs +++ b/src/Foundatio.Minio/Storage/MinioFileStorageConnectionStringBuilder.cs @@ -2,30 +2,38 @@ using System.Collections.Generic; using System.Text; -namespace Foundatio.Storage { - public class MinioFileStorageConnectionStringBuilder : MinioConnectionStringBuilder { +namespace Foundatio.Storage +{ + public class MinioFileStorageConnectionStringBuilder : MinioConnectionStringBuilder + { private string _bucket; - public MinioFileStorageConnectionStringBuilder() { + public MinioFileStorageConnectionStringBuilder() + { } - public MinioFileStorageConnectionStringBuilder(string connectionString) : base(connectionString) { + public MinioFileStorageConnectionStringBuilder(string connectionString) : base(connectionString) + { } - public string Bucket { + public string Bucket + { get => string.IsNullOrEmpty(_bucket) ? "storage" : _bucket; set => _bucket = value; } - protected override bool ParseItem(string key, string value) { - if (String.Equals(key, "Bucket", StringComparison.OrdinalIgnoreCase)) { + protected override bool ParseItem(string key, string value) + { + if (String.Equals(key, "Bucket", StringComparison.OrdinalIgnoreCase)) + { Bucket = value; return true; } return base.ParseItem(key, value); } - public override string ToString() { + public override string ToString() + { var connectionString = base.ToString(); if (!string.IsNullOrEmpty(_bucket)) connectionString += "Bucket=" + Bucket + ";"; diff --git a/src/Foundatio.Minio/Storage/MinioFileStorageOptions.cs b/src/Foundatio.Minio/Storage/MinioFileStorageOptions.cs index acce71d..0c17a10 100644 --- a/src/Foundatio.Minio/Storage/MinioFileStorageOptions.cs +++ b/src/Foundatio.Minio/Storage/MinioFileStorageOptions.cs @@ -1,20 +1,25 @@ using System; -namespace Foundatio.Storage { - public class MinioFileStorageOptions : SharedOptions { +namespace Foundatio.Storage +{ + public class MinioFileStorageOptions : SharedOptions + { public string ConnectionString { get; set; } public bool AutoCreateBucket { get; set; } } - public class MinioFileStorageOptionsBuilder : SharedOptionsBuilder { - public MinioFileStorageOptionsBuilder ConnectionString(string connectionString) { + public class MinioFileStorageOptionsBuilder : SharedOptionsBuilder + { + public MinioFileStorageOptionsBuilder ConnectionString(string connectionString) + { if (String.IsNullOrEmpty(connectionString)) throw new ArgumentNullException(nameof(connectionString)); Target.ConnectionString = connectionString; return this; } - public MinioFileStorageOptionsBuilder AutoCreateBuckets(bool shouldAutoCreateBuckets = true) { + public MinioFileStorageOptionsBuilder AutoCreateBuckets(bool shouldAutoCreateBuckets = true) + { Target.AutoCreateBucket = shouldAutoCreateBuckets; return this; } diff --git a/tests/Foundatio.Minio.Tests/ConnectionStringBuilderTests.cs b/tests/Foundatio.Minio.Tests/ConnectionStringBuilderTests.cs index fccd9dd..f6e157f 100644 --- a/tests/Foundatio.Minio.Tests/ConnectionStringBuilderTests.cs +++ b/tests/Foundatio.Minio.Tests/ConnectionStringBuilderTests.cs @@ -1,19 +1,24 @@ using System; using Xunit; -namespace Foundatio.Minio.Tests { - public abstract class ConnectionStringBuilderTests { +namespace Foundatio.Minio.Tests +{ + public abstract class ConnectionStringBuilderTests + { protected abstract MinioConnectionStringBuilder CreateConnectionStringBuilder(string connectionString); protected abstract MinioConnectionStringBuilder CreateConnectionStringBuilder(); - public virtual void InvalidKeyShouldThrow() { + public virtual void InvalidKeyShouldThrow() + { var exception = Assert.Throws("connectionString", () => CreateConnectionStringBuilder("wrongaccess=TestAccessKey;SecretKey=TestSecretKey")); Assert.Equal("The option 'wrongaccess' cannot be recognized in connection string. (Parameter 'connectionString')", exception.Message); } - public virtual void CanParseAccessKey() { - foreach (string key in new[] { "AccessKey", "AccessKeyId", "Access Key", "Access Key ID", "Id", "accessKey", "access key", "access key id", "id" }) { + public virtual void CanParseAccessKey() + { + foreach (string key in new[] { "AccessKey", "AccessKeyId", "Access Key", "Access Key ID", "Id", "accessKey", "access key", "access key id", "id" }) + { var connectionStringBuilder = CreateConnectionStringBuilder($"{key}=TestAccessKey;SecretKey=TestSecretKey;"); Assert.Equal("TestAccessKey", connectionStringBuilder.AccessKey); Assert.Equal("TestSecretKey", connectionStringBuilder.SecretKey); @@ -21,8 +26,10 @@ public virtual void CanParseAccessKey() { } } - public virtual void CanParseSecretKey() { - foreach (string key in new[] { "SecretKey", "Secret Key", "Secret", "secretKey", "secret key", "secret" }) { + public virtual void CanParseSecretKey() + { + foreach (string key in new[] { "SecretKey", "Secret Key", "Secret", "secretKey", "secret key", "secret" }) + { var connectionStringBuilder = CreateConnectionStringBuilder($"AccessKey=TestAccessKey;{key}=TestSecretKey;"); Assert.Equal("TestAccessKey", connectionStringBuilder.AccessKey); Assert.Equal("TestSecretKey", connectionStringBuilder.SecretKey); @@ -30,8 +37,10 @@ public virtual void CanParseSecretKey() { } } - public virtual void CanParseRegion() { - foreach (string key in new[] { "Region", "region" }) { + public virtual void CanParseRegion() + { + foreach (string key in new[] { "Region", "region" }) + { var connectionStringBuilder = CreateConnectionStringBuilder($"AccessKey=TestAccessKey;SecretKey=TestSecretKey;{key}=TestRegion;"); Assert.Equal("TestAccessKey", connectionStringBuilder.AccessKey); Assert.Equal("TestSecretKey", connectionStringBuilder.SecretKey); @@ -39,8 +48,10 @@ public virtual void CanParseRegion() { } } - public virtual void CanParseEndPoint() { - foreach (string key in new[] { "EndPoint", "End Point", "endPoint", "end point" }) { + public virtual void CanParseEndPoint() + { + foreach (string key in new[] { "EndPoint", "End Point", "endPoint", "end point" }) + { var connectionStringBuilder = CreateConnectionStringBuilder($"AccessKey=TestAccessKey;SecretKey=TestSecretKey;{key}=TestEndPoint;"); Assert.Equal("TestAccessKey", connectionStringBuilder.AccessKey); Assert.Equal("TestSecretKey", connectionStringBuilder.SecretKey); @@ -48,7 +59,8 @@ public virtual void CanParseEndPoint() { } } - public virtual void CanGenerateConnectionString() { + public virtual void CanGenerateConnectionString() + { var connectionStringBuilder = CreateConnectionStringBuilder(); connectionStringBuilder.AccessKey = "TestAccessKey"; connectionStringBuilder.SecretKey = "TestSecretKey"; diff --git a/tests/Foundatio.Minio.Tests/Storage/MinioFileStorageConnectionStringBuilderTests.cs b/tests/Foundatio.Minio.Tests/Storage/MinioFileStorageConnectionStringBuilderTests.cs index d8c1310..41fd1e0 100644 --- a/tests/Foundatio.Minio.Tests/Storage/MinioFileStorageConnectionStringBuilderTests.cs +++ b/tests/Foundatio.Minio.Tests/Storage/MinioFileStorageConnectionStringBuilderTests.cs @@ -1,49 +1,61 @@ using Foundatio.Storage; using Xunit; -namespace Foundatio.Minio.Tests.Storage { - public class MinioFileStorageConnectionStringBuilderTests : ConnectionStringBuilderTests { - protected override MinioConnectionStringBuilder CreateConnectionStringBuilder(string connectionString) { +namespace Foundatio.Minio.Tests.Storage +{ + public class MinioFileStorageConnectionStringBuilderTests : ConnectionStringBuilderTests + { + protected override MinioConnectionStringBuilder CreateConnectionStringBuilder(string connectionString) + { return new MinioFileStorageConnectionStringBuilder(connectionString); } - protected override MinioConnectionStringBuilder CreateConnectionStringBuilder() { + protected override MinioConnectionStringBuilder CreateConnectionStringBuilder() + { return new MinioFileStorageConnectionStringBuilder(); } [Fact] - public override void InvalidKeyShouldThrow() { + public override void InvalidKeyShouldThrow() + { base.InvalidKeyShouldThrow(); } [Fact] - public override void CanParseAccessKey() { + public override void CanParseAccessKey() + { base.CanParseAccessKey(); } [Fact] - public override void CanParseSecretKey() { + public override void CanParseSecretKey() + { base.CanParseSecretKey(); } [Fact] - public override void CanParseRegion() { + public override void CanParseRegion() + { base.CanParseRegion(); } [Fact] - public override void CanParseEndPoint() { + public override void CanParseEndPoint() + { base.CanParseEndPoint(); } [Fact] - public override void CanGenerateConnectionString() { + public override void CanGenerateConnectionString() + { base.CanGenerateConnectionString(); } [Fact] - public void CanParseBucket() { - foreach (var key in new[] { "Bucket", "bucket" }) { + public void CanParseBucket() + { + foreach (var key in new[] { "Bucket", "bucket" }) + { var connectionStringBuilder = CreateConnectionStringBuilder($"AccessKey=TestAccessKey;SecretKey=TestSecretKey;{key}=TestBucket"); Assert.Equal("TestAccessKey", connectionStringBuilder.AccessKey); Assert.Equal("TestSecretKey", connectionStringBuilder.SecretKey); @@ -53,7 +65,8 @@ public void CanParseBucket() { } [Fact] - public void CanGenerateConnectionStringWithBucket() { + public void CanGenerateConnectionStringWithBucket() + { var connectionStringBuilder = (MinioFileStorageConnectionStringBuilder)CreateConnectionStringBuilder(); connectionStringBuilder.AccessKey = "TestAccessKey"; connectionStringBuilder.SecretKey = "TestSecretKey"; diff --git a/tests/Foundatio.Minio.Tests/Storage/MinioFileStorageTests.cs b/tests/Foundatio.Minio.Tests/Storage/MinioFileStorageTests.cs index 3a138a7..720e992 100644 --- a/tests/Foundatio.Minio.Tests/Storage/MinioFileStorageTests.cs +++ b/tests/Foundatio.Minio.Tests/Storage/MinioFileStorageTests.cs @@ -6,13 +6,17 @@ using Xunit; using Xunit.Abstractions; -namespace Foundatio.Minio.Tests.Storage { - public class MinioFileStorageTests : FileStorageTestsBase { +namespace Foundatio.Minio.Tests.Storage +{ + public class MinioFileStorageTests : FileStorageTestsBase + { public MinioFileStorageTests(ITestOutputHelper output) : base(output) { } - protected override IFileStorage GetStorage() { + protected override IFileStorage GetStorage() + { var section = Configuration.GetSection("Minio"); - var connectionStringBuilder = new MinioFileStorageConnectionStringBuilder { + var connectionStringBuilder = new MinioFileStorageConnectionStringBuilder + { AccessKey = section["ACCESS_KEY_ID"], SecretKey = section["SECRET_ACCESS_KEY"], EndPoint = section["ENDPOINT"], @@ -25,92 +29,110 @@ protected override IFileStorage GetStorage() { } [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 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] - 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(); } }