mirror of https://github.com/Squidex/squidex.git
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
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);
|
|
}
|
|
}
|
|
}
|