Skip to content

Commit a1aec11

Browse files
Merge pull request #393 from SixLabors/js/feature-client-provider
Fix, cleanup, and optimize new cloud provider code.
2 parents 8509b73 + ce6e448 commit a1aec11

15 files changed

+202
-88
lines changed
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Six Labors Split License.
3+
4+
using Amazon.S3;
5+
6+
namespace SixLabors.ImageSharp.Web;
7+
8+
/// <summary>
9+
/// Represents a scoped Amazon S3 client instance that is explicitly associated with a single S3 bucket.
10+
/// This wrapper provides a strongly-typed link between the client and the bucket it operates on,
11+
/// and optionally manages the lifetime of the underlying <see cref="AmazonS3Client"/>.
12+
/// </summary>
13+
public sealed class AmazonS3BucketClient : IDisposable
14+
{
15+
private readonly bool disposeClient;
16+
17+
/// <summary>
18+
/// Initializes a new instance of the <see cref="AmazonS3BucketClient"/> class.
19+
/// </summary>
20+
/// <param name="bucketName">
21+
/// The bucket name associated with this client instance.
22+
/// </param>
23+
/// <param name="client">
24+
/// The underlying Amazon S3 client instance. This should be an already configured instance of <see cref="AmazonS3Client"/>.
25+
/// </param>
26+
/// <param name="disposeClient">
27+
/// A value indicating whether the underlying client should be disposed when this instance is disposed.
28+
/// </param>
29+
public AmazonS3BucketClient(string bucketName, AmazonS3Client client, bool disposeClient = true)
30+
{
31+
Guard.NotNullOrWhiteSpace(bucketName, nameof(bucketName));
32+
Guard.NotNull(client, nameof(client));
33+
this.BucketName = bucketName;
34+
this.Client = client;
35+
this.disposeClient = disposeClient;
36+
}
37+
38+
/// <summary>
39+
/// Gets the bucket name associated with this client instance.
40+
/// </summary>
41+
public string BucketName { get; }
42+
43+
/// <summary>
44+
/// Gets the underlying Amazon S3 client instance.
45+
/// </summary>
46+
public AmazonS3Client Client { get; }
47+
48+
/// <inheritdoc/>
49+
public void Dispose()
50+
{
51+
if (this.disposeClient)
52+
{
53+
this.Client.Dispose();
54+
}
55+
}
56+
}

src/ImageSharp.Web.Providers.AWS/AmazonS3ClientFactory.cs

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,25 +14,20 @@ internal static class AmazonS3ClientFactory
1414
/// with the same name does not already exist.
1515
/// </summary>
1616
/// <param name="options">The AWS S3 Storage cache options.</param>
17-
/// <param name="serviceProvider">The current service provider.</param>
1817
/// <returns>
1918
/// A new <see cref="AmazonS3Client"/>.
2019
/// </returns>
2120
/// <exception cref="ArgumentException">Invalid configuration.</exception>
22-
public static AmazonS3Client CreateClient(IAWSS3BucketClientOptions options, IServiceProvider serviceProvider)
21+
public static AmazonS3BucketClient CreateClient(IAWSS3BucketClientOptions options)
2322
{
24-
if (options.S3ClientProvider != null)
25-
{
26-
return options.S3ClientProvider(options, serviceProvider);
27-
}
28-
else if (!string.IsNullOrWhiteSpace(options.Endpoint))
23+
if (!string.IsNullOrWhiteSpace(options.Endpoint))
2924
{
3025
// AccessKey can be empty.
3126
// AccessSecret can be empty.
3227
// PathStyle endpoint doesn't support AccelerateEndpoint.
3328
AmazonS3Config config = new() { ServiceURL = options.Endpoint, ForcePathStyle = true, AuthenticationRegion = options.Region };
3429
SetTimeout(config, options.Timeout);
35-
return new AmazonS3Client(options.AccessKey, options.AccessSecret, config);
30+
return new(options.BucketName, new AmazonS3Client(options.AccessKey, options.AccessSecret, config));
3631
}
3732
else if (!string.IsNullOrWhiteSpace(options.AccessKey))
3833
{
@@ -41,14 +36,14 @@ public static AmazonS3Client CreateClient(IAWSS3BucketClientOptions options, ISe
4136
RegionEndpoint region = RegionEndpoint.GetBySystemName(options.Region);
4237
AmazonS3Config config = new() { RegionEndpoint = region, UseAccelerateEndpoint = options.UseAccelerateEndpoint };
4338
SetTimeout(config, options.Timeout);
44-
return new AmazonS3Client(options.AccessKey, options.AccessSecret, config);
39+
return new(options.BucketName, new AmazonS3Client(options.AccessKey, options.AccessSecret, config));
4540
}
4641
else if (!string.IsNullOrWhiteSpace(options.Region))
4742
{
4843
RegionEndpoint region = RegionEndpoint.GetBySystemName(options.Region);
4944
AmazonS3Config config = new() { RegionEndpoint = region, UseAccelerateEndpoint = options.UseAccelerateEndpoint };
5045
SetTimeout(config, options.Timeout);
51-
return new AmazonS3Client(config);
46+
return new(options.BucketName, new AmazonS3Client(config));
5247
}
5348
else
5449
{

src/ImageSharp.Web.Providers.AWS/Caching/AWSS3StorageCache.cs

Lines changed: 44 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,12 @@ namespace SixLabors.ImageSharp.Web.Caching.AWS;
1313
/// <summary>
1414
/// Implements an AWS S3 Storage based cache.
1515
/// </summary>
16-
public class AWSS3StorageCache : IImageCache
16+
public class AWSS3StorageCache : IImageCache, IDisposable
1717
{
18-
private readonly IAmazonS3 amazonS3Client;
18+
private readonly AmazonS3BucketClient amazonS3Client;
1919
private readonly string bucketName;
2020
private readonly string cacheFolder;
21+
private bool isDisposed;
2122

2223
/// <summary>
2324
/// Initializes a new instance of the <see cref="AWSS3StorageCache"/> class.
@@ -28,8 +29,13 @@ public AWSS3StorageCache(IOptions<AWSS3StorageCacheOptions> cacheOptions, IServi
2829
{
2930
Guard.NotNull(cacheOptions, nameof(cacheOptions));
3031
AWSS3StorageCacheOptions options = cacheOptions.Value;
31-
this.bucketName = options.BucketName;
32-
this.amazonS3Client = AmazonS3ClientFactory.CreateClient(options, serviceProvider);
32+
33+
this.amazonS3Client =
34+
options.S3ClientFactory?.Invoke(options, serviceProvider)
35+
?? AmazonS3ClientFactory.CreateClient(options);
36+
37+
this.bucketName = this.amazonS3Client.BucketName;
38+
3339
this.cacheFolder = string.IsNullOrEmpty(options.CacheFolder)
3440
? string.Empty
3541
: options.CacheFolder.Trim().Trim('/') + '/';
@@ -43,8 +49,8 @@ public AWSS3StorageCache(IOptions<AWSS3StorageCacheOptions> cacheOptions, IServi
4349
try
4450
{
4551
// HEAD request throws a 404 if not found.
46-
MetadataCollection metadata = (await this.amazonS3Client.GetObjectMetadataAsync(request)).Metadata;
47-
return new AWSS3StorageCacheResolver(this.amazonS3Client, this.bucketName, keyWithFolder, metadata);
52+
MetadataCollection metadata = (await this.amazonS3Client.Client.GetObjectMetadataAsync(request)).Metadata;
53+
return new AWSS3StorageCacheResolver(this.amazonS3Client.Client, this.bucketName, keyWithFolder, metadata);
4854
}
4955
catch
5056
{
@@ -70,7 +76,7 @@ public Task SetAsync(string key, Stream stream, ImageCacheMetadata metadata)
7076
request.Metadata.Add(d.Key, d.Value);
7177
}
7278

73-
return this.amazonS3Client.PutObjectAsync(request);
79+
return this.amazonS3Client.Client.PutObjectAsync(request);
7480
}
7581

7682
/// <summary>
@@ -84,23 +90,42 @@ public Task SetAsync(string key, Stream stream, ImageCacheMetadata metadata)
8490
/// and object data. <see cref="S3CannedACL.Private"/> specifies that the bucket
8591
/// data is private to the account owner.
8692
/// </param>
87-
/// <param name="serviceProvider">The current service provider.</param>
8893
/// <returns>
8994
/// If the bucket does not already exist, a <see cref="PutBucketResponse"/> describing the newly
9095
/// created bucket. If the container already exists, <see langword="null"/>.
9196
/// </returns>
92-
public static PutBucketResponse? CreateIfNotExists(
93-
AWSS3StorageCacheOptions options,
94-
S3CannedACL acl,
95-
IServiceProvider serviceProvider)
96-
=> AsyncHelper.RunSync(() => CreateIfNotExistsAsync(options, acl, serviceProvider));
97-
98-
private static async Task<PutBucketResponse?> CreateIfNotExistsAsync(
99-
AWSS3StorageCacheOptions options,
100-
S3CannedACL acl,
101-
IServiceProvider serviceProvider)
97+
public static PutBucketResponse? CreateIfNotExists(AWSS3StorageCacheOptions options, S3CannedACL acl)
98+
=> AsyncHelper.RunSync(() => CreateIfNotExistsAsync(options, acl));
99+
100+
/// <summary>
101+
/// Releases the unmanaged resources used by the <see cref="AWSS3StorageCache"/> and optionally releases the managed resources.
102+
/// </summary>
103+
/// <param name="disposing">true to release both managed and unmanaged resources; false to release only unmanaged resources.</param>
104+
protected virtual void Dispose(bool disposing)
105+
{
106+
if (!this.isDisposed)
107+
{
108+
if (disposing)
109+
{
110+
this.amazonS3Client?.Dispose();
111+
}
112+
113+
this.isDisposed = true;
114+
}
115+
}
116+
117+
/// <inheritdoc/>
118+
public void Dispose()
119+
{
120+
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
121+
this.Dispose(disposing: true);
122+
GC.SuppressFinalize(this);
123+
}
124+
125+
private static async Task<PutBucketResponse?> CreateIfNotExistsAsync(AWSS3StorageCacheOptions options, S3CannedACL acl)
102126
{
103-
AmazonS3Client client = AmazonS3ClientFactory.CreateClient(options, serviceProvider);
127+
using AmazonS3BucketClient bucketClient = AmazonS3ClientFactory.CreateClient(options);
128+
AmazonS3Client client = bucketClient.Client;
104129

105130
bool foundBucket = false;
106131
ListBucketsResponse listBucketsResponse = await client.ListBucketsAsync();

src/ImageSharp.Web.Providers.AWS/Caching/AWSS3StorageCacheOptions.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
// Copyright (c) Six Labors.
22
// Licensed under the Six Labors Split License.
33

4-
using Amazon.S3;
5-
64
namespace SixLabors.ImageSharp.Web.Caching.AWS;
75

86
/// <summary>
@@ -11,7 +9,7 @@ namespace SixLabors.ImageSharp.Web.Caching.AWS;
119
public class AWSS3StorageCacheOptions : IAWSS3BucketClientOptions
1210
{
1311
/// <inheritdoc/>
14-
public Func<IAWSS3BucketClientOptions, IServiceProvider, AmazonS3Client>? S3ClientProvider { get; set; } = null!;
12+
public Func<IAWSS3BucketClientOptions, IServiceProvider, AmazonS3BucketClient>? S3ClientFactory { get; set; }
1513

1614
/// <inheritdoc/>
1715
public string? Region { get; set; }
Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
// Copyright (c) Six Labors.
22
// Licensed under the Six Labors Split License.
33

4-
using Amazon.S3;
5-
64
namespace SixLabors.ImageSharp.Web;
75

86
/// <summary>
@@ -11,49 +9,54 @@ namespace SixLabors.ImageSharp.Web;
119
public interface IAWSS3BucketClientOptions
1210
{
1311
/// <summary>
14-
/// Gets or sets a custom Azure AmazonS3Client provider
12+
/// Gets or sets a custom factory method to create an <see cref="AmazonS3BucketClient"/>.
1513
/// </summary>
16-
Func<IAWSS3BucketClientOptions, IServiceProvider, AmazonS3Client>? S3ClientProvider { get; set; }
14+
public Func<IAWSS3BucketClientOptions, IServiceProvider, AmazonS3BucketClient>? S3ClientFactory { get; set; }
1715

1816
/// <summary>
1917
/// Gets or sets the AWS region endpoint (us-east-1/us-west-1/ap-southeast-2).
2018
/// </summary>
21-
string? Region { get; set; }
19+
public string? Region { get; set; }
2220

2321
/// <summary>
2422
/// Gets or sets the AWS bucket name.
23+
/// Cannot be <see langword="null"/> when <see cref="S3ClientFactory"/> is not set.
2524
/// </summary>
26-
string BucketName { get; set; }
25+
public string BucketName { get; set; }
2726

2827
/// <summary>
2928
/// Gets or sets the AWS key - Can be used to override keys provided by the environment.
3029
/// If deploying inside an EC2 instance AWS keys will already be available via environment
31-
/// variables and don't need to be specified. Follow AWS best security practices on <see href="https://docs.aws.amazon.com/general/latest/gr/aws-access-keys-best-practices.html"/>.
30+
/// variables and don't need to be specified. Follow AWS best security practices on
31+
/// <see href="https://docs.aws.amazon.com/general/latest/gr/aws-access-keys-best-practices.html"/>.
3232
/// </summary>
33-
string? AccessKey { get; set; }
33+
public string? AccessKey { get; set; }
3434

3535
/// <summary>
3636
/// Gets or sets the AWS endpoint - used to override the default service endpoint.
3737
/// If deploying inside an EC2 instance AWS keys will already be available via environment
38-
/// variables and don't need to be specified. Follow AWS best security practices on <see href="https://docs.aws.amazon.com/general/latest/gr/aws-access-keys-best-practices.html"/>.
38+
/// variables and don't need to be specified. Follow AWS best security practices on
39+
/// <see href="https://docs.aws.amazon.com/general/latest/gr/aws-access-keys-best-practices.html"/>.
3940
/// </summary>
40-
string? AccessSecret { get; set; }
41+
public string? AccessSecret { get; set; }
4142

4243
/// <summary>
4344
/// Gets or sets the AWS endpoint - used for testing to over region endpoint allowing it
4445
/// to be set to localhost.
4546
/// </summary>
46-
string? Endpoint { get; set; }
47+
public string? Endpoint { get; set; }
4748

4849
/// <summary>
4950
/// Gets or sets a value indicating whether the S3 accelerate endpoint is used.
50-
/// The feature must be enabled on the bucket. Follow AWS instruction on <see href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/transfer-acceleration.html"/>.
51+
/// The feature must be enabled on the bucket. Follow AWS instruction on
52+
/// <see href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/transfer-acceleration.html"/>.
5153
/// </summary>
52-
bool UseAccelerateEndpoint { get; set; }
54+
public bool UseAccelerateEndpoint { get; set; }
5355

5456
/// <summary>
5557
/// Gets or sets a value indicating the timeout for the S3 client.
56-
/// If the value is set, the value is assigned to the Timeout property of the HttpWebRequest/HttpClient object used to send requests.
58+
/// If the value is set, the value is assigned to the Timeout property of the HttpWebRequest/HttpClient
59+
/// object used to send requests.
5760
/// </summary>
58-
TimeSpan? Timeout { get; set; }
61+
public TimeSpan? Timeout { get; set; }
5962
}

0 commit comments

Comments
 (0)