Browse Source

Merge pull request #22962 from abpframework/aws-service-url

Support S3 Compatible Blob Storage Providers
pull/25517/head
Ma Liming 5 months ago
committed by GitHub
parent
commit
52f2d18473
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 78
      docs/en/framework/infrastructure/blob-storing/aws.md
  2. 14
      docs/en/framework/infrastructure/blob-storing/index.md
  3. 3
      framework/src/Volo.Abp.BlobStoring.Aws/Volo/Abp/BlobStoring/Aws/AwsBlobProvider.cs
  4. 50
      framework/src/Volo.Abp.BlobStoring.Aws/Volo/Abp/BlobStoring/Aws/AwsBlobProviderConfiguration.cs
  5. 2
      framework/src/Volo.Abp.BlobStoring.Aws/Volo/Abp/BlobStoring/Aws/AwsBlobProviderConfigurationNames.cs
  6. 41
      framework/src/Volo.Abp.BlobStoring.Aws/Volo/Abp/BlobStoring/Aws/DefaultAmazonS3ClientFactory.cs
  7. 148
      framework/test/Volo.Abp.BlobStoring.Aws.Tests/Volo/Abp/BlobStoring/Aws/AbpBlobStoringAwsTestModule.cs
  8. 26
      framework/test/Volo.Abp.BlobStoring.Aws.Tests/Volo/Abp/BlobStoring/Aws/AwsBlobContainer_Tests.cs
  9. 72
      framework/test/Volo.Abp.BlobStoring.Aws.Tests/Volo/Abp/BlobStoring/Aws/AwsBlobProviderConfiguration_Tests.cs
  10. 114
      framework/test/Volo.Abp.BlobStoring.Aws.Tests/Volo/Abp/BlobStoring/Aws/DefaultAmazonS3ClientFactory_Tests.cs

78
docs/en/framework/infrastructure/blob-storing/aws.md

@ -7,7 +7,7 @@
# 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.
@ -41,7 +41,8 @@ Configure<AbpBlobStoringOptions>(options =>
Aws.UseTemporaryFederatedCredentials = "set true to use temporary federated credentials";
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.Region = "the AWS region system name, e.g. us-east-1";
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";
@ -64,7 +65,9 @@ Configure<AbpBlobStoringOptions>(options =>
* **UseTemporaryFederatedCredentials** (bool): Use [federated user temporary credentials](https://docs.aws.amazon.com/AmazonS3/latest/dev/AuthUsingTempFederationToken.html) to access AWS services, default : `false`.
* **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.
* **Region** (string): The system name of the AWS region (e.g., `us-east-1`). **Required** for real AWS S3. Optional when `ServiceURL` is configured for an S3-compatible service; some services accept any value (or `auto` for Cloudflare R2).
* **ServiceURL** (string): Custom service URL for S3-compatible APIs (e.g., MinIO, DigitalOcean Spaces, Cloudflare R2). 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`). The AWS SDK automatically appends a trailing slash to the configured value.
* **DisablePayloadSigning** (bool): Default `false`. When set to `true`, the provider sends `x-amz-content-sha256: UNSIGNED-PAYLOAD` on `PutObject` requests instead of the streaming chunked signature (`STREAMING-AWS4-HMAC-SHA256-PAYLOAD`) that the AWS SDK v4 uses by default. Required for Cloudflare R2 and other S3-compatible services that do not implement streaming signing. The endpoint must be HTTPS when this option is enabled. Leave as `false` for real AWS S3.
* **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):
@ -77,6 +80,75 @@ Configure<AbpBlobStoringOptions>(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<AbpBlobStoringOptions>(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<AbpBlobStoringOptions>(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;
});
});
});
````
### Cloudflare R2 Configuration
````csharp
Configure<AbpBlobStoringOptions>(options =>
{
options.Containers.ConfigureDefault(container =>
{
container.UseAws(aws =>
{
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.DisablePayloadSigning = true; // R2 does not implement streaming chunked payload signing
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.
> **Note on `DisablePayloadSigning`**: AWS SDK v4 sends `PutObject` requests with `x-amz-content-sha256: STREAMING-AWS4-HMAC-SHA256-PAYLOAD`. Cloudflare R2 (and some other S3-compatible services) return `501 NotImplemented` for this signing mode. Setting `DisablePayloadSigning = true` switches to `UNSIGNED-PAYLOAD`, which these services accept. The endpoint must be HTTPS. Leave it `false` for real AWS S3.
## 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:

14
docs/en/framework/infrastructure/blob-storing/index.md

@ -36,6 +36,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, configure the `ServiceURL` property in the AWS provider configuration to point to your S3-compatible endpoint. Some services (e.g., Cloudflare R2) also require `DisablePayloadSigning = true` because they do not implement the streaming chunked payload signing that AWS SDK v4 uses by default. See the [AWS provider document](aws.md) for full configuration examples.
> BLOB storing system can not work unless you **configure a storage provider**. Refer to the linked documents for the storage provider configurations.
## Installation

3
framework/src/Volo.Abp.BlobStoring.Aws/Volo/Abp/BlobStoring/Aws/AwsBlobProvider.cs

@ -47,7 +47,8 @@ public class AwsBlobProvider : BlobProviderBase, ITransientDependency
{
BucketName = containerName,
Key = blobName,
InputStream = args.BlobStream
InputStream = args.BlobStream,
DisablePayloadSigning = configuration.DisablePayloadSigning
});
}
}

50
framework/src/Volo.Abp.BlobStoring.Aws/Volo/Abp/BlobStoring/Aws/AwsBlobProviderConfiguration.cs

@ -57,9 +57,53 @@ public class AwsBlobProviderConfiguration
set => _containerConfiguration.SetConfiguration(AwsBlobProviderConfigurationNames.Policy, value);
}
public string Region {
get => _containerConfiguration.GetConfiguration<string>(AwsBlobProviderConfigurationNames.Region);
set => _containerConfiguration.SetConfiguration(AwsBlobProviderConfigurationNames.Region, Check.NotNull(value, nameof(value)));
/// <summary>
/// The system name of the AWS region (e.g., <c>us-east-1</c>). Required for real AWS S3.
/// Optional when <see cref="ServiceURL"/> is configured for an S3-compatible service;
/// some services accept any value (or <c>auto</c> for Cloudflare R2).
/// </summary>
public string? Region {
get => _containerConfiguration.GetConfigurationOrDefault<string>(AwsBlobProviderConfigurationNames.Region);
set
{
if (value == null)
{
_containerConfiguration.ClearConfiguration(AwsBlobProviderConfigurationNames.Region);
return;
}
_containerConfiguration.SetConfiguration(AwsBlobProviderConfigurationNames.Region, value);
}
}
/// <summary>
/// Custom service URL for S3-compatible APIs (e.g., MinIO, DigitalOcean Spaces, Cloudflare R2).
/// When omitted, <see cref="Region"/> becomes required and the default AWS S3 endpoint for that
/// region is used. The AWS SDK automatically appends a trailing slash to the configured value.
/// </summary>
public string? ServiceURL {
get => _containerConfiguration.GetConfigurationOrDefault<string>(AwsBlobProviderConfigurationNames.ServiceURL);
set
{
if (value == null)
{
_containerConfiguration.ClearConfiguration(AwsBlobProviderConfigurationNames.ServiceURL);
return;
}
_containerConfiguration.SetConfiguration(AwsBlobProviderConfigurationNames.ServiceURL, value);
}
}
/// <summary>
/// When true, payload signing is disabled on PutObject upload requests so the SDK sends
/// <c>x-amz-content-sha256: UNSIGNED-PAYLOAD</c> instead of the streaming chunked signature
/// (<c>STREAMING-AWS4-HMAC-SHA256-PAYLOAD</c>) that AWS SDK v4 uses by default. Required for
/// Cloudflare R2 and other S3-compatible services that do not implement streaming signing.
/// The endpoint must be HTTPS when this option is enabled (AWS SDK rejects unsigned payloads
/// over HTTP). Default: false (keep AWS SDK defaults for real AWS S3 and MinIO).
/// </summary>
public bool DisablePayloadSigning {
get => _containerConfiguration.GetConfigurationOrDefault(AwsBlobProviderConfigurationNames.DisablePayloadSigning, false);
set => _containerConfiguration.SetConfiguration(AwsBlobProviderConfigurationNames.DisablePayloadSigning, value);
}
/// <summary>

2
framework/src/Volo.Abp.BlobStoring.Aws/Volo/Abp/BlobStoring/Aws/AwsBlobProviderConfigurationNames.cs

@ -14,6 +14,8 @@ 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 DisablePayloadSigning = "Aws.DisablePayloadSigning";
public const string ContainerName = "Aws.ContainerName";
public const string CreateContainerIfNotExists = "Aws.CreateContainerIfNotExists";
}

41
framework/src/Volo.Abp.BlobStoring.Aws/Volo/Abp/BlobStoring/Aws/DefaultAmazonS3ClientFactory.cs

@ -30,31 +30,60 @@ public class DefaultAmazonS3ClientFactory : IAmazonS3ClientFactory, ITransientDe
public virtual async Task<AmazonS3Client> GetAmazonS3Client(
AwsBlobProviderConfiguration configuration)
{
var region = RegionEndpoint.GetBySystemName(configuration.Region);
if (configuration.Region.IsNullOrWhiteSpace() && configuration.ServiceURL.IsNullOrWhiteSpace())
{
throw new AbpException(
$"Either {nameof(AwsBlobProviderConfiguration.Region)} or {nameof(AwsBlobProviderConfiguration.ServiceURL)} must be configured on {nameof(AwsBlobProviderConfiguration)}.");
}
var region = !configuration.Region.IsNullOrWhiteSpace()
? RegionEndpoint.GetBySystemName(configuration.Region)
: null;
var clientConfig = await CreateS3ClientConfigAsync(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 Task<AmazonS3Config> CreateS3ClientConfigAsync(AwsBlobProviderConfiguration configuration, RegionEndpoint? region)
{
var clientConfig = new AmazonS3Config();
if (region != null)
{
clientConfig.RegionEndpoint = region;
}
if (!configuration.ServiceURL.IsNullOrWhiteSpace())
{
clientConfig.ServiceURL = configuration.ServiceURL;
clientConfig.ForcePathStyle = true;
clientConfig.RequestChecksumCalculation = RequestChecksumCalculation.WHEN_REQUIRED;
clientConfig.ResponseChecksumValidation = ResponseChecksumValidation.WHEN_REQUIRED;
}
return Task.FromResult(clientConfig);
}
protected virtual AWSCredentials? GetAwsCredentials(

148
framework/test/Volo.Abp.BlobStoring.Aws.Tests/Volo/Abp/BlobStoring/Aws/AbpBlobStoringAwsTestModule.cs

@ -1,11 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Amazon.S3;
using Amazon.S3.Model;
using Amazon.S3.Util;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Volo.Abp.Modularity;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Threading;
namespace Volo.Abp.BlobStoring.Aws;
@ -28,9 +32,19 @@ public class AbpBlobStoringAwsTestModule : AbpModule
{
private const string UserSecretsId = "9f0d2c00-80c1-435b-bfab-2c39c8249091";
private readonly string _randomContainerName = "abp-aws-test-container-" + Guid.NewGuid().ToString("N");
private readonly string _runId = Guid.NewGuid().ToString("N");
private readonly string _randomContainerName;
private readonly string _externalRunPrefix;
private AwsBlobProviderConfiguration _configuration;
private string _actualContainerName;
private bool _externalContainer;
public AbpBlobStoringAwsTestModule()
{
_randomContainerName = "abp-aws-test-container-" + _runId;
_externalRunPrefix = "abp-aws-test-run-" + _runId + "/";
}
public override void ConfigureServices(ServiceConfigurationContext context)
{
@ -42,7 +56,30 @@ public class AbpBlobStoringAwsTestModule : AbpModule
var configuration = context.Services.GetConfiguration();
var accessKeyId = configuration["Aws:AccessKeyId"];
var secretAccessKey = configuration["Aws:SecretAccessKey"];
// No credentials configured (e.g., CI without user secrets) → skip container wiring.
// `BlobContainerConfiguration.SetConfiguration` rejects nulls, so any attempt to resolve
// a container with null values from configuration would throw at runtime.
if (string.IsNullOrEmpty(accessKeyId) || string.IsNullOrEmpty(secretAccessKey))
{
return;
}
var region = configuration["Aws:Region"];
var serviceUrl = configuration["Aws:ServiceURL"];
var disablePayloadSigning = bool.TryParse(configuration["Aws:DisablePayloadSigning"], out var dps) && dps;
var configuredContainerName = configuration["Aws:ContainerName"];
var createContainerIfNotExists = !bool.TryParse(configuration["Aws:CreateContainerIfNotExists"], out var cci) || cci;
_externalContainer = !string.IsNullOrWhiteSpace(configuredContainerName);
_actualContainerName = _externalContainer ? configuredContainerName : _randomContainerName;
if (_externalContainer)
{
var prefix = _externalRunPrefix;
context.Services.Replace(ServiceDescriptor.Transient<IAwsBlobNameCalculator>(sp =>
new TestRunPrefixedAwsBlobNameCalculator(sp.GetRequiredService<ICurrentTenant>(), prefix)));
}
Configure<AbpBlobStoringOptions>(options =>
{
@ -53,8 +90,10 @@ public class AbpBlobStoringAwsTestModule : AbpModule
aws.AccessKeyId = accessKeyId;
aws.SecretAccessKey = secretAccessKey;
aws.Region = region;
aws.CreateContainerIfNotExists = true;
aws.ContainerName = _randomContainerName;
aws.ServiceURL = serviceUrl;
aws.DisablePayloadSigning = disablePayloadSigning;
aws.CreateContainerIfNotExists = createContainerIfNotExists;
aws.ContainerName = _actualContainerName;
_configuration = aws;
});
@ -64,28 +103,107 @@ public class AbpBlobStoringAwsTestModule : AbpModule
public override void OnApplicationShutdown(ApplicationShutdownContext context)
{
AsyncHelper.RunSync(() => DeleteBucketAsync(context));
AsyncHelper.RunSync(() => CleanupAsync(context));
}
private async Task DeleteBucketAsync(ApplicationShutdownContext context)
private async Task CleanupAsync(ApplicationShutdownContext context)
{
var amazonS3Client = await context.ServiceProvider.GetRequiredService<IAmazonS3ClientFactory>()
.GetAmazonS3Client(_configuration);
if (_configuration == null ||
string.IsNullOrWhiteSpace(_configuration.AccessKeyId) ||
string.IsNullOrWhiteSpace(_configuration.SecretAccessKey) ||
(string.IsNullOrWhiteSpace(_configuration.Region) && string.IsNullOrWhiteSpace(_configuration.ServiceURL)))
{
return;
}
try
{
using var amazonS3Client = await context.ServiceProvider.GetRequiredService<IAmazonS3ClientFactory>()
.GetAmazonS3Client(_configuration);
if (!await AmazonS3Util.DoesS3BucketExistV2Async(amazonS3Client, _actualContainerName))
{
return;
}
await DeleteObjectsAsync(amazonS3Client, _externalContainer ? _externalRunPrefix : null);
if (!_externalContainer)
{
await amazonS3Client.DeleteBucketAsync(_actualContainerName);
}
}
catch
{
// Ignore errors during test cleanup
}
}
if (await AmazonS3Util.DoesS3BucketExistV2Async(amazonS3Client, _randomContainerName))
private async Task DeleteObjectsAsync(AmazonS3Client client, string? prefix)
{
string? continuationToken = null;
do
{
var blobs = await amazonS3Client.ListObjectsAsync(_randomContainerName);
var listResponse = await client.ListObjectsV2Async(new ListObjectsV2Request
{
BucketName = _actualContainerName,
Prefix = prefix,
ContinuationToken = continuationToken,
MaxKeys = 1000
});
if (blobs.S3Objects.Any())
if (listResponse.S3Objects.Any())
{
await amazonS3Client.DeleteObjectsAsync(new DeleteObjectsRequest
foreach (var batch in Chunk(listResponse.S3Objects, 1000))
{
BucketName = _randomContainerName,
Objects = blobs.S3Objects.Select(o => new KeyVersion { Key = o.Key }).ToList()
});
await client.DeleteObjectsAsync(new DeleteObjectsRequest
{
BucketName = _actualContainerName,
Objects = batch.Select(o => new KeyVersion { Key = o.Key }).ToList()
});
}
}
await amazonS3Client.DeleteBucketAsync(_randomContainerName);
continuationToken = listResponse.IsTruncated == true ? listResponse.NextContinuationToken : null;
} while (continuationToken != null);
}
private static IEnumerable<List<S3Object>> Chunk(IEnumerable<S3Object> source, int size)
{
var bucket = new List<S3Object>(size);
foreach (var item in source)
{
bucket.Add(item);
if (bucket.Count == size)
{
yield return bucket;
bucket = new List<S3Object>(size);
}
}
if (bucket.Count > 0)
{
yield return bucket;
}
}
}
internal sealed class TestRunPrefixedAwsBlobNameCalculator : IAwsBlobNameCalculator
{
private readonly ICurrentTenant _currentTenant;
private readonly string _runPrefix;
public TestRunPrefixedAwsBlobNameCalculator(ICurrentTenant currentTenant, string runPrefix)
{
_currentTenant = currentTenant;
_runPrefix = runPrefix;
}
public string Calculate(BlobProviderArgs args)
{
var baseName = _currentTenant.Id == null
? $"host/{args.BlobName}"
: $"tenants/{_currentTenant.Id.Value:D}/{args.BlobName}";
return _runPrefix + baseName;
}
}

26
framework/test/Volo.Abp.BlobStoring.Aws.Tests/Volo/Abp/BlobStoring/Aws/AwsBlobContainer_Tests.cs

@ -1 +1,25 @@
namespace Volo.Abp.BlobStoring.Aws;
namespace Volo.Abp.BlobStoring.Aws;
/*
To run this integration test, populate user secrets (id `9f0d2c00-80c1-435b-bfab-2c39c8249091`)
with credentials for real AWS S3 or an S3-compatible service (Cloudflare R2, MinIO, etc.):
{
"Aws:AccessKeyId": "...",
"Aws:SecretAccessKey": "...",
"Aws:Region": "us-east-1", // "auto" for Cloudflare R2
"Aws:ServiceURL": "", // e.g. https://<account>.r2.cloudflarestorage.com for R2, http://localhost:9000 for MinIO
"Aws:DisablePayloadSigning": "false", // set "true" for R2 (requires HTTPS); leave "false" for MinIO and AWS S3
"Aws:ContainerName": "", // leave empty to let the test create a unique bucket; set to a pre-existing bucket for services that disallow bucket creation
"Aws:CreateContainerIfNotExists": "true" // set "false" when using an existing bucket
}
When `ContainerName` is supplied, the test prefixes every object with `abp-aws-test-run-{guid}/`
so cleanup deletes only what this run created (the bucket itself is left untouched).
Then uncomment the class below:
public class AwsBlobContainer_Tests : BlobContainer_Tests<AbpBlobStoringAwsTestModule>
{
}
*/

72
framework/test/Volo.Abp.BlobStoring.Aws.Tests/Volo/Abp/BlobStoring/Aws/AwsBlobProviderConfiguration_Tests.cs

@ -0,0 +1,72 @@
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);
}
[Fact]
public void Should_Default_DisablePayloadSigning_To_False()
{
var containerConfiguration = new BlobContainerConfiguration();
var awsConfiguration = new AwsBlobProviderConfiguration(containerConfiguration);
awsConfiguration.DisablePayloadSigning.ShouldBeFalse();
}
[Fact]
public void Should_Set_And_Get_DisablePayloadSigning()
{
var containerConfiguration = new BlobContainerConfiguration();
var awsConfiguration = new AwsBlobProviderConfiguration(containerConfiguration);
awsConfiguration.DisablePayloadSigning = true;
awsConfiguration.DisablePayloadSigning.ShouldBeTrue();
}
}

114
framework/test/Volo.Abp.BlobStoring.Aws.Tests/Volo/Abp/BlobStoring/Aws/DefaultAmazonS3ClientFactory_Tests.cs

@ -0,0 +1,114 @@
using System.Threading.Tasks;
using Amazon.Runtime;
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<IAmazonS3ClientFactory>();
}
[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?.TrimEnd('/').ShouldBe(serviceUrl);
((AmazonS3Config)s3Client.Config).ForcePathStyle.ShouldBeTrue();
}
[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
}
[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?.TrimEnd('/').ShouldBe("https://minio.example.com:9000");
((AmazonS3Config)s3Client.Config).ForcePathStyle.ShouldBeTrue();
}
[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?.TrimEnd('/').ShouldBe("https://r2.cloudflarestorage.com");
config.ForcePathStyle.ShouldBeTrue();
config.RequestChecksumCalculation.ShouldBe(RequestChecksumCalculation.WHEN_REQUIRED);
config.ResponseChecksumValidation.ShouldBe(ResponseChecksumValidation.WHEN_REQUIRED);
}
}
Loading…
Cancel
Save