Skip to content

Commit 0771cd1

Browse files
committed
feature: B2 integration
1 parent 6136fd0 commit 0771cd1

File tree

11 files changed

+6882
-0
lines changed

11 files changed

+6882
-0
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using Amazon.S3;
2+
using Microsoft.AspNetCore.StaticFiles;
3+
using Microsoft.Extensions.DependencyInjection;
4+
using Our.Umbraco.StorageProviders.AWSS3.IO;
5+
6+
namespace Umbraco.Community.FileSystemProviders.B2;
7+
8+
public class B2FileSystemFactory(AmazonS3Client client, IServiceProvider serviceProvider)
9+
{
10+
public IAWSS3FileSystem Create(AWSS3FileSystemOptions options) =>
11+
ActivatorUtilities.CreateInstance<AWSS3FileSystem>(serviceProvider, new FileExtensionContentTypeProvider(), options, client);
12+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using Microsoft.Extensions.Options;
2+
using SixLabors.ImageSharp.Web;
3+
using SixLabors.ImageSharp.Web.Caching;
4+
using SixLabors.ImageSharp.Web.Caching.AWS;
5+
using SixLabors.ImageSharp.Web.Resolvers;
6+
7+
namespace Umbraco.Community.FileSystemProviders.B2;
8+
9+
public class B2FileSystemImageCache(IOptions<AWSS3StorageCacheOptions> options) : IImageCache
10+
{
11+
private readonly AWSS3StorageCache _baseCache = new(options);
12+
13+
public async Task<IImageCacheResolver?> GetAsync(string key) =>
14+
await _baseCache.GetAsync(Path.Combine("cache/", key));
15+
16+
public async Task SetAsync(string key, Stream stream, ImageCacheMetadata metadata) =>
17+
await _baseCache.SetAsync(Path.Combine("cache/", key), stream, metadata);
18+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using System.Collections.Concurrent;
2+
using Microsoft.Extensions.Options;
3+
using Our.Umbraco.StorageProviders.AWSS3.IO;
4+
5+
namespace Umbraco.Community.FileSystemProviders.B2;
6+
7+
public class B2FileSystemProvider(B2FileSystemFactory factory, IOptionsMonitor<AWSS3FileSystemOptions> options) : IAWSS3FileSystemProvider
8+
{
9+
private readonly ConcurrentDictionary<string, IAWSS3FileSystem> _fileSystems = new();
10+
11+
public IAWSS3FileSystem GetFileSystem(string name) =>
12+
_fileSystems.GetOrAdd(name, key =>
13+
{
14+
var config = options.Get(key);
15+
return factory.Create(config);
16+
});
17+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using Our.Umbraco.StorageProviders.AWSS3;
2+
using Umbraco.Cms.Core.Composing;
3+
using Umbraco.Cms.Core.DependencyInjection;
4+
5+
namespace Umbraco.Community.FileSystemProviders.B2.Composing;
6+
7+
#if NET7_0_OR_GREATER
8+
[ComposeAfter(typeof(global::Umbraco.Cms.Imaging.ImageSharp.ImageSharpComposer))]
9+
#endif
10+
[ComposeAfter(typeof(AWSS3Composer))]
11+
public class Composer : IComposer
12+
{
13+
public void Compose(IUmbracoBuilder builder)
14+
{
15+
builder.ManifestFilters().Append<ManifestFilter>();
16+
builder.AddB2MediaFileSystem();
17+
}
18+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using Umbraco.Cms.Core.Manifest;
2+
3+
namespace Umbraco.Community.FileSystemProviders.B2.Composing;
4+
5+
internal class ManifestFilter : IManifestFilter
6+
{
7+
public void Filter(List<PackageManifest> manifests)
8+
{
9+
manifests.Add(new PackageManifest
10+
{
11+
PackageName = "Umbraco.Community.FileSystemProviders.B2",
12+
Version = GetType().Assembly.GetName().Version?.ToString(3) ?? "0.1.0",
13+
AllowPackageTelemetry = true
14+
});
15+
}
16+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
using Amazon.S3;
2+
using Microsoft.AspNetCore.Builder;
3+
using Microsoft.Extensions.Configuration;
4+
using Microsoft.Extensions.DependencyInjection;
5+
using Microsoft.Extensions.DependencyInjection.Extensions;
6+
using Microsoft.Extensions.Options;
7+
using Our.Umbraco.StorageProviders.AWSS3;
8+
using Our.Umbraco.StorageProviders.AWSS3.DependencyInjection;
9+
using Our.Umbraco.StorageProviders.AWSS3.Imaging;
10+
using Our.Umbraco.StorageProviders.AWSS3.IO;
11+
using Our.Umbraco.StorageProviders.AWSS3.Services;
12+
using SixLabors.ImageSharp.Web.Caching;
13+
using SixLabors.ImageSharp.Web.Caching.AWS;
14+
using SixLabors.ImageSharp.Web.DependencyInjection;
15+
using Umbraco.Cms.Core.Configuration.Models;
16+
using Umbraco.Cms.Core.DependencyInjection;
17+
#if NET7_0_OR_GREATER
18+
using Umbraco.Cms.Imaging.ImageSharp.ImageProcessors;
19+
#else
20+
using Umbraco.Cms.Web.Common.ImageProcessors;
21+
#endif
22+
using Umbraco.Cms.Infrastructure.DependencyInjection;
23+
using Umbraco.Cms.Web.Common.ApplicationBuilder;
24+
using Umbraco.Community.FileSystemProviders.B2.HealthChecks;
25+
using Umbraco.Community.FileSystemProviders.B2.Models;
26+
27+
namespace Umbraco.Community.FileSystemProviders.B2.Composing;
28+
29+
public static class UmbracoBuilderExtensions
30+
{
31+
public static void AddB2MediaFileSystem(this IUmbracoBuilder builder)
32+
{
33+
var b2Section = builder.Config.GetSection("Umbraco:Storage:B2:Media");
34+
var opt = b2Section.Get<B2Options>();
35+
if (!opt?.Enabled ?? true)
36+
{
37+
builder.HealthChecks().Exclude<ApiHealthCheck>();
38+
return;
39+
}
40+
41+
builder.Services
42+
.AddImageSharp()
43+
.ClearProviders()
44+
.AddProvider<AWSS3FileSystemImageProvider>()
45+
.AddProcessor<CropWebProcessor>()
46+
.SetCache<B2FileSystemImageCache>();
47+
48+
builder.Services.AddOptions<B2Options>().Bind(b2Section);
49+
builder.Services.AddSingleton(CreateS3Client);
50+
builder.Services.AddSingleton<IAmazonS3>(CreateS3Client);
51+
builder.Services.AddSingleton<IImageCache, B2FileSystemImageCache>();
52+
builder.Services
53+
.AddOptions<AWSS3StorageCacheOptions>()
54+
.Configure<IOptions<B2Options>>((x, options) =>
55+
{
56+
var b2 = options.Value;
57+
if (!b2.Enabled)
58+
{
59+
throw new InvalidOperationException("B2 is not enabled or credentials are invalid.");
60+
}
61+
62+
x.BucketName = b2.BucketName;
63+
x.AccessKey = b2.Credentials.KeyId;
64+
x.AccessSecret = b2.Credentials.ApplicationKey;
65+
x.UseAccelerateEndpoint = b2.UseAccelerateEndpoint;
66+
x.Endpoint = b2.ServiceUrl;
67+
});
68+
69+
builder.Services
70+
.AddOptions<AWSS3FileSystemOptions>("Media")
71+
.Configure<IOptions<B2Options>, IOptions<GlobalSettings>>((x, config, settings) =>
72+
{
73+
x.BucketName = config.Value.BucketName;
74+
x.VirtualPath = settings.Value.UmbracoMediaPath;
75+
});
76+
77+
builder.Services.AddSingleton<IAWSS3FileSystemProvider, B2FileSystemProvider>();
78+
builder.Services.AddSingleton<B2FileSystemProvider>();
79+
builder.Services.AddSingleton<B2FileSystemFactory>();
80+
builder.Services.AddSingleton<AWSS3FileSystemMiddleware>();
81+
82+
builder.Services.TryAddSingleton<AWSS3FileSystemImageProvider>();
83+
builder.Services.TryAddSingleton<IMimeTypeResolver, MimeTypeResolver>();
84+
builder.SetMediaFileSystem(x =>
85+
{
86+
var provider = x.GetRequiredService<B2FileSystemProvider>();
87+
return provider.GetFileSystem("Media");
88+
});
89+
90+
builder.Services.Configure<UmbracoPipelineOptions>(options =>
91+
options.AddFilter(new UmbracoPipelineFilter("B2MediaFileSystem")
92+
{
93+
PrePipeline = app => app.UseB2MediaFileSystem(),
94+
}));
95+
}
96+
97+
private static AmazonS3Client CreateS3Client(IServiceProvider x)
98+
{
99+
var config = x.GetRequiredService<IOptions<B2Options>>().Value;
100+
var aws = new AmazonS3Config
101+
{
102+
ServiceURL = config.ServiceUrl,
103+
ForcePathStyle = true
104+
};
105+
return new AmazonS3Client(config.ToCredentials(), aws);
106+
}
107+
108+
private static void UseB2MediaFileSystem(this IApplicationBuilder builder)
109+
{
110+
var options = builder.ApplicationServices.GetRequiredService<IOptions<B2Options>>().Value;
111+
if (options.Enabled)
112+
{
113+
builder.UseAWSS3MediaFileSystem();
114+
}
115+
}
116+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using Microsoft.AspNetCore.StaticFiles;
2+
using Our.Umbraco.StorageProviders.AWSS3.Services;
3+
4+
namespace Umbraco.Community.FileSystemProviders.B2;
5+
6+
internal class MimeTypeResolver : IMimeTypeResolver
7+
{
8+
private readonly IContentTypeProvider _contentTypeProvider = new FileExtensionContentTypeProvider();
9+
10+
public string Resolve(string filename) =>
11+
_contentTypeProvider.TryGetContentType(filename, out var contentType) ? contentType : "application/octet-stream";
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using System.Diagnostics.CodeAnalysis;
2+
3+
namespace Umbraco.Community.FileSystemProviders.B2.Models;
4+
5+
public class B2Credentials
6+
{
7+
public string? ApplicationKey { get; set; }
8+
public string? KeyId { get; set; }
9+
10+
[MemberNotNullWhen(true, nameof(ApplicationKey), nameof(KeyId))]
11+
public bool Valid => !string.IsNullOrEmpty(ApplicationKey) && !string.IsNullOrEmpty(KeyId);
12+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using System.Diagnostics.CodeAnalysis;
2+
using Amazon.Runtime;
3+
4+
namespace Umbraco.Community.FileSystemProviders.B2.Models;
5+
6+
public class B2Options
7+
{
8+
public string? ServiceUrl { get; set; }
9+
public string? BucketName { get; set; }
10+
public B2Credentials? Credentials { get; set; }
11+
public bool UseAccelerateEndpoint { get; set; }
12+
13+
[MemberNotNullWhen(true, nameof(ServiceUrl), nameof(Credentials))]
14+
public bool Enabled => !string.IsNullOrWhiteSpace(BucketName) && !string.IsNullOrWhiteSpace(ServiceUrl) && (Credentials?.Valid ?? false);
15+
16+
public AWSCredentials ToCredentials() => new BasicAWSCredentials(Credentials?.KeyId, Credentials?.KeyId);
17+
}

src/Umbraco.Community.FileSystemProviders.B2/Umbraco.Community.FileSystemProviders.B2.csproj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
<Product>Umbraco.Community.FileSystemProviders.B2</Product>
1111
<PackageId>Umbraco.Community.FileSystemProviders.B2</PackageId>
1212
<Title>B2 Media File System Provider</Title>
13+
<Description>An implementation of the Umbraco IFileSystem connecting your Umbraco Media section to a BackBlaze B2 Storage account.</Description>
1314
<PackageTags>umbraco;umbraco-marketplace</PackageTags>
1415
<Authors>James C</Authors>
1516
<Copyright>$([System.DateTime]::UtcNow.ToString(`yyyy`)) ©</Copyright>
@@ -28,20 +29,25 @@
2829
<PackageReference Include="Umbraco.Cms.Core" Version="[10.4.0,11.0.0)"/>
2930
<PackageReference Include="Umbraco.Cms.Web.BackOffice" Version="[10.4.0,11.0.0)"/>
3031
<PackageReference Include="Umbraco.Cms.Web.Common" Version="[10.4.0,11.0.0)"/>
32+
<PackageReference Include="Our.Umbraco.StorageProviders.AWSS3" Version="1.3.0"/>
3133

3234
</ItemGroup>
3335

3436
<ItemGroup Condition="'$(TargetFramework)' == 'net7.0'">
3537
<PackageReference Include="Umbraco.Cms.Core" Version="[11.0.0,13.0.0)"/>
3638
<PackageReference Include="Umbraco.Cms.Web.BackOffice" Version="[11.0.0,13.0.0)"/>
3739
<PackageReference Include="Umbraco.Cms.Web.Common" Version="[11.0.0,13.0.0)"/>
40+
<PackageReference Include="Umbraco.Cms.Imaging.ImageSharp" Version="[11.0.0,13.0.0)"/>
41+
<PackageReference Include="Our.Umbraco.StorageProviders.AWSS3" Version="1.3.0"/>
3842

3943
</ItemGroup>
4044

4145
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
4246
<PackageReference Include="Umbraco.Cms.Core" Version="[13.0.0,14.0.0)"/>
4347
<PackageReference Include="Umbraco.Cms.Web.BackOffice" Version="[13.0.0,14.0.0)"/>
4448
<PackageReference Include="Umbraco.Cms.Web.Common" Version="[13.0.0,14.0.0)"/>
49+
<PackageReference Include="Umbraco.Cms.Imaging.ImageSharp" Version="[13.0.0,14.0.0)"/>
50+
<PackageReference Include="Our.Umbraco.StorageProviders.AWSS3" Version="1.3.0"/>
4551

4652
</ItemGroup>
4753

0 commit comments

Comments
 (0)