Skip to content

Serve Media and App_Plugins using WebRootFileProvider (and allow changing the physical media path) #11783

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

Merged
Merged
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
31 changes: 20 additions & 11 deletions src/Umbraco.Core/Configuration/Models/GlobalSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,21 +31,19 @@ public class GlobalSettings
internal const bool StaticSanitizeTinyMce = false;

/// <summary>
/// Gets or sets a value for the reserved URLs.
/// It must end with a comma
/// Gets or sets a value for the reserved URLs (must end with a comma).
/// </summary>
[DefaultValue(StaticReservedUrls)]
public string ReservedUrls { get; set; } = StaticReservedUrls;

/// <summary>
/// Gets or sets a value for the reserved paths.
/// It must end with a comma
/// Gets or sets a value for the reserved paths (must end with a comma).
/// </summary>
[DefaultValue(StaticReservedPaths)]
public string ReservedPaths { get; set; } = StaticReservedPaths;

/// <summary>
/// Gets or sets a value for the timeout
/// Gets or sets a value for the back-office login timeout.
/// </summary>
[DefaultValue(StaticTimeOut)]
public TimeSpan TimeOut { get; set; } = TimeSpan.Parse(StaticTimeOut);
Expand Down Expand Up @@ -104,11 +102,19 @@ public class GlobalSettings
public string UmbracoScriptsPath { get; set; } = StaticUmbracoScriptsPath;

/// <summary>
/// Gets or sets a value for the Umbraco media path.
/// Gets or sets a value for the Umbraco media request path.
/// </summary>
[DefaultValue(StaticUmbracoMediaPath)]
public string UmbracoMediaPath { get; set; } = StaticUmbracoMediaPath;

/// <summary>
/// Gets or sets a value for the physical Umbraco media root path (falls back to <see cref="UmbracoMediaPath" /> when empty).
/// </summary>
/// <remarks>
/// If the value is a virtual path, it's resolved relative to the webroot.
/// </remarks>
public string UmbracoMediaPhysicalRootPath { get; set; }

/// <summary>
/// Gets or sets a value indicating whether to install the database when it is missing.
/// </summary>
Expand All @@ -131,6 +137,9 @@ public class GlobalSettings
/// </summary>
public string MainDomLock { get; set; } = string.Empty;

/// <summary>
/// Gets or sets the telemetry ID.
/// </summary>
public string Id { get; set; } = string.Empty;

/// <summary>
Expand Down Expand Up @@ -164,19 +173,19 @@ public class GlobalSettings
/// </summary>
public bool IsPickupDirectoryLocationConfigured => !string.IsNullOrWhiteSpace(Smtp?.PickupDirectoryLocation);

/// Gets a value indicating whether TinyMCE scripting sanitization should be applied
/// <summary>
/// Gets a value indicating whether TinyMCE scripting sanitization should be applied.
/// </summary>
[DefaultValue(StaticSanitizeTinyMce)]
public bool SanitizeTinyMce => StaticSanitizeTinyMce;

/// <summary>
/// An int value representing the time in milliseconds to lock the database for a write operation
/// Gets a value representing the time in milliseconds to lock the database for a write operation.
/// </summary>
/// <remarks>
/// The default value is 5000 milliseconds
/// The default value is 5000 milliseconds.
/// </remarks>
/// <value>The timeout in milliseconds.</value>
[DefaultValue(StaticSqlWriteLockTimeOut)]
public TimeSpan SqlWriteLockTimeOut { get; } = TimeSpan.Parse(StaticSqlWriteLockTimeOut);
}
}
}
2 changes: 2 additions & 0 deletions src/Umbraco.Core/Constants-SystemDirectories.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ public static class SystemDirectories

public const string AppPlugins = "/App_Plugins";
public static string AppPluginIcons => "/Backoffice/Icons";
public const string CreatedPackages = "/created-packages";


public const string MvcViews = "~/Views";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,25 @@ namespace Umbraco.Cms.Core.DependencyInjection
public static partial class UmbracoBuilderExtensions
{

private static IUmbracoBuilder AddUmbracoOptions<TOptions>(this IUmbracoBuilder builder)
private static IUmbracoBuilder AddUmbracoOptions<TOptions>(this IUmbracoBuilder builder, Action<OptionsBuilder<TOptions>> configure = null)
where TOptions : class
{
var umbracoOptionsAttribute = typeof(TOptions).GetCustomAttribute<UmbracoOptionsAttribute>();

if (umbracoOptionsAttribute is null)
{
throw new ArgumentException("typeof(TOptions) do not have the UmbracoOptionsAttribute");
throw new ArgumentException($"{typeof(TOptions)} do not have the UmbracoOptionsAttribute.");
}


builder.Services.AddOptions<TOptions>()
.Bind(builder.Config.GetSection(umbracoOptionsAttribute.ConfigurationKey),
o => o.BindNonPublicProperties = umbracoOptionsAttribute.BindNonPublicProperties)
var optionsBuilder = builder.Services.AddOptions<TOptions>()
.Bind(
builder.Config.GetSection(umbracoOptionsAttribute.ConfigurationKey),
o => o.BindNonPublicProperties = umbracoOptionsAttribute.BindNonPublicProperties
)
.ValidateDataAnnotations();

return builder;
configure?.Invoke(optionsBuilder);

return builder;
}

/// <summary>
Expand All @@ -52,7 +54,13 @@ public static IUmbracoBuilder AddConfiguration(this IUmbracoBuilder builder)
.AddUmbracoOptions<ContentSettings>()
.AddUmbracoOptions<CoreDebugSettings>()
.AddUmbracoOptions<ExceptionFilterSettings>()
.AddUmbracoOptions<GlobalSettings>()
.AddUmbracoOptions<GlobalSettings>(optionsBuilder => optionsBuilder.PostConfigure(options =>
{
if (string.IsNullOrEmpty(options.UmbracoMediaPhysicalRootPath))
{
options.UmbracoMediaPhysicalRootPath = options.UmbracoMediaPath;
}
}))
.AddUmbracoOptions<HealthChecksSettings>()
.AddUmbracoOptions<HostingSettings>()
.AddUmbracoOptions<ImagingSettings>()
Expand Down
20 changes: 20 additions & 0 deletions src/Umbraco.Core/IO/FileSystemExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using Microsoft.Extensions.FileProviders;
using Umbraco.Cms.Core.IO;

namespace Umbraco.Extensions
Expand Down Expand Up @@ -87,5 +88,24 @@ public static void CreateFolder(this IFileSystem fs, string folderPath)
}
fs.DeleteFile(tempFile);
}

/// <summary>
/// Creates a new <see cref="IFileProvider" /> from the file system.
/// </summary>
/// <param name="fileSystem">The file system.</param>
/// <param name="fileProvider">When this method returns, contains an <see cref="IFileProvider"/> created from the file system.</param>
/// <returns>
/// <c>true</c> if the <see cref="IFileProvider" /> was successfully created; otherwise, <c>false</c>.
/// </returns>
public static bool TryCreateFileProvider(this IFileSystem fileSystem, out IFileProvider fileProvider)
{
fileProvider = fileSystem switch
{
IFileProviderFactory fileProviderFactory => fileProviderFactory.Create(),
_ => null
};

return fileProvider != null;
}
}
}
18 changes: 18 additions & 0 deletions src/Umbraco.Core/IO/IFileProviderFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using Microsoft.Extensions.FileProviders;

namespace Umbraco.Cms.Core.IO
{
/// <summary>
/// Factory for creating <see cref="IFileProvider" /> instances.
/// </summary>
public interface IFileProviderFactory
{
/// <summary>
/// Creates a new <see cref="IFileProvider" /> instance.
/// </summary>
/// <returns>
/// The newly created <see cref="IFileProvider" /> instance (or <c>null</c> if not supported).
/// </returns>
IFileProvider Create();
}
}
27 changes: 17 additions & 10 deletions src/Umbraco.Core/IO/MediaFileManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.Strings;
using Umbraco.Extensions;
Expand All @@ -22,29 +21,37 @@ public sealed class MediaFileManager
private readonly IShortStringHelper _shortStringHelper;
private readonly IServiceProvider _serviceProvider;
private MediaUrlGeneratorCollection _mediaUrlGenerators;
private readonly ContentSettings _contentSettings;

/// <summary>
/// Gets the media filesystem.
/// </summary>
public IFileSystem FileSystem { get; }

public MediaFileManager(
IFileSystem fileSystem,
IMediaPathScheme mediaPathScheme,
ILogger<MediaFileManager> logger,
IShortStringHelper shortStringHelper,
IServiceProvider serviceProvider,
IOptions<ContentSettings> contentSettings)
IServiceProvider serviceProvider)
{
_mediaPathScheme = mediaPathScheme;
_logger = logger;
_shortStringHelper = shortStringHelper;
_serviceProvider = serviceProvider;
_contentSettings = contentSettings.Value;
FileSystem = fileSystem;
}

[Obsolete("Use the ctr that doesn't include unused parameters.")]
public MediaFileManager(
IFileSystem fileSystem,
IMediaPathScheme mediaPathScheme,
ILogger<MediaFileManager> logger,
IShortStringHelper shortStringHelper,
IServiceProvider serviceProvider,
IOptions<ContentSettings> contentSettings)
: this(fileSystem, mediaPathScheme, logger, shortStringHelper, serviceProvider)
{ }

/// <summary>
/// Gets the media filesystem.
/// </summary>
public IFileSystem FileSystem { get; }

/// <summary>
/// Delete media files.
/// </summary>
Expand Down
18 changes: 12 additions & 6 deletions src/Umbraco.Core/IO/PhysicalFileSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,17 @@
using System.IO;
using System.Linq;
using System.Threading;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.Hosting;
using Umbraco.Extensions;

namespace Umbraco.Cms.Core.IO
{
public interface IPhysicalFileSystem : IFileSystem {}
public class PhysicalFileSystem : IPhysicalFileSystem
public interface IPhysicalFileSystem : IFileSystem
{ }

public class PhysicalFileSystem : IPhysicalFileSystem, IFileProviderFactory
{
private readonly IIOHelper _ioHelper;
private readonly ILogger<PhysicalFileSystem> _logger;
Expand All @@ -28,7 +31,7 @@ public class PhysicalFileSystem : IPhysicalFileSystem
// eg "" or "/Views" or "/Media" or "/<vpath>/Media" in case of a virtual path
private readonly string _rootUrl;

public PhysicalFileSystem(IIOHelper ioHelper,IHostingEnvironment hostingEnvironment, ILogger<PhysicalFileSystem> logger, string rootPath, string rootUrl)
public PhysicalFileSystem(IIOHelper ioHelper, IHostingEnvironment hostingEnvironment, ILogger<PhysicalFileSystem> logger, string rootPath, string rootUrl)
{
_ioHelper = ioHelper ?? throw new ArgumentNullException(nameof(ioHelper));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
Expand Down Expand Up @@ -270,7 +273,7 @@ public string GetRelativePath(string fullPathOrUrl)
return path.Substring(_rootUrl.Length).TrimStart(Constants.CharArrays.ForwardSlash);

// unchanged - what else?
return path;
return path.TrimStart(Constants.CharArrays.ForwardSlash);
}

/// <summary>
Expand All @@ -285,7 +288,7 @@ public string GetRelativePath(string fullPathOrUrl)
public string GetFullPath(string path)
{
// normalize
var opath = path;
var originalPath = path;
path = EnsureDirectorySeparatorChar(path);

// FIXME: this part should go!
Expand Down Expand Up @@ -318,7 +321,7 @@ public string GetFullPath(string path)

// nothing prevents us to reach the file, security-wise, yet it is outside
// this filesystem's root - throw
throw new UnauthorizedAccessException($"File original: [{opath}] full: [{path}] is outside this filesystem's root.");
throw new UnauthorizedAccessException($"File original: [{originalPath}] full: [{path}] is outside this filesystem's root.");
}

/// <summary>
Expand Down Expand Up @@ -450,6 +453,9 @@ protected void WithRetry(Action action)
}
}

/// <inheritdoc />
public IFileProvider Create() => new PhysicalFileProvider(_rootPath);

#endregion
}
}
8 changes: 6 additions & 2 deletions src/Umbraco.Core/IO/ShadowWrapper.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.Hosting;
using Umbraco.Extensions;

namespace Umbraco.Cms.Core.IO
{
internal class ShadowWrapper : IFileSystem
internal class ShadowWrapper : IFileSystem, IFileProviderFactory
{
private static readonly string ShadowFsPath = Constants.SystemDirectories.TempData.EnsureEndsWith('/') + "ShadowFs";

Expand Down Expand Up @@ -220,5 +221,8 @@ public void AddFile(string path, string physicalPath, bool overrideIfExists = tr
{
FileSystem.AddFile(path, physicalPath, overrideIfExists, copy);
}

/// <inheritdoc />
public IFileProvider Create() => _innerFileSystem.TryCreateFileProvider(out IFileProvider fileProvider) ? fileProvider : null;
}
}
2 changes: 1 addition & 1 deletion src/Umbraco.Core/Packaging/PackagesRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ public PackagesRepository(

_tempFolderPath = tempFolderPath ?? Constants.SystemDirectories.TempData.EnsureEndsWith('/') + "PackageFiles";
_packagesFolderPath = packagesFolderPath ?? Constants.SystemDirectories.Packages;
_mediaFolderPath = mediaFolderPath ?? globalSettings.Value.UmbracoMediaPath + "/created-packages";
_mediaFolderPath = mediaFolderPath ?? Path.Combine(globalSettings.Value.UmbracoMediaPhysicalRootPath, Constants.SystemDirectories.CreatedPackages);

_parser = new PackageDefinitionXmlParser();
_mediaService = mediaService;
Expand Down
2 changes: 1 addition & 1 deletion src/Umbraco.Core/Runtime/EssentialDirectoryCreator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public void Handle(UmbracoApplicationStartingNotification notification)
// ensure we have some essential directories
// every other component can then initialize safely
_ioHelper.EnsurePathExists(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Data));
_ioHelper.EnsurePathExists(_hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoMediaPath));
_ioHelper.EnsurePathExists(_hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoMediaPhysicalRootPath));
_ioHelper.EnsurePathExists(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.MvcViews));
_ioHelper.EnsurePathExists(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.PartialViews));
_ioHelper.EnsurePathExists(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.MacroPartials));
Expand Down
3 changes: 2 additions & 1 deletion src/Umbraco.Core/Umbraco.Core.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
Expand All @@ -17,6 +17,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="5.0.11" />
<PackageReference Include="Microsoft.Extensions.FileProviders.Physical" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Options" Version="5.0.0" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ internal static IUmbracoBuilder AddFileSystems(this IUmbracoBuilder builder)
ILogger<PhysicalFileSystem> logger = factory.GetRequiredService<ILogger<PhysicalFileSystem>>();
GlobalSettings globalSettings = factory.GetRequiredService<IOptions<GlobalSettings>>().Value;

var rootPath = hostingEnvironment.MapPathWebRoot(globalSettings.UmbracoMediaPath);
var rootPath = hostingEnvironment.MapPathWebRoot(globalSettings.UmbracoMediaPhysicalRootPath);
var rootUrl = hostingEnvironment.ToAbsolute(globalSettings.UmbracoMediaPath);
return new PhysicalFileSystem(ioHelper, hostingEnvironment, logger, rootPath, rootUrl);
});
Expand Down
4 changes: 2 additions & 2 deletions src/Umbraco.Infrastructure/Install/FilePermissionHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public FilePermissionHelper(IOptions<GlobalSettings> globalSettings, IIOHelper i
hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoCssPath),
hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Config),
hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Data),
hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoMediaPath),
hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoMediaPhysicalRootPath),
hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Preview)
};
_packagesPermissionsDirs = new[]
Expand All @@ -70,7 +70,7 @@ public bool RunFilePermissionTestSuite(out Dictionary<FilePermissionTest, IEnume
EnsureFiles(_permissionFiles, out errors);
report[FilePermissionTest.FileWriting] = errors.ToList();

EnsureCanCreateSubDirectory(_hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoMediaPath), out errors);
EnsureCanCreateSubDirectory(_hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoMediaPhysicalRootPath), out errors);
report[FilePermissionTest.MediaFolderCreation] = errors.ToList();

return report.Sum(x => x.Value.Count()) == 0;
Expand Down
Loading