mirror of https://github.com/Squidex/squidex.git
committed by
GitHub
10 changed files with 349 additions and 6 deletions
@ -0,0 +1,202 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.IO; |
||||
|
using System.Net; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
using Amazon; |
||||
|
using Amazon.S3; |
||||
|
using Amazon.S3.Model; |
||||
|
using Amazon.S3.Transfer; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.Assets |
||||
|
{ |
||||
|
public sealed class AmazonS3AssetStore : DisposableObjectBase, IAssetStore, IInitializable |
||||
|
{ |
||||
|
private const int BufferSize = 81920; |
||||
|
private readonly string accessKey; |
||||
|
private readonly string secretKey; |
||||
|
private readonly string bucketName; |
||||
|
private readonly string? bucketFolder; |
||||
|
private readonly RegionEndpoint bucketRegion; |
||||
|
private TransferUtility transferUtility; |
||||
|
private IAmazonS3 s3Client; |
||||
|
|
||||
|
public AmazonS3AssetStore(string regionName, string bucketName, string? bucketFolder, string accessKey, string secretKey) |
||||
|
{ |
||||
|
Guard.NotNullOrEmpty(bucketName); |
||||
|
Guard.NotNullOrEmpty(accessKey); |
||||
|
Guard.NotNullOrEmpty(secretKey); |
||||
|
|
||||
|
this.bucketName = bucketName; |
||||
|
this.bucketFolder = bucketFolder; |
||||
|
this.accessKey = accessKey; |
||||
|
this.secretKey = secretKey; |
||||
|
|
||||
|
bucketRegion = RegionEndpoint.GetBySystemName(regionName); |
||||
|
} |
||||
|
|
||||
|
protected override void DisposeObject(bool disposing) |
||||
|
{ |
||||
|
if (disposing) |
||||
|
{ |
||||
|
s3Client?.Dispose(); |
||||
|
|
||||
|
transferUtility?.Dispose(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public async Task InitializeAsync(CancellationToken ct = default) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
s3Client = new AmazonS3Client( |
||||
|
accessKey, |
||||
|
secretKey, |
||||
|
bucketRegion); |
||||
|
|
||||
|
transferUtility = new TransferUtility(s3Client); |
||||
|
|
||||
|
var exists = await s3Client.DoesS3BucketExistAsync(bucketName); |
||||
|
|
||||
|
if (!exists) |
||||
|
{ |
||||
|
throw new ConfigurationException($"Cannot connect to Amazon S3 bucket '${bucketName}'."); |
||||
|
} |
||||
|
} |
||||
|
catch (AmazonS3Exception ex) |
||||
|
{ |
||||
|
throw new ConfigurationException($"Cannot connect to Amazon S3 bucket '${bucketName}'.", ex); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public string? GeneratePublicUrl(string fileName) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
public async Task CopyAsync(string sourceFileName, string targetFileName, CancellationToken ct = default) |
||||
|
{ |
||||
|
Guard.NotNullOrEmpty(sourceFileName); |
||||
|
Guard.NotNullOrEmpty(targetFileName); |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
await EnsureNotExistsAsync(targetFileName, ct); |
||||
|
|
||||
|
var request = new CopyObjectRequest |
||||
|
{ |
||||
|
SourceBucket = bucketName, |
||||
|
SourceKey = GetKey(sourceFileName), |
||||
|
DestinationBucket = bucketName, |
||||
|
DestinationKey = GetKey(targetFileName) |
||||
|
}; |
||||
|
|
||||
|
await s3Client.CopyObjectAsync(request, ct); |
||||
|
} |
||||
|
catch (AmazonS3Exception ex) when (ex.StatusCode == HttpStatusCode.NotFound) |
||||
|
{ |
||||
|
throw new AssetNotFoundException(sourceFileName, ex); |
||||
|
} |
||||
|
catch (AmazonS3Exception ex) when (ex.StatusCode == HttpStatusCode.PreconditionFailed) |
||||
|
{ |
||||
|
throw new AssetAlreadyExistsException(targetFileName); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public async Task DownloadAsync(string fileName, Stream stream, CancellationToken ct = default) |
||||
|
{ |
||||
|
Guard.NotNullOrEmpty(fileName); |
||||
|
Guard.NotNull(stream); |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
var request = new GetObjectRequest { BucketName = bucketName, Key = GetKey(fileName) }; |
||||
|
|
||||
|
using (var response = await s3Client.GetObjectAsync(request, ct)) |
||||
|
{ |
||||
|
await response.ResponseStream.CopyToAsync(stream, BufferSize, ct); |
||||
|
} |
||||
|
} |
||||
|
catch (AmazonS3Exception ex) when (ex.StatusCode == HttpStatusCode.NotFound) |
||||
|
{ |
||||
|
throw new AssetNotFoundException(fileName, ex); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public async Task UploadAsync(string fileName, Stream stream, bool overwrite = false, CancellationToken ct = default) |
||||
|
{ |
||||
|
Guard.NotNullOrEmpty(fileName); |
||||
|
Guard.NotNull(stream); |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
if (!overwrite) |
||||
|
{ |
||||
|
await EnsureNotExistsAsync(fileName, ct); |
||||
|
} |
||||
|
|
||||
|
var request = new TransferUtilityUploadRequest |
||||
|
{ |
||||
|
AutoCloseStream = false, |
||||
|
BucketName = bucketName, |
||||
|
InputStream = stream, |
||||
|
Key = GetKey(fileName) |
||||
|
}; |
||||
|
|
||||
|
await transferUtility.UploadAsync(request, ct); |
||||
|
} |
||||
|
catch (AmazonS3Exception ex) when (ex.StatusCode == HttpStatusCode.PreconditionFailed) |
||||
|
{ |
||||
|
throw new AssetAlreadyExistsException(fileName); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public async Task DeleteAsync(string fileName) |
||||
|
{ |
||||
|
Guard.NotNullOrEmpty(fileName); |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
var request = new DeleteObjectRequest { BucketName = bucketName, Key = fileName }; |
||||
|
|
||||
|
await s3Client.DeleteObjectAsync(request); |
||||
|
} |
||||
|
catch (AmazonS3Exception ex) when (ex.StatusCode == HttpStatusCode.NotFound) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private string GetKey(string fileName) |
||||
|
{ |
||||
|
if (!string.IsNullOrWhiteSpace(bucketFolder)) |
||||
|
{ |
||||
|
return $"{bucketFolder}/{fileName}"; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
return fileName; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private async Task EnsureNotExistsAsync(string fileName, CancellationToken ct) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
await s3Client.GetObjectAsync(bucketName, GetKey(fileName), ct); |
||||
|
} |
||||
|
catch |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
throw new AssetAlreadyExistsException(fileName); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,22 @@ |
|||||
|
<Project Sdk="Microsoft.NET.Sdk"> |
||||
|
<PropertyGroup> |
||||
|
<TargetFramework>netcoreapp3.0</TargetFramework> |
||||
|
<RootNamespace>Squidex.Infrastructure</RootNamespace> |
||||
|
<LangVersion>8.0</LangVersion> |
||||
|
<Nullable>enable</Nullable> |
||||
|
</PropertyGroup> |
||||
|
<ItemGroup> |
||||
|
<PackageReference Include="AWSSDK.S3" Version="3.3.106.4" /> |
||||
|
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> |
||||
|
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" /> |
||||
|
</ItemGroup> |
||||
|
<ItemGroup> |
||||
|
<ProjectReference Include="..\Squidex.Infrastructure\Squidex.Infrastructure.csproj" /> |
||||
|
</ItemGroup> |
||||
|
<PropertyGroup> |
||||
|
<CodeAnalysisRuleSet>..\..\Squidex.ruleset</CodeAnalysisRuleSet> |
||||
|
</PropertyGroup> |
||||
|
<ItemGroup> |
||||
|
<AdditionalFiles Include="..\..\stylecop.json" Link="stylecop.json" /> |
||||
|
</ItemGroup> |
||||
|
</Project> |
||||
@ -0,0 +1,20 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
namespace Squidex.Infrastructure.Assets |
||||
|
{ |
||||
|
public sealed class AmazonS3AssetStoreFixture |
||||
|
{ |
||||
|
public AmazonS3AssetStore AssetStore { get; } |
||||
|
|
||||
|
public AmazonS3AssetStoreFixture() |
||||
|
{ |
||||
|
AssetStore = new AmazonS3AssetStore("eu-central-1", "squidex-test", "squidex-assets", "secret", "secret"); |
||||
|
AssetStore.InitializeAsync().Wait(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,44 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Threading.Tasks; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.Assets |
||||
|
{ |
||||
|
[Trait("Category", "Dependencies")] |
||||
|
public class AmazonS3AssetStoreTests : AssetStoreTests<AmazonS3AssetStore>, IClassFixture<AmazonS3AssetStoreFixture> |
||||
|
{ |
||||
|
private readonly AmazonS3AssetStoreFixture fixture; |
||||
|
|
||||
|
public AmazonS3AssetStoreTests(AmazonS3AssetStoreFixture fixture) |
||||
|
{ |
||||
|
this.fixture = fixture; |
||||
|
} |
||||
|
|
||||
|
public override AmazonS3AssetStore CreateStore() |
||||
|
{ |
||||
|
return fixture.AssetStore; |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_throw_exception_for_invalid_config() |
||||
|
{ |
||||
|
var sut = new AmazonS3AssetStore("invalid", "invalid", null, "invalid", "invalid"); |
||||
|
|
||||
|
await Assert.ThrowsAsync<ConfigurationException>(() => sut.InitializeAsync()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_calculate_source_url() |
||||
|
{ |
||||
|
var url = Sut.GeneratePublicUrl(FileName); |
||||
|
|
||||
|
Assert.Null(url); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue