diff --git a/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoApplicationBuilder.cs b/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoApplicationBuilder.cs
index 8bf36264eb..2212dec425 100644
--- a/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoApplicationBuilder.cs
+++ b/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoApplicationBuilder.cs
@@ -1,4 +1,3 @@
-using Dazinator.Extensions.FileProviders.PrependBasePath;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
@@ -8,6 +7,7 @@ using SixLabors.ImageSharp.Web.DependencyInjection;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Services;
+using Umbraco.Cms.Web.Common.Media;
using Umbraco.Extensions;
using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment;
@@ -96,7 +96,7 @@ public class UmbracoApplicationBuilder : IUmbracoApplicationBuilder, IUmbracoEnd
{
webHostEnvironment.WebRootFileProvider =
webHostEnvironment.WebRootFileProvider.ConcatComposite(
- new PrependBasePathFileProvider(mediaRequestPath, mediaFileProvider));
+ new MediaPrependBasePathFileProvider(mediaRequestPath, mediaFileProvider));
}
}
diff --git a/src/Umbraco.Web.Common/Media/MediaPrependBasePathFileProvider.cs b/src/Umbraco.Web.Common/Media/MediaPrependBasePathFileProvider.cs
new file mode 100644
index 0000000000..c6ce59456d
--- /dev/null
+++ b/src/Umbraco.Web.Common/Media/MediaPrependBasePathFileProvider.cs
@@ -0,0 +1,94 @@
+using Dazinator.Extensions.FileProviders;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.FileProviders;
+using Microsoft.Extensions.Primitives;
+
+namespace Umbraco.Cms.Web.Common.Media;
+
+///
+/// Prepends a base path to files / directories from an underlying file provider.
+///
+///
+/// This is a clone-and-own of PrependBasePathFileProvider from the Dazinator project, cleaned up and tweaked to work
+/// for serving media files with special characters.
+/// Reference issue: https://github.com/umbraco/Umbraco-CMS/issues/12903
+/// A PR has been submitted to the Dazinator project: https://github.com/dazinator/Dazinator.Extensions.FileProviders/pull/53
+/// If that PR is accepted, the Dazinator dependency should be updated and this class should be removed.
+///
+internal class MediaPrependBasePathFileProvider : IFileProvider
+{
+ private readonly PathString _basePath;
+ private readonly IFileProvider _underlyingFileProvider;
+ private readonly IFileInfo _baseDirectoryFileInfo;
+ private static readonly char[] _splitChar = { '/' };
+
+ public MediaPrependBasePathFileProvider(string? basePath, IFileProvider underlyingFileProvider)
+ {
+ _basePath = new PathString(basePath);
+ _baseDirectoryFileInfo = new DirectoryFileInfo(_basePath.ToString().TrimStart(_splitChar));
+ _underlyingFileProvider = underlyingFileProvider;
+ }
+
+ protected virtual bool TryMapSubPath(string originalSubPath, out PathString newSubPath)
+ {
+ if (!string.IsNullOrEmpty(originalSubPath))
+ {
+ PathString originalPathString;
+ originalPathString = originalSubPath[0] != '/' ? new PathString('/' + originalSubPath) : new PathString(originalSubPath);
+
+ if (originalPathString.HasValue && originalPathString.StartsWithSegments(_basePath, out PathString remaining))
+ {
+ // var childPath = originalPathString.Remove(0, _basePath.Value.Length);
+ newSubPath = remaining;
+ return true;
+ }
+ }
+
+ newSubPath = null;
+ return false;
+ }
+
+ public IDirectoryContents GetDirectoryContents(string subpath)
+ {
+ if (string.IsNullOrEmpty(subpath))
+ {
+ // return root / base directory.
+ return new EnumerableDirectoryContents(_baseDirectoryFileInfo);
+ }
+
+ if (TryMapSubPath(subpath, out PathString newPath))
+ {
+ IDirectoryContents? contents = _underlyingFileProvider.GetDirectoryContents(newPath);
+ return contents;
+ }
+
+ return new NotFoundDirectoryContents();
+ }
+
+ public IFileInfo GetFileInfo(string subpath)
+ {
+ if (TryMapSubPath(subpath, out PathString newPath))
+ {
+ // KJA changed: use explicit newPath.Value instead of implicit newPath string operator (which calls ToString())
+ IFileInfo? result = _underlyingFileProvider.GetFileInfo(newPath.Value);
+ return result;
+ }
+
+ return new NotFoundFileInfo(subpath);
+ }
+
+ public IChangeToken Watch(string filter)
+ {
+ // We check if the pattern starts with the base path, and remove it if necessary.
+ // otherwise we just pass the pattern through unaltered.
+ if (TryMapSubPath(filter, out PathString newPath))
+ {
+ // KJA changed: use explicit newPath.Value instead of implicit newPath string operator (which calls ToString())
+ IChangeToken? result = _underlyingFileProvider.Watch(newPath.Value);
+ return result;
+ }
+
+ return _underlyingFileProvider.Watch(newPath);
+ }
+}
+