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