From 24c27154d9d2ed0499640aa5e56a31c5ff6304d2 Mon Sep 17 00:00:00 2001 From: enisn Date: Mon, 26 May 2025 15:15:41 +0300 Subject: [PATCH 1/6] Add ServiceURL configuration for S3-compatible APIs - Introduced a new property `ServiceURL` in `AwsBlobProviderConfiguration` to allow custom service URLs for S3-compatible APIs like MinIO and DigitalOcean Spaces. - Updated `AwsBlobProviderConfigurationNames` to include the new `ServiceURL` constant. - Modified `DefaultAmazonS3ClientFactory` to utilize the `ServiceURL` when creating the S3 client configuration, enabling support for custom endpoints. --- .../Aws/AwsBlobProviderConfiguration.cs | 9 +++++++ .../Aws/AwsBlobProviderConfigurationNames.cs | 1 + .../Aws/DefaultAmazonS3ClientFactory.cs | 27 +++++++++++++++---- 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/framework/src/Volo.Abp.BlobStoring.Aws/Volo/Abp/BlobStoring/Aws/AwsBlobProviderConfiguration.cs b/framework/src/Volo.Abp.BlobStoring.Aws/Volo/Abp/BlobStoring/Aws/AwsBlobProviderConfiguration.cs index 1b31d176c89..77919b80ef3 100644 --- a/framework/src/Volo.Abp.BlobStoring.Aws/Volo/Abp/BlobStoring/Aws/AwsBlobProviderConfiguration.cs +++ b/framework/src/Volo.Abp.BlobStoring.Aws/Volo/Abp/BlobStoring/Aws/AwsBlobProviderConfiguration.cs @@ -62,6 +62,15 @@ public string Region { set => _containerConfiguration.SetConfiguration(AwsBlobProviderConfigurationNames.Region, Check.NotNull(value, nameof(value))); } + /// + /// Custom service URL for S3-compatible APIs (e.g., MinIO, DigitalOcean Spaces). + /// If not specified, the default AWS S3 service URL will be used based on the region. + /// + public string? ServiceURL { + get => _containerConfiguration.GetConfigurationOrDefault(AwsBlobProviderConfigurationNames.ServiceURL); + set => _containerConfiguration.SetConfiguration(AwsBlobProviderConfigurationNames.ServiceURL, value); + } + /// /// This name may only contain lowercase letters, numbers, and hyphens, and must begin with a letter or a number. /// Each hyphen must be preceded and followed by a non-hyphen character. diff --git a/framework/src/Volo.Abp.BlobStoring.Aws/Volo/Abp/BlobStoring/Aws/AwsBlobProviderConfigurationNames.cs b/framework/src/Volo.Abp.BlobStoring.Aws/Volo/Abp/BlobStoring/Aws/AwsBlobProviderConfigurationNames.cs index d1b775c8123..b0654414624 100644 --- a/framework/src/Volo.Abp.BlobStoring.Aws/Volo/Abp/BlobStoring/Aws/AwsBlobProviderConfigurationNames.cs +++ b/framework/src/Volo.Abp.BlobStoring.Aws/Volo/Abp/BlobStoring/Aws/AwsBlobProviderConfigurationNames.cs @@ -14,6 +14,7 @@ public static class AwsBlobProviderConfigurationNames public const string Name = "Aws.Name"; public const string Policy = "Aws.Policy"; public const string Region = "Aws.Region"; + public const string ServiceURL = "Aws.ServiceURL"; public const string ContainerName = "Aws.ContainerName"; public const string CreateContainerIfNotExists = "Aws.CreateContainerIfNotExists"; } diff --git a/framework/src/Volo.Abp.BlobStoring.Aws/Volo/Abp/BlobStoring/Aws/DefaultAmazonS3ClientFactory.cs b/framework/src/Volo.Abp.BlobStoring.Aws/Volo/Abp/BlobStoring/Aws/DefaultAmazonS3ClientFactory.cs index dee0bf50967..dc2a7fe56be 100644 --- a/framework/src/Volo.Abp.BlobStoring.Aws/Volo/Abp/BlobStoring/Aws/DefaultAmazonS3ClientFactory.cs +++ b/framework/src/Volo.Abp.BlobStoring.Aws/Volo/Abp/BlobStoring/Aws/DefaultAmazonS3ClientFactory.cs @@ -31,30 +31,47 @@ public virtual async Task GetAmazonS3Client( AwsBlobProviderConfiguration configuration) { var region = RegionEndpoint.GetBySystemName(configuration.Region); + var clientConfig = CreateS3ClientConfig(configuration, region); if (configuration.UseCredentials) { var awsCredentials = GetAwsCredentials(configuration); return awsCredentials == null - ? new AmazonS3Client(region) - : new AmazonS3Client(awsCredentials, region); + ? new AmazonS3Client(clientConfig) + : new AmazonS3Client(awsCredentials, clientConfig); } if (configuration.UseTemporaryCredentials) { - return new AmazonS3Client(await GetTemporaryCredentialsAsync(configuration), region); + return new AmazonS3Client(await GetTemporaryCredentialsAsync(configuration), clientConfig); } if (configuration.UseTemporaryFederatedCredentials) { return new AmazonS3Client(await GetTemporaryFederatedCredentialsAsync(configuration), - region); + clientConfig); } Check.NotNullOrWhiteSpace(configuration.AccessKeyId, nameof(configuration.AccessKeyId)); Check.NotNullOrWhiteSpace(configuration.SecretAccessKey, nameof(configuration.SecretAccessKey)); - return new AmazonS3Client(configuration.AccessKeyId, configuration.SecretAccessKey, region); + return new AmazonS3Client(configuration.AccessKeyId, configuration.SecretAccessKey, clientConfig); + } + + protected virtual AmazonS3Config CreateS3ClientConfig(AwsBlobProviderConfiguration configuration, RegionEndpoint region) + { + var clientConfig = new AmazonS3Config + { + RegionEndpoint = region + }; + + if (!configuration.ServiceURL.IsNullOrWhiteSpace()) + { + clientConfig.ServiceURL = configuration.ServiceURL; + clientConfig.ForcePathStyle = true; // Required for most S3-compatible services + } + + return clientConfig; } protected virtual AWSCredentials? GetAwsCredentials( From 0f46fb4417cb6829af356723f75a3ef19b4ef458 Mon Sep 17 00:00:00 2001 From: enisn Date: Mon, 26 May 2025 15:16:17 +0300 Subject: [PATCH 2/6] Add unit tests for AwsBlobProviderConfiguration and DefaultAmazonS3ClientFactory - Created `AwsBlobProviderConfiguration_Tests` to validate ServiceURL setting and retrieval. - Implemented `DefaultAmazonS3ClientFactory_Tests` to ensure S3 client creation with and without custom ServiceURL. - Tests cover scenarios for both S3-compatible services and default AWS S3 behavior. --- .../Aws/AwsBlobProviderConfiguration_Tests.cs | 52 +++++++++++++++ .../Aws/DefaultAmazonS3ClientFactory_Tests.cs | 64 +++++++++++++++++++ 2 files changed, 116 insertions(+) create mode 100644 framework/test/Volo.Abp.BlobStoring.Aws.Tests/Volo/Abp/BlobStoring/Aws/AwsBlobProviderConfiguration_Tests.cs create mode 100644 framework/test/Volo.Abp.BlobStoring.Aws.Tests/Volo/Abp/BlobStoring/Aws/DefaultAmazonS3ClientFactory_Tests.cs diff --git a/framework/test/Volo.Abp.BlobStoring.Aws.Tests/Volo/Abp/BlobStoring/Aws/AwsBlobProviderConfiguration_Tests.cs b/framework/test/Volo.Abp.BlobStoring.Aws.Tests/Volo/Abp/BlobStoring/Aws/AwsBlobProviderConfiguration_Tests.cs new file mode 100644 index 00000000000..2e95ed8291f --- /dev/null +++ b/framework/test/Volo.Abp.BlobStoring.Aws.Tests/Volo/Abp/BlobStoring/Aws/AwsBlobProviderConfiguration_Tests.cs @@ -0,0 +1,52 @@ +using Shouldly; +using Xunit; + +namespace Volo.Abp.BlobStoring.Aws; + +public class AwsBlobProviderConfiguration_Tests : AbpBlobStoringAwsTestCommonBase +{ + [Fact] + public void Should_Set_And_Get_ServiceURL() + { + // Arrange + var containerConfiguration = new BlobContainerConfiguration(); + var awsConfiguration = new AwsBlobProviderConfiguration(containerConfiguration); + const string serviceUrl = "https://minio.example.com:9000"; + + // Act + awsConfiguration.ServiceURL = serviceUrl; + + // Assert + awsConfiguration.ServiceURL.ShouldBe(serviceUrl); + } + + [Fact] + public void Should_Return_Null_When_ServiceURL_Not_Set() + { + // Arrange + var containerConfiguration = new BlobContainerConfiguration(); + var awsConfiguration = new AwsBlobProviderConfiguration(containerConfiguration); + + // Act & Assert + awsConfiguration.ServiceURL.ShouldBeNull(); + } + + [Fact] + public void Should_Configure_ServiceURL_Using_UseAws_Extension() + { + // Arrange + var containerConfiguration = new BlobContainerConfiguration(); + const string serviceUrl = "https://spaces.digitalocean.com"; + + // Act + containerConfiguration.UseAws(config => + { + config.ServiceURL = serviceUrl; + config.Region = "us-east-1"; + }); + + // Assert + var awsConfig = containerConfiguration.GetAwsConfiguration(); + awsConfig.ServiceURL.ShouldBe(serviceUrl); + } +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.BlobStoring.Aws.Tests/Volo/Abp/BlobStoring/Aws/DefaultAmazonS3ClientFactory_Tests.cs b/framework/test/Volo.Abp.BlobStoring.Aws.Tests/Volo/Abp/BlobStoring/Aws/DefaultAmazonS3ClientFactory_Tests.cs new file mode 100644 index 00000000000..cf5b973ee0d --- /dev/null +++ b/framework/test/Volo.Abp.BlobStoring.Aws.Tests/Volo/Abp/BlobStoring/Aws/DefaultAmazonS3ClientFactory_Tests.cs @@ -0,0 +1,64 @@ +using System.Threading.Tasks; +using Amazon.S3; +using Microsoft.Extensions.DependencyInjection; +using Shouldly; +using Xunit; + +namespace Volo.Abp.BlobStoring.Aws; + +public class DefaultAmazonS3ClientFactory_Tests : AbpBlobStoringAwsTestBase +{ + private readonly IAmazonS3ClientFactory _amazonS3ClientFactory; + + public DefaultAmazonS3ClientFactory_Tests() + { + _amazonS3ClientFactory = GetRequiredService(); + } + + [Fact] + public async Task Should_Create_S3Client_With_Custom_ServiceURL() + { + // Arrange + var containerConfiguration = new BlobContainerConfiguration(); + const string serviceUrl = "https://minio.example.com:9000"; + + var awsConfiguration = new AwsBlobProviderConfiguration(containerConfiguration) + { + AccessKeyId = "test-access-key", + SecretAccessKey = "test-secret-key", + Region = "us-east-1", + ServiceURL = serviceUrl + }; + + // Act + using var s3Client = await _amazonS3ClientFactory.GetAmazonS3Client(awsConfiguration); + + // Assert + s3Client.ShouldNotBeNull(); + s3Client.Config.ServiceURL.ShouldBe(serviceUrl + "/"); // AWS SDK automatically appends trailing slash + ((AmazonS3Config)s3Client.Config).ForcePathStyle.ShouldBeTrue(); // Should be enabled for S3-compatible services + } + + [Fact] + public async Task Should_Create_S3Client_Without_Custom_ServiceURL() + { + // Arrange + var containerConfiguration = new BlobContainerConfiguration(); + + var awsConfiguration = new AwsBlobProviderConfiguration(containerConfiguration) + { + AccessKeyId = "test-access-key", + SecretAccessKey = "test-secret-key", + Region = "us-east-1" + // ServiceURL not set + }; + + // Act + using var s3Client = await _amazonS3ClientFactory.GetAmazonS3Client(awsConfiguration); + + // Assert + s3Client.ShouldNotBeNull(); + s3Client.Config.ServiceURL.ShouldBeNull(); // Should use default AWS S3 service + ((AmazonS3Config)s3Client.Config).ForcePathStyle.ShouldBeFalse(); // Should be false for AWS S3 + } +} \ No newline at end of file From 6acf535ef8bde4192a9aac2686d40cfb70ace710 Mon Sep 17 00:00:00 2001 From: enisn Date: Mon, 26 May 2025 15:23:00 +0300 Subject: [PATCH 3/6] Enhance AWS Blob Storing Documentation for S3-Compatible Services - Updated the AWS provider documentation to clarify support for S3-compatible storage services, including MinIO, DigitalOcean Spaces, and others. - Added details on configuring the `ServiceURL` property for S3-compatible APIs. - Included example configurations for MinIO, DigitalOcean Spaces, and Wasabi to assist users in setting up their environments. --- .../infrastructure/blob-storing/aws.md | 70 ++++++++++++++++++- .../infrastructure/blob-storing/index.md | 14 ++++ 2 files changed, 83 insertions(+), 1 deletion(-) diff --git a/docs/en/framework/infrastructure/blob-storing/aws.md b/docs/en/framework/infrastructure/blob-storing/aws.md index 4eac484e924..514becab5e3 100644 --- a/docs/en/framework/infrastructure/blob-storing/aws.md +++ b/docs/en/framework/infrastructure/blob-storing/aws.md @@ -1,6 +1,6 @@ # BLOB Storing Aws Provider -BLOB Storing Aws Provider can store BLOBs in [Amazon Simple Storage Service](https://aws.amazon.com/s3/). +BLOB Storing Aws Provider can store BLOBs in [Amazon Simple Storage Service](https://aws.amazon.com/s3/) and **S3-compatible storage services** like MinIO, DigitalOcean Spaces, Cloudflare R2, and others. > Read the [BLOB Storing document](../blob-storing) to understand how to use the BLOB storing system. This document only covers how to configure containers to use a Aws BLOB as the storage provider. @@ -35,6 +35,7 @@ Configure(options => Aws.ProfileName = "the name of the profile to get credentials from"; Aws.ProfilesLocation = "the path to the aws credentials file to look at"; Aws.Region = "the system name of the service"; + Aws.ServiceURL = "custom service URL for S3-compatible APIs (optional)"; Aws.Name = "the name of the federated user"; Aws.Policy = "policy"; Aws.DurationSeconds = "expiration date"; @@ -58,6 +59,7 @@ Configure(options => * **ProfileName** (string): The [name of the profile](https://docs.aws.amazon.com/sdk-for-net/v3/developer-guide/net-dg-config-creds.html) to get credentials from. * **ProfilesLocation** (string): The path to the aws credentials file to look at. * **Region** (string): The system name of the service. +* **ServiceURL** (string): Custom service URL for S3-compatible APIs (e.g., MinIO, DigitalOcean Spaces). If not specified, the default AWS S3 service URL will be used based on the region. When using S3-compatible services, this should point to your service endpoint (e.g., `https://minio.example.com:9000`). * **Policy** (string): An IAM policy in JSON format that you want to use as an inline session policy. * **DurationSeconds** (int): Validity period(s) of a temporary access certificate,minimum is 900 and the maximum is 3600. **note**: Using sub-accounts operated OSS,if the value is 0. * **ContainerName** (string): You can specify the container name in Aws. If this is not specified, it uses the name of the BLOB container defined with the `BlobContainerName` attribute (see the [BLOB storing document](../blob-storing)). Please note that Aws has some **rules for naming containers**. A container name must be a valid DNS name, conforming to the [following naming rules](https://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html): @@ -70,6 +72,72 @@ Configure(options => * Buckets used with Amazon S3 Transfer Acceleration can't have dots (.) in their names. For more information about transfer acceleration, see Amazon S3 Transfer Acceleration. * **CreateContainerIfNotExists** (bool): Default value is `false`, If a container does not exist in Aws, `AwsBlobProvider` will try to create it. +## S3-Compatible Services + +The AWS provider supports S3-compatible storage services by configuring the `ServiceURL` property. Here are some examples: + +### MinIO Configuration + +````csharp +Configure(options => +{ + options.Containers.ConfigureDefault(container => + { + container.UseAws(aws => + { + aws.AccessKeyId = "your-minio-access-key"; + aws.SecretAccessKey = "your-minio-secret-key"; + aws.ServiceURL = "https://minio.example.com:9000"; + aws.Region = "us-east-1"; // MinIO region (can be any valid region) + aws.ContainerName = "my-bucket"; + aws.CreateContainerIfNotExists = true; + }); + }); +}); +```` + +### DigitalOcean Spaces Configuration + +````csharp +Configure(options => +{ + options.Containers.ConfigureDefault(container => + { + container.UseAws(aws => + { + aws.AccessKeyId = "your-spaces-access-key"; + aws.SecretAccessKey = "your-spaces-secret-key"; + aws.ServiceURL = "https://nyc3.digitaloceanspaces.com"; + aws.Region = "us-east-1"; // DigitalOcean Spaces region + aws.ContainerName = "my-space"; + aws.CreateContainerIfNotExists = true; + }); + }); +}); +```` + +### Wasabi Configuration + +````csharp +Configure(options => +{ + options.Containers.ConfigureDefault(container => + { + container.UseAws(aws => + { + aws.AccessKeyId = "your-wasabi-access-key"; + aws.SecretAccessKey = "your-wasabi-secret-key"; + aws.ServiceURL = "https://s3.us-east-1.wasabisys.com"; + aws.Region = "us-east-1"; + aws.ContainerName = "my-bucket"; + aws.CreateContainerIfNotExists = true; + }); + }); +}); +```` + +> **Note**: When using S3-compatible services, the provider automatically enables path-style requests which are required by most S3-compatible implementations. + ## Aws Blob Name Calculator Aws Blob Provider organizes BLOB name and implements some conventions. The full name of a BLOB is determined by the following rules by default: diff --git a/docs/en/framework/infrastructure/blob-storing/index.md b/docs/en/framework/infrastructure/blob-storing/index.md index 676757ad805..75514bff4fe 100644 --- a/docs/en/framework/infrastructure/blob-storing/index.md +++ b/docs/en/framework/infrastructure/blob-storing/index.md @@ -29,6 +29,20 @@ More providers will be implemented by the time. You can [request](https://github Multiple providers **can be used together** by the help of the **container system**, where each container can uses a different provider. +### S3 Compatibility + +The [AWS provider](./aws.md) supports not only Amazon S3 but also **S3-compatible APIs** from various cloud providers and self-hosted solutions. This means you can use the same AWS provider to connect to: + +* **Amazon S3** - The original AWS S3 service +* **MinIO** - Self-hosted S3-compatible object storage +* **Cloudflare R2** - Cloudflare's S3-compatible object storage +* **DigitalOcean Spaces** - DigitalOcean's S3-compatible object storage +* **Wasabi** - S3-compatible cloud storage +* **Backblaze B2** - S3-compatible cloud storage +* **Any other S3-compatible storage** - Including private cloud solutions + +To use S3-compatible services, simply configure the `ServiceURL` property in the AWS provider configuration to point to your S3-compatible endpoint. The provider will automatically handle the necessary protocol adjustments for compatibility. + > BLOB storing system can not work unless you **configure a storage provider**. Refer to the linked documents for the storage provider configurations. ## Installation From 898a3134bb969cc56f02fa0f2a17f039212d0956 Mon Sep 17 00:00:00 2001 From: enisn Date: Mon, 26 May 2025 15:27:25 +0300 Subject: [PATCH 4/6] Add CloudFlare R2 example documentation --- docs/en/framework/infrastructure/blob-storing/aws.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/en/framework/infrastructure/blob-storing/aws.md b/docs/en/framework/infrastructure/blob-storing/aws.md index 514becab5e3..1475e5beb4c 100644 --- a/docs/en/framework/infrastructure/blob-storing/aws.md +++ b/docs/en/framework/infrastructure/blob-storing/aws.md @@ -116,7 +116,7 @@ Configure(options => }); ```` -### Wasabi Configuration +### Cloudflare R2 Configuration ````csharp Configure(options => @@ -125,10 +125,10 @@ Configure(options => { container.UseAws(aws => { - aws.AccessKeyId = "your-wasabi-access-key"; - aws.SecretAccessKey = "your-wasabi-secret-key"; - aws.ServiceURL = "https://s3.us-east-1.wasabisys.com"; - aws.Region = "us-east-1"; + aws.AccessKeyId = "your-r2-access-key"; + aws.SecretAccessKey = "your-r2-secret-key"; + aws.ServiceURL = "https://your-account-id.r2.cloudflarestorage.com"; + aws.Region = "auto"; // Cloudflare R2 uses 'auto' as region aws.ContainerName = "my-bucket"; aws.CreateContainerIfNotExists = true; }); From 4f2a1410467be81e14a778a2e7a75a922d5cd789 Mon Sep 17 00:00:00 2001 From: enisn Date: Mon, 26 May 2025 16:25:37 +0300 Subject: [PATCH 5/6] Make region optional for some S3 compatible APIs --- .../Aws/AwsBlobProviderConfiguration.cs | 6 +-- .../Aws/DefaultAmazonS3ClientFactory.cs | 15 +++++--- .../Aws/AbpBlobStoringAwsTestModule.cs | 38 +++++++++++++------ .../Aws/DefaultAmazonS3ClientFactory_Tests.cs | 23 +++++++++++ 4 files changed, 63 insertions(+), 19 deletions(-) diff --git a/framework/src/Volo.Abp.BlobStoring.Aws/Volo/Abp/BlobStoring/Aws/AwsBlobProviderConfiguration.cs b/framework/src/Volo.Abp.BlobStoring.Aws/Volo/Abp/BlobStoring/Aws/AwsBlobProviderConfiguration.cs index 77919b80ef3..e5a8a853d75 100644 --- a/framework/src/Volo.Abp.BlobStoring.Aws/Volo/Abp/BlobStoring/Aws/AwsBlobProviderConfiguration.cs +++ b/framework/src/Volo.Abp.BlobStoring.Aws/Volo/Abp/BlobStoring/Aws/AwsBlobProviderConfiguration.cs @@ -57,9 +57,9 @@ public string? Policy { set => _containerConfiguration.SetConfiguration(AwsBlobProviderConfigurationNames.Policy, value); } - public string Region { - get => _containerConfiguration.GetConfiguration(AwsBlobProviderConfigurationNames.Region); - set => _containerConfiguration.SetConfiguration(AwsBlobProviderConfigurationNames.Region, Check.NotNull(value, nameof(value))); + public string? Region { + get => _containerConfiguration.GetConfigurationOrDefault(AwsBlobProviderConfigurationNames.Region); + set => _containerConfiguration.SetConfiguration(AwsBlobProviderConfigurationNames.Region, value); } /// diff --git a/framework/src/Volo.Abp.BlobStoring.Aws/Volo/Abp/BlobStoring/Aws/DefaultAmazonS3ClientFactory.cs b/framework/src/Volo.Abp.BlobStoring.Aws/Volo/Abp/BlobStoring/Aws/DefaultAmazonS3ClientFactory.cs index dc2a7fe56be..1572adca7c0 100644 --- a/framework/src/Volo.Abp.BlobStoring.Aws/Volo/Abp/BlobStoring/Aws/DefaultAmazonS3ClientFactory.cs +++ b/framework/src/Volo.Abp.BlobStoring.Aws/Volo/Abp/BlobStoring/Aws/DefaultAmazonS3ClientFactory.cs @@ -30,7 +30,9 @@ public DefaultAmazonS3ClientFactory( public virtual async Task GetAmazonS3Client( AwsBlobProviderConfiguration configuration) { - var region = RegionEndpoint.GetBySystemName(configuration.Region); + var region = !configuration.Region.IsNullOrWhiteSpace() + ? RegionEndpoint.GetBySystemName(configuration.Region) + : null; var clientConfig = CreateS3ClientConfig(configuration, region); if (configuration.UseCredentials) @@ -58,12 +60,15 @@ public virtual async Task GetAmazonS3Client( return new AmazonS3Client(configuration.AccessKeyId, configuration.SecretAccessKey, clientConfig); } - protected virtual AmazonS3Config CreateS3ClientConfig(AwsBlobProviderConfiguration configuration, RegionEndpoint region) + protected virtual AmazonS3Config CreateS3ClientConfig(AwsBlobProviderConfiguration configuration, RegionEndpoint? region) { - var clientConfig = new AmazonS3Config + var clientConfig = new AmazonS3Config(); + + // Set region only if it's provided (for AWS S3) + if (region != null) { - RegionEndpoint = region - }; + clientConfig.RegionEndpoint = region; + } if (!configuration.ServiceURL.IsNullOrWhiteSpace()) { diff --git a/framework/test/Volo.Abp.BlobStoring.Aws.Tests/Volo/Abp/BlobStoring/Aws/AbpBlobStoringAwsTestModule.cs b/framework/test/Volo.Abp.BlobStoring.Aws.Tests/Volo/Abp/BlobStoring/Aws/AbpBlobStoringAwsTestModule.cs index 6edd0fc6696..984a66bbc16 100644 --- a/framework/test/Volo.Abp.BlobStoring.Aws.Tests/Volo/Abp/BlobStoring/Aws/AbpBlobStoringAwsTestModule.cs +++ b/framework/test/Volo.Abp.BlobStoring.Aws.Tests/Volo/Abp/BlobStoring/Aws/AbpBlobStoringAwsTestModule.cs @@ -69,23 +69,39 @@ public override void OnApplicationShutdown(ApplicationShutdownContext context) private async Task DeleteBucketAsync(ApplicationShutdownContext context) { - var amazonS3Client = await context.ServiceProvider.GetRequiredService() - .GetAmazonS3Client(_configuration); + // Skip bucket deletion if configuration is not properly set (e.g., in unit tests) + if (_configuration == null || + string.IsNullOrWhiteSpace(_configuration.AccessKeyId) || + string.IsNullOrWhiteSpace(_configuration.SecretAccessKey) || + (string.IsNullOrWhiteSpace(_configuration.Region) && string.IsNullOrWhiteSpace(_configuration.ServiceURL))) + { + return; + } - if (await AmazonS3Util.DoesS3BucketExistV2Async(amazonS3Client, _randomContainerName)) + try { - var blobs = await amazonS3Client.ListObjectsAsync(_randomContainerName); + var amazonS3Client = await context.ServiceProvider.GetRequiredService() + .GetAmazonS3Client(_configuration); - if (blobs.S3Objects.Any()) + if (await AmazonS3Util.DoesS3BucketExistV2Async(amazonS3Client, _randomContainerName)) { - await amazonS3Client.DeleteObjectsAsync(new DeleteObjectsRequest + var blobs = await amazonS3Client.ListObjectsAsync(_randomContainerName); + + if (blobs.S3Objects.Any()) { - BucketName = _randomContainerName, - Objects = blobs.S3Objects.Select(o => new KeyVersion { Key = o.Key }).ToList() - }); - } + await amazonS3Client.DeleteObjectsAsync(new DeleteObjectsRequest + { + BucketName = _randomContainerName, + Objects = blobs.S3Objects.Select(o => new KeyVersion { Key = o.Key }).ToList() + }); + } - await amazonS3Client.DeleteBucketAsync(_randomContainerName); + await amazonS3Client.DeleteBucketAsync(_randomContainerName); + } + } + catch + { + // Ignore errors during test cleanup } } } diff --git a/framework/test/Volo.Abp.BlobStoring.Aws.Tests/Volo/Abp/BlobStoring/Aws/DefaultAmazonS3ClientFactory_Tests.cs b/framework/test/Volo.Abp.BlobStoring.Aws.Tests/Volo/Abp/BlobStoring/Aws/DefaultAmazonS3ClientFactory_Tests.cs index cf5b973ee0d..5f7539de9eb 100644 --- a/framework/test/Volo.Abp.BlobStoring.Aws.Tests/Volo/Abp/BlobStoring/Aws/DefaultAmazonS3ClientFactory_Tests.cs +++ b/framework/test/Volo.Abp.BlobStoring.Aws.Tests/Volo/Abp/BlobStoring/Aws/DefaultAmazonS3ClientFactory_Tests.cs @@ -61,4 +61,27 @@ public async Task Should_Create_S3Client_Without_Custom_ServiceURL() s3Client.Config.ServiceURL.ShouldBeNull(); // Should use default AWS S3 service ((AmazonS3Config)s3Client.Config).ForcePathStyle.ShouldBeFalse(); // Should be false for AWS S3 } + + [Fact] + public async Task Should_Create_S3Client_Without_Region_For_S3Compatible_Services() + { + // Arrange + var containerConfiguration = new BlobContainerConfiguration(); + + var awsConfiguration = new AwsBlobProviderConfiguration(containerConfiguration) + { + AccessKeyId = "test-access-key", + SecretAccessKey = "test-secret-key", + ServiceURL = "https://minio.example.com:9000" + // Region not set - should work for S3-compatible services + }; + + // Act + using var s3Client = await _amazonS3ClientFactory.GetAmazonS3Client(awsConfiguration); + + // Assert + s3Client.ShouldNotBeNull(); + s3Client.Config.ServiceURL.ShouldBe("https://minio.example.com:9000/"); // AWS SDK automatically appends trailing slash + ((AmazonS3Config)s3Client.Config).ForcePathStyle.ShouldBeTrue(); // Should be enabled for S3-compatible services + } } \ No newline at end of file From c40f4538d9ce0257d4c1b94149913c5fa841827f Mon Sep 17 00:00:00 2001 From: enisn Date: Mon, 26 May 2025 16:43:05 +0300 Subject: [PATCH 6/6] Configure `ResponseChecksumValidation` hwne third party S3 providers used --- .../Aws/DefaultAmazonS3ClientFactory.cs | 5 ++++ .../Aws/DefaultAmazonS3ClientFactory_Tests.cs | 29 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/framework/src/Volo.Abp.BlobStoring.Aws/Volo/Abp/BlobStoring/Aws/DefaultAmazonS3ClientFactory.cs b/framework/src/Volo.Abp.BlobStoring.Aws/Volo/Abp/BlobStoring/Aws/DefaultAmazonS3ClientFactory.cs index 1572adca7c0..0d4575d832b 100644 --- a/framework/src/Volo.Abp.BlobStoring.Aws/Volo/Abp/BlobStoring/Aws/DefaultAmazonS3ClientFactory.cs +++ b/framework/src/Volo.Abp.BlobStoring.Aws/Volo/Abp/BlobStoring/Aws/DefaultAmazonS3ClientFactory.cs @@ -74,6 +74,11 @@ protected virtual AmazonS3Config CreateS3ClientConfig(AwsBlobProviderConfigurati { clientConfig.ServiceURL = configuration.ServiceURL; clientConfig.ForcePathStyle = true; // Required for most S3-compatible services + + // Set checksum properties for S3-compatible services (e.g., Cloudflare R2) + // These settings help with compatibility issues in non-AWS S3 services + clientConfig.RequestChecksumCalculation = RequestChecksumCalculation.WHEN_REQUIRED; + clientConfig.ResponseChecksumValidation = ResponseChecksumValidation.WHEN_REQUIRED; } return clientConfig; diff --git a/framework/test/Volo.Abp.BlobStoring.Aws.Tests/Volo/Abp/BlobStoring/Aws/DefaultAmazonS3ClientFactory_Tests.cs b/framework/test/Volo.Abp.BlobStoring.Aws.Tests/Volo/Abp/BlobStoring/Aws/DefaultAmazonS3ClientFactory_Tests.cs index 5f7539de9eb..fbd51dd0383 100644 --- a/framework/test/Volo.Abp.BlobStoring.Aws.Tests/Volo/Abp/BlobStoring/Aws/DefaultAmazonS3ClientFactory_Tests.cs +++ b/framework/test/Volo.Abp.BlobStoring.Aws.Tests/Volo/Abp/BlobStoring/Aws/DefaultAmazonS3ClientFactory_Tests.cs @@ -84,4 +84,33 @@ public async Task Should_Create_S3Client_Without_Region_For_S3Compatible_Service s3Client.Config.ServiceURL.ShouldBe("https://minio.example.com:9000/"); // AWS SDK automatically appends trailing slash ((AmazonS3Config)s3Client.Config).ForcePathStyle.ShouldBeTrue(); // Should be enabled for S3-compatible services } + + [Fact] + public async Task Should_Set_Checksum_Properties_For_S3Compatible_Services() + { + // Arrange + var containerConfiguration = new BlobContainerConfiguration(); + + var awsConfiguration = new AwsBlobProviderConfiguration(containerConfiguration) + { + AccessKeyId = "test-access-key", + SecretAccessKey = "test-secret-key", + ServiceURL = "https://r2.cloudflarestorage.com", + Region = "auto" + }; + + // Act + using var s3Client = await _amazonS3ClientFactory.GetAmazonS3Client(awsConfiguration); + + // Assert + s3Client.ShouldNotBeNull(); + var config = (AmazonS3Config)s3Client.Config; + config.ServiceURL.ShouldBe("https://r2.cloudflarestorage.com/"); + config.ForcePathStyle.ShouldBeTrue(); + + // Verify checksum properties are set for S3-compatible services (required for Cloudflare R2) + // We just verify they are not null/default, indicating they have been set + config.RequestChecksumCalculation.ShouldNotBe(default); + config.ResponseChecksumValidation.ShouldNotBe(default); + } } \ No newline at end of file