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

Added BlobFileStoreFactory to allow DefaultAzureCredential or Connectionstring authentication for Azure Blob storage #12874

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment isn't needed here

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,17 @@
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.ApplicationServices.AddAzureBlobFileStorage();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

new line before return.

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would remove the comments here.

/// </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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New line before return

Then either remove the comment or change the comment block to something like this

// TryAddSingleton is used to prevent registering the service again.

}
}
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,63 @@
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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add new line after the namespace.

public class BlobFileStoreFactory
{
private readonly IAzureClientFactory<BlobServiceClient> _clientFactory;
private readonly IServiceProvider _serviceProvider;

private const string DefaultStorageName = "Default";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rename the variable to _defaultStorageName


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)
{
// return a container client from connectionstring, if specified
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// return a container client from connectionstring, if specified
// Create a container using the specified connection string.

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

// else use the clientFactory to lookup a BlobService with the specified name
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// else use the clientFactory to lookup a BlobService with the specified name
// Create a container using the specified Blob service.

if (!String.IsNullOrEmpty(options.BlobServiceName))
{
return _clientFactory
.CreateClient(options.BlobServiceName)
.GetBlobContainerClient(options.ContainerName);
}

// fall-back to the default registered blob storage
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// fall-back to the default registered blob storage
// Create a container using the Default client.

return _clientFactory
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we want to have a fall back option instead of logging an error? Maybe we should do option validation before? @jtkech, thoughts?

.CreateClient(DefaultStorageName)
.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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simplify the comments here by writing something like this
/// The Azure Blob service name.

/// 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would just remove this comment.

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