Skip to content

Commit 4fa5aad

Browse files
Fix memory leak in AWS library.
1 parent e06dac5 commit 4fa5aad

File tree

8 files changed

+84
-30
lines changed

8 files changed

+84
-30
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: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ internal static class AmazonS3ClientFactory
1818
/// A new <see cref="AmazonS3Client"/>.
1919
/// </returns>
2020
/// <exception cref="ArgumentException">Invalid configuration.</exception>
21-
public static AmazonS3Client CreateClient(IAWSS3BucketClientOptions options)
21+
public static AmazonS3BucketClient CreateClient(IAWSS3BucketClientOptions options)
2222
{
2323
if (!string.IsNullOrWhiteSpace(options.Endpoint))
2424
{
@@ -27,7 +27,7 @@ public static AmazonS3Client CreateClient(IAWSS3BucketClientOptions options)
2727
// PathStyle endpoint doesn't support AccelerateEndpoint.
2828
AmazonS3Config config = new() { ServiceURL = options.Endpoint, ForcePathStyle = true, AuthenticationRegion = options.Region };
2929
SetTimeout(config, options.Timeout);
30-
return new AmazonS3Client(options.AccessKey, options.AccessSecret, config);
30+
return new(options.BucketName, new AmazonS3Client(options.AccessKey, options.AccessSecret, config));
3131
}
3232
else if (!string.IsNullOrWhiteSpace(options.AccessKey))
3333
{
@@ -36,14 +36,14 @@ public static AmazonS3Client CreateClient(IAWSS3BucketClientOptions options)
3636
RegionEndpoint region = RegionEndpoint.GetBySystemName(options.Region);
3737
AmazonS3Config config = new() { RegionEndpoint = region, UseAccelerateEndpoint = options.UseAccelerateEndpoint };
3838
SetTimeout(config, options.Timeout);
39-
return new AmazonS3Client(options.AccessKey, options.AccessSecret, config);
39+
return new(options.BucketName, new AmazonS3Client(options.AccessKey, options.AccessSecret, config));
4040
}
4141
else if (!string.IsNullOrWhiteSpace(options.Region))
4242
{
4343
RegionEndpoint region = RegionEndpoint.GetBySystemName(options.Region);
4444
AmazonS3Config config = new() { RegionEndpoint = region, UseAccelerateEndpoint = options.UseAccelerateEndpoint };
4545
SetTimeout(config, options.Timeout);
46-
return new AmazonS3Client(config);
46+
return new(options.BucketName, new AmazonS3Client(config));
4747
}
4848
else
4949
{

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

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ namespace SixLabors.ImageSharp.Web.Caching.AWS;
1515
/// </summary>
1616
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;
2121
private bool isDisposed;
@@ -29,12 +29,13 @@ public AWSS3StorageCache(IOptions<AWSS3StorageCacheOptions> cacheOptions, IServi
2929
{
3030
Guard.NotNull(cacheOptions, nameof(cacheOptions));
3131
AWSS3StorageCacheOptions options = cacheOptions.Value;
32-
this.bucketName = options.BucketName;
3332

3433
this.amazonS3Client =
3534
options.S3ClientFactory?.Invoke(options, serviceProvider)
3635
?? AmazonS3ClientFactory.CreateClient(options);
3736

37+
this.bucketName = this.amazonS3Client.BucketName;
38+
3839
this.cacheFolder = string.IsNullOrEmpty(options.CacheFolder)
3940
? string.Empty
4041
: options.CacheFolder.Trim().Trim('/') + '/';
@@ -48,8 +49,8 @@ public AWSS3StorageCache(IOptions<AWSS3StorageCacheOptions> cacheOptions, IServi
4849
try
4950
{
5051
// HEAD request throws a 404 if not found.
51-
MetadataCollection metadata = (await this.amazonS3Client.GetObjectMetadataAsync(request)).Metadata;
52-
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);
5354
}
5455
catch
5556
{
@@ -75,7 +76,7 @@ public Task SetAsync(string key, Stream stream, ImageCacheMetadata metadata)
7576
request.Metadata.Add(d.Key, d.Value);
7677
}
7778

78-
return this.amazonS3Client.PutObjectAsync(request);
79+
return this.amazonS3Client.Client.PutObjectAsync(request);
7980
}
8081

8182
/// <summary>
@@ -123,7 +124,8 @@ public void Dispose()
123124

124125
private static async Task<PutBucketResponse?> CreateIfNotExistsAsync(AWSS3StorageCacheOptions options, S3CannedACL acl)
125126
{
126-
AmazonS3Client client = AmazonS3ClientFactory.CreateClient(options);
127+
using AmazonS3BucketClient bucketClient = AmazonS3ClientFactory.CreateClient(options);
128+
AmazonS3Client client = bucketClient.Client;
127129

128130
bool foundBucket = false;
129131
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>? S3ClientFactory { get; set; }
12+
public Func<IAWSS3BucketClientOptions, IServiceProvider, AmazonS3BucketClient>? S3ClientFactory { get; set; }
1513

1614
/// <inheritdoc/>
1715
public string? Region { get; set; }

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,16 @@ public interface IAWSS3BucketClientOptions
1313
/// <summary>
1414
/// Gets or sets a custom a factory method to create an <see cref="AmazonS3Client"/>.
1515
/// </summary>
16-
public Func<IAWSS3BucketClientOptions, IServiceProvider, AmazonS3Client>? S3ClientFactory { get; set; }
16+
public Func<IAWSS3BucketClientOptions, IServiceProvider, AmazonS3BucketClient>? S3ClientFactory { get; set; }
1717

1818
/// <summary>
1919
/// Gets or sets the AWS region endpoint (us-east-1/us-west-1/ap-southeast-2).
2020
/// </summary>
2121
public string? Region { get; set; }
2222

2323
/// <summary>
24-
/// Gets or sets the AWS bucket name. Cannot be <see langword="null"/>.
24+
/// Gets or sets the AWS bucket name.
25+
/// Cannot be <see langword="null"/> when <see cref="S3ClientFactory"/> is not set.
2526
/// </summary>
2627
public string BucketName { get; set; }
2728

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

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public class AWSS3StorageImageProvider : IImageProvider, IDisposable
2424
/// <summary>
2525
/// The containers for the blob services.
2626
/// </summary>
27-
private readonly Dictionary<string, AmazonS3Client> buckets
27+
private readonly Dictionary<string, AmazonS3BucketClient> buckets
2828
= new();
2929

3030
private readonly AWSS3StorageImageProviderOptions storageOptions;
@@ -55,11 +55,11 @@ public AWSS3StorageImageProvider(
5555

5656
foreach (AWSS3BucketClientOptions bucket in this.storageOptions.S3Buckets)
5757
{
58-
AmazonS3Client s3Client =
58+
AmazonS3BucketClient s3Client =
5959
bucket.S3ClientFactory?.Invoke(bucket, serviceProvider)
6060
?? AmazonS3ClientFactory.CreateClient(bucket);
6161

62-
this.buckets.Add(bucket.BucketName, s3Client);
62+
this.buckets.Add(s3Client.BucketName, s3Client);
6363
}
6464
}
6565

@@ -84,7 +84,7 @@ public bool IsValidRequest(HttpContext context)
8484
// the remaining path string as the key.
8585
// Path has already been correctly parsed before here.
8686
string bucketName = string.Empty;
87-
IAmazonS3? s3Client = null;
87+
AmazonS3Client? s3Client = null;
8888

8989
// We want an exact match here to ensure that bucket names starting with
9090
// the same prefix are not mixed up.
@@ -103,7 +103,7 @@ public bool IsValidRequest(HttpContext context)
103103
if (nameToMatch.Equals(k, StringComparison.OrdinalIgnoreCase))
104104
{
105105
bucketName = k;
106-
s3Client = this.buckets[k];
106+
s3Client = this.buckets[k].Client;
107107
break;
108108
}
109109
}
@@ -190,14 +190,13 @@ private static async Task<KeyExistsResult> KeyExists(IAmazonS3 s3Client, string
190190
/// Releases the unmanaged resources used by the <see cref="AWSS3StorageImageProvider"/> and optionally releases the managed resources.
191191
/// </summary>
192192
/// <param name="disposing">true to release both managed and unmanaged resources; false to release only unmanaged resources.</param>
193-
194193
protected virtual void Dispose(bool disposing)
195194
{
196195
if (!this.isDisposed)
197196
{
198197
if (disposing)
199198
{
200-
foreach (AmazonS3Client client in this.buckets.Values)
199+
foreach (AmazonS3BucketClient client in this.buckets.Values)
201200
{
202201
client?.Dispose();
203202
}

src/ImageSharp.Web.Providers.AWS/Providers/AWSS3StorageImageProviderOptions.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.Providers.AWS;
75

86
/// <summary>
@@ -22,7 +20,7 @@ public class AWSS3StorageImageProviderOptions
2220
public class AWSS3BucketClientOptions : IAWSS3BucketClientOptions
2321
{
2422
/// <inheritdoc/>
25-
public Func<IAWSS3BucketClientOptions, IServiceProvider, AmazonS3Client>? S3ClientFactory { get; set; }
23+
public Func<IAWSS3BucketClientOptions, IServiceProvider, AmazonS3BucketClient>? S3ClientFactory { get; set; }
2624

2725
/// <inheritdoc/>
2826
public string? Region { get; set; }

tests/ImageSharp.Web.Tests/TestUtilities/AWSS3StorageImageProviderFactory.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ private static async Task InitializeAWSStorageAsync(IServiceProvider services, A
2727
{
2828
// Upload an image to the AWS Test Storage;
2929
AWSS3BucketClientOptions bucketOptions = options.S3Buckets.First();
30-
AmazonS3Client amazonS3Client = AmazonS3ClientFactory.CreateClient(bucketOptions);
31-
ListBucketsResponse listBucketsResponse = await amazonS3Client.ListBucketsAsync();
30+
using AmazonS3BucketClient amazonS3Client = AmazonS3ClientFactory.CreateClient(bucketOptions);
31+
ListBucketsResponse listBucketsResponse = await amazonS3Client.Client.ListBucketsAsync();
3232

3333
bool foundBucket = false;
3434
foreach (S3Bucket b in listBucketsResponse.Buckets)
@@ -51,7 +51,7 @@ private static async Task InitializeAWSStorageAsync(IServiceProvider services, A
5151
CannedACL = S3CannedACL.PublicRead
5252
};
5353

54-
await amazonS3Client.PutBucketAsync(putBucketRequest);
54+
await amazonS3Client.Client.PutBucketAsync(putBucketRequest);
5555
}
5656
catch (AmazonS3Exception e)
5757
{
@@ -76,7 +76,7 @@ private static async Task InitializeAWSStorageAsync(IServiceProvider services, A
7676
Key = TestConstants.ImagePath
7777
};
7878

79-
await amazonS3Client.GetObjectAsync(request);
79+
await amazonS3Client.Client.GetObjectAsync(request);
8080
}
8181
catch
8282
{
@@ -105,7 +105,7 @@ private static async Task InitializeAWSStorageAsync(IServiceProvider services, A
105105
UseChunkEncoding = false,
106106
};
107107

108-
await amazonS3Client.PutObjectAsync(putRequest);
108+
await amazonS3Client.Client.PutObjectAsync(putRequest);
109109
}
110110
}
111111
}

0 commit comments

Comments
 (0)