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

288 lines
11 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;
using Umbraco.Core.Logging;
2017-05-30 15:56:27 +02:00
using Umbraco.Core.Composing;
using Umbraco.Core.Configuration;
namespace Umbraco.Core.IO
2017-05-12 14:49:44 +02:00
{
2018-07-20 09:49:05 +02:00
public class FileSystems : IFileSystems
{
2018-11-28 11:05:41 +01:00
private readonly IFactory _container;
private readonly ILogger _logger;
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(IFactory container, ILogger logger, IIOHelper ioHelper, IGlobalSettings globalSettings)
{
2018-10-26 15:06:53 +02:00
_container = container;
_logger = logger;
_ioHelper = ioHelper;
_globalSettings = globalSettings;
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
2017-05-31 15:25:24 +02:00
internal 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
{
var macroPartialFileSystem = new PhysicalFileSystem(Constants.SystemDirectories.MacroPartials);
var partialViewsFileSystem = new PhysicalFileSystem(Constants.SystemDirectories.PartialViews);
var stylesheetsFileSystem = new PhysicalFileSystem(_globalSettings.UmbracoCssPath);
var scriptsFileSystem = new PhysicalFileSystem(_globalSettings.UmbracoScriptsPath);
var mvcViewsFileSystem = new PhysicalFileSystem(Constants.SystemDirectories.MvcViews);
2017-05-12 14:49:44 +02:00
2019-02-20 16:32:28 +01:00
_macroPartialFileSystem = new ShadowWrapper(macroPartialFileSystem, "macro-partials", IsScoped);
_partialViewsFileSystem = new ShadowWrapper(partialViewsFileSystem, "partials", IsScoped);
2018-11-19 14:40:59 +01:00
_stylesheetsFileSystem = new ShadowWrapper(stylesheetsFileSystem, "css", IsScoped);
_scriptsFileSystem = new ShadowWrapper(scriptsFileSystem, "scripts", IsScoped);
2019-02-20 16:32:28 +01:00
_mvcViewsFileSystem = new ShadowWrapper(mvcViewsFileSystem, "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>();
private IGlobalSettings _globalSettings;
2019-02-20 16:32:28 +01:00
// internal for tests
internal IReadOnlyDictionary<Type, string> Paths => _paths;
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.
2019-02-20 14:10:15 +01:00
internal ICompletable Shadow()
{
2017-06-23 18:54:42 +02:00
if (Volatile.Read(ref _wkfsInitialized) == false) EnsureWellKnownFileSystems();
2019-02-20 14:10:15 +01:00
var id = ShadowWrapper.CreateShadowId();
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;
2019-02-20 14:10:15 +01:00
_logger.Debug<ShadowFileSystems>("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.");
_logger.Debug<ShadowFileSystems>("UnShadow '{ShadowId}' {Status}", id, completed ? "complete" : "abort");
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)
{
2018-11-19 14:40:59 +01:00
var wrapper = new ShadowWrapper(filesystem, 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
}
}