diff --git a/Umbraco.StorageProviders.sln b/Umbraco.StorageProviders.sln
index db5fc0b..ddd4e39 100644
--- a/Umbraco.StorageProviders.sln
+++ b/Umbraco.StorageProviders.sln
@@ -15,6 +15,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
README.md = README.md
EndProjectSection
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.StorageProviders", "src\Umbraco.StorageProviders\Umbraco.StorageProviders.csproj", "{5EC38982-2C9A-4D8D-AAE2-743A690FCD71}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -37,6 +39,18 @@ Global
{99A3FCBE-FDC6-4580-BDB8-7D219C6D98C3}.Release|x64.Build.0 = Release|Any CPU
{99A3FCBE-FDC6-4580-BDB8-7D219C6D98C3}.Release|x86.ActiveCfg = Release|Any CPU
{99A3FCBE-FDC6-4580-BDB8-7D219C6D98C3}.Release|x86.Build.0 = Release|Any CPU
+ {5EC38982-2C9A-4D8D-AAE2-743A690FCD71}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5EC38982-2C9A-4D8D-AAE2-743A690FCD71}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5EC38982-2C9A-4D8D-AAE2-743A690FCD71}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {5EC38982-2C9A-4D8D-AAE2-743A690FCD71}.Debug|x64.Build.0 = Debug|Any CPU
+ {5EC38982-2C9A-4D8D-AAE2-743A690FCD71}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {5EC38982-2C9A-4D8D-AAE2-743A690FCD71}.Debug|x86.Build.0 = Debug|Any CPU
+ {5EC38982-2C9A-4D8D-AAE2-743A690FCD71}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5EC38982-2C9A-4D8D-AAE2-743A690FCD71}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5EC38982-2C9A-4D8D-AAE2-743A690FCD71}.Release|x64.ActiveCfg = Release|Any CPU
+ {5EC38982-2C9A-4D8D-AAE2-743A690FCD71}.Release|x64.Build.0 = Release|Any CPU
+ {5EC38982-2C9A-4D8D-AAE2-743A690FCD71}.Release|x86.ActiveCfg = Release|Any CPU
+ {5EC38982-2C9A-4D8D-AAE2-743A690FCD71}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/src/Umbraco.StorageProviders.AzureBlob/AzureBlobFileSystemMiddleware.cs b/src/Umbraco.StorageProviders.AzureBlob/AzureBlobFileSystemMiddleware.cs
deleted file mode 100644
index 752d409..0000000
--- a/src/Umbraco.StorageProviders.AzureBlob/AzureBlobFileSystemMiddleware.cs
+++ /dev/null
@@ -1,385 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Linq;
-using System.Net;
-using System.Threading;
-using System.Threading.Tasks;
-using Azure;
-using Azure.Storage.Blobs;
-using Azure.Storage.Blobs.Models;
-using Microsoft.AspNetCore.Http;
-using Microsoft.Extensions.Options;
-using Microsoft.Net.Http.Headers;
-using Umbraco.Cms.Core.Hosting;
-using Umbraco.StorageProviders.AzureBlob.IO;
-
-namespace Umbraco.StorageProviders.AzureBlob
-{
- ///
- /// The Azure Blob file system middleware.
- ///
- ///
- public class AzureBlobFileSystemMiddleware : IMiddleware
- {
- private readonly string _name;
- private readonly IAzureBlobFileSystemProvider _fileSystemProvider;
- private string _rootPath;
- private string _containerRootPath;
- private readonly TimeSpan? _maxAge = TimeSpan.FromDays(7);
-
- ///
- /// Creates a new instance of .
- ///
- /// The options.
- /// The file system provider.
- /// The hosting environment.
-
- public AzureBlobFileSystemMiddleware(IOptionsMonitor options, IAzureBlobFileSystemProvider fileSystemProvider, IHostingEnvironment hostingEnvironment)
- : this(AzureBlobFileSystemOptions.MediaFileSystemName, options, fileSystemProvider, hostingEnvironment)
- { }
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The name.
- /// The options.
- /// The file system provider.
- /// The hosting environment.
- /// options
- /// or
- /// hostingEnvironment
- /// or
- /// name
- /// or
- /// fileSystemProvider
- protected AzureBlobFileSystemMiddleware(string name, IOptionsMonitor options, IAzureBlobFileSystemProvider fileSystemProvider, IHostingEnvironment hostingEnvironment)
- {
- if (options == null) throw new ArgumentNullException(nameof(options));
- if (hostingEnvironment == null) throw new ArgumentNullException(nameof(hostingEnvironment));
-
- _name = name ?? throw new ArgumentNullException(nameof(name));
- _fileSystemProvider = fileSystemProvider ?? throw new ArgumentNullException(nameof(fileSystemProvider));
-
- var fileSystemOptions = options.Get(name);
- _rootPath = hostingEnvironment.ToAbsolute(fileSystemOptions.VirtualPath);
- _containerRootPath = fileSystemOptions.ContainerRootPath ?? _rootPath;
-
- options.OnChange((o, n) => OptionsOnChange(o, n, hostingEnvironment));
- }
-
- ///
- public Task InvokeAsync(HttpContext context, RequestDelegate next)
- {
- if (context == null) throw new ArgumentNullException(nameof(context));
- if (next == null) throw new ArgumentNullException(nameof(next));
-
- return HandleRequestAsync(context, next);
- }
-
- private async Task HandleRequestAsync(HttpContext context, RequestDelegate next)
- {
- var request = context.Request;
- var response = context.Response;
-
- if (!context.Request.Path.StartsWithSegments(_rootPath, StringComparison.InvariantCultureIgnoreCase))
- {
- await next(context).ConfigureAwait(false);
- return;
- }
-
- string containerPath = $"{_containerRootPath.TrimEnd('/')}/{(request.Path.Value.Remove(0, _rootPath.Length)).TrimStart('/')}";
- var blob = _fileSystemProvider.GetFileSystem(_name).GetBlobClient(containerPath);
-
- var blobRequestConditions = GetAccessCondition(context.Request);
-
- Response properties;
- var ignoreRange = false;
-
- try
- {
- properties = await blob.GetPropertiesAsync(blobRequestConditions, context.RequestAborted).ConfigureAwait(false);
- }
- catch (RequestFailedException ex) when (ex.Status == (int)HttpStatusCode.NotFound)
- {
- // the blob or file does not exist, let other middleware handle it
- await next(context).ConfigureAwait(false);
- return;
- }
- catch (RequestFailedException ex) when (ex.Status == (int)HttpStatusCode.PreconditionFailed)
- {
- // If-Range or If-Unmodified-Since is not met
- // if the resource has been modified, we need to send the whole file back with a 200 OK
- // a Content-Range header is needed with the new length
- ignoreRange = true;
- properties = await blob.GetPropertiesAsync().ConfigureAwait(false);
- response.Headers.Append("Content-Range", $"bytes */{properties.Value.ContentLength}");
- }
- catch (RequestFailedException ex) when (ex.Status == (int)HttpStatusCode.NotModified)
- {
- // If-None-Match or If-Modified-Since is not met
- // we need to pass the status code back to the client
- // so it knows it can reuse the cached data
- response.StatusCode = (int)HttpStatusCode.NotModified;
- return;
- }
- // for some reason we get an internal exception type with the message
- // and not a request failed with status NotModified :(
- catch (Exception ex) when (ex.Message == "The condition specified using HTTP conditional header(s) is not met.")
- {
- if (blobRequestConditions != null
- && (blobRequestConditions.IfMatch.HasValue || blobRequestConditions.IfUnmodifiedSince.HasValue))
- {
- // If-Range or If-Unmodified-Since is not met
- // if the resource has been modified, we need to send the whole file back with a 200 OK
- // a Content-Range header is needed with the new length
- ignoreRange = true;
- properties = await blob.GetPropertiesAsync().ConfigureAwait(false);
- response.Headers.Append("Content-Range", $"bytes */{properties.Value.ContentLength}");
- }
- else
- {
- // If-None-Match or If-Modified-Since is not met
- // we need to pass the status code back to the client
- // so it knows it can reuse the cached data
- response.StatusCode = (int)HttpStatusCode.NotModified;
- return;
- }
- }
- catch (TaskCanceledException)
- {
- // client cancelled the request before it could finish, just ignore
- return;
- }
-
- var responseHeaders = response.GetTypedHeaders();
-
- responseHeaders.CacheControl =
- new CacheControlHeaderValue
- {
- Public = true,
- MustRevalidate = true,
- MaxAge = _maxAge,
- };
-
- responseHeaders.LastModified = properties.Value.LastModified;
- responseHeaders.ETag = new EntityTagHeaderValue($"\"{properties.Value.ETag}\"");
- responseHeaders.Append(HeaderNames.Vary, "Accept-Encoding");
-
- var requestHeaders = request.GetTypedHeaders();
-
- var rangeHeader = requestHeaders.Range;
-
- if (!ignoreRange && rangeHeader != null)
- {
- if (!ValidateRanges(rangeHeader.Ranges, properties.Value.ContentLength))
- {
- // no ranges could be parsed
- response.Clear();
- response.StatusCode = (int)HttpStatusCode.RequestedRangeNotSatisfiable;
- responseHeaders.ContentRange = new ContentRangeHeaderValue(properties.Value.ContentLength);
- return;
- }
-
- if (rangeHeader.Ranges.Count == 1)
- {
- var range = rangeHeader.Ranges.First();
- var contentRange = GetRangeHeader(properties, range);
-
- response.StatusCode = (int)HttpStatusCode.PartialContent;
- response.ContentType = properties.Value.ContentType;
- responseHeaders.ContentRange = contentRange;
-
- await DownloadRangeToStreamAsync(blob, properties, response.Body, contentRange, context.RequestAborted).ConfigureAwait(false);
- return;
- }
-
- if (rangeHeader.Ranges.Count > 1)
- {
- // handle multipart ranges
- var boundary = Guid.NewGuid().ToString();
- response.StatusCode = (int)HttpStatusCode.PartialContent;
- response.ContentType = $"multipart/byteranges; boundary={boundary}";
-
- foreach (var range in rangeHeader.Ranges)
- {
- var contentRange = GetRangeHeader(properties, range);
-
- await response.WriteAsync($"--{boundary}").ConfigureAwait(false);
- await response.WriteAsync("\n").ConfigureAwait(false);
- await response.WriteAsync($"{HeaderNames.ContentType}: {properties.Value.ContentType}").ConfigureAwait(false);
- await response.WriteAsync("\n").ConfigureAwait(false);
- await response.WriteAsync($"{HeaderNames.ContentRange}: {contentRange}").ConfigureAwait(false);
- await response.WriteAsync("\n").ConfigureAwait(false);
- await response.WriteAsync("\n").ConfigureAwait(false);
-
- await DownloadRangeToStreamAsync(blob, properties, response.Body, contentRange, context.RequestAborted).ConfigureAwait(false);
- await response.WriteAsync("\n").ConfigureAwait(false);
- }
-
- await response.WriteAsync($"--{boundary}--").ConfigureAwait(false);
- await response.WriteAsync("\n").ConfigureAwait(false);
- return;
- }
- }
- response.StatusCode = (int)HttpStatusCode.OK;
- response.ContentType = properties.Value.ContentType;
- responseHeaders.ContentLength = properties.Value.ContentLength;
- responseHeaders.Append(HeaderNames.AcceptRanges, "bytes");
-
- await response.StartAsync().ConfigureAwait(false);
- await DownloadRangeToStreamAsync(blob, response.Body, 0L, properties.Value.ContentLength, context.RequestAborted).ConfigureAwait(false);
- }
-
- private static BlobRequestConditions? GetAccessCondition(HttpRequest request)
- {
- var range = request.Headers["Range"];
- if (string.IsNullOrEmpty(range))
- {
- // etag
- var ifNoneMatch = request.Headers["If-None-Match"];
- if (!string.IsNullOrEmpty(ifNoneMatch))
- {
- return new BlobRequestConditions
- {
- IfNoneMatch = new ETag(ifNoneMatch)
- };
- }
-
- var ifModifiedSince = request.Headers["If-Modified-Since"];
- if (!string.IsNullOrEmpty(ifModifiedSince))
- {
- return new BlobRequestConditions
- {
- IfModifiedSince = DateTimeOffset.Parse(ifModifiedSince, CultureInfo.InvariantCulture)
- };
- }
- }
- else
- {
- // handle If-Range header, it can be either an etag or a date
- // see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Range and https://tools.ietf.org/html/rfc7233#section-3.2
- var ifRange = request.Headers["If-Range"];
- if (!string.IsNullOrEmpty(ifRange))
- {
- var conditions = new BlobRequestConditions();
-
- if (DateTimeOffset.TryParse(ifRange, out var date))
- {
- conditions.IfUnmodifiedSince = date;
- }
- else
- {
- conditions.IfMatch = new ETag(ifRange);
- }
- }
-
- var ifUnmodifiedSince = request.Headers["If-Unmodified-Since"];
- if (!string.IsNullOrEmpty(ifUnmodifiedSince))
- {
- return new BlobRequestConditions
- {
- IfUnmodifiedSince = DateTimeOffset.Parse(ifUnmodifiedSince, CultureInfo.InvariantCulture)
- };
- }
- }
-
- return null;
- }
-
- private static bool ValidateRanges(ICollection ranges, long length)
- {
- if (ranges.Count == 0)
- return false;
-
- foreach (var range in ranges)
- {
- if (range.From > range.To)
- return false;
- if (range.To >= length)
- return false;
- }
-
- return true;
- }
-
- private static ContentRangeHeaderValue GetRangeHeader(BlobProperties properties, RangeItemHeaderValue range)
- {
- var length = properties.ContentLength - 1;
-
- long from;
- long to;
- if (range.To.HasValue)
- {
- if (range.From.HasValue)
- {
- to = Math.Min(range.To.Value, length);
- from = range.From.Value;
- }
- else
- {
- to = length;
- from = Math.Max(properties.ContentLength - range.To.Value, 0L);
- }
- }
- else if (range.From.HasValue)
- {
- to = length;
- from = range.From.Value;
- }
- else
- {
- to = length;
- from = 0L;
- }
-
- return new ContentRangeHeaderValue(from, to, properties.ContentLength);
- }
-
- private static async Task DownloadRangeToStreamAsync(BlobClient blob, BlobProperties properties,
- Stream outputStream, ContentRangeHeaderValue contentRange, CancellationToken cancellationToken)
- {
- var offset = contentRange.From.GetValueOrDefault(0L);
- var length = properties.ContentLength;
-
- if (contentRange.To.HasValue && contentRange.From.HasValue)
- {
- length = contentRange.To.Value - contentRange.From.Value + 1;
- }
- else if (contentRange.To.HasValue)
- {
- length = contentRange.To.Value + 1;
- }
- else if (contentRange.From.HasValue)
- {
- length = properties.ContentLength - contentRange.From.Value + 1;
- }
-
- await DownloadRangeToStreamAsync(blob, outputStream, offset, length, cancellationToken).ConfigureAwait(false);
- }
-
- private static async Task DownloadRangeToStreamAsync(BlobClient blob, Stream outputStream,
- long offset, long length, CancellationToken cancellationToken)
- {
- try
- {
- if (length == 0) return;
- var response = await blob.DownloadAsync(new HttpRange(offset, length), cancellationToken: cancellationToken).ConfigureAwait(false);
- await response.Value.Content.CopyToAsync(outputStream, cancellationToken).ConfigureAwait(false);
- }
- catch (TaskCanceledException)
- {
- // client cancelled the request before it could finish, just ignore
- }
- }
-
- private void OptionsOnChange(AzureBlobFileSystemOptions options, string name, IHostingEnvironment hostingEnvironment)
- {
- if (name != _name) return;
-
- _rootPath = hostingEnvironment.ToAbsolute(options.VirtualPath);
- _containerRootPath = options.ContainerRootPath ?? _rootPath;
- }
- }
-}
diff --git a/src/Umbraco.StorageProviders.AzureBlob/DependencyInjection/AzureBlobCdnMediaUrlProviderExtensions.cs b/src/Umbraco.StorageProviders.AzureBlob/DependencyInjection/AzureBlobCdnMediaUrlProviderExtensions.cs
new file mode 100644
index 0000000..bb4e930
--- /dev/null
+++ b/src/Umbraco.StorageProviders.AzureBlob/DependencyInjection/AzureBlobCdnMediaUrlProviderExtensions.cs
@@ -0,0 +1,47 @@
+using System;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+using Umbraco.StorageProviders;
+using Umbraco.StorageProviders.AzureBlob.IO;
+
+// ReSharper disable once CheckNamespace
+// uses same namespace as Umbraco Core for easier discoverability
+namespace Umbraco.Cms.Core.DependencyInjection
+{
+ ///
+ /// Extension methods to help registering a CDN media URL provider.
+ ///
+ public static class AzureBlobCdnMediaUrlProviderExtensions
+ {
+ ///
+ /// Registers and configures the .
+ ///
+ /// The .
+ ///
+ /// The .
+ ///
+ /// builder
+ public static IUmbracoBuilder AddAzureBlobCdnMediaUrlProvider(this IUmbracoBuilder builder)
+ {
+ if (builder == null) throw new ArgumentNullException(nameof(builder));
+
+ builder.AddCdnMediaUrlProvider();
+
+ builder.Services.AddOptions()
+ .BindConfiguration("Umbraco:Storage:AzureBlob:Media:Cdn")
+ .Configure>(
+ (options, factory) =>
+ {
+ var mediaOptions = factory.Create(AzureBlobFileSystemOptions.MediaFileSystemName);
+ if (!string.IsNullOrEmpty(mediaOptions.ContainerName))
+ {
+ options.Url = new Uri(options.Url, mediaOptions.ContainerName);
+ }
+ }
+ )
+ .ValidateDataAnnotations();
+
+ return builder;
+ }
+ }
+}
diff --git a/src/Umbraco.StorageProviders.AzureBlob/DependencyInjection/AzureBlobMediaFileSystemExtensions.cs b/src/Umbraco.StorageProviders.AzureBlob/DependencyInjection/AzureBlobMediaFileSystemExtensions.cs
index b23289b..27e1f1e 100644
--- a/src/Umbraco.StorageProviders.AzureBlob/DependencyInjection/AzureBlobMediaFileSystemExtensions.cs
+++ b/src/Umbraco.StorageProviders.AzureBlob/DependencyInjection/AzureBlobMediaFileSystemExtensions.cs
@@ -1,17 +1,20 @@
using System;
using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.StaticFiles.Infrastructure;
using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
+using Microsoft.Net.Http.Headers;
using SixLabors.ImageSharp.Web.Caching;
using SixLabors.ImageSharp.Web.Providers;
using Umbraco.Cms.Core.Configuration.Models;
+using Umbraco.Cms.Core.Hosting;
using Umbraco.Cms.Infrastructure.DependencyInjection;
using Umbraco.Cms.Web.Common.ApplicationBuilder;
using Umbraco.Extensions;
-using Umbraco.StorageProviders.AzureBlob;
using Umbraco.StorageProviders.AzureBlob.Imaging;
using Umbraco.StorageProviders.AzureBlob.IO;
+using Umbraco.StorageProviders.IO;
// ReSharper disable once CheckNamespace
// uses same namespace as Umbraco Core for easier discoverability
@@ -41,8 +44,6 @@ public static IUmbracoBuilder AddAzureBlobMediaFileSystem(this IUmbracoBuilder b
options.VirtualPath = globalSettingsOptions.Value.UmbracoMediaPath;
});
- builder.Services.TryAddSingleton();
-
// ImageSharp image provider/cache
builder.Services.Insert(0, ServiceDescriptor.Singleton());
builder.Services.AddUnique();
@@ -104,7 +105,7 @@ public static IUmbracoBuilder AddAzureBlobMediaFileSystem(this IUmbracoBuilder b
}
///
- /// Adds the .
+ /// Adds the .
///
/// The .
///
@@ -121,7 +122,7 @@ public static IUmbracoApplicationBuilderContext UseAzureBlobMediaFileSystem(this
}
///
- /// Adds the .
+ /// Adds the .
///
/// The .
///
@@ -132,7 +133,32 @@ public static IApplicationBuilder UseAzureBlobMediaFileSystem(this IApplicationB
{
if (app == null) throw new ArgumentNullException(nameof(app));
- app.UseMiddleware();
+ var fileSystem = app.ApplicationServices.GetRequiredService().GetFileSystem(AzureBlobFileSystemOptions.MediaFileSystemName);
+ var options = app.ApplicationServices.GetRequiredService>().Create(AzureBlobFileSystemOptions.MediaFileSystemName);
+ var hostingEnvironment = app.ApplicationServices.GetRequiredService();
+
+ var requestPath = hostingEnvironment.ToAbsolute(options.VirtualPath);
+
+ var sharedOptions = new SharedOptions()
+ {
+ FileProvider = new FileSystemFileProvider(fileSystem, requestPath),
+ RequestPath = requestPath
+ };
+
+ app.UseStaticFiles(new StaticFileOptions(sharedOptions)
+ {
+ OnPrepareResponse = ctx =>
+ {
+ // TODO Make this configurable
+ var headers = ctx.Context.Response.GetTypedHeaders();
+ headers.CacheControl = new CacheControlHeaderValue
+ {
+ Public = true,
+ MustRevalidate = true,
+ MaxAge = TimeSpan.FromDays(7)
+ };
+ }
+ });
return app;
}
diff --git a/src/Umbraco.StorageProviders.AzureBlob/Umbraco.StorageProviders.AzureBlob.csproj b/src/Umbraco.StorageProviders.AzureBlob/Umbraco.StorageProviders.AzureBlob.csproj
index 3391ec1..31c1bb5 100644
--- a/src/Umbraco.StorageProviders.AzureBlob/Umbraco.StorageProviders.AzureBlob.csproj
+++ b/src/Umbraco.StorageProviders.AzureBlob/Umbraco.StorageProviders.AzureBlob.csproj
@@ -4,13 +4,14 @@
enable
AllEnabledByDefault
true
- Azure blob storage plugin for Umbraco CMS
-
+
-
-
+
+
+
+
diff --git a/src/Umbraco.StorageProviders.AzureBlob/version.json b/src/Umbraco.StorageProviders.AzureBlob/version.json
deleted file mode 100644
index 4330f5c..0000000
--- a/src/Umbraco.StorageProviders.AzureBlob/version.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "version": "0.1",
- "nugetPackageVersion": {
- "semVer": 2
- },
- "pathFilters": ["."]
-}
diff --git a/src/Umbraco.StorageProviders.AzureBlob/CdnMediaUrlProvider.cs b/src/Umbraco.StorageProviders/CdnMediaUrlProvider.cs
similarity index 98%
rename from src/Umbraco.StorageProviders.AzureBlob/CdnMediaUrlProvider.cs
rename to src/Umbraco.StorageProviders/CdnMediaUrlProvider.cs
index 5c7c55d..e11a770 100644
--- a/src/Umbraco.StorageProviders.AzureBlob/CdnMediaUrlProvider.cs
+++ b/src/Umbraco.StorageProviders/CdnMediaUrlProvider.cs
@@ -4,7 +4,7 @@
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.Routing;
-namespace Umbraco.StorageProviders.AzureBlob
+namespace Umbraco.StorageProviders
{
///
/// A that returns a CDN URL for a media item.
diff --git a/src/Umbraco.StorageProviders.AzureBlob/CdnMediaUrlProviderOptions.cs b/src/Umbraco.StorageProviders/CdnMediaUrlProviderOptions.cs
similarity index 94%
rename from src/Umbraco.StorageProviders.AzureBlob/CdnMediaUrlProviderOptions.cs
rename to src/Umbraco.StorageProviders/CdnMediaUrlProviderOptions.cs
index cf4e596..fd9f7c1 100644
--- a/src/Umbraco.StorageProviders.AzureBlob/CdnMediaUrlProviderOptions.cs
+++ b/src/Umbraco.StorageProviders/CdnMediaUrlProviderOptions.cs
@@ -1,7 +1,7 @@
using System;
using System.ComponentModel.DataAnnotations;
-namespace Umbraco.StorageProviders.AzureBlob
+namespace Umbraco.StorageProviders
{
///
/// The CDN media URL provider options.
diff --git a/src/Umbraco.StorageProviders.AzureBlob/DependencyInjection/CdnMediaUrlProviderExtensions.cs b/src/Umbraco.StorageProviders/DependencyInjection/CdnMediaUrlProviderExtensions.cs
similarity index 82%
rename from src/Umbraco.StorageProviders.AzureBlob/DependencyInjection/CdnMediaUrlProviderExtensions.cs
rename to src/Umbraco.StorageProviders/DependencyInjection/CdnMediaUrlProviderExtensions.cs
index 0d06038..fc9fd53 100644
--- a/src/Umbraco.StorageProviders.AzureBlob/DependencyInjection/CdnMediaUrlProviderExtensions.cs
+++ b/src/Umbraco.StorageProviders/DependencyInjection/CdnMediaUrlProviderExtensions.cs
@@ -1,8 +1,6 @@
using System;
using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Options;
-using Umbraco.StorageProviders.AzureBlob;
-using Umbraco.StorageProviders.AzureBlob.IO;
+using Umbraco.StorageProviders;
// ReSharper disable once CheckNamespace
// uses same namespace as Umbraco Core for easier discoverability
@@ -25,22 +23,12 @@ public static IUmbracoBuilder AddCdnMediaUrlProvider(this IUmbracoBuilder builde
{
if (builder == null) throw new ArgumentNullException(nameof(builder));
+ builder.MediaUrlProviders().Insert();
+
builder.Services.AddOptions()
- .BindConfiguration("Umbraco:Storage:AzureBlob:Media:Cdn")
- .Configure>(
- (options, factory) =>
- {
- var mediaOptions = factory.Create(AzureBlobFileSystemOptions.MediaFileSystemName);
- if (!string.IsNullOrEmpty(mediaOptions.ContainerName))
- {
- options.Url = new Uri(options.Url, mediaOptions.ContainerName);
- }
- }
- )
+ .BindConfiguration("Umbraco:Storage:Media:Cdn")
.ValidateDataAnnotations();
- builder.MediaUrlProviders().Insert();
-
return builder;
}
diff --git a/src/Umbraco.StorageProviders/IO/FileSystemDirectoryContents.cs b/src/Umbraco.StorageProviders/IO/FileSystemDirectoryContents.cs
new file mode 100644
index 0000000..3bf7eec
--- /dev/null
+++ b/src/Umbraco.StorageProviders/IO/FileSystemDirectoryContents.cs
@@ -0,0 +1,60 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Extensions.FileProviders;
+using Umbraco.Cms.Core.IO;
+
+namespace Umbraco.StorageProviders.IO
+{
+ ///
+ /// Represents the directory contents in an .
+ ///
+ ///
+ public class FileSystemDirectoryContents : IDirectoryContents
+ {
+ private readonly IFileSystem _fileSystem;
+ private readonly string _subpath;
+ private IEnumerable _entries = null!;
+
+ ///
+ public bool Exists => true;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The file system.
+ /// The subpath.
+ ///
+ /// fileSystem
+ /// or
+ /// subpath
+ ///
+ public FileSystemDirectoryContents(IFileSystem fileSystem, string subpath)
+ {
+ _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem));
+ _subpath = subpath ?? throw new ArgumentNullException(nameof(subpath));
+ }
+
+ ///
+ public IEnumerator GetEnumerator()
+ {
+ EnsureInitialized();
+ return _entries.GetEnumerator();
+ }
+
+ ///
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ EnsureInitialized();
+ return _entries.GetEnumerator();
+ }
+
+ private void EnsureInitialized()
+ {
+ _entries = _fileSystem.GetDirectories(_subpath).Select(d => new FileSystemDirectoryInfo(_fileSystem, d))
+ .Union(_fileSystem.GetFiles(_subpath).Select(f => new FileSystemFileInfo(_fileSystem, f)))
+ .ToList();
+ }
+ }
+}
diff --git a/src/Umbraco.StorageProviders/IO/FileSystemDirectoryInfo.cs b/src/Umbraco.StorageProviders/IO/FileSystemDirectoryInfo.cs
new file mode 100644
index 0000000..6a5dc75
--- /dev/null
+++ b/src/Umbraco.StorageProviders/IO/FileSystemDirectoryInfo.cs
@@ -0,0 +1,49 @@
+using System;
+using System.IO;
+using Microsoft.Extensions.FileProviders;
+using Umbraco.Cms.Core.IO;
+
+namespace Umbraco.StorageProviders.IO
+{
+ ///
+ /// Represents a directory in an .
+ ///
+ ///
+ public class FileSystemDirectoryInfo : IFileInfo
+ {
+ private readonly IFileSystem _fileSystem;
+ private readonly string _subpath;
+
+ ///
+ public bool Exists => true;
+
+ ///
+ public bool IsDirectory => true;
+
+ ///
+ public DateTimeOffset LastModified => _fileSystem.GetLastModified(_subpath);
+
+ ///
+ public long Length => -1;
+
+ ///
+ public string Name => _fileSystem.GetRelativePath(_subpath);
+
+ ///
+ public string PhysicalPath => null!;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The file system.
+ /// The subpath.
+ public FileSystemDirectoryInfo(IFileSystem fileSystem, string subpath)
+ {
+ _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem));
+ _subpath = subpath ?? throw new ArgumentNullException(nameof(subpath));
+ }
+
+ ///
+ public Stream CreateReadStream() => throw new InvalidOperationException("Cannot create a stream for a directory.");
+ }
+}
diff --git a/src/Umbraco.StorageProviders/IO/FileSystemFileInfo.cs b/src/Umbraco.StorageProviders/IO/FileSystemFileInfo.cs
new file mode 100644
index 0000000..0e44fe4
--- /dev/null
+++ b/src/Umbraco.StorageProviders/IO/FileSystemFileInfo.cs
@@ -0,0 +1,49 @@
+using System;
+using System.IO;
+using Microsoft.Extensions.FileProviders;
+using Umbraco.Cms.Core.IO;
+
+namespace Umbraco.StorageProviders.IO
+{
+ ///
+ /// Represents a file in an .
+ ///
+ ///
+ public class FileSystemFileInfo : IFileInfo
+ {
+ private readonly IFileSystem _fileSystem;
+ private readonly string _subpath;
+
+ ///
+ public bool Exists => true;
+
+ ///
+ public bool IsDirectory => false;
+
+ ///
+ public DateTimeOffset LastModified => _fileSystem.GetLastModified(_subpath);
+
+ ///
+ public long Length => _fileSystem.GetSize(_subpath);
+
+ ///
+ public string Name => _fileSystem.GetRelativePath(_subpath);
+
+ ///
+ public string PhysicalPath => null!;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The file system.
+ /// The subpath.
+ public FileSystemFileInfo(IFileSystem fileSystem, string subpath)
+ {
+ _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem));
+ _subpath = subpath ?? throw new ArgumentNullException(nameof(subpath));
+ }
+
+ ///
+ public Stream CreateReadStream() => _fileSystem.OpenFile(_subpath);
+ }
+}
diff --git a/src/Umbraco.StorageProviders/IO/FileSystemFileProvider.cs b/src/Umbraco.StorageProviders/IO/FileSystemFileProvider.cs
new file mode 100644
index 0000000..9612d5c
--- /dev/null
+++ b/src/Umbraco.StorageProviders/IO/FileSystemFileProvider.cs
@@ -0,0 +1,58 @@
+using System;
+using Microsoft.Extensions.FileProviders;
+using Microsoft.Extensions.Primitives;
+using Umbraco.Cms.Core.IO;
+
+namespace Umbraco.StorageProviders.IO
+{
+ ///
+ /// Exposes an as an .
+ ///
+ ///
+ public class FileSystemFileProvider : IFileProvider
+ {
+ private readonly IFileSystem _fileSystem;
+ private readonly string? _pathPrefix;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The file system.
+ /// The path prefix.
+ /// fileSystem
+ public FileSystemFileProvider(IFileSystem fileSystem, string? pathPrefix = null)
+ {
+ _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem));
+ _pathPrefix = pathPrefix;
+ }
+
+ ///
+ public IDirectoryContents GetDirectoryContents(string subpath)
+ {
+ var path = _pathPrefix + subpath;
+
+ if (path == null || _fileSystem.DirectoryExists(path) == false)
+ {
+ return NotFoundDirectoryContents.Singleton;
+ }
+
+ return new FileSystemDirectoryContents(_fileSystem, path);
+ }
+
+ ///
+ public IFileInfo GetFileInfo(string subpath)
+ {
+ var path = _pathPrefix + subpath;
+
+ if (path == null || _fileSystem.FileExists(path) == false)
+ {
+ return new NotFoundFileInfo(path);
+ }
+
+ return new FileSystemFileInfo(_fileSystem, path);
+ }
+
+ ///
+ public IChangeToken Watch(string filter) => NullChangeToken.Singleton;
+ }
+}
diff --git a/src/Umbraco.StorageProviders/Properties/AssemblyInfo.cs b/src/Umbraco.StorageProviders/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..55acad6
--- /dev/null
+++ b/src/Umbraco.StorageProviders/Properties/AssemblyInfo.cs
@@ -0,0 +1,3 @@
+using System;
+
+[assembly:CLSCompliant(false)]
diff --git a/src/Umbraco.StorageProviders/Umbraco.StorageProviders.csproj b/src/Umbraco.StorageProviders/Umbraco.StorageProviders.csproj
new file mode 100644
index 0000000..5b3e5c4
--- /dev/null
+++ b/src/Umbraco.StorageProviders/Umbraco.StorageProviders.csproj
@@ -0,0 +1,13 @@
+
+
+ net5.0
+ enable
+ AllEnabledByDefault
+ true
+
+
+
+
+
+
+