Files
Umbraco-CMS/src/Umbraco.Core/IO/FileSystems.cs

297 lines
12 KiB
C#
Raw Normal View History

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
2017-05-31 15:25:24 +02:00
using System.Threading;
2020-09-17 11:35:29 +02:00
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Hosting;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.IO
2017-05-12 14:49:44 +02:00
{
2018-07-20 09:49:05 +02:00
public class FileSystems : IFileSystems
{
private readonly IServiceProvider _container;
2020-09-17 11:35:29 +02:00
private readonly ILogger<FileSystems> _logger;
private readonly ILoggerFactory _loggerFactory;
private readonly IIOHelper _ioHelper;
2018-11-24 15:38:00 +01:00
private readonly ConcurrentDictionary<Type, Lazy<IFileSystem>> _filesystems = new ConcurrentDictionary<Type, Lazy<IFileSystem>>();
2017-05-12 14:49:44 +02:00
// wrappers for shadow support
2017-05-12 14:49:44 +02:00
private ShadowWrapper _macroPartialFileSystem;
private ShadowWrapper _partialViewsFileSystem;
private ShadowWrapper _stylesheetsFileSystem;
private ShadowWrapper _scriptsFileSystem;
private ShadowWrapper _mvcViewsFileSystem;
2018-11-19 14:40:59 +01:00
2017-05-31 15:25:24 +02:00
// well-known file systems lazy initialization
private object _wkfsLock = new object();
private bool _wkfsInitialized;
2018-11-19 14:40:59 +01:00
private object _wkfsObject; // unused
2017-05-31 15:25:24 +02:00
2018-11-19 14:40:59 +01:00
// shadow support
private readonly List<ShadowWrapper> _shadowWrappers = new List<ShadowWrapper>();
private readonly object _shadowLocker = new object();
2019-02-20 14:10:15 +01:00
private static string _shadowCurrentId; // static - unique!!
2017-05-12 14:49:44 +02:00
#region Constructor
2017-05-22 17:22:10 +02:00
// DI wants a public ctor
public FileSystems(IServiceProvider container, ILogger<FileSystems> logger, ILoggerFactory loggerFactory, IIOHelper ioHelper, IOptions<GlobalSettings> globalSettings, IHostingEnvironment hostingEnvironment)
{
2018-10-26 15:06:53 +02:00
_container = container;
_logger = logger;
2020-09-17 11:35:29 +02:00
_loggerFactory = loggerFactory;
_ioHelper = ioHelper;
_globalSettings = globalSettings.Value;
_hostingEnvironment = hostingEnvironment;
2017-05-12 14:49:44 +02:00
}
2017-05-12 14:49:44 +02:00
// for tests only, totally unsafe
internal void Reset()
{
2018-11-19 14:40:59 +01:00
_shadowWrappers.Clear();
2017-05-12 14:49:44 +02:00
_filesystems.Clear();
2017-05-31 15:25:24 +02:00
Volatile.Write(ref _wkfsInitialized, false);
2019-02-20 14:10:15 +01:00
_shadowCurrentId = null;
2018-11-19 14:40:59 +01:00
}
// for tests only, totally unsafe
internal static void ResetShadowId()
{
2019-02-20 14:10:15 +01:00
_shadowCurrentId = null;
}
2018-11-19 14:40:59 +01:00
// set by the scope provider when taking control of filesystems
public Func<bool> IsScoped { get; set; } = () => false;
#endregion
#region Well-Known FileSystems
2018-10-26 15:06:53 +02:00
/// <inheritdoc />
2017-05-31 15:25:24 +02:00
public IFileSystem MacroPartialsFileSystem
{
get
{
if (Volatile.Read(ref _wkfsInitialized) == false) EnsureWellKnownFileSystems();
return _macroPartialFileSystem;
}
}
2018-10-26 15:06:53 +02:00
/// <inheritdoc />
2017-05-31 15:25:24 +02:00
public IFileSystem PartialViewsFileSystem
{
get
{
if (Volatile.Read(ref _wkfsInitialized) == false) EnsureWellKnownFileSystems();
return _partialViewsFileSystem;
}
}
2018-10-26 15:06:53 +02:00
/// <inheritdoc />
2017-05-31 15:25:24 +02:00
public IFileSystem StylesheetsFileSystem
{
get
{
if (Volatile.Read(ref _wkfsInitialized) == false) EnsureWellKnownFileSystems();
return _stylesheetsFileSystem;
}
}
2018-10-26 15:06:53 +02:00
/// <inheritdoc />
2017-05-31 15:25:24 +02:00
public IFileSystem ScriptsFileSystem
{
get
{
if (Volatile.Read(ref _wkfsInitialized) == false) EnsureWellKnownFileSystems();
return _scriptsFileSystem;
}
}
2018-10-26 15:06:53 +02:00
/// <inheritdoc />
2017-05-31 15:25:24 +02:00
public IFileSystem MvcViewsFileSystem
{
get
{
if (Volatile.Read(ref _wkfsInitialized) == false) EnsureWellKnownFileSystems();
return _mvcViewsFileSystem;
}
}
2017-05-12 14:49:44 +02:00
2017-05-31 15:25:24 +02:00
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()
2017-05-12 14:49:44 +02:00
{
2020-09-24 10:29:53 +02:00
var logger = _loggerFactory.CreateLogger<PhysicalFileSystem>();
//TODO this is fucked, why do PhysicalFileSystem has a root url? Mvc views cannot be accessed by url!
var macroPartialFileSystem = new PhysicalFileSystem(_ioHelper, _hostingEnvironment, logger, _hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.MacroPartials), _hostingEnvironment.ToAbsolute(Constants.SystemDirectories.MacroPartials));
var partialViewsFileSystem = new PhysicalFileSystem(_ioHelper, _hostingEnvironment, logger, _hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.PartialViews), _hostingEnvironment.ToAbsolute(Constants.SystemDirectories.PartialViews));
var stylesheetsFileSystem = new PhysicalFileSystem(_ioHelper, _hostingEnvironment, logger, _hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoCssPath), _hostingEnvironment.ToAbsolute(_globalSettings.UmbracoCssPath));
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));
2020-09-17 11:35:29 +02:00
_macroPartialFileSystem = new ShadowWrapper(macroPartialFileSystem, _ioHelper, _hostingEnvironment, _loggerFactory, "macro-partials", IsScoped);
_partialViewsFileSystem = new ShadowWrapper(partialViewsFileSystem, _ioHelper, _hostingEnvironment, _loggerFactory, "partials", IsScoped);
_stylesheetsFileSystem = new ShadowWrapper(stylesheetsFileSystem, _ioHelper, _hostingEnvironment, _loggerFactory, "css", IsScoped);
_scriptsFileSystem = new ShadowWrapper(scriptsFileSystem, _ioHelper, _hostingEnvironment, _loggerFactory, "scripts", IsScoped);
_mvcViewsFileSystem = new ShadowWrapper(mvcViewsFileSystem, _ioHelper, _hostingEnvironment, _loggerFactory, "views", IsScoped);
2018-11-19 14:40:59 +01:00
// TODO: do we need a lock here?
2018-11-19 14:40:59 +01:00
_shadowWrappers.Add(_macroPartialFileSystem);
_shadowWrappers.Add(_partialViewsFileSystem);
_shadowWrappers.Add(_stylesheetsFileSystem);
_shadowWrappers.Add(_scriptsFileSystem);
_shadowWrappers.Add(_mvcViewsFileSystem);
2017-05-31 15:25:24 +02:00
return null;
2017-05-12 14:49:44 +02:00
}
#endregion
#region Providers
2019-02-20 16:32:28 +01:00
private readonly Dictionary<Type, string> _paths = new Dictionary<Type, string>();
// internal for tests
internal IReadOnlyDictionary<Type, string> Paths => _paths;
private GlobalSettings _globalSettings;
private readonly IHostingEnvironment _hostingEnvironment;
2019-02-20 16:32:28 +01:00
2017-05-12 14:49:44 +02:00
/// <summary>
/// Gets a strongly-typed filesystem.
/// </summary>
/// <typeparam name="TFileSystem">The type of the filesystem.</typeparam>
/// <returns>A strongly-typed filesystem of the specified type.</returns>
/// <remarks>
/// <para>Note that any filesystem created by this method *after* shadowing begins, will *not* be
/// shadowing (and an exception will be thrown by the ShadowWrapper).</para>
/// </remarks>
2018-11-24 15:38:00 +01:00
public TFileSystem GetFileSystem<TFileSystem>(IFileSystem supporting)
2017-05-12 14:49:44 +02:00
where TFileSystem : FileSystemWrapper
{
2018-11-19 14:40:59 +01:00
if (Volatile.Read(ref _wkfsInitialized) == false) EnsureWellKnownFileSystems();
2018-10-26 15:06:53 +02:00
2018-11-24 15:38:00 +01:00
return (TFileSystem) _filesystems.GetOrAdd(typeof(TFileSystem), _ => new Lazy<IFileSystem>(() =>
2018-11-19 14:40:59 +01:00
{
2019-02-20 16:32:28 +01:00
var typeofTFileSystem = typeof(TFileSystem);
// path must be unique and not collide with paths used in CreateWellKnownFileSystems
// for our well-known 'media' filesystem we can use the short 'media' path
// for others, put them under 'x/' and use ... something
string path;
if (typeofTFileSystem == typeof(MediaFileSystem))
{
path = "media";
}
else
{
lock (_paths)
{
if (!_paths.TryGetValue(typeofTFileSystem, out path))
{
path = Guid.NewGuid().ToString("N").Substring(0, 6);
while (_paths.ContainsValue(path)) // this can't loop forever, right?
path = Guid.NewGuid().ToString("N").Substring(0, 6);
_paths[typeofTFileSystem] = path;
}
}
path = "x/" + path;
}
var shadowWrapper = CreateShadowWrapper(supporting, path);
2018-11-24 16:41:27 +01:00
return _container.CreateInstance<TFileSystem>(shadowWrapper);
2018-10-26 15:06:53 +02:00
})).Value;
2017-05-12 14:49:44 +02:00
}
#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.
public ICompletable Shadow()
{
2017-06-23 18:54:42 +02:00
if (Volatile.Read(ref _wkfsInitialized) == false) EnsureWellKnownFileSystems();
var id = ShadowWrapper.CreateShadowId(_hostingEnvironment);
2018-11-19 14:40:59 +01:00
return new ShadowFileSystems(this, id); // will invoke BeginShadow and EndShadow
}
2019-02-20 14:10:15 +01:00
internal void BeginShadow(string id)
{
2018-11-19 14:40:59 +01:00
lock (_shadowLocker)
{
2018-11-19 14:40:59 +01:00
// if we throw here, it means that something very wrong happened.
2019-02-20 14:10:15 +01:00
if (_shadowCurrentId != null)
2018-11-19 14:40:59 +01:00
throw new InvalidOperationException("Already shadowing.");
2019-02-20 14:10:15 +01:00
2018-11-19 14:40:59 +01:00
_shadowCurrentId = id;
2020-09-16 10:24:05 +02:00
_logger.LogDebug("Shadow '{ShadowId}'", _shadowCurrentId);
2018-11-19 14:40:59 +01:00
foreach (var wrapper in _shadowWrappers)
2019-02-20 14:10:15 +01:00
wrapper.Shadow(_shadowCurrentId);
}
2018-11-19 14:40:59 +01:00
}
2019-02-20 14:10:15 +01:00
internal void EndShadow(string id, bool completed)
2018-11-19 14:40:59 +01:00
{
lock (_shadowLocker)
{
2018-11-19 14:40:59 +01:00
// if we throw here, it means that something very wrong happened.
2019-02-20 14:10:15 +01:00
if (_shadowCurrentId == null)
2018-11-19 14:40:59 +01:00
throw new InvalidOperationException("Not shadowing.");
if (id != _shadowCurrentId)
throw new InvalidOperationException("Not the current shadow.");
2020-09-16 10:24:05 +02:00
_logger.LogDebug("UnShadow '{ShadowId}' {Status}", id, completed ? "complete" : "abort");
2018-11-19 14:40:59 +01:00
var exceptions = new List<Exception>();
foreach (var wrapper in _shadowWrappers)
{
2018-11-19 14:40:59 +01:00
try
{
// this may throw an AggregateException if some of the changes could not be applied
wrapper.UnShadow(completed);
}
catch (AggregateException ae)
{
exceptions.Add(ae);
}
}
2018-11-19 14:40:59 +01:00
2019-02-20 14:10:15 +01:00
_shadowCurrentId = null;
2018-11-19 14:40:59 +01:00
if (exceptions.Count > 0)
throw new AggregateException(completed ? "Failed to apply all changes (see exceptions)." : "Failed to abort (see exceptions).", exceptions);
}
2018-11-19 14:40:59 +01:00
}
2018-11-19 14:40:59 +01:00
private ShadowWrapper CreateShadowWrapper(IFileSystem filesystem, string shadowPath)
{
lock (_shadowLocker)
{
Netcore: Alternate approach for MSDI refactor (#9247) * Doesn't make much sense to have Concrete on IRegister, only on IFactory * Handle FilesTreeController requires IFileSystem of type PhysicalFileSystem * Handle registration of default MediaFileSystem without using RegisterUniqueFor * Remove RegisterFor / RegisterUniqueFor from IRegister * Switch over from LightInject to wrappers around MSDI * Made mapper dependencies more explicit * Remove registration for AngularJsonMediaTypeFormatter It's dependencies aren't registered so container validation fails * Resolve lifetime issue for EnsureValidSessionId by service locating else resolve scoped in singleton * Make registration more explicit for backoffice UserManager * Make install step registrations more explicit * Disable service provider validation so site can launch Maybe this is a problem maybe not, we build about 8000 service providers so maybe everything is fine later... * Further cleanup of IFactory interface * Further cleanup of IRegister interface * Revert "Make registration more explicit for backoffice UserManager" This reverts commit 7215fe836103c597cd0873c66737a79b91ed4c49. * Resolve issue where NewInstallStep would fail to reset password for "SuperUser" Before MSDI, somehow BackOfficeIdentityOptions would be configured with token provider map from IdentityBuilder.AddDefaultTokenProviders. After switchover those config actions are lost. Subclass IdentityBuilder to ensure BackOfficeIdentityOptions doesn't miss config setup upstream. * Initialize current. * Add todo to turn container validation back on. * Migrated ScopeFileSystemsTests to integration tests Signed-off-by: Bjarke Berg <mail@bergmania.dk> * Resolve issue where MediaFileSystem was skipping ShadowFileSystem * Attempt to fix ScopeFileSystemsTests on azure devops Signed-off-by: Bjarke Berg <mail@bergmania.dk> * Be interesting to know what the actual full path is in pipeline. * Clarify intent of CreateMediaTest Doesn't help resolve weird UnauthorizedAccessException but it cuts so much cognitive overhead for the future. * Use ILoggerfactory rather than mock for the manually constructed file PhysicalFileSystem * Maybe resolve failing test on azure pipeline. Co-authored-by: Bjarke Berg <mail@bergmania.dk>
2020-10-26 10:47:14 +00:00
var wrapper = new ShadowWrapper(filesystem, _ioHelper, _hostingEnvironment, _loggerFactory, shadowPath,() => IsScoped());
2019-02-20 14:10:15 +01:00
if (_shadowCurrentId != null)
2018-11-19 14:40:59 +01:00
wrapper.Shadow(_shadowCurrentId);
_shadowWrappers.Add(wrapper);
return wrapper;
}
}
2018-11-19 14:40:59 +01:00
#endregion
}
}