Refactor filesystems

This commit is contained in:
Stephan
2018-10-26 15:06:53 +02:00
parent 1644434ea7
commit eee7222e0b
25 changed files with 380 additions and 466 deletions

View File

@@ -1,36 +1,20 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Configuration;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using Umbraco.Core.Configuration;
using Umbraco.Core.Logging;
using Umbraco.Core.Composing;
namespace Umbraco.Core.IO
{
public interface IFileSystems // fixme move!
{
IFileSystem MacroPartialsFileSystem { get; }
IFileSystem PartialViewsFileSystem { get; }
IFileSystem StylesheetsFileSystem { get; }
IFileSystem ScriptsFileSystem { get; }
IFileSystem MasterPagesFileSystem { get; }
IFileSystem MvcViewsFileSystem { get; }
MediaFileSystem MediaFileSystem { get; }
}
public class FileSystems : IFileSystems
{
private readonly IFileSystemProvidersSection _config;
private readonly ConcurrentSet<ShadowWrapper> _wrappers = new ConcurrentSet<ShadowWrapper>();
private readonly IContainer _container;
private readonly ILogger _logger;
private readonly ConcurrentDictionary<string, ProviderConstructionInfo> _providerLookup = new ConcurrentDictionary<string, ProviderConstructionInfo>();
private readonly ConcurrentDictionary<string, IFileSystem> _filesystems = new ConcurrentDictionary<string, IFileSystem>();
private readonly ConcurrentDictionary<string, Lazy<IFileSystem>> _filesystems = new ConcurrentDictionary<string, Lazy<IFileSystem>>();
// wrappers for shadow support
private ShadowWrapper _macroPartialFileSystem;
@@ -50,11 +34,9 @@ namespace Umbraco.Core.IO
#region Constructor
// DI wants a public ctor
// but IScopeProviderInternal is not public
public FileSystems(ILogger logger)
public FileSystems(IContainer container, ILogger logger)
{
// fixme inject config section => can be used by tests
_config = (FileSystemProvidersSection) ConfigurationManager.GetSection("umbracoConfiguration/FileSystemProviders");
_container = container;
_logger = logger;
}
@@ -62,7 +44,6 @@ namespace Umbraco.Core.IO
internal void Reset()
{
_wrappers.Clear();
_providerLookup.Clear();
_filesystems.Clear();
Volatile.Write(ref _wkfsInitialized, false);
}
@@ -73,6 +54,7 @@ namespace Umbraco.Core.IO
#region Well-Known FileSystems
/// <inheritdoc />
public IFileSystem MacroPartialsFileSystem
{
get
@@ -82,6 +64,7 @@ namespace Umbraco.Core.IO
}
}
/// <inheritdoc />
public IFileSystem PartialViewsFileSystem
{
get
@@ -91,6 +74,7 @@ namespace Umbraco.Core.IO
}
}
/// <inheritdoc />
public IFileSystem StylesheetsFileSystem
{
get
@@ -100,6 +84,7 @@ namespace Umbraco.Core.IO
}
}
/// <inheritdoc />
public IFileSystem ScriptsFileSystem
{
get
@@ -108,16 +93,18 @@ namespace Umbraco.Core.IO
return _scriptsFileSystem;
}
}
/// <inheritdoc />
public IFileSystem MasterPagesFileSystem
{
get
{
if (Volatile.Read(ref _wkfsInitialized) == false) EnsureWellKnownFileSystems();
return _masterPagesFileSystem;// fixme - see 7.6?!
return _masterPagesFileSystem;
}
}
/// <inheritdoc />
public IFileSystem MvcViewsFileSystem
{
get
@@ -127,7 +114,8 @@ namespace Umbraco.Core.IO
}
}
public MediaFileSystem MediaFileSystem
/// <inheritdoc />
public IMediaFileSystem MediaFileSystem
{
get
{
@@ -160,7 +148,7 @@ namespace Umbraco.Core.IO
_mvcViewsFileSystem = new ShadowWrapper(mvcViewsFileSystem, "Views", () => IsScoped());
// filesystems obtained from GetFileSystemProvider are already wrapped and do not need to be wrapped again
_mediaFileSystem = GetFileSystemProvider<MediaFileSystem>();
_mediaFileSystem = GetFileSystem<MediaFileSystem>();
return null;
}
@@ -169,155 +157,42 @@ namespace Umbraco.Core.IO
#region Providers
/// <summary>
/// used to cache the lookup of how to construct this object so we don't have to reflect each time.
/// </summary>
private class ProviderConstructionInfo
{
public object[] Parameters { get; set; }
public ConstructorInfo Constructor { get; set; }
//public string ProviderAlias { get; set; }
}
/// <summary>
/// Gets an underlying (non-typed) filesystem supporting a strongly-typed filesystem.
/// </summary>
/// <param name="alias">The alias of the strongly-typed filesystem.</param>
/// <returns>The non-typed filesystem supporting the strongly-typed filesystem with the specified alias.</returns>
/// <remarks>This method should not be used directly, used <see cref="GetFileSystemProvider{TFileSystem}()"/> instead.</remarks>
internal IFileSystem GetUnderlyingFileSystemProvider(string alias)
{
return GetUnderlyingFileSystemProvider(alias, null);
}
/// <summary>
/// Gets an underlying (non-typed) filesystem supporting a strongly-typed filesystem.
/// </summary>
/// <param name="alias">The alias of the strongly-typed filesystem.</param>
/// <param name="fallback">A fallback creator for the filesystem.</param>
/// <returns>The non-typed filesystem supporting the strongly-typed filesystem with the specified alias.</returns>
/// <remarks>This method should not be used directly, used <see cref="GetFileSystem{TFileSystem}"/> instead.</remarks>
internal IFileSystem GetUnderlyingFileSystemProvider(string alias, Func<IFileSystem> fallback)
{
// either get the constructor info from cache or create it and add to cache
var ctorInfo = _providerLookup.GetOrAdd(alias, _ => GetUnderlyingFileSystemCtor(alias, fallback));
return ctorInfo == null ? fallback() : (IFileSystem) ctorInfo.Constructor.Invoke(ctorInfo.Parameters);
}
private IFileSystem GetUnderlyingFileSystemNoCache(string alias, Func<IFileSystem> fallback)
{
var ctorInfo = GetUnderlyingFileSystemCtor(alias, fallback);
return ctorInfo == null ? fallback() : (IFileSystem) ctorInfo.Constructor.Invoke(ctorInfo.Parameters);
}
private ProviderConstructionInfo GetUnderlyingFileSystemCtor(string alias, Func<IFileSystem> fallback)
{
// get config
if (_config.Providers.TryGetValue(alias, out var providerConfig) == false)
{
if (fallback != null) return null;
throw new ArgumentException($"No provider found with alias {alias}.");
}
// get the filesystem type
var providerType = Type.GetType(providerConfig.Type);
if (providerType == null)
throw new InvalidOperationException($"Could not find type {providerConfig.Type}.");
// ensure it implements IFileSystem
if (providerType.IsAssignableFrom(typeof (IFileSystem)))
throw new InvalidOperationException($"Type {providerType.FullName} does not implement IFileSystem.");
// find a ctor matching the config parameters
var paramCount = providerConfig.Parameters?.Count ?? 0;
var constructor = providerType.GetConstructors().SingleOrDefault(x
=> x.GetParameters().Length == paramCount && x.GetParameters().All(y => providerConfig.Parameters.Keys.Contains(y.Name)));
if (constructor == null)
throw new InvalidOperationException($"Type {providerType.FullName} has no ctor matching the {paramCount} configuration parameter(s).");
var parameters = new object[paramCount];
if (providerConfig.Parameters != null) // keeps ReSharper happy
{
var allKeys = providerConfig.Parameters.Keys.ToArray();
for (var i = 0; i < paramCount; i++)
parameters[i] = providerConfig.Parameters[allKeys[i]];
}
return new ProviderConstructionInfo
{
Constructor = constructor,
Parameters = parameters,
//ProviderAlias = s
};
}
/// <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>Ideally, this should cache the instances, but that would break backward compatibility, so we
/// only do it for our own MediaFileSystem - for everything else, it's the responsibility of the caller
/// to ensure that they maintain singletons. This is important for singletons, as each filesystem maintains
/// its own shadow and having multiple instances would lead to inconsistencies.</para>
/// <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>
// fixme - should it change for v8?
public TFileSystem GetFileSystemProvider<TFileSystem>()
where TFileSystem : FileSystemWrapper
{
return GetFileSystemProvider<TFileSystem>(null);
}
/// <summary>
/// Gets a strongly-typed filesystem.
/// </summary>
/// <typeparam name="TFileSystem">The type of the filesystem.</typeparam>
/// <param name="fallback">A fallback creator for the inner filesystem.</param>
/// <returns>A strongly-typed filesystem of the specified type.</returns>
/// <remarks>
/// <para>The fallback creator is used only if nothing is configured.</para>
/// <para>Ideally, this should cache the instances, but that would break backward compatibility, so we
/// only do it for our own MediaFileSystem - for everything else, it's the responsibility of the caller
/// to ensure that they maintain singletons. This is important for singletons, as each filesystem maintains
/// its own shadow and having multiple instances would lead to inconsistencies.</para>
/// <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>
public TFileSystem GetFileSystemProvider<TFileSystem>(Func<IFileSystem> fallback)
public TFileSystem GetFileSystem<TFileSystem>()
where TFileSystem : FileSystemWrapper
{
var alias = GetFileSystemAlias<TFileSystem>();
return (TFileSystem)_filesystems.GetOrAdd(alias, _ =>
{
// gets the inner fs, create the strongly-typed fs wrapping the inner fs, register & return
// so we are double-wrapping here
// could be optimized by having FileSystemWrapper inherit from ShadowWrapper, maybe
var innerFs = GetUnderlyingFileSystemNoCache(alias, fallback);
var shadowWrapper = new ShadowWrapper(innerFs, "typed/" + alias, () => IsScoped());
var fs = (IFileSystem)Activator.CreateInstance(typeof(TFileSystem), shadowWrapper);
_wrappers.Add(shadowWrapper); // keeping a reference to the wrapper
return fs;
});
// note: GetOrAdd can run multiple times - and here, since we have side effects
// (adding to _wrappers) we want to be sure the factory runs only once, hence the
// additional Lazy.
return (TFileSystem) _filesystems.GetOrAdd(alias, _ => new Lazy<IFileSystem>(() =>
{
var supportingFileSystem = _container.GetInstance<IFileSystem>(alias);
var shadowWrapper = new ShadowWrapper(supportingFileSystem, "typed/" + alias, () => IsScoped());
_wrappers.Add(shadowWrapper); // _wrappers is a concurrent set - this is safe
return _container.CreateInstance<TFileSystem>(new { innerFileSystem = shadowWrapper});
})).Value;
}
private string GetFileSystemAlias<TFileSystem>()
{
var fsType = typeof(TFileSystem);
// validate the ctor
var constructor = fsType.GetConstructors().SingleOrDefault(x
=> x.GetParameters().Length >= 1 && TypeHelper.IsTypeAssignableFrom<IFileSystem>(x.GetParameters().First().ParameterType));
if (constructor == null)
throw new InvalidOperationException("Type " + fsType.FullName + " must inherit from FileSystemWrapper and have a constructor that accepts one parameter of type " + typeof(IFileSystem).FullName + ".");
var fileSystemType = typeof(TFileSystem);
// find the attribute and get the alias
var attr = (FileSystemProviderAttribute)fsType.GetCustomAttributes(typeof(FileSystemProviderAttribute), false).SingleOrDefault();
var attr = (FileSystemAttribute) fileSystemType.GetCustomAttributes(typeof(FileSystemAttribute), false).SingleOrDefault();
if (attr == null)
throw new InvalidOperationException("Type " + fsType.FullName + "is missing the required FileSystemProviderAttribute.");
throw new InvalidOperationException("Type " + fileSystemType.FullName + "is missing the required FileSystemProviderAttribute.");
return attr.Alias;
}
@@ -334,9 +209,9 @@ namespace Umbraco.Core.IO
// be created directly (via ctor) or via GetFileSystem<T> is *not* shadowed.
// shadow must be enabled in an app event handler before anything else ie before any filesystem
// is actually created and used - after, it is too late - enabling shadow has a neglictible perfs
// is actually created and used - after, it is too late - enabling shadow has a negligible perfs
// impact.
// NO! by the time an app event handler is instanciated it is already too late, see note in ctor.
// NO! by the time an app event handler is instantiated it is already too late, see note in ctor.
//internal void EnableShadow()
//{
// if (_mvcViewsFileSystem != null) // test one of the fs...