mirror of https://github.com/Squidex/squidex.git
6 changed files with 0 additions and 579 deletions
@ -1,278 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// 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.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, nameof(options.Bucket)); |
|
||||
Guard.NotNullOrEmpty(options.AccessKey, nameof(options.AccessKey)); |
|
||||
Guard.NotNullOrEmpty(options.SecretKey, nameof(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<long> GetSizeAsync(string fileName, CancellationToken ct = default) |
|
||||
{ |
|
||||
var key = GetKey(fileName, nameof(fileName)); |
|
||||
|
|
||||
try |
|
||||
{ |
|
||||
var request = new GetObjectMetadataRequest |
|
||||
{ |
|
||||
BucketName = options.Bucket, |
|
||||
Key = key |
|
||||
}; |
|
||||
|
|
||||
var metadata = await s3Client.GetObjectMetadataAsync(request, ct); |
|
||||
|
|
||||
return metadata.ContentLength; |
|
||||
} |
|
||||
catch (AmazonS3Exception ex) when (ex.StatusCode == HttpStatusCode.NotFound) |
|
||||
{ |
|
||||
throw new AssetNotFoundException(fileName, ex); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public async Task CopyAsync(string sourceFileName, string targetFileName, CancellationToken ct = default) |
|
||||
{ |
|
||||
var sourceKey = GetKey(sourceFileName, nameof(sourceFileName)); |
|
||||
var targetKey = GetKey(targetFileName, nameof(targetFileName)); |
|
||||
|
|
||||
try |
|
||||
{ |
|
||||
await EnsureNotExistsAsync(targetKey, targetFileName, ct); |
|
||||
|
|
||||
var request = new CopyObjectRequest |
|
||||
{ |
|
||||
SourceBucket = options.Bucket, |
|
||||
SourceKey = sourceKey, |
|
||||
DestinationBucket = options.Bucket, |
|
||||
DestinationKey = targetKey |
|
||||
}; |
|
||||
|
|
||||
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.NotNull(stream, nameof(stream)); |
|
||||
|
|
||||
var key = GetKey(fileName, nameof(fileName)); |
|
||||
|
|
||||
try |
|
||||
{ |
|
||||
var request = new GetObjectRequest |
|
||||
{ |
|
||||
BucketName = options.Bucket, |
|
||||
Key = key |
|
||||
}; |
|
||||
|
|
||||
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.NotNull(stream, nameof(stream)); |
|
||||
|
|
||||
var key = GetKey(fileName, nameof(fileName)); |
|
||||
|
|
||||
try |
|
||||
{ |
|
||||
if (!overwrite) |
|
||||
{ |
|
||||
await EnsureNotExistsAsync(key, fileName, ct); |
|
||||
} |
|
||||
|
|
||||
var request = new TransferUtilityUploadRequest |
|
||||
{ |
|
||||
BucketName = options.Bucket, |
|
||||
Key = key |
|
||||
}; |
|
||||
|
|
||||
if (!HasContentLength(stream)) |
|
||||
{ |
|
||||
var tempFileName = Path.GetTempFileName(); |
|
||||
|
|
||||
var tempStream = new FileStream(tempFileName, |
|
||||
FileMode.Create, |
|
||||
FileAccess.ReadWrite, |
|
||||
FileShare.Delete, 1024 * 16, |
|
||||
FileOptions.Asynchronous | |
|
||||
FileOptions.DeleteOnClose | |
|
||||
FileOptions.SequentialScan); |
|
||||
|
|
||||
using (tempStream) |
|
||||
{ |
|
||||
await stream.CopyToAsync(tempStream, ct); |
|
||||
|
|
||||
request.InputStream = tempStream; |
|
||||
|
|
||||
await transferUtility.UploadAsync(request, ct); |
|
||||
} |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
request.InputStream = new SeekFakerStream(stream); |
|
||||
|
|
||||
request.AutoCloseStream = false; |
|
||||
|
|
||||
await transferUtility.UploadAsync(request, ct); |
|
||||
} |
|
||||
} |
|
||||
catch (AmazonS3Exception ex) when (ex.StatusCode == HttpStatusCode.PreconditionFailed) |
|
||||
{ |
|
||||
throw new AssetAlreadyExistsException(fileName); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public async Task DeleteAsync(string fileName) |
|
||||
{ |
|
||||
var key = GetKey(fileName, nameof(fileName)); |
|
||||
|
|
||||
try |
|
||||
{ |
|
||||
var request = new DeleteObjectRequest |
|
||||
{ |
|
||||
BucketName = options.Bucket, |
|
||||
Key = key |
|
||||
}; |
|
||||
|
|
||||
await s3Client.DeleteObjectAsync(request); |
|
||||
} |
|
||||
catch (AmazonS3Exception ex) when (ex.StatusCode == HttpStatusCode.NotFound) |
|
||||
{ |
|
||||
return; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private string GetKey(string fileName, string parameterName) |
|
||||
{ |
|
||||
Guard.NotNullOrEmpty(fileName, parameterName); |
|
||||
|
|
||||
if (!string.IsNullOrWhiteSpace(options.BucketFolder)) |
|
||||
{ |
|
||||
return $"{options.BucketFolder}/{fileName}"; |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
return fileName; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private async Task EnsureNotExistsAsync(string key, string fileName, CancellationToken ct) |
|
||||
{ |
|
||||
try |
|
||||
{ |
|
||||
await s3Client.GetObjectAsync(options.Bucket, key, ct); |
|
||||
} |
|
||||
catch |
|
||||
{ |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
throw new AssetAlreadyExistsException(fileName); |
|
||||
} |
|
||||
|
|
||||
private static bool HasContentLength(Stream stream) |
|
||||
{ |
|
||||
try |
|
||||
{ |
|
||||
return stream.Length > 0; |
|
||||
} |
|
||||
catch |
|
||||
{ |
|
||||
return false; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,26 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
namespace Squidex.Assets |
|
||||
{ |
|
||||
public sealed class AmazonS3Options |
|
||||
{ |
|
||||
public string? ServiceUrl { get; set; } |
|
||||
|
|
||||
public string? RegionName { get; set; } |
|
||||
|
|
||||
public string Bucket { get; set; } |
|
||||
|
|
||||
public string? BucketFolder { get; set; } |
|
||||
|
|
||||
public string AccessKey { get; set; } |
|
||||
|
|
||||
public string SecretKey { get; set; } |
|
||||
|
|
||||
public bool ForcePathStyle { get; set; } |
|
||||
} |
|
||||
} |
|
||||
@ -1,85 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System; |
|
||||
using System.IO; |
|
||||
|
|
||||
namespace Squidex.Assets |
|
||||
{ |
|
||||
public sealed class SeekFakerStream : Stream |
|
||||
{ |
|
||||
private readonly Stream inner; |
|
||||
|
|
||||
public override bool CanRead |
|
||||
{ |
|
||||
get { return inner.CanRead; } |
|
||||
} |
|
||||
|
|
||||
public override bool CanSeek |
|
||||
{ |
|
||||
get { return true; } |
|
||||
} |
|
||||
|
|
||||
public override bool CanWrite |
|
||||
{ |
|
||||
get { return false; } |
|
||||
} |
|
||||
|
|
||||
public override long Length |
|
||||
{ |
|
||||
get { return inner.Length; } |
|
||||
} |
|
||||
|
|
||||
public override long Position |
|
||||
{ |
|
||||
get { return inner.Position; } |
|
||||
set { throw new NotSupportedException(); } |
|
||||
} |
|
||||
|
|
||||
public SeekFakerStream(Stream inner) |
|
||||
{ |
|
||||
Guard.NotNull(inner, nameof(inner)); |
|
||||
|
|
||||
if (!inner.CanRead) |
|
||||
{ |
|
||||
throw new ArgumentException("Inner stream must be readable."); |
|
||||
} |
|
||||
|
|
||||
this.inner = inner; |
|
||||
} |
|
||||
|
|
||||
public override int Read(byte[] buffer, int offset, int count) |
|
||||
{ |
|
||||
return inner.Read(buffer, offset, count); |
|
||||
} |
|
||||
|
|
||||
public override void Flush() |
|
||||
{ |
|
||||
throw new NotSupportedException(); |
|
||||
} |
|
||||
|
|
||||
public override long Seek(long offset, SeekOrigin origin) |
|
||||
{ |
|
||||
if (offset != 0 || origin != SeekOrigin.Begin) |
|
||||
{ |
|
||||
throw new NotSupportedException(); |
|
||||
} |
|
||||
|
|
||||
return 0; |
|
||||
} |
|
||||
|
|
||||
public override void SetLength(long value) |
|
||||
{ |
|
||||
throw new NotSupportedException(); |
|
||||
} |
|
||||
|
|
||||
public override void Write(byte[] buffer, int offset, int count) |
|
||||
{ |
|
||||
throw new NotSupportedException(); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,22 +0,0 @@ |
|||||
<Project Sdk="Microsoft.NET.Sdk"> |
|
||||
<PropertyGroup> |
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework> |
|
||||
<RootNamespace>Squidex.Infrastructure</RootNamespace> |
|
||||
<LangVersion>8.0</LangVersion> |
|
||||
<Nullable>enable</Nullable> |
|
||||
</PropertyGroup> |
|
||||
<ItemGroup> |
|
||||
<PackageReference Include="AWSSDK.S3" Version="3.5.3.2" /> |
|
||||
<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> |
|
||||
@ -1,141 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System; |
|
||||
using System.IO; |
|
||||
using System.Net; |
|
||||
using System.Net.Http.Headers; |
|
||||
using System.Threading; |
|
||||
using System.Threading.Tasks; |
|
||||
using Google; |
|
||||
using Google.Cloud.Storage.V1; |
|
||||
|
|
||||
namespace Squidex.Assets |
|
||||
{ |
|
||||
public sealed class GoogleCloudAssetStore : IAssetStore, IInitializable |
|
||||
{ |
|
||||
private static readonly UploadObjectOptions IfNotExists = new UploadObjectOptions { IfGenerationMatch = 0 }; |
|
||||
private static readonly CopyObjectOptions IfNotExistsCopy = new CopyObjectOptions { IfGenerationMatch = 0 }; |
|
||||
private readonly string bucketName; |
|
||||
private StorageClient storageClient; |
|
||||
|
|
||||
public GoogleCloudAssetStore(string bucketName) |
|
||||
{ |
|
||||
Guard.NotNullOrEmpty(bucketName, nameof(bucketName)); |
|
||||
|
|
||||
this.bucketName = bucketName; |
|
||||
} |
|
||||
|
|
||||
public async Task InitializeAsync(CancellationToken ct = default) |
|
||||
{ |
|
||||
try |
|
||||
{ |
|
||||
storageClient = await StorageClient.CreateAsync(); |
|
||||
|
|
||||
await storageClient.GetBucketAsync(bucketName, cancellationToken: ct); |
|
||||
} |
|
||||
catch (Exception ex) |
|
||||
{ |
|
||||
throw new ConfigurationException($"Cannot connect to google cloud bucket '{bucketName}'.", ex); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public string? GeneratePublicUrl(string fileName) |
|
||||
{ |
|
||||
return null; |
|
||||
} |
|
||||
|
|
||||
public async Task<long> GetSizeAsync(string fileName, CancellationToken ct = default) |
|
||||
{ |
|
||||
Guard.NotNullOrEmpty(fileName, nameof(fileName)); |
|
||||
|
|
||||
try |
|
||||
{ |
|
||||
var obj = await storageClient.GetObjectAsync(bucketName, fileName, null, ct); |
|
||||
|
|
||||
if (!obj.Size.HasValue) |
|
||||
{ |
|
||||
throw new AssetNotFoundException(fileName); |
|
||||
} |
|
||||
|
|
||||
return (long)obj.Size.Value; |
|
||||
} |
|
||||
catch (GoogleApiException ex) when (ex.HttpStatusCode == HttpStatusCode.NotFound) |
|
||||
{ |
|
||||
throw new AssetNotFoundException(fileName, ex); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public async Task CopyAsync(string sourceFileName, string targetFileName, CancellationToken ct = default) |
|
||||
{ |
|
||||
Guard.NotNullOrEmpty(sourceFileName, nameof(sourceFileName)); |
|
||||
Guard.NotNullOrEmpty(targetFileName, nameof(targetFileName)); |
|
||||
|
|
||||
try |
|
||||
{ |
|
||||
await storageClient.CopyObjectAsync(bucketName, sourceFileName, bucketName, targetFileName, IfNotExistsCopy, ct); |
|
||||
} |
|
||||
catch (GoogleApiException ex) when (ex.HttpStatusCode == HttpStatusCode.NotFound) |
|
||||
{ |
|
||||
throw new AssetNotFoundException(sourceFileName, ex); |
|
||||
} |
|
||||
catch (GoogleApiException ex) when (ex.HttpStatusCode == HttpStatusCode.PreconditionFailed) |
|
||||
{ |
|
||||
throw new AssetAlreadyExistsException(targetFileName); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public async Task DownloadAsync(string fileName, Stream stream, BytesRange range = default, CancellationToken ct = default) |
|
||||
{ |
|
||||
Guard.NotNullOrEmpty(fileName, nameof(fileName)); |
|
||||
|
|
||||
try |
|
||||
{ |
|
||||
var downloadOptions = new DownloadObjectOptions(); |
|
||||
|
|
||||
if (range.IsDefined) |
|
||||
{ |
|
||||
downloadOptions.Range = new RangeHeaderValue(range.From, range.To); |
|
||||
} |
|
||||
|
|
||||
await storageClient.DownloadObjectAsync(bucketName, fileName, stream, downloadOptions, ct); |
|
||||
} |
|
||||
catch (GoogleApiException ex) when (ex.HttpStatusCode == HttpStatusCode.NotFound) |
|
||||
{ |
|
||||
throw new AssetNotFoundException(fileName, ex); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public async Task UploadAsync(string fileName, Stream stream, bool overwrite = false, CancellationToken ct = default) |
|
||||
{ |
|
||||
Guard.NotNullOrEmpty(fileName, nameof(fileName)); |
|
||||
|
|
||||
try |
|
||||
{ |
|
||||
await storageClient.UploadObjectAsync(bucketName, fileName, "application/octet-stream", stream, overwrite ? null : IfNotExists, ct); |
|
||||
} |
|
||||
catch (GoogleApiException ex) when (ex.HttpStatusCode == HttpStatusCode.PreconditionFailed) |
|
||||
{ |
|
||||
throw new AssetAlreadyExistsException(fileName); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public async Task DeleteAsync(string fileName) |
|
||||
{ |
|
||||
Guard.NotNullOrEmpty(fileName, nameof(fileName)); |
|
||||
|
|
||||
try |
|
||||
{ |
|
||||
await storageClient.DeleteObjectAsync(bucketName, fileName); |
|
||||
} |
|
||||
catch (GoogleApiException ex) when (ex.HttpStatusCode == HttpStatusCode.NotFound) |
|
||||
{ |
|
||||
return; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,27 +0,0 @@ |
|||||
<Project Sdk="Microsoft.NET.Sdk"> |
|
||||
<PropertyGroup> |
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework> |
|
||||
<RootNamespace>Squidex.Infrastructure</RootNamespace> |
|
||||
<LangVersion>8.0</LangVersion> |
|
||||
<Nullable>enable</Nullable> |
|
||||
</PropertyGroup> |
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> |
|
||||
<DebugType>full</DebugType> |
|
||||
<DebugSymbols>True</DebugSymbols> |
|
||||
</PropertyGroup> |
|
||||
<ItemGroup> |
|
||||
<PackageReference Include="Google.Cloud.Storage.V1" Version="3.3.0" /> |
|
||||
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> |
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" /> |
|
||||
<PackageReference Include="System.ValueTuple" Version="4.5.0" /> |
|
||||
</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> |
|
||||
Loading…
Reference in new issue