diff --git a/mkdocs.yml b/mkdocs.yml
index abbace90287..44dd824117e 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -131,13 +131,13 @@ nav:
- Navigate between pages: docs/topics/navigation/README.md
- Query and Search data: docs/topics/search/README.md
- Secure your application: docs/topics/security/README.md
- # - Data: docs/topics/data/README.md
- # - Configuration: docs/topics/configuration/README.md
+ - Data: docs/topics/data/README.md
+ - Configuration: docs/topics/configuration/README.md
- Workflows: docs/topics/workflows/README.md
- Publishing a new release: docs/topics/publishing-releases/README.md
- Using Docker: docs/topics/docker/README.md
- Using local NuGet packages: docs/topics/local-nuget-packages/README.md
- - Managing the Orchard Core Red Hat Ecosystem Catalog certification: docs/topics/red-hat-ecosystem-catalog-certification/README.md
+ - Managing the Orchard Core Red Hat Ecosystem Catalog certification: docs/topics/red-hat-ecosystem-catalog-certification/README.md
- Reference:
- docs/reference/README.md
- CMS Modules:
@@ -164,11 +164,6 @@ nav:
- Placements: docs/reference/modules/Placements/README.md
- Themes: docs/reference/modules/Themes/README.md
- Liquid: docs/reference/modules/Liquid/README.md
- - Indexing: docs/reference/modules/Indexing/README.md
- - SQL Indexing: docs/reference/modules/SQLIndexing/README.md
- - Lucene: docs/reference/modules/Lucene/README.md
- - Elasticsearch: docs/reference/modules/Elasticsearch/README.md
- - Queries: docs/reference/modules/Queries/README.md
- Media:
- Media: docs/reference/modules/Media/README.md
- Media Slugify: docs/reference/modules/Media.Slugify/README.md
@@ -177,6 +172,13 @@ nav:
- ReCaptcha: docs/reference/modules/ReCaptcha/README.md
- Resources: docs/reference/modules/Resources/README.md
- Rules: docs/reference/modules/Rules/README.md
+ - Search, Indexing, Querying:
+ - Azure AI Search: docs/reference/modules/AzureAISearch/README.md
+ - Elasticsearch: docs/reference/modules/Elasticsearch/README.md
+ - Indexing: docs/reference/modules/Indexing/README.md
+ - Lucene: docs/reference/modules/Lucene/README.md
+ - SQL Indexing: docs/reference/modules/SQLIndexing/README.md
+ - Queries: docs/reference/modules/Queries/README.md
- Shortcodes: docs/reference/modules/Shortcodes/README.md
- Sitemaps: docs/reference/modules/Sitemaps/README.md
- SMS: docs/reference/modules/Sms/README.md
@@ -214,6 +216,7 @@ nav:
- Placement: docs/reference/core/Placement/README.md
- Data: docs/reference/core/Data/README.md
- Data Migrations: docs/reference/modules/Migrations/README.md
+ - Diagnostics: docs/reference/modules/Diagnostics/README.md
- Dynamic Cache: docs/reference/modules/DynamicCache/README.md
- Email: docs/reference/modules/Email/README.md
- SMTP Provider: docs/reference/modules/Email.Smtp/README.md
diff --git a/src/OrchardCore.Build/Dependencies.props b/src/OrchardCore.Build/Dependencies.props
index d30fd62b4a6..2935a034fda 100644
--- a/src/OrchardCore.Build/Dependencies.props
+++ b/src/OrchardCore.Build/Dependencies.props
@@ -63,6 +63,8 @@
+
+
diff --git a/src/OrchardCore.Cms.Web/appsettings.json b/src/OrchardCore.Cms.Web/appsettings.json
index 972ed2fdc5a..77a207c80da 100644
--- a/src/OrchardCore.Cms.Web/appsettings.json
+++ b/src/OrchardCore.Cms.Web/appsettings.json
@@ -28,8 +28,8 @@
// See https://docs.orchardcore.net/en/latest/docs/reference/modules/DataProtection.Azure/#configuration to configure data protection key storage in Azure Blob Storage.
//"OrchardCore_DataProtection_Azure": {
// "ConnectionString": "", // Set to your Azure Storage account connection string.
- // "ContainerName": "dataprotection", // Default to dataprotection. Templatable, refer docs.
- // "BlobName": "", // Optional, defaults to Sites/tenant_name/DataProtectionKeys.xml. Templatable, refer docs.
+ // "ContainerName": "dataprotection", // Default to dataprotection. Templatable, refer to docs.
+ // "BlobName": "", // Optional, defaults to Sites/tenant_name/DataProtectionKeys.xml. Templatable, refer to docs.
// "CreateContainer": true // Creates the container during app startup if it does not already exist.
//},
// See https://docs.orchardcore.net/en/latest/docs/reference/modules/Markdown/#markdown-configuration
@@ -62,17 +62,38 @@
// "SecretKey": "",
// "AccessKey": ""
// },
- // "BasePath": "/media",
+ // "BasePath": "/media", // Optionally, set to a path to store media in a subdirectory inside your bucket. Templatable, refer to docs.
// "CreateBucket": true,
// "RemoveBucket": true, // Whether the 'Bucket' is deleted if the tenant is removed, false by default.
- // "BucketName": ""
+ // "BucketName": "media" // Set the bucket's name (mandatory). Templatable, refer to docs.
+ //},
+ // See https://docs.orchardcore.net/en/latest/docs/reference/modules/Media.AmazonS3/#configuration_1 to configure media storage in Amazon S3 Storage.
+ //"OrchardCore_Media_AmazonS3_ImageSharp_Cache": {
+ // "Region": "eu-central-1",
+ // "Profile": "default",
+ // "ProfilesLocation": "",
+ // "Credentials": {
+ // "SecretKey": "",
+ // "AccessKey": ""
+ // },
+ // "BasePath": "/media", // Optionally, set to a path to store media in a subdirectory inside your bucket. Templatable, refer to docs.
+ // "CreateBucket": true,
+ // "RemoveBucket": true, // Whether the 'Bucket' is deleted if the tenant is removed, false by default.
+ // "BucketName": "imagesharp" // Set the bucket's name (mandatory). Templatable, refer to docs.
//},
// See https://docs.orchardcore.net/en/latest/docs/reference/modules/Media.Azure/#configuration to configure media storage in Azure Blob Storage.
- //"OrchardCore_Media_Azure":
- //{
+ //"OrchardCore_Media_Azure": {
+ // "ConnectionString": "", // Set to your Azure Storage account connection string.
+ // "ContainerName": "somecontainer", // Set to the Azure Blob container name. Templatable, refer to docs.
+ // "BasePath": "some/base/path", // Optionally, set to a path to store media in a subdirectory inside your container. Templatable, refer to docs.
+ // "CreateContainer": true, // Activates an event to create the container if it does not already exist.
+ // "RemoveContainer": true // Whether the 'Container' is deleted if the tenant is removed, false by default.
+ //},
+ // See http://127.0.0.1:8000/docs/reference/modules/Media.Azure/#configuration_1
+ //"OrchardCore_Media_Azure_ImageSharp_Cache": {
// "ConnectionString": "", // Set to your Azure Storage account connection string.
- // "ContainerName": "somecontainer", // Set to the Azure Blob container name. Templatable, refer docs.
- // "BasePath": "some/base/path", // Optionally, set to a path to store media in a subdirectory inside your container. Templatable, refer docs.
+ // "ContainerName": "somecontainer", // Set to the Azure Blob container name. Templatable, refer to docs.
+ // "BasePath": "some/base/path", // Optionally, set to a path to store media in a subdirectory inside your container. Templatable, refer to docs.
// "CreateContainer": true, // Activates an event to create the container if it does not already exist.
// "RemoveContainer": true // Whether the 'Container' is deleted if the tenant is removed, false by default.
//},
diff --git a/src/OrchardCore.Modules/OrchardCore.Media.AmazonS3/AmazonS3Constants.cs b/src/OrchardCore.Modules/OrchardCore.Media.AmazonS3/AmazonS3Constants.cs
new file mode 100644
index 00000000000..8c456fe0586
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Media.AmazonS3/AmazonS3Constants.cs
@@ -0,0 +1,22 @@
+namespace OrchardCore.Media.AmazonS3;
+
+internal static class AmazonS3Constants
+{
+ internal static class ValidationMessages
+ {
+ public const string BucketNameIsEmpty = "BucketName is required attribute for S3 storage.";
+ public const string RegionAndServiceUrlAreEmpty = "Region or ServiceURL is a required attribute for S3 storage.";
+ }
+
+ internal static class AwsCredentialParamNames
+ {
+ public const string SecretKey = "SecretKey";
+ public const string AccessKey = "AccessKey";
+ }
+
+ internal static class ConfigSections
+ {
+ public const string AmazonS3 = "OrchardCore_Media_AmazonS3";
+ public const string AmazonS3ImageSharpCache = "OrchardCore_Media_AmazonS3_ImageSharp_Cache";
+ }
+}
diff --git a/src/OrchardCore.Modules/OrchardCore.Media.AmazonS3/AwsStorageOptionsConfiguration.cs b/src/OrchardCore.Modules/OrchardCore.Media.AmazonS3/AwsStorageOptionsConfiguration.cs
deleted file mode 100644
index 7c3229a4b56..00000000000
--- a/src/OrchardCore.Modules/OrchardCore.Media.AmazonS3/AwsStorageOptionsConfiguration.cs
+++ /dev/null
@@ -1,80 +0,0 @@
-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;
-
-namespace OrchardCore.Media.AmazonS3;
-
-public class AwsStorageOptionsConfiguration : IConfigureOptions
-{
- 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 logger)
- {
- _shellConfiguration = shellConfiguration;
- _shellSettings = shellSettings;
- _logger = logger;
- }
-
- public void Configure(AwsStorageOptions options)
- {
- options.BindConfiguration(_shellConfiguration, _logger);
-
- var templateOptions = new TemplateOptions();
- var templateContext = new TemplateContext(templateOptions);
- templateOptions.MemberAccessStrategy.Register();
- templateOptions.MemberAccessStrategy.Register();
- 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)
- .Replace("\n", string.Empty)
- .Trim();
- }
- catch (Exception e)
- {
- _logger.LogCritical(e, "Unable to parse Amazon S3 Media Storage bucket name.");
- }
- }
-
- 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("\n", string.Empty)
- .Trim();
- }
- catch (Exception e)
- {
- _logger.LogCritical(e, "Unable to parse Amazon S3 Media Storage base path.");
- }
- }
-}
diff --git a/src/OrchardCore.Modules/OrchardCore.Media.AmazonS3/AwsStorageOptionsExtension.cs b/src/OrchardCore.Modules/OrchardCore.Media.AmazonS3/AwsStorageOptionsExtension.cs
index 40b1a8b9dca..c9e6d8472ee 100644
--- a/src/OrchardCore.Modules/OrchardCore.Media.AmazonS3/AwsStorageOptionsExtension.cs
+++ b/src/OrchardCore.Modules/OrchardCore.Media.AmazonS3/AwsStorageOptionsExtension.cs
@@ -12,25 +12,25 @@ namespace OrchardCore.Media.AmazonS3;
public static class AwsStorageOptionsExtension
{
- public static IEnumerable Validate(this AwsStorageOptions options)
+ public static IEnumerable Validate(this AwsStorageOptionsBase options)
{
if (string.IsNullOrWhiteSpace(options.BucketName))
{
- yield return new ValidationResult(Constants.ValidationMessages.BucketNameIsEmpty);
+ yield return new ValidationResult(AmazonS3Constants.ValidationMessages.BucketNameIsEmpty);
}
if (options.AwsOptions is not null)
{
if (options.AwsOptions.Region is null && options.AwsOptions.DefaultClientConfig.ServiceURL is null)
{
- yield return new ValidationResult(Constants.ValidationMessages.RegionEndpointIsEmpty);
+ yield return new ValidationResult(AmazonS3Constants.ValidationMessages.RegionAndServiceUrlAreEmpty);
}
}
}
- public static AwsStorageOptions BindConfiguration(this AwsStorageOptions options, IShellConfiguration shellConfiguration, ILogger logger)
+ public static AwsStorageOptionsBase BindConfiguration(this AwsStorageOptionsBase options, string configSection, IShellConfiguration shellConfiguration, ILogger logger)
{
- var section = shellConfiguration.GetSection("OrchardCore_Media_AmazonS3");
+ var section = shellConfiguration.GetSection(configSection);
if (!section.Exists())
{
@@ -53,8 +53,8 @@ public static AwsStorageOptions BindConfiguration(this AwsStorageOptions options
var credentials = section.GetSection("Credentials");
if (credentials.Exists())
{
- var secretKey = credentials.GetValue(Constants.AwsCredentialParamNames.SecretKey, string.Empty);
- var accessKey = credentials.GetValue(Constants.AwsCredentialParamNames.AccessKey, string.Empty);
+ var secretKey = credentials.GetValue(AmazonS3Constants.AwsCredentialParamNames.SecretKey, string.Empty);
+ var accessKey = credentials.GetValue(AmazonS3Constants.AwsCredentialParamNames.AccessKey, string.Empty);
if (!string.IsNullOrWhiteSpace(accessKey) ||
!string.IsNullOrWhiteSpace(secretKey))
diff --git a/src/OrchardCore.Modules/OrchardCore.Media.AmazonS3/Constants.cs b/src/OrchardCore.Modules/OrchardCore.Media.AmazonS3/Constants.cs
deleted file mode 100644
index 9816984574d..00000000000
--- a/src/OrchardCore.Modules/OrchardCore.Media.AmazonS3/Constants.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-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 RegionEndpointIsEmpty = "Region is required attribute for S3 Media";
- }
-
- internal static class AwsCredentialParamNames
- {
- public const string SecretKey = "SecretKey";
- public const string AccessKey = "AccessKey";
- }
-}
diff --git a/src/OrchardCore.Modules/OrchardCore.Media.AmazonS3/Helpers/OptionsFluidParserHelper.cs b/src/OrchardCore.Modules/OrchardCore.Media.AmazonS3/Helpers/OptionsFluidParserHelper.cs
new file mode 100644
index 00000000000..3326aa8c556
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Media.AmazonS3/Helpers/OptionsFluidParserHelper.cs
@@ -0,0 +1,45 @@
+using Fluid;
+using OrchardCore.Environment.Shell;
+
+namespace OrchardCore.Media.AmazonS3.Helpers;
+
+// This is almost the same as in OrchardCore.Media.Azure but there isn't really a good common place for it.
+internal sealed class OptionsFluidParserHelper where TOptions : class
+{
+ // Local instance since it can be discarded once the startup is over.
+ private readonly FluidParser _fluidParser = new();
+ private readonly ShellSettings _shellSettings;
+
+ private TemplateContext _templateContext;
+
+ public OptionsFluidParserHelper(ShellSettings shellSettings)
+ {
+ _shellSettings = shellSettings;
+ }
+
+ public string ParseAndFormat(string template)
+ {
+ var templateContext = GetTemplateContext();
+
+ // Use Fluid directly as this is transient and cannot invoke _liquidTemplateManager.
+ var parsedTemplate = _fluidParser.Parse(template);
+ return parsedTemplate.Render(templateContext, NullEncoder.Default)
+ .Replace("\r", string.Empty)
+ .Replace("\n", string.Empty)
+ .Trim();
+ }
+
+ private TemplateContext GetTemplateContext()
+ {
+ if (_templateContext == null)
+ {
+ var templateOptions = new TemplateOptions();
+ _templateContext = new TemplateContext(templateOptions);
+ templateOptions.MemberAccessStrategy.Register();
+ templateOptions.MemberAccessStrategy.Register();
+ _templateContext.SetValue("ShellSettings", _shellSettings);
+ }
+
+ return _templateContext;
+ }
+}
diff --git a/src/OrchardCore.Modules/OrchardCore.Media.AmazonS3/Manifest.cs b/src/OrchardCore.Modules/OrchardCore.Media.AmazonS3/Manifest.cs
index 8b80f659975..88a5d8ec543 100644
--- a/src/OrchardCore.Modules/OrchardCore.Media.AmazonS3/Manifest.cs
+++ b/src/OrchardCore.Modules/OrchardCore.Media.AmazonS3/Manifest.cs
@@ -10,10 +10,22 @@
[assembly: Feature(
Id = "OrchardCore.Media.AmazonS3",
Name = "Amazon Media Storage",
- Description = "Enables support for storing media files in Amazon S3 Bucket.",
+ Description = "Enables support for storing media files in Amazon S3.",
Dependencies =
[
"OrchardCore.Media.Cache"
],
Category = "Hosting"
)]
+
+[assembly: Feature(
+ Id = "OrchardCore.Media.AmazonS3.ImageSharpImageCache",
+ Name = "Amazon Media ImageSharp Image Cache",
+ Description = "Provides storage of ImageSharp-generated images within the Amazon S3 storage service.",
+ Dependencies =
+ [
+ "OrchardCore.Media",
+ "OrchardCore.Media.AmazonS3"
+ ],
+ Category = "Hosting"
+)]
diff --git a/src/OrchardCore.Modules/OrchardCore.Media.AmazonS3/OrchardCore.Media.AmazonS3.csproj b/src/OrchardCore.Modules/OrchardCore.Media.AmazonS3/OrchardCore.Media.AmazonS3.csproj
index ce30d21a95b..24da12ec92f 100644
--- a/src/OrchardCore.Modules/OrchardCore.Media.AmazonS3/OrchardCore.Media.AmazonS3.csproj
+++ b/src/OrchardCore.Modules/OrchardCore.Media.AmazonS3/OrchardCore.Media.AmazonS3.csproj
@@ -24,6 +24,11 @@
+
+
+
+
+
diff --git a/src/OrchardCore.Modules/OrchardCore.Media.AmazonS3/Services/AWSS3StorageCacheOptionsConfiguration.cs b/src/OrchardCore.Modules/OrchardCore.Media.AmazonS3/Services/AWSS3StorageCacheOptionsConfiguration.cs
new file mode 100644
index 00000000000..9aac7e289f1
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Media.AmazonS3/Services/AWSS3StorageCacheOptionsConfiguration.cs
@@ -0,0 +1,30 @@
+using Microsoft.Extensions.Options;
+using OrchardCore.FileStorage.AmazonS3;
+using SixLabors.ImageSharp.Web.Caching.AWS;
+
+namespace OrchardCore.Media.AmazonS3.Services;
+
+// Configuration for ImageSharp's own configuration object. We just pass the settings from the Orchard Core
+// configuration.
+internal sealed class AWSS3StorageCacheOptionsConfiguration : IConfigureOptions
+{
+ private readonly AwsImageSharpImageCacheOptions _options;
+
+ public AWSS3StorageCacheOptionsConfiguration(IOptions options)
+ {
+ _options = options.Value;
+ }
+
+ public void Configure(AWSS3StorageCacheOptions options)
+ {
+ var credentials = _options.AwsOptions.Credentials.GetCredentials();
+
+ // Only Endpoint or Region is necessary.
+ options.Endpoint = _options.AwsOptions.DefaultClientConfig.ServiceURL;
+ options.Region = _options.AwsOptions.Region?.SystemName;
+ options.BucketName = _options.BucketName;
+ options.AccessKey = credentials.AccessKey;
+ options.AccessSecret = credentials.SecretKey;
+ options.CacheFolder = _options.BasePath;
+ }
+}
diff --git a/src/OrchardCore.Modules/OrchardCore.Media.AmazonS3/Services/AwsImageSharpImageCacheOptionsConfiguration.cs b/src/OrchardCore.Modules/OrchardCore.Media.AmazonS3/Services/AwsImageSharpImageCacheOptionsConfiguration.cs
new file mode 100644
index 00000000000..9ef22cd195e
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Media.AmazonS3/Services/AwsImageSharpImageCacheOptionsConfiguration.cs
@@ -0,0 +1,51 @@
+using System;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using OrchardCore.Environment.Shell;
+using OrchardCore.Environment.Shell.Configuration;
+using OrchardCore.FileStorage.AmazonS3;
+using OrchardCore.Media.AmazonS3.Helpers;
+
+namespace OrchardCore.Media.AmazonS3.Services;
+
+internal sealed class AwsImageSharpImageCacheOptionsConfiguration : IConfigureOptions
+{
+ private readonly IShellConfiguration _shellConfiguration;
+ private readonly ShellSettings _shellSettings;
+ private readonly ILogger _logger;
+
+ public AwsImageSharpImageCacheOptionsConfiguration(
+ IShellConfiguration shellConfiguration,
+ ShellSettings shellSettings,
+ ILogger logger)
+ {
+ _shellConfiguration = shellConfiguration;
+ _shellSettings = shellSettings;
+ _logger = logger;
+ }
+
+ public void Configure(AwsImageSharpImageCacheOptions options)
+ {
+ options.BindConfiguration(AmazonS3Constants.ConfigSections.AmazonS3ImageSharpCache, _shellConfiguration, _logger);
+
+ var fluidParserHelper = new OptionsFluidParserHelper(_shellSettings);
+
+ try
+ {
+ options.BucketName = fluidParserHelper.ParseAndFormat(options.BucketName);
+ }
+ catch (Exception e)
+ {
+ _logger.LogCritical(e, "Unable to parse Amazon S3 ImageSharp Image Cache bucket name.");
+ }
+
+ try
+ {
+ options.BasePath = fluidParserHelper.ParseAndFormat(options.BasePath);
+ }
+ catch (Exception e)
+ {
+ _logger.LogCritical(e, "Unable to parse Amazon S3 ImageSharp Image Cache base path.");
+ }
+ }
+}
diff --git a/src/OrchardCore.Modules/OrchardCore.Media.AmazonS3/Services/AwsStorageOptionsConfiguration.cs b/src/OrchardCore.Modules/OrchardCore.Media.AmazonS3/Services/AwsStorageOptionsConfiguration.cs
new file mode 100644
index 00000000000..3ee2934d4a6
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Media.AmazonS3/Services/AwsStorageOptionsConfiguration.cs
@@ -0,0 +1,51 @@
+using System;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using OrchardCore.Environment.Shell;
+using OrchardCore.Environment.Shell.Configuration;
+using OrchardCore.FileStorage.AmazonS3;
+using OrchardCore.Media.AmazonS3.Helpers;
+
+namespace OrchardCore.Media.AmazonS3.Services;
+
+internal sealed class AwsStorageOptionsConfiguration : IConfigureOptions
+{
+ private readonly IShellConfiguration _shellConfiguration;
+ private readonly ShellSettings _shellSettings;
+ private readonly ILogger _logger;
+
+ public AwsStorageOptionsConfiguration(
+ IShellConfiguration shellConfiguration,
+ ShellSettings shellSettings,
+ ILogger logger)
+ {
+ _shellConfiguration = shellConfiguration;
+ _shellSettings = shellSettings;
+ _logger = logger;
+ }
+
+ public void Configure(AwsStorageOptions options)
+ {
+ options.BindConfiguration(AmazonS3Constants.ConfigSections.AmazonS3, _shellConfiguration, _logger);
+
+ var fluidParserHelper = new OptionsFluidParserHelper(_shellSettings);
+
+ try
+ {
+ options.BucketName = fluidParserHelper.ParseAndFormat(options.BucketName);
+ }
+ catch (Exception e)
+ {
+ _logger.LogCritical(e, "Unable to parse Amazon S3 Media Storage bucket name.");
+ }
+
+ try
+ {
+ options.BasePath = fluidParserHelper.ParseAndFormat(options.BasePath);
+ }
+ catch (Exception e)
+ {
+ _logger.LogCritical(e, "Unable to parse Amazon S3 Media Storage base path.");
+ }
+ }
+}
diff --git a/src/OrchardCore.Modules/OrchardCore.Media.AmazonS3/MediaS3BucketTenantEvents.cs b/src/OrchardCore.Modules/OrchardCore.Media.AmazonS3/Services/AwsTenantEventsBase.cs
similarity index 89%
rename from src/OrchardCore.Modules/OrchardCore.Media.AmazonS3/MediaS3BucketTenantEvents.cs
rename to src/OrchardCore.Modules/OrchardCore.Media.AmazonS3/Services/AwsTenantEventsBase.cs
index bed1647e533..17129325272 100644
--- a/src/OrchardCore.Modules/OrchardCore.Media.AmazonS3/MediaS3BucketTenantEvents.cs
+++ b/src/OrchardCore.Modules/OrchardCore.Media.AmazonS3/Services/AwsTenantEventsBase.cs
@@ -4,32 +4,31 @@
using Amazon.S3.Util;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Options;
using OrchardCore.Environment.Shell;
using OrchardCore.Environment.Shell.Removing;
using OrchardCore.FileStorage.AmazonS3;
using OrchardCore.Modules;
-namespace OrchardCore.Media.AmazonS3;
+namespace OrchardCore.Media.AmazonS3.Services;
-public class MediaS3BucketTenantEvents : ModularTenantEvents
+public abstract class AwsTenantEventsBase : ModularTenantEvents
{
private readonly ShellSettings _shellSettings;
- private readonly AwsStorageOptions _options;
+ private readonly AwsStorageOptionsBase _options;
private readonly IAmazonS3 _amazonS3Client;
- protected readonly IStringLocalizer S;
+ private readonly IStringLocalizer S;
private readonly ILogger _logger;
- public MediaS3BucketTenantEvents(
+ protected AwsTenantEventsBase(
ShellSettings shellSettings,
IAmazonS3 amazonS3Client,
- IOptions options,
- IStringLocalizer localizer,
- ILogger logger)
+ AwsStorageOptionsBase options,
+ IStringLocalizer localizer,
+ ILogger logger)
{
_shellSettings = shellSettings;
_amazonS3Client = amazonS3Client;
- _options = options.Value;
+ _options = options;
S = localizer;
_logger = logger;
}
diff --git a/src/OrchardCore.Modules/OrchardCore.Media.AmazonS3/Services/ImageSharpS3ImageCacheBucketTenantEvents.cs b/src/OrchardCore.Modules/OrchardCore.Media.AmazonS3/Services/ImageSharpS3ImageCacheBucketTenantEvents.cs
new file mode 100644
index 00000000000..e1ffe2d9be3
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Media.AmazonS3/Services/ImageSharpS3ImageCacheBucketTenantEvents.cs
@@ -0,0 +1,21 @@
+using Amazon.S3;
+using Microsoft.Extensions.Localization;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using OrchardCore.Environment.Shell;
+using OrchardCore.FileStorage.AmazonS3;
+
+namespace OrchardCore.Media.AmazonS3.Services;
+
+public class ImageSharpS3ImageCacheBucketTenantEvents : AwsTenantEventsBase
+{
+ public ImageSharpS3ImageCacheBucketTenantEvents(
+ ShellSettings shellSettings,
+ IAmazonS3 amazonS3Client,
+ IOptions options,
+ IStringLocalizer localizer,
+ ILogger logger)
+ : base(shellSettings, amazonS3Client, options.Value, localizer, logger)
+ {
+ }
+}
diff --git a/src/OrchardCore.Modules/OrchardCore.Media.AmazonS3/Services/MediaS3BucketTenantEvents.cs b/src/OrchardCore.Modules/OrchardCore.Media.AmazonS3/Services/MediaS3BucketTenantEvents.cs
new file mode 100644
index 00000000000..e604617f736
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Media.AmazonS3/Services/MediaS3BucketTenantEvents.cs
@@ -0,0 +1,21 @@
+using Amazon.S3;
+using Microsoft.Extensions.Localization;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using OrchardCore.Environment.Shell;
+using OrchardCore.FileStorage.AmazonS3;
+
+namespace OrchardCore.Media.AmazonS3.Services;
+
+public class MediaS3BucketTenantEvents : AwsTenantEventsBase
+{
+ public MediaS3BucketTenantEvents(
+ ShellSettings shellSettings,
+ IAmazonS3 amazonS3Client,
+ IOptions options,
+ IStringLocalizer localizer,
+ ILogger logger)
+ : base(shellSettings, amazonS3Client, options.Value, localizer, logger)
+ {
+ }
+}
diff --git a/src/OrchardCore.Modules/OrchardCore.Media.AmazonS3/Startup.cs b/src/OrchardCore.Modules/OrchardCore.Media.AmazonS3/Startup.cs
index 3a30f3a99de..33b8d8cd2a2 100644
--- a/src/OrchardCore.Modules/OrchardCore.Media.AmazonS3/Startup.cs
+++ b/src/OrchardCore.Modules/OrchardCore.Media.AmazonS3/Startup.cs
@@ -13,12 +13,15 @@
using OrchardCore.Environment.Shell.Configuration;
using OrchardCore.FileStorage;
using OrchardCore.FileStorage.AmazonS3;
+using OrchardCore.Media.AmazonS3.Services;
using OrchardCore.Media.Core;
using OrchardCore.Media.Core.Events;
using OrchardCore.Media.Events;
using OrchardCore.Modules;
using OrchardCore.Navigation;
using OrchardCore.Security.Permissions;
+using SixLabors.ImageSharp.Web.Caching;
+using SixLabors.ImageSharp.Web.Caching.AWS;
namespace OrchardCore.Media.AmazonS3;
@@ -38,7 +41,7 @@ public override void ConfigureServices(IServiceCollection services)
services.AddScoped();
services.AddTransient, AwsStorageOptionsConfiguration>();
- var storeOptions = new AwsStorageOptions().BindConfiguration(_configuration, _logger);
+ var storeOptions = new AwsStorageOptions().BindConfiguration(AmazonS3Constants.ConfigSections.AmazonS3, _configuration, _logger);
var validationErrors = storeOptions.Validate().ToList();
var stringBuilder = new StringBuilder();
@@ -132,3 +135,55 @@ public override void ConfigureServices(IServiceCollection services)
private static string GetMediaCachePath(IWebHostEnvironment hostingEnvironment, ShellSettings shellSettings, string assetsPath)
=> PathExtensions.Combine(hostingEnvironment.WebRootPath, shellSettings.Name, assetsPath);
}
+
+[Feature("OrchardCore.Media.AmazonS3.ImageSharpImageCache")]
+public class ImageSharpAmazonS3CacheStartup : Modules.StartupBase
+{
+ private readonly IShellConfiguration _configuration;
+ private readonly ILogger _logger;
+
+ public ImageSharpAmazonS3CacheStartup(
+ IShellConfiguration configuration,
+ ILogger logger)
+ {
+ _configuration = configuration;
+ _logger = logger;
+ }
+
+ // The order should exceed that of the 'OrchardCore.Media' module to substitute the default implementation of 'IImageCache'.
+ // there.
+ public override int Order => Media.MediaConstants.StartupOrder + 5;
+
+ public override void ConfigureServices(IServiceCollection services)
+ {
+ services.AddTransient, AwsImageSharpImageCacheOptionsConfiguration>();
+ services.AddTransient, AWSS3StorageCacheOptionsConfiguration>();
+
+ var storeOptions = new AwsStorageOptions().BindConfiguration(AmazonS3Constants.ConfigSections.AmazonS3ImageSharpCache, _configuration, _logger);
+ var validationErrors = storeOptions.Validate().ToList();
+ var stringBuilder = new StringBuilder();
+
+ if (validationErrors.Count > 0)
+ {
+ foreach (var error in validationErrors)
+ {
+ stringBuilder.Append(error.ErrorMessage);
+ }
+
+ _logger.LogError("S3 ImageSharp Image Cache configuration validation failed with errors: {Errors} fallback to local file storage.", stringBuilder);
+ }
+ else
+ {
+ _logger.LogInformation(
+ "Starting with ImageSharp Image Cache configuration. BucketName: {BucketName}; BasePath: {BasePath}", storeOptions.BucketName, storeOptions.BasePath);
+
+ // Following https://docs.sixlabors.com/articles/imagesharp.web/imagecaches.html we'd use
+ // SetCache() but that's only available on IImageSharpBuilder after AddImageSharp(),
+ // what happens in OrchardCore.Media. Thus, an explicit Replace() is necessary.
+ services.Replace(ServiceDescriptor.Singleton());
+
+ services.AddScoped();
+ }
+
+ }
+}
diff --git a/src/OrchardCore.Modules/OrchardCore.Media.Azure/Helpers/OptionsFluidParserHelper.cs b/src/OrchardCore.Modules/OrchardCore.Media.Azure/Helpers/OptionsFluidParserHelper.cs
new file mode 100644
index 00000000000..c99a83cf036
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Media.Azure/Helpers/OptionsFluidParserHelper.cs
@@ -0,0 +1,44 @@
+using Fluid;
+using OrchardCore.Environment.Shell;
+
+namespace OrchardCore.Media.Azure.Helpers;
+
+// This is almost the same as in OrchardCore.Media.Azure but there isn't really a good common place for it.
+internal sealed class OptionsFluidParserHelper where TOptions : class
+{
+ // Local instance since it can be discarded once the startup is over.
+ private readonly FluidParser _fluidParser = new();
+ private readonly ShellSettings _shellSettings;
+
+ private TemplateContext _templateContext;
+
+ public OptionsFluidParserHelper(ShellSettings shellSettings)
+ {
+ _shellSettings = shellSettings;
+ }
+
+ public string ParseAndFormat(string template)
+ {
+ var templateContext = GetTemplateContext();
+
+ // Use Fluid directly as this is transient and cannot invoke _liquidTemplateManager.
+ var parsedTemplate = _fluidParser.Parse(template);
+ return parsedTemplate.Render(templateContext, NullEncoder.Default)
+ .Replace("\r", string.Empty)
+ .Replace("\n", string.Empty);
+ }
+
+ private TemplateContext GetTemplateContext()
+ {
+ if (_templateContext == null)
+ {
+ var templateOptions = new TemplateOptions();
+ _templateContext = new TemplateContext(templateOptions);
+ templateOptions.MemberAccessStrategy.Register();
+ templateOptions.MemberAccessStrategy.Register();
+ _templateContext.SetValue("ShellSettings", _shellSettings);
+ }
+
+ return _templateContext;
+ }
+}
diff --git a/src/OrchardCore.Modules/OrchardCore.Media.Azure/ImageSharpBlobImageCacheOptions.cs b/src/OrchardCore.Modules/OrchardCore.Media.Azure/ImageSharpBlobImageCacheOptions.cs
new file mode 100644
index 00000000000..b8d3df1f91a
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Media.Azure/ImageSharpBlobImageCacheOptions.cs
@@ -0,0 +1,5 @@
+namespace OrchardCore.Media.Azure;
+
+public class ImageSharpBlobImageCacheOptions : MediaBlobStorageOptionsBase
+{
+}
diff --git a/src/OrchardCore.Modules/OrchardCore.Media.Azure/Manifest.cs b/src/OrchardCore.Modules/OrchardCore.Media.Azure/Manifest.cs
index 0449bb985a8..696ac3c848c 100644
--- a/src/OrchardCore.Modules/OrchardCore.Media.Azure/Manifest.cs
+++ b/src/OrchardCore.Modules/OrchardCore.Media.Azure/Manifest.cs
@@ -1,7 +1,7 @@
using OrchardCore.Modules.Manifest;
[assembly: Module(
- Name = "Microsoft Azure Media",
+ Name = "Azure Media",
Author = ManifestConstants.OrchardCoreTeam,
Website = ManifestConstants.OrchardCoreWebsite,
Version = ManifestConstants.OrchardCoreVersion
@@ -17,3 +17,14 @@
],
Category = "Hosting"
)]
+
+[assembly: Feature(
+ Id = "OrchardCore.Media.Azure.ImageSharpImageCache",
+ Name = "Azure Media ImageSharp Image Cache",
+ Description = "Enables support for storing cached images resized via ImageSharp in Microsoft Azure Blob Storage.",
+ Dependencies =
+ [
+ "OrchardCore.Media"
+ ],
+ Category = "Hosting"
+)]
diff --git a/src/OrchardCore.Modules/OrchardCore.Media.Azure/MediaBlobContainerTenantEvents.cs b/src/OrchardCore.Modules/OrchardCore.Media.Azure/MediaBlobContainerTenantEvents.cs
deleted file mode 100644
index 8d9905048f4..00000000000
--- a/src/OrchardCore.Modules/OrchardCore.Media.Azure/MediaBlobContainerTenantEvents.cs
+++ /dev/null
@@ -1,90 +0,0 @@
-using System.Threading.Tasks;
-using Azure;
-using Azure.Storage.Blobs;
-using Azure.Storage.Blobs.Models;
-using Microsoft.Extensions.Localization;
-using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Options;
-using OrchardCore.Environment.Shell;
-using OrchardCore.Environment.Shell.Removing;
-using OrchardCore.Modules;
-
-namespace OrchardCore.Media.Azure
-{
- public class MediaBlobContainerTenantEvents : ModularTenantEvents
- {
- private readonly MediaBlobStorageOptions _options;
- private readonly ShellSettings _shellSettings;
- protected readonly IStringLocalizer S;
- private readonly ILogger _logger;
-
- public MediaBlobContainerTenantEvents(
- IOptions options,
- ShellSettings shellSettings,
- IStringLocalizer localizer,
- ILogger logger
- )
- {
- _options = options.Value;
- _shellSettings = shellSettings;
- S = localizer;
- _logger = logger;
- }
-
- public override async Task ActivatingAsync()
- {
- // Only create container if options are valid.
- if (_shellSettings.IsUninitialized() ||
- string.IsNullOrEmpty(_options.ConnectionString) ||
- string.IsNullOrEmpty(_options.ContainerName) ||
- !_options.CreateContainer
- )
- {
- return;
- }
-
- _logger.LogDebug("Testing Azure Media Storage container {ContainerName} existence", _options.ContainerName);
-
- try
- {
- var _blobContainer = new BlobContainerClient(_options.ConnectionString, _options.ContainerName);
- var response = await _blobContainer.CreateIfNotExistsAsync(PublicAccessType.None);
-
- _logger.LogDebug("Azure Media Storage container {ContainerName} created.", _options.ContainerName);
- }
- catch (RequestFailedException ex)
- {
- _logger.LogError(ex, "Unable to create Azure Media Storage Container.");
- }
- }
-
- public override async Task RemovingAsync(ShellRemovingContext context)
- {
- // Only remove container if options are valid.
- if (!_options.RemoveContainer ||
- string.IsNullOrEmpty(_options.ConnectionString) ||
- string.IsNullOrEmpty(_options.ContainerName))
- {
- return;
- }
-
- try
- {
- var _blobContainer = new BlobContainerClient(_options.ConnectionString, _options.ContainerName);
-
- var response = await _blobContainer.DeleteIfExistsAsync();
- if (!response.Value)
- {
- _logger.LogError("Unable to remove the Azure Media Storage Container {ContainerName}.", _options.ContainerName);
- context.ErrorMessage = S["Unable to remove the Azure Media Storage Container '{0}'.", _options.ContainerName];
- }
- }
- catch (RequestFailedException ex)
- {
- _logger.LogError(ex, "Failed to remove the Azure Media Storage Container {ContainerName}.", _options.ContainerName);
- context.ErrorMessage = S["Failed to remove the Azure Media Storage Container '{0}'.", _options.ContainerName];
- context.Error = ex;
- }
- }
- }
-}
diff --git a/src/OrchardCore.Modules/OrchardCore.Media.Azure/MediaBlobStorageOptions.cs b/src/OrchardCore.Modules/OrchardCore.Media.Azure/MediaBlobStorageOptions.cs
index 46e1e1945a9..2d08de06bdc 100644
--- a/src/OrchardCore.Modules/OrchardCore.Media.Azure/MediaBlobStorageOptions.cs
+++ b/src/OrchardCore.Modules/OrchardCore.Media.Azure/MediaBlobStorageOptions.cs
@@ -1,17 +1,5 @@
-using OrchardCore.FileStorage.AzureBlob;
+namespace OrchardCore.Media.Azure;
-namespace OrchardCore.Media.Azure
+public class MediaBlobStorageOptions : MediaBlobStorageOptionsBase
{
- public class MediaBlobStorageOptions : BlobStorageOptions
- {
- ///
- /// Create blob container on startup if it does not exist.
- ///
- public bool CreateContainer { get; set; }
-
- ///
- /// Remove blob container on tenant removal if it exists.
- ///
- public bool RemoveContainer { get; set; }
- }
}
diff --git a/src/OrchardCore.Modules/OrchardCore.Media.Azure/MediaBlobStorageOptionsBase.cs b/src/OrchardCore.Modules/OrchardCore.Media.Azure/MediaBlobStorageOptionsBase.cs
new file mode 100644
index 00000000000..3ed7051c8bd
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Media.Azure/MediaBlobStorageOptionsBase.cs
@@ -0,0 +1,16 @@
+using OrchardCore.FileStorage.AzureBlob;
+
+namespace OrchardCore.Media.Azure;
+
+public abstract class MediaBlobStorageOptionsBase : BlobStorageOptions
+{
+ ///
+ /// Create a Blob container on startup if one does not exist.
+ ///
+ public bool CreateContainer { get; set; } = true;
+
+ ///
+ /// Remove Blob container on tenant removal if it exists.
+ ///
+ public bool RemoveContainer { get; set; }
+}
diff --git a/src/OrchardCore.Modules/OrchardCore.Media.Azure/MediaBlobStorageOptionsConfiguration.cs b/src/OrchardCore.Modules/OrchardCore.Media.Azure/MediaBlobStorageOptionsConfiguration.cs
deleted file mode 100644
index 522b5b5db25..00000000000
--- a/src/OrchardCore.Modules/OrchardCore.Media.Azure/MediaBlobStorageOptionsConfiguration.cs
+++ /dev/null
@@ -1,85 +0,0 @@
-using System;
-using Fluid;
-using Microsoft.Extensions.Configuration;
-using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Options;
-using OrchardCore.Environment.Shell;
-using OrchardCore.Environment.Shell.Configuration;
-
-namespace OrchardCore.Media.Azure
-{
- public class MediaBlobStorageOptionsConfiguration : IConfigureOptions
- {
- 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 MediaBlobStorageOptionsConfiguration(
- IShellConfiguration shellConfiguration,
- ShellSettings shellSettings,
- ILogger logger
- )
- {
- _shellConfiguration = shellConfiguration;
- _shellSettings = shellSettings;
- _logger = logger;
- }
-
- public void Configure(MediaBlobStorageOptions options)
- {
- var section = _shellConfiguration.GetSection("OrchardCore_Media_Azure");
-
- options.BasePath = section.GetValue(nameof(options.BasePath), string.Empty);
- options.ContainerName = section.GetValue(nameof(options.ContainerName), string.Empty);
- options.ConnectionString = section.GetValue(nameof(options.ConnectionString), string.Empty);
- options.CreateContainer = section.GetValue(nameof(options.CreateContainer), true);
- options.RemoveContainer = section.GetValue(nameof(options.RemoveContainer), false);
-
- var templateOptions = new TemplateOptions();
- var templateContext = new TemplateContext(templateOptions);
- templateOptions.MemberAccessStrategy.Register();
- templateOptions.MemberAccessStrategy.Register();
- templateContext.SetValue("ShellSettings", _shellSettings);
-
- ParseContainerName(options, templateContext);
- ParseBasePath(options, templateContext);
- }
-
- private void ParseContainerName(MediaBlobStorageOptions options, TemplateContext templateContext)
- {
- // Use Fluid directly as this is transient and cannot invoke _liquidTemplateManager.
- try
- {
- var template = _fluidParser.Parse(options.ContainerName);
-
- // container name must be lowercase
- options.ContainerName = template.Render(templateContext, NullEncoder.Default).ToLower();
- options.ContainerName = options.ContainerName.Replace("\r", string.Empty).Replace("\n", string.Empty);
- }
- catch (Exception e)
- {
- _logger.LogCritical(e, "Unable to parse Azure Media Storage container name.");
- throw;
- }
- }
-
- private void ParseBasePath(MediaBlobStorageOptions options, TemplateContext templateContext)
- {
- try
- {
- var template = _fluidParser.Parse(options.BasePath);
-
- options.BasePath = template.Render(templateContext, NullEncoder.Default);
- options.BasePath = options.BasePath.Replace("\r", string.Empty).Replace("\n", string.Empty);
- }
- catch (Exception e)
- {
- _logger.LogCritical(e, "Unable to parse Azure Media Storage base path.");
- throw;
- }
- }
- }
-}
diff --git a/src/OrchardCore.Modules/OrchardCore.Media.Azure/OrchardCore.Media.Azure.csproj b/src/OrchardCore.Modules/OrchardCore.Media.Azure/OrchardCore.Media.Azure.csproj
index ec4a798ea28..11b796956a9 100644
--- a/src/OrchardCore.Modules/OrchardCore.Media.Azure/OrchardCore.Media.Azure.csproj
+++ b/src/OrchardCore.Modules/OrchardCore.Media.Azure/OrchardCore.Media.Azure.csproj
@@ -23,6 +23,11 @@
+
+
+
+
+
diff --git a/src/OrchardCore.Modules/OrchardCore.Media.Azure/Services/AzureBlobStorageCacheOptionsConfiguration.cs b/src/OrchardCore.Modules/OrchardCore.Media.Azure/Services/AzureBlobStorageCacheOptionsConfiguration.cs
new file mode 100644
index 00000000000..873e81452dd
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Media.Azure/Services/AzureBlobStorageCacheOptionsConfiguration.cs
@@ -0,0 +1,23 @@
+using Microsoft.Extensions.Options;
+using SixLabors.ImageSharp.Web.Caching.Azure;
+
+namespace OrchardCore.Media.Azure.Services;
+
+// Configuration for ImageSharp's own configuration object. We just pass the settings from the Orchard Core
+// configuration.
+internal sealed class AzureBlobStorageCacheOptionsConfiguration : IConfigureOptions
+{
+ private readonly ImageSharpBlobImageCacheOptions _options;
+
+ public AzureBlobStorageCacheOptionsConfiguration(IOptions options)
+ {
+ _options = options.Value;
+ }
+
+ public void Configure(AzureBlobStorageCacheOptions options)
+ {
+ options.ConnectionString = _options.ConnectionString;
+ options.ContainerName = _options.ContainerName;
+ options.CacheFolder = _options.BasePath;
+ }
+}
diff --git a/src/OrchardCore.Modules/OrchardCore.Media.Azure/Services/ImageSharpBlobImageCacheOptionsConfiguration.cs b/src/OrchardCore.Modules/OrchardCore.Media.Azure/Services/ImageSharpBlobImageCacheOptionsConfiguration.cs
new file mode 100644
index 00000000000..45cb4eeda6f
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Media.Azure/Services/ImageSharpBlobImageCacheOptionsConfiguration.cs
@@ -0,0 +1,55 @@
+using System;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using OrchardCore.Environment.Shell;
+using OrchardCore.Environment.Shell.Configuration;
+using OrchardCore.Media.Azure.Helpers;
+
+namespace OrchardCore.Media.Azure.Services;
+
+internal sealed class ImageSharpBlobImageCacheOptionsConfiguration : IConfigureOptions
+{
+ private readonly IShellConfiguration _shellConfiguration;
+ private readonly ShellSettings _shellSettings;
+ private readonly ILogger _logger;
+
+ public ImageSharpBlobImageCacheOptionsConfiguration(
+ IShellConfiguration shellConfiguration,
+ ShellSettings shellSettings,
+ ILogger logger)
+ {
+ _shellConfiguration = shellConfiguration;
+ _shellSettings = shellSettings;
+ _logger = logger;
+ }
+
+ public void Configure(ImageSharpBlobImageCacheOptions options)
+ {
+ var section = _shellConfiguration.GetSection("OrchardCore_Media_Azure_ImageSharp_Cache");
+ section.Bind(options);
+
+ var fluidParserHelper = new OptionsFluidParserHelper(_shellSettings);
+
+ try
+ {
+ // Container name must be lowercase.
+ options.ContainerName = fluidParserHelper.ParseAndFormat(options.ContainerName).ToLowerInvariant();
+ }
+ catch (Exception ex)
+ {
+ _logger.LogCritical(ex, "Unable to parse Azure Media ImageSharp Image Cache container name.");
+ throw;
+ }
+
+ try
+ {
+ options.BasePath = fluidParserHelper.ParseAndFormat(options.BasePath);
+ }
+ catch (Exception e)
+ {
+ _logger.LogCritical(e, "Unable to parse Azure Media ImageSharp Image Cache base path.");
+ throw;
+ }
+ }
+}
diff --git a/src/OrchardCore.Modules/OrchardCore.Media.Azure/Services/ImageSharpBlobImageCacheTenantEvents.cs b/src/OrchardCore.Modules/OrchardCore.Media.Azure/Services/ImageSharpBlobImageCacheTenantEvents.cs
new file mode 100644
index 00000000000..ecd255ed6c7
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Media.Azure/Services/ImageSharpBlobImageCacheTenantEvents.cs
@@ -0,0 +1,82 @@
+using System.Threading.Tasks;
+using Azure;
+using Azure.Storage.Blobs;
+using Azure.Storage.Blobs.Models;
+using Microsoft.Extensions.Localization;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using OrchardCore.Environment.Shell;
+using OrchardCore.Environment.Shell.Removing;
+using OrchardCore.Modules;
+
+namespace OrchardCore.Media.Azure.Services;
+
+internal sealed class ImageSharpBlobImageCacheTenantEvents : ModularTenantEvents
+{
+ private readonly ImageSharpBlobImageCacheOptions _options;
+ private readonly ShellSettings _shellSettings;
+ private readonly ILogger _logger;
+
+ private readonly IStringLocalizer S;
+
+ public ImageSharpBlobImageCacheTenantEvents(
+ IOptions options,
+ ShellSettings shellSettings,
+ ILogger logger,
+ IStringLocalizer localizer)
+ {
+ _options = options.Value;
+ _shellSettings = shellSettings;
+ _logger = logger;
+ S = localizer;
+ }
+
+ public override async Task ActivatingAsync()
+ {
+ // Only create container if options are valid.
+ if (_shellSettings.IsUninitialized() || !_options.IsConfigured() || !_options.CreateContainer)
+ {
+ return;
+ }
+
+ _logger.LogDebug("Testing Azure Media ImageSharp Image Cache container {ContainerName} existence", _options.ContainerName);
+
+ try
+ {
+ var blobContainer = new BlobContainerClient(_options.ConnectionString, _options.ContainerName);
+ var response = await blobContainer.CreateIfNotExistsAsync(PublicAccessType.None);
+
+ _logger.LogDebug("Azure Media ImageSharp Image Cache container {ContainerName} created.", _options.ContainerName);
+ }
+ catch (RequestFailedException ex)
+ {
+ _logger.LogError(ex, "Unable to create Azure Media ImageSharp Image Cache Container.");
+ }
+ }
+
+ public override async Task RemovingAsync(ShellRemovingContext context)
+ {
+ // Only remove container if options are valid.
+ if (!_options.RemoveContainer || !_options.IsConfigured())
+ {
+ return;
+ }
+
+ try
+ {
+ var blobContainer = new BlobContainerClient(_options.ConnectionString, _options.ContainerName);
+ var response = await blobContainer.DeleteIfExistsAsync();
+ if (!response.Value)
+ {
+ _logger.LogError("Unable to remove the Azure Media ImageSharp Image Cache Container {ContainerName}.", _options.ContainerName);
+ context.ErrorMessage = S["Unable to remove the Azure Media ImageSharp Image Cache Container '{0}'.", _options.ContainerName];
+ }
+ }
+ catch (RequestFailedException ex)
+ {
+ _logger.LogError(ex, "Failed to remove the Azure Media ImageSharp Image Cache Container {ContainerName}.", _options.ContainerName);
+ context.ErrorMessage = S["Failed to remove the Azure Media ImageSharp Image Cache Container '{0}'.", _options.ContainerName];
+ context.Error = ex;
+ }
+ }
+}
diff --git a/src/OrchardCore.Modules/OrchardCore.Media.Azure/Services/MediaBlobContainerTenantEvents.cs b/src/OrchardCore.Modules/OrchardCore.Media.Azure/Services/MediaBlobContainerTenantEvents.cs
new file mode 100644
index 00000000000..3fd2d6568b8
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Media.Azure/Services/MediaBlobContainerTenantEvents.cs
@@ -0,0 +1,84 @@
+using System.Threading.Tasks;
+using Azure;
+using Azure.Storage.Blobs;
+using Azure.Storage.Blobs.Models;
+using Microsoft.Extensions.Localization;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using OrchardCore.Environment.Shell;
+using OrchardCore.Environment.Shell.Removing;
+using OrchardCore.Modules;
+
+namespace OrchardCore.Media.Azure.Services;
+
+public class MediaBlobContainerTenantEvents : ModularTenantEvents
+{
+ private readonly MediaBlobStorageOptions _options;
+ private readonly ShellSettings _shellSettings;
+ private readonly ILogger _logger;
+
+ protected readonly IStringLocalizer S;
+
+ public MediaBlobContainerTenantEvents(
+ IOptions options,
+ ShellSettings shellSettings,
+ IStringLocalizer localizer,
+ ILogger logger
+ )
+ {
+ _options = options.Value;
+ _shellSettings = shellSettings;
+ S = localizer;
+ _logger = logger;
+ }
+
+ public override async Task ActivatingAsync()
+ {
+ // Only create container if options are valid.
+ if (_shellSettings.IsUninitialized() || !_options.IsConfigured() || !_options.CreateContainer)
+ {
+ return;
+ }
+
+ _logger.LogDebug("Testing Azure Media Storage container {ContainerName} existence", _options.ContainerName);
+
+ try
+ {
+ var blobContainer = new BlobContainerClient(_options.ConnectionString, _options.ContainerName);
+ var response = await blobContainer.CreateIfNotExistsAsync(PublicAccessType.None);
+
+ _logger.LogDebug("Azure Media Storage container {ContainerName} created.", _options.ContainerName);
+ }
+ catch (RequestFailedException ex)
+ {
+ _logger.LogError(ex, "Unable to create Azure Media Storage Container.");
+ }
+ }
+
+ public override async Task RemovingAsync(ShellRemovingContext context)
+ {
+ // Only remove container if options are valid.
+ if (!_options.RemoveContainer || !_options.IsConfigured())
+ {
+ return;
+ }
+
+ try
+ {
+ var blobContainer = new BlobContainerClient(_options.ConnectionString, _options.ContainerName);
+
+ var response = await blobContainer.DeleteIfExistsAsync();
+ if (!response.Value)
+ {
+ _logger.LogError("Unable to remove the Azure Media Storage Container {ContainerName}.", _options.ContainerName);
+ context.ErrorMessage = S["Unable to remove the Azure Media Storage Container '{0}'.", _options.ContainerName];
+ }
+ }
+ catch (RequestFailedException ex)
+ {
+ _logger.LogError(ex, "Failed to remove the Azure Media Storage Container {ContainerName}.", _options.ContainerName);
+ context.ErrorMessage = S["Failed to remove the Azure Media Storage Container '{0}'.", _options.ContainerName];
+ context.Error = ex;
+ }
+ }
+}
diff --git a/src/OrchardCore.Modules/OrchardCore.Media.Azure/Services/MediaBlobStorageOptionsConfiguration.cs b/src/OrchardCore.Modules/OrchardCore.Media.Azure/Services/MediaBlobStorageOptionsConfiguration.cs
new file mode 100644
index 00000000000..0d5d243a116
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Media.Azure/Services/MediaBlobStorageOptionsConfiguration.cs
@@ -0,0 +1,55 @@
+using System;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using OrchardCore.Environment.Shell;
+using OrchardCore.Environment.Shell.Configuration;
+using OrchardCore.Media.Azure.Helpers;
+
+namespace OrchardCore.Media.Azure.Services;
+
+internal sealed class MediaBlobStorageOptionsConfiguration : IConfigureOptions
+{
+ private readonly IShellConfiguration _shellConfiguration;
+ private readonly ShellSettings _shellSettings;
+ private readonly ILogger _logger;
+
+ public MediaBlobStorageOptionsConfiguration(
+ IShellConfiguration shellConfiguration,
+ ShellSettings shellSettings,
+ ILogger logger
+ )
+ {
+ _shellConfiguration = shellConfiguration;
+ _shellSettings = shellSettings;
+ _logger = logger;
+ }
+
+ public void Configure(MediaBlobStorageOptions options)
+ {
+ _shellConfiguration.GetSection("OrchardCore_Media_Azure").Bind(options);
+
+ var fluidParserHelper = new OptionsFluidParserHelper(_shellSettings);
+
+ try
+ {
+ // Container name must be lowercase.
+ options.ContainerName = fluidParserHelper.ParseAndFormat(options.ContainerName).ToLowerInvariant();
+ }
+ catch (Exception e)
+ {
+ _logger.LogCritical(e, "Unable to parse Azure Media Storage container name.");
+ throw;
+ }
+
+ try
+ {
+ options.BasePath = fluidParserHelper.ParseAndFormat(options.BasePath);
+ }
+ catch (Exception e)
+ {
+ _logger.LogCritical(e, "Unable to parse Azure Media Storage base path.");
+ throw;
+ }
+ }
+}
diff --git a/src/OrchardCore.Modules/OrchardCore.Media.Azure/Startup.cs b/src/OrchardCore.Modules/OrchardCore.Media.Azure/Startup.cs
index 435dc5f42cb..f7f37542ec4 100644
--- a/src/OrchardCore.Modules/OrchardCore.Media.Azure/Startup.cs
+++ b/src/OrchardCore.Modules/OrchardCore.Media.Azure/Startup.cs
@@ -3,6 +3,7 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.StaticFiles;
+using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
@@ -11,12 +12,15 @@
using OrchardCore.Environment.Shell.Configuration;
using OrchardCore.FileStorage;
using OrchardCore.FileStorage.AzureBlob;
+using OrchardCore.Media.Azure.Services;
using OrchardCore.Media.Core;
using OrchardCore.Media.Core.Events;
using OrchardCore.Media.Events;
using OrchardCore.Modules;
using OrchardCore.Navigation;
using OrchardCore.Security.Permissions;
+using SixLabors.ImageSharp.Web.Caching;
+using SixLabors.ImageSharp.Web.Caching.Azure;
namespace OrchardCore.Media.Azure
{
@@ -32,7 +36,7 @@ public Startup(ILogger logger, IShellConfiguration configuration)
_configuration = configuration;
}
- public override int Order => 10;
+ public override int Order => MediaConstants.StartupOrder + 10;
public override void ConfigureServices(IServiceCollection services)
{
@@ -41,8 +45,9 @@ public override void ConfigureServices(IServiceCollection services)
services.AddTransient, MediaBlobStorageOptionsConfiguration>();
// Only replace default implementation if options are valid.
- var connectionString = _configuration[$"OrchardCore_Media_Azure:{nameof(MediaBlobStorageOptions.ConnectionString)}"];
- var containerName = _configuration[$"OrchardCore_Media_Azure:{nameof(MediaBlobStorageOptions.ContainerName)}"];
+ var section = _configuration.GetSection("OrchardCore_Media_Azure");
+ var connectionString = section.GetValue(nameof(MediaBlobStorageOptions.ConnectionString));
+ var containerName = section.GetValue(nameof(MediaBlobStorageOptions.ContainerName));
if (CheckOptions(connectionString, containerName, _logger))
{
@@ -136,4 +141,67 @@ private static bool CheckOptions(string connectionString, string containerName,
return optionsAreValid;
}
}
+
+ [Feature("OrchardCore.Media.Azure.ImageSharpImageCache")]
+ public class ImageSharpAzureBlobCacheStartup : Modules.StartupBase
+ {
+ private readonly IShellConfiguration _configuration;
+ private readonly ILogger _logger;
+
+ public ImageSharpAzureBlobCacheStartup(
+ IShellConfiguration configuration,
+ ILogger logger)
+ {
+ _configuration = configuration;
+ _logger = logger;
+ }
+
+ // The order should exceed that of the 'OrchardCore.Media' module to substitute the default implementation of 'IImageCache'.
+ // there.
+ public override int Order => MediaConstants.StartupOrder + 5;
+
+ public override void ConfigureServices(IServiceCollection services)
+ {
+ services.AddTransient, ImageSharpBlobImageCacheOptionsConfiguration>();
+ services.AddTransient, AzureBlobStorageCacheOptionsConfiguration>();
+
+ // Only replace default implementation if options are valid.
+ var section = _configuration.GetSection("OrchardCore_Media_Azure_ImageSharp_Cache");
+ var connectionString = section.GetValue(nameof(MediaBlobStorageOptions.ConnectionString));
+ var containerName = section.GetValue(nameof(MediaBlobStorageOptions.ContainerName));
+
+ if (!CheckOptions(connectionString, containerName))
+ {
+ return;
+ }
+
+ // Following https://docs.sixlabors.com/articles/imagesharp.web/imagecaches.html we'd use
+ // SetCache() but that's only available on IImageSharpBuilder after AddImageSharp(),
+ // what happens in OrchardCore.Media. Thus, an explicit Replace() is necessary.
+ services.Replace(ServiceDescriptor.Singleton());
+
+ services.AddScoped();
+ }
+
+ private bool CheckOptions(string connectionString, string containerName)
+ {
+ var optionsAreValid = true;
+
+ if (string.IsNullOrWhiteSpace(connectionString))
+ {
+ _logger.LogError(
+ "Azure Media ImageSharp Image Cache is enabled but not active because the 'ConnectionString' is missing or empty in application configuration.");
+ optionsAreValid = false;
+ }
+
+ if (string.IsNullOrWhiteSpace(containerName))
+ {
+ _logger.LogError(
+ "Azure Media ImageSharp Image Cache is enabled but not active because the 'ContainerName' is missing or empty in application configuration.");
+ optionsAreValid = false;
+ }
+
+ return optionsAreValid;
+ }
+ }
}
diff --git a/src/OrchardCore.Modules/OrchardCore.Media/MediaConstants.cs b/src/OrchardCore.Modules/OrchardCore.Media/MediaConstants.cs
new file mode 100644
index 00000000000..3584f67cce2
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Media/MediaConstants.cs
@@ -0,0 +1,10 @@
+namespace OrchardCore.Media;
+
+public static class MediaConstants
+{
+ ///
+ /// The value the Order set by . If you want to run a Startup class before or after
+ /// that of the Media module's, you can use this value as the starting point.
+ ///
+ public const int StartupOrder = 0;
+}
diff --git a/src/OrchardCore.Modules/OrchardCore.Media/Startup.cs b/src/OrchardCore.Modules/OrchardCore.Media/Startup.cs
index 58c196e338b..01e4fd1da3a 100644
--- a/src/OrchardCore.Modules/OrchardCore.Media/Startup.cs
+++ b/src/OrchardCore.Modules/OrchardCore.Media/Startup.cs
@@ -55,6 +55,8 @@ namespace OrchardCore.Media
{
public class Startup : StartupBase
{
+ public override int Order => MediaConstants.StartupOrder;
+
private const string ImageSharpCacheFolder = "is-cache";
private readonly ShellSettings _shellSettings;
diff --git a/src/OrchardCore/OrchardCore.FileStorage.AmazonS3/AwsImageSharpImageCacheOptions.cs b/src/OrchardCore/OrchardCore.FileStorage.AmazonS3/AwsImageSharpImageCacheOptions.cs
new file mode 100644
index 00000000000..cb7396ef974
--- /dev/null
+++ b/src/OrchardCore/OrchardCore.FileStorage.AmazonS3/AwsImageSharpImageCacheOptions.cs
@@ -0,0 +1,8 @@
+namespace OrchardCore.FileStorage.AmazonS3;
+
+///
+/// Configuration for ImageSharp's AWSS3StorageCache.
+///
+public class AwsImageSharpImageCacheOptions : AwsStorageOptionsBase
+{
+}
diff --git a/src/OrchardCore/OrchardCore.FileStorage.AmazonS3/AwsStorageOptions.cs b/src/OrchardCore/OrchardCore.FileStorage.AmazonS3/AwsStorageOptions.cs
index a7c8022fd67..dc7fa85c2db 100644
--- a/src/OrchardCore/OrchardCore.FileStorage.AmazonS3/AwsStorageOptions.cs
+++ b/src/OrchardCore/OrchardCore.FileStorage.AmazonS3/AwsStorageOptions.cs
@@ -1,34 +1,8 @@
-using Amazon.Extensions.NETCore.Setup;
-
namespace OrchardCore.FileStorage.AmazonS3;
///
/// AWS storage options.
///
-public class AwsStorageOptions
+public class AwsStorageOptions : AwsStorageOptionsBase
{
- ///
- /// AWS S3 bucket name.
- ///
- public string BucketName { get; set; }
-
- ///
- /// The base directory path to use inside the container for this store's contents.
- ///
- public string BasePath { get; set; }
-
- ///
- /// Indicates if bucket should be created on module startup.
- ///
- public bool CreateBucket { get; set; }
-
- ///
- /// Gets or sets the AWS Options.
- ///
- public AWSOptions AwsOptions { get; set; }
-
- ///
- /// Indicates if bucket should be removed on tenant removal.
- ///
- public bool RemoveBucket { get; set; }
}
diff --git a/src/OrchardCore/OrchardCore.FileStorage.AmazonS3/AwsStorageOptionsBase.cs b/src/OrchardCore/OrchardCore.FileStorage.AmazonS3/AwsStorageOptionsBase.cs
new file mode 100644
index 00000000000..7a52c29f040
--- /dev/null
+++ b/src/OrchardCore/OrchardCore.FileStorage.AmazonS3/AwsStorageOptionsBase.cs
@@ -0,0 +1,31 @@
+using Amazon.Extensions.NETCore.Setup;
+
+namespace OrchardCore.FileStorage.AmazonS3;
+
+public abstract class AwsStorageOptionsBase
+{
+ ///
+ /// AWS S3 bucket name.
+ ///
+ public string BucketName { get; set; }
+
+ ///
+ /// The base directory path to use inside the container for this store's contents.
+ ///
+ public string BasePath { get; set; }
+
+ ///
+ /// Indicates if bucket should be created on module startup.
+ ///
+ public bool CreateBucket { get; set; }
+
+ ///
+ /// Gets or sets the AWS Options.
+ ///
+ public AWSOptions AwsOptions { get; set; }
+
+ ///
+ /// Indicates if bucket should be removed on tenant removal.
+ ///
+ public bool RemoveBucket { get; set; }
+}
diff --git a/src/OrchardCore/OrchardCore.FileStorage.AzureBlob/BlobStorageOptions.cs b/src/OrchardCore/OrchardCore.FileStorage.AzureBlob/BlobStorageOptions.cs
index 24130b7f512..2b94e2c8971 100644
--- a/src/OrchardCore/OrchardCore.FileStorage.AzureBlob/BlobStorageOptions.cs
+++ b/src/OrchardCore/OrchardCore.FileStorage.AzureBlob/BlobStorageOptions.cs
@@ -13,8 +13,16 @@ public abstract class BlobStorageOptions
public string ContainerName { get; set; }
///
- /// The base directory path to use inside the container for this stores contents.
+ /// The base directory path to use inside the container for this store's content.
///
- public string BasePath { get; set; }
+ public string BasePath { get; set; } = "";
+
+ ///
+ /// Returns a value indicating whether the basic state of the configuration is valid.
+ ///
+ public virtual bool IsConfigured()
+ {
+ return !string.IsNullOrEmpty(ConnectionString) && !string.IsNullOrEmpty(ContainerName);
+ }
}
}
diff --git a/src/docs/reference/README.md b/src/docs/reference/README.md
index 4a3c8d8a045..47b3823cc12 100644
--- a/src/docs/reference/README.md
+++ b/src/docs/reference/README.md
@@ -138,7 +138,7 @@ Here's a categorized overview of all built-in Orchard Core features at a glance.
### Search, Indexing, Querying
- [Azure AI Search](modules/AzureAISearch/README.md)
-- [SQL](modules/SQLIndexing/README.md)
+- [SQL Indexing](modules/SQLIndexing/README.md)
- [Lucene](modules/Lucene/README.md)
- [Elasticsearch](modules/Elasticsearch/README.md)
- [Queries](modules/Queries/README.md)
diff --git a/src/docs/reference/modules/Media.AmazonS3/README.md b/src/docs/reference/modules/Media.AmazonS3/README.md
index 678c47683b1..1baf8aec776 100644
--- a/src/docs/reference/modules/Media.AmazonS3/README.md
+++ b/src/docs/reference/modules/Media.AmazonS3/README.md
@@ -1,6 +1,8 @@
-# Amazon S3 Media Storage (`OrchardCore.Media.AmazonS3`)
+# Amazon S3 Media (`OrchardCore.Media.AmazonS3`)
-The Amazon Media Storage feature enables support for storing assets in Amazon S3 Bucket.
+The Amazon S3 Media module enables support for storing assets in Amazon S3 Buckets.
+
+## Amazon S3 Media Storage (`OrchardCore.Media.AmazonS3`)
The feature replaces the default App_Data file-based media store with an Amazon Media Storage Provider.
@@ -10,7 +12,7 @@ This allows the Amazon Media Storage feature to support image resizing on the fl
The URL generated by the AssetUrl helpers points to the Orchard Core website.
-## Configuration
+### Configuration
The following configuration values are used by default and can be customized:
@@ -49,11 +51,11 @@ In case you are hosting Orchard Core outside of AWS, you should fill the `Creden
You can find region endpoints in the [Official AWS S3 Documentation](https://docs.aws.amazon.com/general/latest/gr/s3.html), see Region column. For example for the Frankfurt region you should use `eu-central-1`
-## AWS Credentials and its loading order
+### AWS Credentials and its loading order
`OrchardCore_Media_AmazonS3` is a subset of `AWSOptions` configuration and should be configured the same as a generic [AWSOptions](https://docs.aws.amazon.com/sdk-for-net/v3/developer-guide/net-dg-config-netcore.html).
-### Credentials loading order
+#### Credentials loading order
1. Credentials property of `AWSOptions˙.
2. Shared Credentials File (Custom Location). When both the profile and profile location are specified.
@@ -70,7 +72,7 @@ You can find region endpoints in the [Official AWS S3 Documentation](https://doc
The AWS team wants to encourage using profiles instead of embedding credentials directly into `appsettings.X.json` files where they would accidentally get checked into source control.
If you have an option to use profiles or environment variables - you should use it instead of direct credentials.
-## AWS S3 Bucket Configuration
+### AWS S3 Bucket Configuration
If `CreateBucket` was configured as `true` and `BucketName` follows official [Bucket naming rules](https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucketnamingrules.html), then a new bucket will be created.
The new bucket will be created without [Access Control Lists](https://docs.aws.amazon.com/AmazonS3/latest/userguide/acl-overview.html) due to security reasons. If you create the bucket manually then you need to do it with ACLs enabled. When using a previously created bucket, you may need to configure ACLs manually:
@@ -80,7 +82,7 @@ The new bucket will be created without [Access Control Lists](https://docs.aws.a
3. Edit "Block public access".
4. Tick "Block all public access".
-### S3 Bucket policies
+#### S3 Bucket policies
By default, AWS 3S Bucket has limitations for newly uploaded files. If you want media files to be available from the outside of AWS, you should set up your bucket permissions.
@@ -103,7 +105,7 @@ The simplest way of doing it is to add a policy:
After this policy will be added to your bucket permissions all newly added files will have Read permission and will be available from the outside of the Amazon Cloud.
-## Templating Configuration
+### Templating Configuration
Optionally you may use Liquid templating to further configure Amazon Media Storage, perhaps creating a bucket per tenant,
or a single bucket with a base path per tenant.
@@ -114,7 +116,7 @@ The `BucketName` property and the `BasePath` property are the only templatable p
!!! note
When templating the `BucketName` using `{{ ShellSettings.Name }}`, the tenant's name will be automatically lowercased, however, you must also make sure the `BucketName` conforms to other Amazon S3 naming conventions as set out in Amazon's documentation.
-### Configuring a bucket per tenant
+#### Configuring a bucket per tenant
```json
{
@@ -134,7 +136,7 @@ The `BucketName` property and the `BasePath` property are the only templatable p
}
```
-### Configuring a single bucket, with a base folder per tenant
+#### Configuring a single bucket, with a base folder per tenant
```json
{
@@ -199,7 +201,7 @@ docker run -d -e MODE=IN_MEMORY -p 9444:80 luofuxiang/local-s3
docker run -p 9444:9090 -t adobe/s3mock
```
-## Media Cache
+### Media Cache
The Media Cache feature will automatically be enabled when Amazon Media Storage is enabled.
@@ -217,3 +219,40 @@ CDN providers also clear their caches at pre-determined times of their own devis
!!! note
The Media Feature is designed to support one storage provider at a time, whether that is
local File Storage (the default), Azure Blob Storage, or Amazon S3 Storage.
+
+## Amazon Media ImageSharp Image Cache (`OrchardCore.Media.AmazonS3.ImageSharpImageCache`)
+
+The feature replaces the default `PhysicalFileSystemCache` of ImageSharp that stores resized images in the `App_Data` folder with [`AWSS3StorageCache`](https://docs.sixlabors.com/articles/imagesharp.web/imagecaches.html#awss3storagecache) that stores them in Amazon S3 storage. Depending on your use case, this can provide the following advantages:
+
+- Persistent image cache not to have to repeatedly resize images, even if the local file system of the webserver is ephemeral. This helps if you e.g. use containers to host the app, or do clean deployments to the webserver that remove all previously existing files.
+- Better performance if disk IO is a bottleneck: The local storage of the webserver may be slow or access to it deliberately throttled. Using S3 can alleviate pressure on the local disk, leaving more resources available to serve other requests, as well as offer a higher request per second limit for image requests.
+
+!!! note
+ Cache files are only removed for a tenant when removing the tenant itself if you use a separate bucket for each tenant.
+
+### Configuration
+
+The following configuration values are used by default and can be customized:
+
+```json
+{
+ "OrchardCore": {
+ "OrchardCore_Media_AmazonS3_ImageSharp_Cache": {
+ "Region": "eu-central-1",
+ "Profile": "default",
+ "ProfilesLocation": "",
+ "Credentials": {
+ "SecretKey": "",
+ "AccessKey": ""
+ },
+ "BasePath": "/media", // Optionally, set to a path to store media in a subdirectory inside your bucket.
+ "CreateBucket": true,
+ "RemoveBucket": true, // Whether the 'Bucket' is deleted if the tenant is removed, false by default.
+ "BucketName": "imagesharp" // Set the bucket's name (mandatory).
+ },
+ }
+}
+```
+
+!!! note
+ Templating the configuration and configuring a bucket per tenant, as well as using a local emulator work the same way as for Amazon S3 Media Storage; follow its documentation above.
diff --git a/src/docs/reference/modules/Media.Azure/README.md b/src/docs/reference/modules/Media.Azure/README.md
index 411a2393c63..57015578b2d 100644
--- a/src/docs/reference/modules/Media.Azure/README.md
+++ b/src/docs/reference/modules/Media.Azure/README.md
@@ -1,6 +1,8 @@
-# Azure Media Storage (`OrchardCore.Media.Azure`)
+# Azure Media (`OrchardCore.Media.Azure`)
-The Azure Media Storage feature enables support for storing assets in Microsoft Azure Blob Storage.
+The Azure Media module enables support for storing assets in Microsoft Azure Blob Storage.
+
+## Azure Media Storage (`OrchardCore.Media.Azure.Storage`)
The feature replaces the default `App_Data` file based media store with an Azure Media Storage Provider.
@@ -11,13 +13,13 @@ This allows the Azure Media Storage feature to support image resizing on the fly
The url generated by the `AssetUrl` helpers, points to the Orchard Core web site.
-## Configuration
+### Configuration
The following configuration values are used by default and can be customized:
```json
{
- "OrchardCore": {
+ "OrchardCore": {
"OrchardCore_Media_Azure": {
// Set to your Azure Storage account connection string.
"ConnectionString": "",
@@ -25,7 +27,10 @@ The following configuration values are used by default and can be customized:
"ContainerName": "somecontainer",
// Optionally, set to a path to store media in a subdirectory inside your container.
"BasePath": "some/base/path",
- "CreateContainer": true
+ // Activates an event to create the container if it does not already exist.
+ "CreateContainer": true,
+ // Whether the 'Container' is deleted if the tenant is removed, false by default.
+ "RemoveContainer": true
}
}
}
@@ -40,7 +45,7 @@ Set `CreateContainer` to `false` to disable this check if your container already
If these are not present in `appSettings.json`, it will not enable the feature, and report an error message in the log file.
-## Templating Configuration
+### Templating Configuration
Optionally you may use liquid templating to further configure Azure Media Storage, perhaps creating a container per tenant,
or a single container with a base path per tenant.
@@ -51,7 +56,7 @@ The `ContainerName` property and the `BasePath` property are the only templatabl
!!! note
When templating the `ContainerName` using `{{ ShellSettings.Name }}`, the tenant's name will be automatically lowercased, however, you must also make sure the `ContainerName` conforms to other Azure Blob naming conventions as set out in Azure's documentation.
-### Configuring a container per tenant
+#### Configuring a container per tenant
```json
{
@@ -69,7 +74,7 @@ The `ContainerName` property and the `BasePath` property are the only templatabl
}
```
-### Configuring a single container, with a base folder per tenant
+#### Configuring a single container, with a base folder per tenant
```json
{
@@ -91,7 +96,7 @@ The `ContainerName` property and the `BasePath` property are the only templatabl
Only the default Liquid filters and tags are available during parsing of the Liquid template.
Extra filters like `slugify` will not be available.
-## Media Cache
+### Media Cache
The Media Cache feature will automatically be enabled when Azure Media Storage is enabled.
@@ -115,3 +120,39 @@ re-fetch the source file, as and when required, which the Media Cache Module wil
!!! note
The Media Feature is designed to support one storage provider at a time, whether that is
local File Storage (the default), Azure Blob Storage, or Amazon S3 Storage.
+
+## Azure Media ImageSharp Image Cache (`OrchardCore.Media.Azure.ImageSharpImageCache`)
+
+The feature replaces the default `PhysicalFileSystemCache` of ImageSharp that stores resized images in the `App_Data` folder with [`AzureBlobStorageImageCache`](https://docs.sixlabors.com/articles/imagesharp.web/imagecaches.html#azureblobstorageimagecache) that stores them in Azure Blob Storage. Depending on your use case, this can provide the following advantages:
+
+- Persistent image cache not to have to repeatedly resize images, even if the local file system of the webserver is ephemeral. This helps if you e.g. use containers to host the app, or do clean deployments to the webserver that remove all previously existing files.
+- Better performance if disk IO is a bottleneck: The local storage of the webserver may be slow or access to it deliberately throttled, like it is the case with Azure App Services. Using Blob Storage can alleviate pressure on the local disk, leaving more resources available to serve other requests, as well as offer a higher request per second limit for image requests.
+
+!!! note
+ Cache files are only removed for a tenant when removing the tenant itself if you use a separate container for each tenant.
+
+### Configuration
+
+The following configuration values are used by default and can be customized:
+
+```json
+{
+ "OrchardCore": {
+ "OrchardCore_Media_Azure_ImageSharp_Cache": {
+ // Set to your Azure Storage account connection string.
+ "ConnectionString": "",
+ // Set to the Azure Blob container name. A container name must be a valid DNS name and conform to Azure container naming rules eg. lowercase only.
+ "ContainerName": "somecontainer",
+ // Optionally, set to a path to store media in a subdirectory inside your container.
+ "BasePath": "some/base/path",
+ // Activates an event to create the container if it does not already exist.
+ "CreateContainer": true,
+ // Whether the 'Container' is deleted if the tenant is removed, false by default.
+ "RemoveContainer": true
+ }
+ }
+}
+```
+
+!!! note
+ Templating the configuration and configuring a container per tenant work the same way as for Azure Media Storage; follow its documentation above.
\ No newline at end of file
diff --git a/src/docs/releases/1.9.0.md b/src/docs/releases/1.9.0.md
index cf158fd7cfc..8c334fe1fda 100644
--- a/src/docs/releases/1.9.0.md
+++ b/src/docs/releases/1.9.0.md
@@ -233,6 +233,10 @@ These adjustments ensure compatibility and adherence to the latest conventions w
Introducing a new "Azure AI Search" module, designed to empower you in the administration of Azure AI Search indices. When enabled with the "Search" module, it facilitates frontend full-text search capabilities through Azure AI Search. For more info read the [Azure AI Search](../reference/modules/AzureAISearch/README.md) docs.
+### New ImageSharp Image Caches for Azure Blob and AWS S3 Storage
+
+The Microsoft Azure Media and Amazon S3 Media modules have new features for replacing the default `PhysicalFileSystemCache` of ImageSharp that stores resized images in the local `App_Data` folder. Instead, you can now use Azure Blob Storage with the Azure Media ImageSharp Image Cache feature (that utilizes [`AzureBlobStorageImageCache`](https://docs.sixlabors.com/articles/imagesharp.web/imagecaches.html#azureblobstorageimagecache)), and AWS S3 with the Amazon Media ImageSharp Image Cache feature (that utilizes [`AWSS3StorageCache`](https://docs.sixlabors.com/articles/imagesharp.web/imagecaches.html#awss3storagecache)). Depending on your use case, this can provide various advantages. Check out [the Azure Media](../reference/modules/Media.Azure/README.md) and [the Amazon S3 Media](../reference/modules/Media.AmazonS3/README.md) docs for details.
+
### Deployment Module
Added new extensions to make registering custom deployment step easier: