forked from SixLabors/ImageSharp.Web
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathAWSS3StorageImageProvider.cs
More file actions
185 lines (152 loc) · 5.94 KB
/
AWSS3StorageImageProvider.cs
File metadata and controls
185 lines (152 loc) · 5.94 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using Amazon.S3;
using Amazon.S3.Model;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.Extensions.Options;
using SixLabors.ImageSharp.Web.Resolvers;
using SixLabors.ImageSharp.Web.Resolvers.AWS;
namespace SixLabors.ImageSharp.Web.Providers.AWS;
/// <summary>
/// Returns images stored in AWS S3.
/// </summary>
public class AWSS3StorageImageProvider : IImageProvider
{
/// <summary>
/// Character array to remove from paths.
/// </summary>
private static readonly char[] SlashChars = { '\\', '/' };
/// <summary>
/// The containers for the blob services.
/// </summary>
private readonly Dictionary<string, AmazonS3Client> buckets
= new();
private readonly AWSS3StorageImageProviderOptions storageOptions;
private Func<HttpContext, bool>? match;
/// <summary>
/// Contains various helper methods based on the current configuration.
/// </summary>
private readonly FormatUtilities formatUtilities;
/// <summary>
/// Initializes a new instance of the <see cref="AWSS3StorageImageProvider"/> class.
/// </summary>
/// <param name="storageOptions">The S3 storage options</param>
/// <param name="formatUtilities">Contains various format helper methods based on the current configuration.</param>
/// <param name="serviceProvider">The current service provider.</param>
public AWSS3StorageImageProvider(IOptions<AWSS3StorageImageProviderOptions> storageOptions, FormatUtilities formatUtilities, IServiceProvider serviceProvider)
{
Guard.NotNull(storageOptions, nameof(storageOptions));
this.storageOptions = storageOptions.Value;
this.formatUtilities = formatUtilities;
foreach (AWSS3BucketClientOptions bucket in this.storageOptions.S3Buckets)
{
this.buckets.Add(bucket.BucketName, AmazonS3ClientFactory.CreateClient(bucket, serviceProvider));
}
}
/// <inheritdoc/>
public ProcessingBehavior ProcessingBehavior { get; } = ProcessingBehavior.All;
/// <inheritdoc />
public Func<HttpContext, bool> Match
{
get => this.match ?? this.IsMatch;
set => this.match = value;
}
/// <inheritdoc />
public bool IsValidRequest(HttpContext context)
=> this.formatUtilities.TryGetExtensionFromUri(context.Request.GetDisplayUrl(), out _);
/// <inheritdoc />
public async Task<IImageResolver?> GetAsync(HttpContext context)
{
// Strip the leading slash and bucket name from the HTTP request path and treat
// the remaining path string as the key.
// Path has already been correctly parsed before here.
string bucketName = string.Empty;
IAmazonS3? s3Client = null;
// We want an exact match here to ensure that bucket names starting with
// the same prefix are not mixed up.
string? path = context.Request.Path.Value?.TrimStart(SlashChars);
if (path is null)
{
return null;
}
int index = path.IndexOfAny(SlashChars);
string nameToMatch = index != -1 ? path.Substring(0, index) : path;
foreach (string k in this.buckets.Keys)
{
if (nameToMatch.Equals(k, StringComparison.OrdinalIgnoreCase))
{
bucketName = k;
s3Client = this.buckets[k];
break;
}
}
// Something has gone horribly wrong for this to happen but check anyway.
if (s3Client is null)
{
return null;
}
// Key should be the remaining path string.
string key = path.Substring(bucketName.Length).TrimStart(SlashChars);
if (string.IsNullOrWhiteSpace(key))
{
return null;
}
KeyExistsResult keyExists = await KeyExists(s3Client, bucketName, key);
if (!keyExists.Exists)
{
return null;
}
return new AWSS3StorageImageResolver(s3Client, bucketName, key, keyExists.Metadata);
}
private bool IsMatch(HttpContext context)
{
// Only match loosly here for performance.
// Path matching conflicts should be dealt with by configuration.
string? path = context.Request.Path.Value?.TrimStart(SlashChars);
if (path is null)
{
return false;
}
foreach (string bucket in this.buckets.Keys)
{
if (path.StartsWith(bucket, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}
// ref https://github.com/aws/aws-sdk-net/blob/master/sdk/src/Services/S3/Custom/_bcl/IO/S3FileInfo.cs#L118
private static async Task<KeyExistsResult> KeyExists(IAmazonS3 s3Client, string bucketName, string key)
{
try
{
GetObjectMetadataRequest request = new() { BucketName = bucketName, Key = key };
// If the object doesn't exist then a "NotFound" will be thrown
GetObjectMetadataResponse metadata = await s3Client.GetObjectMetadataAsync(request);
return new KeyExistsResult(metadata);
}
catch (AmazonS3Exception e)
{
if (string.Equals(e.ErrorCode, "NoSuchBucket", StringComparison.Ordinal))
{
return default;
}
if (string.Equals(e.ErrorCode, "NotFound", StringComparison.Ordinal))
{
return default;
}
// If the object exists but the client is not authorized to access it, then a "Forbidden" will be thrown.
if (string.Equals(e.ErrorCode, "Forbidden", StringComparison.Ordinal))
{
return default;
}
throw;
}
}
private readonly record struct KeyExistsResult(GetObjectMetadataResponse Metadata)
{
public bool Exists => this.Metadata is not null;
}
}