Skip to content

Commit

Permalink
Feature/s3 (#446)
Browse files Browse the repository at this point in the history
Amazon S3 for assets.
  • Loading branch information
SebastianStehle authored Nov 13, 2019
1 parent 4d26ebf commit 3bcaf82
Show file tree
Hide file tree
Showing 10 changed files with 349 additions and 6 deletions.
15 changes: 15 additions & 0 deletions backend/Squidex.sln
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "shared", "shared", "{7EDE8C
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Web", "src\Squidex.Web\Squidex.Web.csproj", "{5B2D251F-46E3-486A-AE16-E3FE06B559ED}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Squidex.Infrastructure.Amazon", "src\Squidex.Infrastructure.Amazon\Squidex.Infrastructure.Amazon.csproj", "{32DA4B56-7EFA-4E34-A29D-30E00579A894}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -339,6 +341,18 @@ Global
{5B2D251F-46E3-486A-AE16-E3FE06B559ED}.Release|x64.Build.0 = Release|Any CPU
{5B2D251F-46E3-486A-AE16-E3FE06B559ED}.Release|x86.ActiveCfg = Release|Any CPU
{5B2D251F-46E3-486A-AE16-E3FE06B559ED}.Release|x86.Build.0 = Release|Any CPU
{32DA4B56-7EFA-4E34-A29D-30E00579A894}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{32DA4B56-7EFA-4E34-A29D-30E00579A894}.Debug|Any CPU.Build.0 = Debug|Any CPU
{32DA4B56-7EFA-4E34-A29D-30E00579A894}.Debug|x64.ActiveCfg = Debug|Any CPU
{32DA4B56-7EFA-4E34-A29D-30E00579A894}.Debug|x64.Build.0 = Debug|Any CPU
{32DA4B56-7EFA-4E34-A29D-30E00579A894}.Debug|x86.ActiveCfg = Debug|Any CPU
{32DA4B56-7EFA-4E34-A29D-30E00579A894}.Debug|x86.Build.0 = Debug|Any CPU
{32DA4B56-7EFA-4E34-A29D-30E00579A894}.Release|Any CPU.ActiveCfg = Release|Any CPU
{32DA4B56-7EFA-4E34-A29D-30E00579A894}.Release|Any CPU.Build.0 = Release|Any CPU
{32DA4B56-7EFA-4E34-A29D-30E00579A894}.Release|x64.ActiveCfg = Release|Any CPU
{32DA4B56-7EFA-4E34-A29D-30E00579A894}.Release|x64.Build.0 = Release|Any CPU
{32DA4B56-7EFA-4E34-A29D-30E00579A894}.Release|x86.ActiveCfg = Release|Any CPU
{32DA4B56-7EFA-4E34-A29D-30E00579A894}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -367,6 +381,7 @@ Global
{7E8CC864-4C6E-496F-A672-9F9AD8874835} = {7EDE8CF1-B1E4-4005-B154-834B944E0D7A}
{F3C41B82-6A67-409A-B7FE-54543EE4F38B} = {FB8BC3A2-2010-4C3C-A87D-D4A98C05EE52}
{5B2D251F-46E3-486A-AE16-E3FE06B559ED} = {7EDE8CF1-B1E4-4005-B154-834B944E0D7A}
{32DA4B56-7EFA-4E34-A29D-30E00579A894} = {8CF53B92-5EB1-461D-98F8-70DA9B603FBF}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {02F2E872-3141-44F5-BD6A-33CD84E9FE08}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,6 @@ public static NamedId<long> NamedId(this IField field)
return NamedIdStatic.Of(field.Id, field.Name);
}

public static IEnumerable<T> NonHidden<T>(this FieldCollection<T> fields, bool withHidden = false) where T : IField
{
return fields.Ordered.ForApi(withHidden);
}

public static IEnumerable<T> ForApi<T>(this IEnumerable<T> fields, bool withHidden = false) where T : IField
{
return fields.Where(x => IsForApi(x, withHidden));
Expand Down
202 changes: 202 additions & 0 deletions backend/src/Squidex.Infrastructure.Amazon/Assets/AmazonS3AssetStore.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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>
13 changes: 13 additions & 0 deletions backend/src/Squidex/Config/Domain/AssetServices.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,19 @@ public static void AddSquidexAssetInfrastructure(this IServiceCollection service
services.AddSingletonAs(c => new AzureBlobAssetStore(connectionString, containerName))
.As<IAssetStore>();
},
["AmazonS3"] = () =>
{
var regionName = config.GetRequiredValue("assetStore:amazonS3:regionName");

var bucketName = config.GetRequiredValue("assetStore:amazonS3:bucket");
var bucketFolder = config.GetRequiredValue("assetStore:amazonS3:bucketFolder");

var accessKey = config.GetRequiredValue("assetStore:amazonS3:accessKey");
var secretKey = config.GetRequiredValue("assetStore:amazonS3:secretKey");

services.AddSingletonAs(c => new AmazonS3AssetStore(regionName, bucketName, bucketFolder, accessKey, secretKey))
.As<IAssetStore>();
},
["MongoDb"] = () =>
{
var mongoConfiguration = config.GetRequiredValue("assetStore:mongoDb:configuration");
Expand Down
1 change: 1 addition & 0 deletions backend/src/Squidex/Squidex.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
<ProjectReference Include="..\Squidex.Domain.Apps.Events\Squidex.Domain.Apps.Events.csproj" />
<ProjectReference Include="..\Squidex.Domain.Users\Squidex.Domain.Users.csproj" />
<ProjectReference Include="..\Squidex.Domain.Users.MongoDb\Squidex.Domain.Users.MongoDb.csproj" />
<ProjectReference Include="..\Squidex.Infrastructure.Amazon\Squidex.Infrastructure.Amazon.csproj" />
<ProjectReference Include="..\Squidex.Infrastructure.Azure\Squidex.Infrastructure.Azure.csproj" />
<ProjectReference Include="..\Squidex.Infrastructure.GetEventStore\Squidex.Infrastructure.GetEventStore.csproj" />
<ProjectReference Include="..\Squidex.Infrastructure.GoogleCloud\Squidex.Infrastructure.GoogleCloud.csproj" />
Expand Down
32 changes: 31 additions & 1 deletion backend/src/Squidex/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@
/*
* Define the type of the read store.
*
* Supported: Folder (local folder), MongoDb (GridFS), GoogleCloud (hosted in Google Cloud only), AzureBlob, FTP (not recommended).
* Supported: Folder (local folder), MongoDb (GridFS), GoogleCloud (hosted in Google Cloud only), AzureBlob, AmazonS3, FTP (not recommended).
*/
"type": "Folder",
"folder": {
Expand All @@ -264,6 +264,36 @@
*/
"connectionString": "UseDevelopmentStorage=true"
},
"AmazonS3": {
/*
* The name of your bucket.
*/
"bucketName": "squidex-assets",

/*
* The optional folder within the bucket.
*/
"bucketFolder": "squidex-assets",

/*
* The region name of your bucket.
*/
"regionName": "eu-central-1",

/*
* The access key for your user.
*
* Read More: https://supsystic.com/documentation/id-secret-access-key-amazon-s3/
*/
"accessKey": "<MY_KEY>",

/*
* The secret key for your user.
*
* Read More: https://supsystic.com/documentation/id-secret-access-key-amazon-s3/
*/
"secretKey": "<MY_SECRET>"
},
"mongoDb": {
/*
* The connection string to your Mongo Server.
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
Loading

0 comments on commit 3bcaf82

Please sign in to comment.