Headless CMS and Content Managment Hub
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

219 lines
7.0 KiB

// ==========================================================================
// 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 AmazonS3Options options;
private TransferUtility transferUtility;
private IAmazonS3 s3Client;
public AmazonS3AssetStore(AmazonS3Options options)
{
Guard.NotNullOrEmpty(options.Bucket);
Guard.NotNullOrEmpty(options.AccessKey);
Guard.NotNullOrEmpty(options.SecretKey);
this.options = options;
}
protected override void DisposeObject(bool disposing)
{
if (disposing)
{
s3Client?.Dispose();
transferUtility?.Dispose();
}
}
public async Task InitializeAsync(CancellationToken ct = default)
{
try
{
var amazonS3Config = new AmazonS3Config { ForcePathStyle = options.ForcePathStyle };
if (!string.IsNullOrWhiteSpace(options.ServiceUrl))
{
amazonS3Config.ServiceURL = options.ServiceUrl;
}
else
{
amazonS3Config.RegionEndpoint = RegionEndpoint.GetBySystemName(options.RegionName);
}
s3Client = new AmazonS3Client(options.AccessKey, options.SecretKey, amazonS3Config);
transferUtility = new TransferUtility(s3Client);
var exists = await s3Client.DoesS3BucketExistAsync(options.Bucket);
if (!exists)
{
throw new ConfigurationException($"Cannot connect to Amazon S3 bucket '{options.Bucket}'.");
}
}
catch (AmazonS3Exception ex)
{
throw new ConfigurationException($"Cannot connect to Amazon S3 bucket '{options.Bucket}'.", 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 = options.Bucket,
SourceKey = GetKey(sourceFileName),
DestinationBucket = options.Bucket,
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, BytesRange range = default, CancellationToken ct = default)
{
Guard.NotNullOrEmpty(fileName);
Guard.NotNull(stream);
try
{
var request = new GetObjectRequest { BucketName = options.Bucket, Key = GetKey(fileName) };
if (range.IsDefined)
{
request.ByteRange = new ByteRange(range.ToString());
}
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
{
Key = GetKey(fileName)
};
ConfigureDefaults(request);
SetStream(stream, request);
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 = options.Bucket, Key = fileName };
await s3Client.DeleteObjectAsync(request);
}
catch (AmazonS3Exception ex) when (ex.StatusCode == HttpStatusCode.NotFound)
{
return;
}
}
private string GetKey(string fileName)
{
if (!string.IsNullOrWhiteSpace(options.BucketFolder))
{
return $"{options.BucketFolder}/{fileName}";
}
else
{
return fileName;
}
}
private async Task EnsureNotExistsAsync(string fileName, CancellationToken ct)
{
try
{
await s3Client.GetObjectAsync(options.Bucket, GetKey(fileName), ct);
}
catch
{
return;
}
throw new AssetAlreadyExistsException(fileName);
}
private void ConfigureDefaults(TransferUtilityUploadRequest request)
{
request.AutoCloseStream = false;
request.BucketName = options.Bucket;
}
private static void SetStream(Stream stream, TransferUtilityUploadRequest request)
{
// Amazon S3 requires a seekable stream, but does not seek anything.
request.InputStream = new SeekFakerStream(stream);
}
}
}