Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Amazon S3 Media Storage support #11738

Merged
merged 29 commits into from
May 30, 2022
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
a2bcc94
Amazon S3 Media Storage feature impl
neglectedvalue May 21, 2022
6741b4c
Amazon S3 Media Storage coment update
neglectedvalue May 21, 2022
d43d612
Redundant Feature attribute was removed from Manifest and Startup
neglectedvalue May 22, 2022
c1f3427
Manifest changes
neglectedvalue May 22, 2022
857128f
Update src/OrchardCore.Cms.Web/appsettings.json
neglectedvalue May 23, 2022
6f2f4cb
Update src/OrchardCore/OrchardCore.FileStorage.AmazonS3/AwsDirectory.cs
neglectedvalue May 23, 2022
b446d71
Update src/OrchardCore/OrchardCore.FileStorage.AmazonS3/AwsFile.cs
neglectedvalue May 23, 2022
d88aafe
Update src/OrchardCore.Modules/OrchardCore.Media.AmazonS3/OrchardCore…
neglectedvalue May 23, 2022
efc8f33
Update src/OrchardCore/OrchardCore.FileStorage.AmazonS3/AwsFile.cs
neglectedvalue May 23, 2022
e2600f6
Update src/OrchardCore/OrchardCore.FileStorage.AmazonS3/AwsStorageOpt…
neglectedvalue May 23, 2022
35cd290
Update src/OrchardCore/OrchardCore.FileStorage.AmazonS3/AwsStorageOpt…
neglectedvalue May 23, 2022
13dbfa7
Update src/OrchardCore.Modules/OrchardCore.Media.AmazonS3/AwsStorageO…
neglectedvalue May 23, 2022
3fa2206
Update src/OrchardCore.Modules/OrchardCore.Media.AmazonS3/Startup.cs
neglectedvalue May 23, 2022
2ad2279
Changes after Piedone review
neglectedvalue May 23, 2022
9c90f5c
MediaConfigurationException was added. Small corrections to the doc f…
neglectedvalue May 23, 2022
4b4100f
Documentation update.
neglectedvalue May 24, 2022
992adbc
Apply suggestions from code review
neglectedvalue May 24, 2022
b6f5f54
Corrections after review.
neglectedvalue May 24, 2022
c878686
Documentation update
neglectedvalue May 24, 2022
1fbcac3
Fixes after review.
neglectedvalue May 25, 2022
cbfe4d6
Update src/OrchardCore.Modules/OrchardCore.Media.AmazonS3/AwsStorageO…
neglectedvalue May 25, 2022
501932a
Update src/OrchardCore.Modules/OrchardCore.Media.AmazonS3/CreateMedia…
neglectedvalue May 25, 2022
a42fd5a
ACL and bucket configuration documentation
neglectedvalue May 25, 2022
d92cd07
Section naming fix
neglectedvalue May 25, 2022
f492d1d
Update src/docs/reference/modules/Media.AmazonS3/README.md
neglectedvalue May 25, 2022
a77cf06
sebastienros and hishamco remarks fix
neglectedvalue May 26, 2022
fd78ac4
Changes after discussion.
neglectedvalue May 26, 2022
dc24e5a
Trim removed, NewLine styling
neglectedvalue May 27, 2022
b464a5d
Typo in logger param, NewLine changed to support different config sou…
neglectedvalue May 28, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions OrchardCore.sln
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Examples.Themes.AssyAttrib.
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Examples.OrchardCoreThemes.Alpha", "test\OrchardCore.Tests.Themes\Examples.OrchardCoreThemes.Alpha\Examples.OrchardCoreThemes.Alpha.csproj", "{ECCBDB6C-BF57-43B8-9A58-5F3D60357D0D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OrchardCore.FileStorage.AmazonS3", "src\OrchardCore\OrchardCore.FileStorage.AmazonS3\OrchardCore.FileStorage.AmazonS3.csproj", "{38F43FA0-5BA8-4D6B-8F66-C708D590EF76}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OrchardCore.Media.AmazonS3", "src\OrchardCore.Modules\OrchardCore.Media.AmazonS3\OrchardCore.Media.AmazonS3.csproj", "{FF1C550C-6D30-499A-AF11-68DE7C8B6869}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -1224,6 +1228,14 @@ Global
{ECCBDB6C-BF57-43B8-9A58-5F3D60357D0D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{ECCBDB6C-BF57-43B8-9A58-5F3D60357D0D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{ECCBDB6C-BF57-43B8-9A58-5F3D60357D0D}.Release|Any CPU.Build.0 = Release|Any CPU
{38F43FA0-5BA8-4D6B-8F66-C708D590EF76}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{38F43FA0-5BA8-4D6B-8F66-C708D590EF76}.Debug|Any CPU.Build.0 = Debug|Any CPU
{38F43FA0-5BA8-4D6B-8F66-C708D590EF76}.Release|Any CPU.ActiveCfg = Release|Any CPU
{38F43FA0-5BA8-4D6B-8F66-C708D590EF76}.Release|Any CPU.Build.0 = Release|Any CPU
{FF1C550C-6D30-499A-AF11-68DE7C8B6869}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FF1C550C-6D30-499A-AF11-68DE7C8B6869}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FF1C550C-6D30-499A-AF11-68DE7C8B6869}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FF1C550C-6D30-499A-AF11-68DE7C8B6869}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -1438,6 +1450,8 @@ Global
{95406CBA-9382-4019-B422-EE339D47CE8F} = {61436BAE-FB36-4ADA-8E5D-EE64C4E04522}
{80E34B32-9043-42FE-AB5C-CE1178F2B90D} = {61436BAE-FB36-4ADA-8E5D-EE64C4E04522}
{ECCBDB6C-BF57-43B8-9A58-5F3D60357D0D} = {61436BAE-FB36-4ADA-8E5D-EE64C4E04522}
{38F43FA0-5BA8-4D6B-8F66-C708D590EF76} = {F23AC6C2-DE44-4699-999D-3C478EF3D691}
{FF1C550C-6D30-499A-AF11-68DE7C8B6869} = {90030E85-0C4F-456F-B879-443E8A3F220D}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {46A1D25A-78D1-4476-9CBF-25B75E296341}
Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ nav:
- Media:
- Media: docs/reference/modules/Media/README.md
- Media Slugify: docs/reference/modules/Media.Slugify/README.md
- Media Amazon S3: docs/reference/modules/Media.AmazonS3/README.md
- Media Azure: docs/reference/modules/Media.Azure/README.md
- ReCaptcha: docs/reference/modules/ReCaptcha/README.md
- Resources: docs/reference/modules/Resources/README.md
Expand Down
2 changes: 2 additions & 0 deletions src/OrchardCore.Build/Dependencies.props
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

<ItemGroup>
<PackageManagement Include="AngleSharp" Version="0.16.1" />
<PackageManagement Include="AWSSDK.S3" Version="3.7.9.4" />
<PackageManagement Include="AWSSDK.SecurityToken" Version="3.7.1.153" />
<PackageManagement Include="Azure.Extensions.AspNetCore.Configuration.Secrets" Version="1.2.2" />
<PackageManagement Include="Azure.Extensions.AspNetCore.DataProtection.Blobs" Version="1.2.1" />
<PackageManagement Include="Azure.Identity" Version="1.6.0" />
Expand Down
17 changes: 16 additions & 1 deletion src/OrchardCore.Cms.Web/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,21 @@
// "AllowedFileExtensions": [".jpg",".jpeg",".png",".gif",".ico",".svg",".pdf",".doc",".docx",".ppt",".pptx",".pps",".ppsx",".odt",".xls",".xlsx",".psd",".mp3",".m4a",".ogg",".wav",".mp4",".m4v",".mov",".wmv",".avi",".mpg",".ogv",".3gp"],
// "ContentSecurityPolicy": "default-src 'self'; style-src 'unsafe-inline'"
//}
// See https://docs.orchardcore.net/en/latest/docs/reference/modules/Media.AmazonS3/#configuration to configure media storage in Amazon S3 Storage.
//"OrchardCore_Media_AmazonS3": {
// "BucketName": "somebucketname",
// Credentials section needed only if Orchard will be hosted not in the AWS Cloud
// since in AWS you don't need to store this info in the appsettings file, you just
// need to set BucketName and BasePath.
// "Credentials": {
// "SecretKey": "",
// "AccessKeyId": "",
// "RegionEndpoint": "eu-central-1"
// },
// "BasePath": "/media",
// "ProfileName": "",
// "CreateBucket" : true
//},
// See https://docs.orchardcore.net/en/latest/docs/reference/modules/Media.Azure/#configuration to configure media storage in Azure Blob Storage.
//"OrchardCore_Media_Azure":
//{
Expand Down Expand Up @@ -129,6 +144,6 @@
//},
//"OrchardCore_HealthChecks": {
// "Url": "/health/live"
//}
//},
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
using System;
using Fluid;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using OrchardCore.Environment.Shell;
using OrchardCore.Environment.Shell.Configuration;
using OrchardCore.FileStorage.AmazonS3;
using static System.Environment;

namespace OrchardCore.Media.AmazonS3;

public class AwsStorageOptionsConfiguration : IConfigureOptions<AwsStorageOptions>
{
private readonly IShellConfiguration _shellConfiguration;
private readonly ShellSettings _shellSettings;
private readonly ILogger _logger;

// Local instance since it can be discarded once the startup is over
private readonly FluidParser _fluidParser = new ();

public AwsStorageOptionsConfiguration(
IShellConfiguration shellConfiguration,
ShellSettings shellSettings,
ILogger<AwsStorageOptions> logger)
{
_shellConfiguration = shellConfiguration;
_shellSettings = shellSettings;
_logger = logger;
}

public void Configure(AwsStorageOptions options)
{
options.BindConfiguration(_shellConfiguration);

var templateOptions = new TemplateOptions();
var templateContext = new TemplateContext(templateOptions);
templateOptions.MemberAccessStrategy.Register<ShellSettings>();
templateOptions.MemberAccessStrategy.Register<AwsStorageOptions>();
templateContext.SetValue("ShellSettings", _shellSettings);

ParseBucketName(options, templateContext);
ParseBasePath(options, templateContext);
}

private void ParseBucketName(AwsStorageOptions options, TemplateContext templateContext)
{
// Use Fluid directly as this is transient and cannot invoke _liquidTemplateManager.
try
{
var template = _fluidParser.Parse(options.BucketName);

options.BucketName = template
.Render(templateContext, NullEncoder.Default)
.Replace("\r", String.Empty)
jtkech marked this conversation as resolved.
Show resolved Hide resolved
.Replace(NewLine, String.Empty);
}
catch (Exception e)
{
_logger.LogCritical(e, "Unable to parse Amazon S3 Media Storage bucket name.");
throw;
}
}

private void ParseBasePath(AwsStorageOptions options, TemplateContext templateContext)
{
try
{
var template = _fluidParser.Parse(options.BasePath);

options.BasePath = template
.Render(templateContext, NullEncoder.Default)
.Replace("\r", String.Empty)
.Replace(NewLine, String.Empty);
}
catch (Exception e)
{
_logger.LogCritical(e, "Unable to parse Amazon S3 Media Storage base path.");
throw;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Amazon;
using Amazon.Runtime.CredentialManagement;
using Microsoft.Extensions.Configuration;
using OrchardCore.Environment.Shell.Configuration;
using OrchardCore.FileStorage.AmazonS3;

namespace OrchardCore.Media.AmazonS3;

public static class AwsStorageOptionsExtension
{
public static IEnumerable<ValidationResult> Validate(this AwsStorageOptions options)
{
if (String.IsNullOrWhiteSpace(options.BucketName))
{
yield return new ValidationResult(Constants.ValidationMessages.BucketNameIsEmpty);
}

if (options.Credentials != null)
{
if (String.IsNullOrWhiteSpace(options.Credentials.SecretKey))
{
yield return new ValidationResult(Constants.ValidationMessages.SecretKeyIsEmpty);
}

if (String.IsNullOrWhiteSpace(options.Credentials.AccessKeyId))
{
yield return new ValidationResult(Constants.ValidationMessages.AccessKeyIdIsEmpty);
}

if (String.IsNullOrWhiteSpace(options.Credentials.RegionEndpoint))
{
yield return new ValidationResult(Constants.ValidationMessages.RegionEndpointIsEmpty);
}
}
}

public static AwsStorageOptions BindConfiguration(this AwsStorageOptions options, IShellConfiguration shellConfiguration)
{
var section = shellConfiguration.GetSection("OrchardCore_Media_AmazonS3");

if (section == null)
{
return options;
}

options.BucketName = section.GetValue(nameof(options.BucketName), String.Empty);
options.BasePath = section.GetValue(nameof(options.BasePath), String.Empty);
options.CreateBucket = section.GetValue(nameof(options.CreateBucket), false);

var credentials = section.GetSection("Credentials");
if (credentials.Exists())
{
options.Credentials = new AwsStorageCredentials
{
RegionEndpoint =
credentials.GetValue(nameof(options.Credentials.RegionEndpoint), RegionEndpoint.USEast1.SystemName),
SecretKey = credentials.GetValue(nameof(options.Credentials.SecretKey), String.Empty),
AccessKeyId = credentials.GetValue(nameof(options.Credentials.AccessKeyId), String.Empty),
};

}
else
{
// Attempt to load Credentials from Profile.
var profileName = section.GetValue("ProfileName", String.Empty);
if (!String.IsNullOrEmpty(profileName))
{
var chain = new CredentialProfileStoreChain();
if (chain.TryGetProfile(profileName, out var basicProfile))
{
var awsCredentials = basicProfile.GetAWSCredentials(chain)?.GetCredentials();
if (awsCredentials != null)
{
options.Credentials = new AwsStorageCredentials
{
RegionEndpoint = basicProfile.Region.SystemName ?? RegionEndpoint.USEast1.SystemName,
SecretKey = awsCredentials.SecretKey,
AccessKeyId = awsCredentials.AccessKey
};
}
}
}
}

return options;
}
}
18 changes: 18 additions & 0 deletions src/OrchardCore.Modules/OrchardCore.Media.AmazonS3/Constants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace OrchardCore.Media.AmazonS3;

internal static class Constants
{
internal static class ValidationMessages
{
public const string BucketNameIsEmpty = "BucketName is required attribute for S3 Media";

public const string SecretKeyIsEmpty =
"SecretKey is required attribute for S3 Media, make sure it exists in Credentials section or ProfileName you specified";

public const string AccessKeyIdIsEmpty =
"AccessKeyId is required attribute for S3 Media, make sure it exists in Credentials section or ProfileName you specified";

public const string RegionEndpointIsEmpty =
"Region is required attribute for S3 Media, make sure it exists in Credentials section or ProfileName you specified";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
using System;
using System.Threading.Tasks;
using Amazon.S3;
using Amazon.S3.Model;
using Amazon.S3.Util;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using OrchardCore.Environment.Shell;
using OrchardCore.FileStorage.AmazonS3;
using OrchardCore.Modules;

namespace OrchardCore.Media.AmazonS3;

public class CreateMediaS3BucketEvent : ModularTenantEvents
{
private readonly ShellSettings _shellSettings;
private readonly ILogger _logger;
private readonly AwsStorageOptions _options;
private readonly IAmazonS3 _amazonS3Client;

public CreateMediaS3BucketEvent(ShellSettings shellSettings,
IOptions<AwsStorageOptions> options,
IAmazonS3 amazonS3Client,
ILogger<CreateMediaS3BucketEvent> logger)
{
_shellSettings = shellSettings;
_logger = logger;
_amazonS3Client = amazonS3Client;
_options = options.Value;
}

public override async Task ActivatingAsync()
{
if (_options.CreateBucket &&
_shellSettings.State != Environment.Shell.Models.TenantState.Uninitialized &&
!String.IsNullOrEmpty(_options.BucketName))
{
_logger.LogDebug("Testing Amazon S3 Bucket {BucketName} existence", _options.BucketName);

try
{
var isBucketExists = await AmazonS3Util.DoesS3BucketExistV2Async(_amazonS3Client, _options.BucketName);
if (isBucketExists)
{
_logger.LogInformation("Amazon S3 Bucket {BucketName} already exists.", _options.BucketName);
return;
}

var bucketRequest = new PutBucketRequest
neglectedvalue marked this conversation as resolved.
Show resolved Hide resolved
{
BucketName = _options.BucketName,
UseClientRegion = true
};

// Tying to create bucket
var response = await _amazonS3Client.PutBucketAsync(bucketRequest);

if (!response.IsSuccessful())
{
_logger.LogError("Unable to create Amazon S3 Bucket. {Response}", response);
return;
}

// Blocking public access for the newly created bucket.
var blockConfiguration = new PublicAccessBlockConfiguration
{
BlockPublicAcls = true,
BlockPublicPolicy = true,
IgnorePublicAcls = true,
RestrictPublicBuckets = true
};

await _amazonS3Client.PutPublicAccessBlockAsync(new PutPublicAccessBlockRequest
{
PublicAccessBlockConfiguration = blockConfiguration,
BucketName = _options.BucketName
});

_logger.LogDebug("Amazon S3 Bucket {BucketName} created.", _options.BucketName);
}
catch (Exception e)
{
_logger.LogError(e, "Unable to create Amazon S3 Bucket.");
}
}
}
}
19 changes: 19 additions & 0 deletions src/OrchardCore.Modules/OrchardCore.Media.AmazonS3/Manifest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using OrchardCore.Modules.Manifest;

[assembly: Module(
Name = "Amazon S3 Media",
Author = ManifestConstants.OrchardCoreTeam,
Website = ManifestConstants.OrchardCoreWebsite,
Version = ManifestConstants.OrchardCoreVersion
)]

[assembly: Feature(
Id = "OrchardCore.Media.AmazonS3",
Name = "Amazon Media Storage",
Description = "Enables support for storing media files in Amazon S3 Bucket.",
Dependencies = new[]
{
"OrchardCore.Media.Cache"
},
Category = "Hosting"
)]
Loading