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