Skip to content

Commit

Permalink
fixes OrchardCMS#12639 added BlobFileStoreFactory
Browse files Browse the repository at this point in the history
This allows to use registered clients from Azure.Configuration.Extensions  (and DefaultAzureCredential )or
specify a connectionstring
  • Loading branch information
rikbosch committed Nov 23, 2022
1 parent 073a35b commit a3b198c
Show file tree
Hide file tree
Showing 8 changed files with 116 additions and 16 deletions.
10 changes: 5 additions & 5 deletions src/OrchardCore.Modules/OrchardCore.Media.Azure/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,22 +95,22 @@ public override void ConfigureServices(IServiceCollection services)
services.AddSingleton<IMediaFileStoreCache>(serviceProvider =>
serviceProvider.GetRequiredService<IMediaFileStoreCacheFileProvider>());

// Register the BlobFileStorageFactory
services.AddAzureBlobFileStorage();

// Replace the default media file store with a blob file store.
services.Replace(ServiceDescriptor.Singleton<IMediaFileStore>(serviceProvider =>
{
var blobStorageOptions = serviceProvider.GetRequiredService<IOptions<MediaBlobStorageOptions>>().Value;
var shellOptions = serviceProvider.GetRequiredService<IOptions<ShellOptions>>();
var shellSettings = serviceProvider.GetRequiredService<ShellSettings>();
var mediaOptions = serviceProvider.GetRequiredService<IOptions<MediaOptions>>().Value;
var clock = serviceProvider.GetRequiredService<IClock>();
var contentTypeProvider = serviceProvider.GetRequiredService<IContentTypeProvider>();
var mediaEventHandlers = serviceProvider.GetServices<IMediaEventHandler>();
var mediaCreatingEventHandlers = serviceProvider.GetServices<IMediaCreatingEventHandler>();
var logger = serviceProvider.GetRequiredService<ILogger<DefaultMediaFileStore>>();
var blobStoreFactory = serviceProvider.GetRequiredService<BlobFileStoreFactory>();
var fileStore = new BlobFileStore(blobStorageOptions, clock, contentTypeProvider);
var mediaPath = GetMediaPath(shellOptions.Value, shellSettings, mediaOptions.AssetsPath);
var fileStore = blobStoreFactory.Create(blobStorageOptions);
var mediaUrlBase = "/" + fileStore.Combine(shellSettings.RequestUrlPrefix, mediaOptions.AssetsRequestPath);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using Microsoft.Extensions.DependencyInjection;

namespace OrchardCore.FileStorage.AzureBlob;
public static class AzureBlobFileStorageOrchardCoreBuilderExtensions
{
/// <summary>
/// This registers the AzureBlobFileStorage components.
/// Note: this method is safe to call more then once.
/// </summary>
/// <param name="builder"></param>
/// <returns></returns>
public static OrchardCoreBuilder AddAzureBlobFileStorage(this OrchardCoreBuilder builder)
{
builder.ConfigureServices(services => services.AddAzureBlobFileStorage());

return builder;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;

namespace OrchardCore.FileStorage.AzureBlob;

public static class AzureBlobFileStorageServiceCollectionExtensions
{
/// <summary>
/// Registers the BlobFileStorage services in the ServiceCollection.
/// Note: this method can be called multiple times
/// </summary>
/// <param name="services"></param>
/// <returns></returns>
public static IServiceCollection AddAzureBlobFileStorage(this IServiceCollection services)
{
// always use TryXXX methods because this method can be called multiple times
// by different modules (currently: Azure Media and Azure Shells module)
services.TryAddSingleton<BlobFileStoreFactory>();
return services;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Azure.Storage.Blobs;
using Azure.Storage.Blobs.Models;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.Options;
using OrchardCore.Modules;

namespace OrchardCore.FileStorage.AzureBlob
Expand Down Expand Up @@ -39,23 +40,21 @@ public class BlobFileStore : IFileStore
{
private const string _directoryMarkerFileName = "OrchardCore.Media.txt";

private readonly BlobStorageOptions _options;
private readonly IClock _clock;
private readonly BlobContainerClient _blobContainer;
private readonly IContentTypeProvider _contentTypeProvider;
private readonly string _basePrefix = null;

public BlobFileStore(BlobStorageOptions options, IClock clock, IContentTypeProvider contentTypeProvider)
public BlobFileStore(BlobContainerClient blobContainerClient, string basePath, IClock clock, IContentTypeProvider contentTypeProvider)
{
_options = options;
_clock = clock;
_contentTypeProvider = contentTypeProvider;

_blobContainer = new BlobContainerClient(_options.ConnectionString, _options.ContainerName);
_blobContainer = blobContainerClient;

if (!String.IsNullOrEmpty(_options.BasePath))
if (!String.IsNullOrEmpty(basePath))
{
_basePrefix = NormalizePrefix(_options.BasePath);
_basePrefix = NormalizePrefix(basePath);
}
}

Expand All @@ -69,7 +68,7 @@ public async Task<IFileStoreEntry> GetFileInfoAsync(string path)

return new BlobFile(path, properties.Value.ContentLength, properties.Value.LastModified);
}
catch (RequestFailedException ex) when (ex.ErrorCode == BlobErrorCode.BlobNotFound)
catch (RequestFailedException ex) when (ex.ErrorCode == BlobErrorCode.BlobNotFound)
{
// Instead of ExistsAsync() check which is 'slow' if we're expecting to find the blob we rely on the exception
return null;
Expand Down Expand Up @@ -402,7 +401,7 @@ public async Task<string> CreateFileFromStreamAsync(string path, Stream inputStr

private BlobClient GetBlobReference(string path)
{
var blobPath = this.Combine(_options.BasePath, path);
var blobPath = this.Combine(_basePrefix, path);
var blob = _blobContainer.GetBlobClient(blobPath);

return blob;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using System;
using Azure.Storage.Blobs;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.Azure;
using Microsoft.Extensions.DependencyInjection;
using OrchardCore.Modules;

namespace OrchardCore.FileStorage.AzureBlob;
public class BlobFileStoreFactory

This comment has been minimized.

Copy link
@goldsam

goldsam Nov 23, 2022

Would it make sense for this to be an interface so that it can be replaced? I see that the Create method is virtual, but using an interface makes fewer assumptions and is cleaner.

P.S. Good work!

{
private readonly IAzureClientFactory<BlobServiceClient> _clientFactory;
private readonly IServiceProvider _serviceProvider;

public BlobFileStoreFactory(IAzureClientFactory<BlobServiceClient> clientFactory, IServiceProvider serviceProvider)
{
_clientFactory = clientFactory;
_serviceProvider = serviceProvider;
}

/// <summary>
/// Creates a <see cref="BlobFileStore"/> from <see cref="BlobStorageOptions"/>
/// If a connectionstring is specified, it will use the connectionstring
/// otherwise it will try to get the BlobServiceClient configured with the specified name in the BlobServiceName property
/// </summary>
/// <param name="options"></param>
/// <returns></returns>
public virtual BlobFileStore Create(BlobStorageOptions options)
{
var containerClient = CreateContainerClientFromOptions(options);

return new BlobFileStore(
containerClient,
options.BasePath,
_serviceProvider.GetRequiredService<IClock>(),
_serviceProvider.GetRequiredService<IContentTypeProvider>());
}


private BlobContainerClient CreateContainerClientFromOptions(BlobStorageOptions options)
{
if (!String.IsNullOrEmpty(options.ConnectionString))
{
return new BlobContainerClient(options.ConnectionString, options.ContainerName);
}

var serviceClient = _clientFactory.CreateClient(options.BlobServiceName);
return serviceClient.GetBlobContainerClient(options.ContainerName);
}

}
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using Azure.Core;

namespace OrchardCore.FileStorage.AzureBlob
{
public abstract class BlobStorageOptions
Expand All @@ -7,6 +9,13 @@ public abstract class BlobStorageOptions
/// </summary>
public string ConnectionString { get; set; }

/// <summary>
/// The reference to a named AzureClient.
/// The client needs to be registered using the AddAzureClients extension method.
/// more info: https://learn.microsoft.com/en-us/dotnet/azure/sdk/dependency-injection#configure-multiple-service-clients-with-different-names
/// </summary>
public string BlobServiceName { get; set; }

/// <summary>
/// The Azure Blob container name.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

<ItemGroup>
<PackageReference Include="Azure.Storage.Blobs" />
<PackageReference Include="Microsoft.Extensions.Azure" Version="1.6.0" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ public static OrchardCoreBuilder AddAzureShellsConfiguration(this OrchardCoreBui

services.TryAddSingleton<IContentTypeProvider, FileExtensionContentTypeProvider>();

// register the azure blob file storage services
builder.AddAzureBlobFileStorage();

services.AddSingleton<IShellsFileStore>(sp =>
{
var configuration = sp.GetRequiredService<IConfiguration>();
Expand All @@ -34,10 +37,9 @@ public static OrchardCoreBuilder AddAzureShellsConfiguration(this OrchardCoreBui
"The 'OrchardCore.Shells.Azure' configuration section must be defined");
}
var clock = sp.GetRequiredService<IClock>();
var contentTypeProvider = sp.GetRequiredService<IContentTypeProvider>();
var blobFileStoreFactory = sp.GetRequiredService<BlobFileStoreFactory>();
var fileStore = new BlobFileStore(blobOptions, clock, contentTypeProvider);
var fileStore = blobFileStoreFactory.Create(blobOptions);
return new BlobShellsFileStore(fileStore);
});
Expand Down

0 comments on commit a3b198c

Please sign in to comment.