[IMPLEMENTED] Pointing the Squidex asset path to Amazon S3 Instance

If i host squidex in Amazon EC2 Instance and if i would like to use Amazon S3 Instance for Asset store. can i configure that ?

1 Like

Not yet, but could write an implementation for that.

If you have time (and skill), I would like to see a PR, otherwise tell me, I can also implement it.

1 Like

Thank you very much for your quick response. We are almost finalysing on where to host either in AWS or Azure. Will definitely get back to you on this.

+1 for this feature.

S3 is important to me because of Minio. This is an open source object storage server with an S3 interface, but supports exporting to many other servers like Google Cloud Storage. I don’t know of a similar technology that is based on Google Cloud or Azure.

I have implemented S3, seems pretty straightforward with s3Client.CopyObjectAsync, s3Client.GetObjectAsync and fileTransferUtility.UploadAsync, s3Client.DeleteObjectAsync.
refer:
https://docs.aws.amazon.com/AmazonS3/latest/dev/

1 Like

Pull request is very welcome?

I am not actively working with the branch but can share the code.

// ==========================================================================
//  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 : IAssetStore, IInitializable
    {
        private const int BufferSize = 81920;
        private readonly string accessKey;
        private readonly string secretKey;
        private readonly string bucketName;
        private readonly RegionEndpoint bucketRegion;
        private static IAmazonS3 s3Client;

        public AmazonS3AssetStore(string bucketName, string regionName, string accessKey, string secretKey) {
            Guard.NotNullOrEmpty(bucketName, nameof(bucketName));
            Guard.NotNullOrEmpty(accessKey, nameof(accessKey));
            Guard.NotNullOrEmpty(secretKey, nameof(secretKey));

            this.bucketName = bucketName;
            this.accessKey = accessKey;
            this.secretKey = secretKey;
            this.bucketRegion = RegionEndpoint.GetBySystemName(regionName);
        }

        public async Task InitializeAsync(CancellationToken ct = default)
        {
            try
            {
                s3Client = new AmazonS3Client(
                    accessKey,
                    secretKey,
                    bucketRegion
                );
            }
            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, nameof(sourceFileName));
            Guard.NotNullOrEmpty(targetFileName, nameof(targetFileName));

            try
            {
                CopyObjectRequest request = new CopyObjectRequest {
                    SourceBucket = bucketName,
                    SourceKey = sourceFileName,
                    DestinationBucket = bucketName,
                    DestinationKey = 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, nameof(fileName));

            try
            {
                GetObjectRequest request = new GetObjectRequest
                {
                    BucketName = bucketName,
                    Key = fileName
                };

                using (GetObjectResponse 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, nameof(fileName));

            try
            {
                var fileTransferUtility = new TransferUtility(s3Client);
                await fileTransferUtility.UploadAsync(stream, bucketName, fileName, ct);
            }
            catch (AmazonS3Exception ex) when (ex.StatusCode == HttpStatusCode.PreconditionFailed)
            {
                throw new AssetAlreadyExistsException(fileName);
            }
        }

        public async Task DeleteAsync(string fileName)
        {
            Guard.NotNullOrEmpty(fileName, nameof(fileName));

            try
            {
                await s3Client.DeleteObjectAsync(
                    new DeleteObjectRequest
                    {
                        BucketName = bucketName,
                        Key = fileName
                    }
                );
            }
            catch (AmazonS3Exception ex) when (ex.StatusCode == HttpStatusCode.NotFound)
            {
                return;
            }
        }
    }
}

And add following in the AssetServices.cs and corresponding entry in the appsettings.json:

            ["AmazonS3"] = () => {
                var bucketName = config.GetRequiredValue("assetStore:amazonS3:bucket");
                var regionName = config.GetRequiredValue("assetStore:amazonS3:regionName");
                var accessKey = config.GetRequiredValue("assetStore:amazonS3:accessKey");
                var secretKey = config.GetRequiredValue("assetStore:amazonS3:secretKey");

                services.AddSingletonAs(c => new AmazonS3AssetStore(bucketName, regionName, accessKey, secretKey))
                    .As<IAssetStore>();
            }

Github PR would be great, also adding tests

1 Like

Please let me know if you have the time for a PR, don’t expect it today or so, but I would just know if you can provide one.

I have merged in your changes from this post :slight_smile:

3 Likes