using System.ComponentModel;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Hosting;
namespace Umbraco.Cms.Core.IO
{
///
/// Provides the system filesystems.
///
public sealed class FileSystems
{
private readonly ILogger _logger;
private readonly ILoggerFactory _loggerFactory;
private readonly IIOHelper _ioHelper;
private GlobalSettings _globalSettings;
private readonly IHostingEnvironment _hostingEnvironment;
// wrappers for shadow support
private ShadowWrapper? _partialViewsFileSystem;
private ShadowWrapper? _stylesheetsFileSystem;
private ShadowWrapper? _scriptsFileSystem;
private ShadowWrapper? _mvcViewsFileSystem;
// well-known file systems lazy initialization
private object _wkfsLock = new();
private bool _wkfsInitialized;
private object? _wkfsObject; // unused
// shadow support
private readonly List _shadowWrappers = new();
private readonly Lock _shadowLocker = new();
private static string? _shadowCurrentId; // static - unique!!
#region Constructor
// DI wants a public ctor
public FileSystems(
ILoggerFactory loggerFactory,
IIOHelper ioHelper,
IOptions globalSettings,
IHostingEnvironment hostingEnvironment)
{
_logger = loggerFactory.CreateLogger();
_loggerFactory = loggerFactory;
_ioHelper = ioHelper;
_globalSettings = globalSettings.Value;
_hostingEnvironment = hostingEnvironment;
}
// Ctor for tests, allows you to set the various filesystems
internal FileSystems(
ILoggerFactory loggerFactory,
IIOHelper ioHelper,
IOptions globalSettings,
IHostingEnvironment hostingEnvironment,
IFileSystem partialViewsFileSystem,
IFileSystem stylesheetFileSystem,
IFileSystem scriptsFileSystem,
IFileSystem mvcViewFileSystem) : this(loggerFactory, ioHelper, globalSettings, hostingEnvironment)
{
_partialViewsFileSystem = CreateShadowWrapperInternal(partialViewsFileSystem, "partials");
_stylesheetsFileSystem = CreateShadowWrapperInternal(stylesheetFileSystem, "css");
_scriptsFileSystem = CreateShadowWrapperInternal(scriptsFileSystem, "scripts");
_mvcViewsFileSystem = CreateShadowWrapperInternal(mvcViewFileSystem, "view");
// Set initialized to true so the filesystems doesn't get overwritten.
_wkfsInitialized = true;
}
///
/// Used be Scope provider to take control over the filesystems, should never be used for anything else.
///
[EditorBrowsable(EditorBrowsableState.Never)]
public Func? IsScoped { get; set; } = () => false;
#endregion
#region Well-Known FileSystems
///
/// Gets the partial views filesystem.
///
public IFileSystem? PartialViewsFileSystem
{
get
{
if (Volatile.Read(ref _wkfsInitialized) == false)
{
EnsureWellKnownFileSystems();
}
return _partialViewsFileSystem;
}
}
///
/// Gets the stylesheets filesystem.
///
public IFileSystem? StylesheetsFileSystem
{
get
{
if (Volatile.Read(ref _wkfsInitialized) == false)
{
EnsureWellKnownFileSystems();
}
return _stylesheetsFileSystem;
}
}
///
/// Gets the scripts filesystem.
///
public IFileSystem? ScriptsFileSystem
{
get
{
if (Volatile.Read(ref _wkfsInitialized) == false)
{
EnsureWellKnownFileSystems();
}
return _scriptsFileSystem;
}
}
///
/// Gets the MVC views filesystem.
///
public IFileSystem? MvcViewsFileSystem
{
get
{
if (Volatile.Read(ref _wkfsInitialized) == false)
{
EnsureWellKnownFileSystems();
}
return _mvcViewsFileSystem;
}
}
///
/// Sets the stylesheet filesystem.
///
///
/// Be careful when using this, the root path and root url must be correct for this to work.
///
/// The .
/// If the is null
/// Throws exception if the StylesheetFileSystem has already been initialized.
/// Throws exception if full path can't be resolved successfully.
public void SetStylesheetFilesystem(IFileSystem fileSystem)
{
if (fileSystem == null)
{
throw new ArgumentNullException(nameof(fileSystem));
}
if (_stylesheetsFileSystem != null)
{
throw new InvalidOperationException(
"The StylesheetFileSystem cannot be changed when it's already been initialized.");
}
// Verify that _rootUrl/_rootPath is correct
// We have to do this because there's a tight coupling
// to the VirtualPath we get with CodeFileDisplay from the frontend.
try
{
fileSystem.GetFullPath("/css/");
}
catch (UnauthorizedAccessException exception)
{
throw new UnauthorizedAccessException(
"Can't register the stylesheet filesystem, "
+ "this is most likely caused by using a PhysicalFileSystem with an incorrect "
+ "rootPath/rootUrl. RootPath must be \\wwwroot\\css"
+ " and rootUrl must be /css",
exception);
}
_stylesheetsFileSystem = CreateShadowWrapperInternal(fileSystem, "css");
}
private void EnsureWellKnownFileSystems() => LazyInitializer.EnsureInitialized(ref _wkfsObject, ref _wkfsInitialized, ref _wkfsLock, CreateWellKnownFileSystems);
// need to return something to LazyInitializer.EnsureInitialized
// but it does not really matter what we return - here, null
private object? CreateWellKnownFileSystems()
{
ILogger logger = _loggerFactory.CreateLogger();
//TODO this is fucked, why do PhysicalFileSystem has a root url? Mvc views cannot be accessed by url!
var partialViewsFileSystem = new PhysicalFileSystem(_ioHelper, _hostingEnvironment, logger, _hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.PartialViews), _hostingEnvironment.ToAbsolute(Constants.SystemDirectories.PartialViews));
var scriptsFileSystem = new PhysicalFileSystem(_ioHelper, _hostingEnvironment, logger, _hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoScriptsPath), _hostingEnvironment.ToAbsolute(_globalSettings.UmbracoScriptsPath));
var mvcViewsFileSystem = new PhysicalFileSystem(_ioHelper, _hostingEnvironment, logger, _hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.MvcViews), _hostingEnvironment.ToAbsolute(Constants.SystemDirectories.MvcViews));
_partialViewsFileSystem = new ShadowWrapper(partialViewsFileSystem, _ioHelper, _hostingEnvironment, _loggerFactory, "partials", IsScoped);
_scriptsFileSystem = new ShadowWrapper(scriptsFileSystem, _ioHelper, _hostingEnvironment, _loggerFactory, "scripts", IsScoped);
_mvcViewsFileSystem = new ShadowWrapper(mvcViewsFileSystem, _ioHelper, _hostingEnvironment, _loggerFactory, "views", IsScoped);
if (_stylesheetsFileSystem == null)
{
var stylesheetsFileSystem = new PhysicalFileSystem(
_ioHelper,
_hostingEnvironment,
logger,
_hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoCssPath),
_hostingEnvironment.ToAbsolute(_globalSettings.UmbracoCssPath));
_stylesheetsFileSystem = new ShadowWrapper(stylesheetsFileSystem, _ioHelper, _hostingEnvironment, _loggerFactory, "css", IsScoped);
_shadowWrappers.Add(_stylesheetsFileSystem);
}
// TODO: do we need a lock here?
_shadowWrappers.Add(_partialViewsFileSystem);
_shadowWrappers.Add(_scriptsFileSystem);
_shadowWrappers.Add(_mvcViewsFileSystem);
return null;
}
#endregion
#region Shadow
// note
// shadowing is thread-safe, but entering and exiting shadow mode is not, and there is only one
// global shadow for the entire application, so great care should be taken to ensure that the
// application is *not* doing anything else when using a shadow.
///
/// Shadows the filesystem, should never be used outside the Scope class.
///
///
[EditorBrowsable(EditorBrowsableState.Never)]
public ICompletable Shadow()
{
if (Volatile.Read(ref _wkfsInitialized) == false)
{
EnsureWellKnownFileSystems();
}
var id = ShadowWrapper.CreateShadowId(_hostingEnvironment);
return new ShadowFileSystems(this, id); // will invoke BeginShadow and EndShadow
}
internal void BeginShadow(string id)
{
lock (_shadowLocker)
{
// if we throw here, it means that something very wrong happened.
if (_shadowCurrentId != null)
{
throw new InvalidOperationException("Already shadowing.");
}
_shadowCurrentId = id;
if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug))
{
_logger.LogDebug("Shadow '{ShadowId}'", _shadowCurrentId);
}
foreach (ShadowWrapper wrapper in _shadowWrappers)
{
wrapper.Shadow(_shadowCurrentId);
}
}
}
internal void EndShadow(string id, bool completed)
{
lock (_shadowLocker)
{
// if we throw here, it means that something very wrong happened.
if (_shadowCurrentId == null)
{
throw new InvalidOperationException("Not shadowing.");
}
if (id != _shadowCurrentId)
{
throw new InvalidOperationException("Not the current shadow.");
}
if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug))
{
_logger.LogDebug("UnShadow '{ShadowId}' {Status}", id, completed ? "complete" : "abort");
}
var exceptions = new List();
foreach (ShadowWrapper wrapper in _shadowWrappers)
{
try
{
// this may throw an AggregateException if some of the changes could not be applied
wrapper.UnShadow(completed);
}
catch (AggregateException ae)
{
exceptions.Add(ae);
}
}
_shadowCurrentId = null;
if (exceptions.Count > 0)
{
throw new AggregateException(completed ? "Failed to apply all changes (see exceptions)." : "Failed to abort (see exceptions).", exceptions);
}
}
}
///
/// Creates a shadow wrapper for a filesystem, should never be used outside UmbracoBuilder or testing
///
///
///
///
[EditorBrowsable(EditorBrowsableState.Never)]
public IFileSystem CreateShadowWrapper(IFileSystem filesystem, string shadowPath) => CreateShadowWrapperInternal(filesystem, shadowPath);
private ShadowWrapper CreateShadowWrapperInternal(IFileSystem filesystem, string shadowPath)
{
lock (_shadowLocker)
{
var wrapper = new ShadowWrapper(filesystem, _ioHelper, _hostingEnvironment, _loggerFactory, shadowPath,() => IsScoped?.Invoke());
if (_shadowCurrentId != null)
{
wrapper.Shadow(_shadowCurrentId);
}
_shadowWrappers.Add(wrapper);
return wrapper;
}
}
#endregion
}
}