using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Threading; using Umbraco.Core.Logging; using Umbraco.Core.Composing; namespace Umbraco.Core.IO { public class FileSystems : IFileSystems { private readonly IFactory _container; private readonly ILogger _logger; private readonly ISystemDirectories _systemDirectories; private readonly ConcurrentDictionary> _filesystems = new ConcurrentDictionary>(); // wrappers for shadow support private ShadowWrapper _macroPartialFileSystem; private ShadowWrapper _partialViewsFileSystem; private ShadowWrapper _stylesheetsFileSystem; private ShadowWrapper _scriptsFileSystem; private ShadowWrapper _mvcViewsFileSystem; // well-known file systems lazy initialization private object _wkfsLock = new object(); private bool _wkfsInitialized; private object _wkfsObject; // unused // shadow support private readonly List _shadowWrappers = new List(); private readonly object _shadowLocker = new object(); private static string _shadowCurrentId; // static - unique!! #region Constructor // DI wants a public ctor public FileSystems(IFactory container, ILogger logger) { _container = container; _logger = logger; _systemDirectories = Current.SystemDirectories; } // for tests only, totally unsafe internal void Reset() { _shadowWrappers.Clear(); _filesystems.Clear(); Volatile.Write(ref _wkfsInitialized, false); _shadowCurrentId = null; } // for tests only, totally unsafe internal static void ResetShadowId() { _shadowCurrentId = null; } // set by the scope provider when taking control of filesystems internal Func IsScoped { get; set; } = () => false; #endregion #region Well-Known FileSystems /// public IFileSystem MacroPartialsFileSystem { get { if (Volatile.Read(ref _wkfsInitialized) == false) EnsureWellKnownFileSystems(); return _macroPartialFileSystem; } } /// public IFileSystem PartialViewsFileSystem { get { if (Volatile.Read(ref _wkfsInitialized) == false) EnsureWellKnownFileSystems(); return _partialViewsFileSystem; } } /// public IFileSystem StylesheetsFileSystem { get { if (Volatile.Read(ref _wkfsInitialized) == false) EnsureWellKnownFileSystems(); return _stylesheetsFileSystem; } } /// public IFileSystem ScriptsFileSystem { get { if (Volatile.Read(ref _wkfsInitialized) == false) EnsureWellKnownFileSystems(); return _scriptsFileSystem; } } /// public IFileSystem MvcViewsFileSystem { get { if (Volatile.Read(ref _wkfsInitialized) == false) EnsureWellKnownFileSystems(); return _mvcViewsFileSystem; } } 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() { var macroPartialFileSystem = new PhysicalFileSystem(_systemDirectories.MacroPartials); var partialViewsFileSystem = new PhysicalFileSystem(_systemDirectories.PartialViews); var stylesheetsFileSystem = new PhysicalFileSystem(_systemDirectories.Css); var scriptsFileSystem = new PhysicalFileSystem(_systemDirectories.Scripts); var mvcViewsFileSystem = new PhysicalFileSystem(_systemDirectories.MvcViews); _macroPartialFileSystem = new ShadowWrapper(macroPartialFileSystem, "macro-partials", IsScoped); _partialViewsFileSystem = new ShadowWrapper(partialViewsFileSystem, "partials", IsScoped); _stylesheetsFileSystem = new ShadowWrapper(stylesheetsFileSystem, "css", IsScoped); _scriptsFileSystem = new ShadowWrapper(scriptsFileSystem, "scripts", IsScoped); _mvcViewsFileSystem = new ShadowWrapper(mvcViewsFileSystem, "views", IsScoped); // TODO: do we need a lock here? _shadowWrappers.Add(_macroPartialFileSystem); _shadowWrappers.Add(_partialViewsFileSystem); _shadowWrappers.Add(_stylesheetsFileSystem); _shadowWrappers.Add(_scriptsFileSystem); _shadowWrappers.Add(_mvcViewsFileSystem); return null; } #endregion #region Providers private readonly Dictionary _paths = new Dictionary(); // internal for tests internal IReadOnlyDictionary Paths => _paths; /// /// Gets a strongly-typed filesystem. /// /// The type of the filesystem. /// A strongly-typed filesystem of the specified type. /// /// Note that any filesystem created by this method *after* shadowing begins, will *not* be /// shadowing (and an exception will be thrown by the ShadowWrapper). /// public TFileSystem GetFileSystem(IFileSystem supporting) where TFileSystem : FileSystemWrapper { if (Volatile.Read(ref _wkfsInitialized) == false) EnsureWellKnownFileSystems(); return (TFileSystem) _filesystems.GetOrAdd(typeof(TFileSystem), _ => new Lazy(() => { 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); return _container.CreateInstance(shadowWrapper); })).Value; } #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. internal ICompletable Shadow() { if (Volatile.Read(ref _wkfsInitialized) == false) EnsureWellKnownFileSystems(); var id = ShadowWrapper.CreateShadowId(); 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; _logger.Debug("Shadow '{ShadowId}'", _shadowCurrentId); foreach (var 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."); _logger.Debug("UnShadow '{ShadowId}' {Status}", id, completed ? "complete" : "abort"); var exceptions = new List(); foreach (var 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); } } private ShadowWrapper CreateShadowWrapper(IFileSystem filesystem, string shadowPath) { lock (_shadowLocker) { var wrapper = new ShadowWrapper(filesystem, shadowPath, IsScoped); if (_shadowCurrentId != null) wrapper.Shadow(_shadowCurrentId); _shadowWrappers.Add(wrapper); return wrapper; } } #endregion } }