diff --git a/src/Umbraco.Core/IO/FileSystemExtensions.cs b/src/Umbraco.Core/IO/FileSystemExtensions.cs index baa8c1d6d4..4b55510ee0 100644 --- a/src/Umbraco.Core/IO/FileSystemExtensions.cs +++ b/src/Umbraco.Core/IO/FileSystemExtensions.cs @@ -37,21 +37,14 @@ namespace Umbraco.Core.IO throw new ArgumentException("Retries must be greater than zero"); } + // GetSize has been added to IFileSystem2 but not IFileSystem + // this is implementing GetSize for IFileSystem, the old way public static long GetSize(this IFileSystem fs, string path) { - // unwrap, eg MediaFileSystem is wrapping an IFileSystem - FileSystemWrapper w; - while ((w = fs as FileSystemWrapper) != null) - fs = w.Wrapped; + // if we reach this point, fs is *not* IFileSystem2 + // so it's not FileSystemWrapper nor shadow nor anything we know + // so... fall back to the old & inefficient method - // no idea why GetSize is not part of IFileSystem, but - // for physical file system we have way better & faster ways - // to get the size, than to read the entire thing in memory! - var physical = fs as PhysicalFileSystem; - if (physical != null) - return physical.GetSize(path); - - // other filesystems... bah... using (var file = fs.OpenFile(path)) using (var sr = new StreamReader(file)) { diff --git a/src/Umbraco.Core/IO/FileSystemProviderManager.cs b/src/Umbraco.Core/IO/FileSystemProviderManager.cs index b2bcaee61f..63828fc1c3 100644 --- a/src/Umbraco.Core/IO/FileSystemProviderManager.cs +++ b/src/Umbraco.Core/IO/FileSystemProviderManager.cs @@ -2,19 +2,43 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Configuration; +using System.IO; using System.Linq; using System.Reflection; -using System.Text; -using Umbraco.Core.CodeAnnotations; +using System.Web.Hosting; using Umbraco.Core.Configuration; +using Umbraco.Core.Logging; namespace Umbraco.Core.IO { public class FileSystemProviderManager { private readonly FileSystemProvidersSection _config; + private readonly object _shadowLocker = new object(); + private readonly WeakSet _fs = new WeakSet(); + private readonly bool _shadowEnabled; + private Guid _shadow = Guid.Empty; + private FileSystemWrapper[] _shadowFs; - #region Singleton + // actual well-known filesystems returned by properties + private readonly IFileSystem2 _macroPartialFileSystem; + private readonly IFileSystem2 _partialViewsFileSystem; + private readonly IFileSystem2 _stylesheetsFileSystem; + private readonly IFileSystem2 _scriptsFileSystem; + private readonly IFileSystem2 _xsltFileSystem; + private readonly IFileSystem2 _masterPagesFileSystem; + private readonly IFileSystem2 _mvcViewsFileSystem; + + // when shadowing is enabled, above filesystems, as wrappers + private readonly FileSystemWrapper2 _macroPartialFileSystemWrapper; + private readonly FileSystemWrapper2 _partialViewsFileSystemWrapper; + private readonly FileSystemWrapper2 _stylesheetsFileSystemWrapper; + private readonly FileSystemWrapper2 _scriptsFileSystemWrapper; + private readonly FileSystemWrapper2 _xsltFileSystemWrapper; + private readonly FileSystemWrapper2 _masterPagesFileSystemWrapper; + private readonly FileSystemWrapper2 _mvcViewsFileSystemWrapper; + + #region Singleton & Constructor private static readonly FileSystemProviderManager Instance = new FileSystemProviderManager(); @@ -23,109 +47,305 @@ namespace Umbraco.Core.IO get { return Instance; } } - #endregion - - #region Constructors - internal FileSystemProviderManager() { - _config = (FileSystemProvidersSection)ConfigurationManager.GetSection("umbracoConfiguration/FileSystemProviders"); + _config = (FileSystemProvidersSection) ConfigurationManager.GetSection("umbracoConfiguration/FileSystemProviders"); + + _macroPartialFileSystem = new PhysicalFileSystem(SystemDirectories.MacroPartials); + _partialViewsFileSystem = new PhysicalFileSystem(SystemDirectories.PartialViews); + _stylesheetsFileSystem = new PhysicalFileSystem(SystemDirectories.Css); + _scriptsFileSystem = new PhysicalFileSystem(SystemDirectories.Scripts); + _xsltFileSystem = new PhysicalFileSystem(SystemDirectories.Xslt); + _masterPagesFileSystem = new PhysicalFileSystem(SystemDirectories.Masterpages); + _mvcViewsFileSystem = new PhysicalFileSystem(SystemDirectories.MvcViews); + + // if shadow is enable we need a mean to replace the filesystem by a shadowed filesystem, however we cannot + // replace the actual filesystem as we don't know if anything is not holding an app-long reference to them, + // so we have to force-wrap each of them and work with the wrapped filesystem. if shadow is not enabled, + // no need to wrap (small perfs improvement). + + // fixme - irks! + // but cannot be enabled by deploy from an application event handler, because by the time an app event handler + // is instanciated it is already too late and some filesystems have been referenced by Core. here we force + // enable for deploy... but maybe it should be some sort of config option? + _shadowEnabled = AppDomain.CurrentDomain.GetAssemblies().Any(x => x.GetName().Name == "Umbraco.Deploy"); + + if (_shadowEnabled) + { + _macroPartialFileSystem = _macroPartialFileSystemWrapper = new FileSystemWrapper2(_macroPartialFileSystem); + _partialViewsFileSystem = _partialViewsFileSystemWrapper = new FileSystemWrapper2(_partialViewsFileSystem); + _stylesheetsFileSystem = _stylesheetsFileSystemWrapper = new FileSystemWrapper2(_stylesheetsFileSystem); + _scriptsFileSystem = _scriptsFileSystemWrapper = new FileSystemWrapper2(_scriptsFileSystem); + _xsltFileSystem = _xsltFileSystemWrapper = new FileSystemWrapper2(_xsltFileSystem); + _masterPagesFileSystem = _masterPagesFileSystemWrapper = new FileSystemWrapper2(_masterPagesFileSystem); + _mvcViewsFileSystem = _mvcViewsFileSystemWrapper = new FileSystemWrapper2(_mvcViewsFileSystem); + } + + // filesystems obtained from GetFileSystemProvider are already wrapped and do not need to be wrapped again, + // whether shadow is enabled or not + + MediaFileSystem = GetFileSystemProvider(); } #endregion - /// - /// used to cache the lookup of how to construct this object so we don't have to reflect each time. - /// - private class ProviderConstructionInfo + #region Well-Known FileSystems + + public IFileSystem2 MacroPartialsFileSystem { get { return _macroPartialFileSystem; } } + public IFileSystem2 PartialViewsFileSystem { get { return _partialViewsFileSystem; } } + public IFileSystem2 StylesheetsFileSystem { get { return _stylesheetsFileSystem; } } + public IFileSystem2 ScriptsFileSystem { get { return _scriptsFileSystem; } } + public IFileSystem2 XsltFileSystem { get { return _xsltFileSystem; } } + public IFileSystem2 MasterPagesFileSystem { get { return _masterPagesFileSystem; } } + public IFileSystem2 MvcViewsFileSystem { get { return _mvcViewsFileSystem; } } + public MediaFileSystem MediaFileSystem { get; private set; } + + #endregion + + #region Providers + + /// + /// used to cache the lookup of how to construct this object so we don't have to reflect each time. + /// + private class ProviderConstructionInfo { public object[] Parameters { get; set; } public ConstructorInfo Constructor { get; set; } - public string ProviderAlias { get; set; } + //public string ProviderAlias { get; set; } } private readonly ConcurrentDictionary _providerLookup = new ConcurrentDictionary(); - private readonly ConcurrentDictionary _wrappedProviderLookup = new ConcurrentDictionary(); + private readonly ConcurrentDictionary _aliases = new ConcurrentDictionary(); /// - /// Returns the underlying (non-typed) file system provider for the alias specified + /// Gets an underlying (non-typed) filesystem supporting a strongly-typed filesystem. /// - /// - /// - /// - /// It is recommended to use the typed GetFileSystemProvider method instead to get a strongly typed provider instance. - /// + /// The alias of the strongly-typed filesystem. + /// The non-typed filesystem supporting the strongly-typed filesystem with the specified alias. + /// This method should not be used directly, used instead. public IFileSystem GetUnderlyingFileSystemProvider(string alias) { - //either get the constructor info from cache or create it and add to cache + // either get the constructor info from cache or create it and add to cache var ctorInfo = _providerLookup.GetOrAdd(alias, s => { + // get config var providerConfig = _config.Providers[s]; if (providerConfig == null) - throw new ArgumentException(string.Format("No provider found with the alias '{0}'", s)); + throw new ArgumentException(string.Format("No provider found with alias {0}.", s)); + // get the filesystem type var providerType = Type.GetType(providerConfig.Type); if (providerType == null) - throw new InvalidOperationException(string.Format("Could not find type '{0}'", providerConfig.Type)); + throw new InvalidOperationException(string.Format("Could not find type {0}.", providerConfig.Type)); + // ensure it implements IFileSystem if (providerType.IsAssignableFrom(typeof (IFileSystem))) - throw new InvalidOperationException(string.Format("The type '{0}' does not implement IFileSystem", providerConfig.Type)); + throw new InvalidOperationException(string.Format("Type {0} does not implement IFileSystem.", providerType.FullName)); + // find a ctor matching the config parameters var paramCount = providerConfig.Parameters != null ? providerConfig.Parameters.Count : 0; - var constructor = providerType.GetConstructors() - .SingleOrDefault(x => x.GetParameters().Count() == paramCount - && x.GetParameters().All(y => providerConfig.Parameters.AllKeys.Contains(y.Name))); + var constructor = providerType.GetConstructors().SingleOrDefault(x + => x.GetParameters().Length == paramCount && x.GetParameters().All(y => providerConfig.Parameters.AllKeys.Contains(y.Name))); if (constructor == null) - throw new InvalidOperationException(string.Format("Could not find constructor for type '{0}' which accepts {1} parameters", providerConfig.Type, paramCount)); + throw new InvalidOperationException(string.Format("Type {0} has no ctor matching the {1} configuration parameter(s).", providerType.FullName, paramCount)); var parameters = new object[paramCount]; - for (var i = 0; i < paramCount; i++) - parameters[i] = providerConfig.Parameters[providerConfig.Parameters.AllKeys[i]].Value; + if (providerConfig.Parameters != null) // keeps ReSharper happy + for (var i = 0; i < paramCount; i++) + parameters[i] = providerConfig.Parameters[providerConfig.Parameters.AllKeys[i]].Value; - //return the new constructor info class to cache so we don't have to do this again. - return new ProviderConstructionInfo() + return new ProviderConstructionInfo { Constructor = constructor, Parameters = parameters, - ProviderAlias = s + //ProviderAlias = s }; }); - var fs = (IFileSystem)ctorInfo.Constructor.Invoke(ctorInfo.Parameters); - return fs; + // create the fs and return + return (IFileSystem) ctorInfo.Constructor.Invoke(ctorInfo.Parameters); } /// - /// Returns the strongly typed file system provider + /// Gets a strongly-typed filesystem. /// - /// - /// - public TProviderTypeFilter GetFileSystemProvider() - where TProviderTypeFilter : FileSystemWrapper + /// The type of the filesystem. + /// A strongly-typed filesystem of the specified type. + public TFileSystem GetFileSystemProvider() + where TFileSystem : FileSystemWrapper { - //get the alias for the type from cache or look it up and add it to the cache, then we don't have to reflect each time - var alias = _wrappedProviderLookup.GetOrAdd(typeof (TProviderTypeFilter), fsType => + // deal with known types - avoid infinite loops! + if (typeof(TFileSystem) == typeof(MediaFileSystem) && MediaFileSystem != null) + return MediaFileSystem as TFileSystem; // else create and return + + // get/cache the alias for the filesystem type + var alias = _aliases.GetOrAdd(typeof (TFileSystem), fsType => { - //validate the ctor - var constructor = fsType.GetConstructors() - .SingleOrDefault(x => - x.GetParameters().Count() == 1 && TypeHelper.IsTypeAssignableFrom(x.GetParameters().Single().ParameterType)); + // validate the ctor + var constructor = fsType.GetConstructors().SingleOrDefault(x + => x.GetParameters().Length == 1 && TypeHelper.IsTypeAssignableFrom(x.GetParameters().Single().ParameterType)); if (constructor == null) - throw new InvalidOperationException("The type of " + fsType + " must inherit from FileSystemWrapper and must have a constructor that accepts one parameter of type " + typeof(IFileSystem)); - - var attr = - (FileSystemProviderAttribute)fsType.GetCustomAttributes(typeof(FileSystemProviderAttribute), false). - SingleOrDefault(); + throw new InvalidOperationException("Type " + fsType.FullName + " must inherit from FileSystemWrapper and have a constructor that accepts one parameter of type " + typeof(IFileSystem).FullName + "."); + // find the attribute and get the alias + var attr = (FileSystemProviderAttribute) fsType.GetCustomAttributes(typeof(FileSystemProviderAttribute), false).SingleOrDefault(); if (attr == null) - throw new InvalidOperationException(string.Format("The provider type filter '{0}' is missing the required FileSystemProviderAttribute", typeof(FileSystemProviderAttribute).FullName)); + throw new InvalidOperationException("Type " + fsType.FullName + "is missing the required FileSystemProviderAttribute."); return attr.Alias; }); + // gets the inner fs, create the strongly-typed fs wrapping the inner fs, register & return var innerFs = GetUnderlyingFileSystemProvider(alias); - var outputFs = Activator.CreateInstance(typeof (TProviderTypeFilter), innerFs); - return (TProviderTypeFilter)outputFs; + var fs = (TFileSystem) Activator.CreateInstance(typeof (TFileSystem), innerFs); + if (_shadowEnabled) + _fs.Add(fs); + return fs; + } + + #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. + // shadow applies to well-known filesystems *only* - at the moment, any other filesystem that would + // be created directly (via ctor) or via GetFileSystemProvider 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 + // impact. + // NO! by the time an app event handler is instanciated it is already too late, see note in ctor. + //internal void EnableShadow() + //{ + // if (_mvcViewsFileSystem != null) // test one of the fs... + // throw new InvalidOperationException("Cannot enable shadow once filesystems have been created."); + // _shadowEnabled = true; + //} + + internal void Shadow(Guid id) + { + lock (_shadowLocker) + { + if (_shadowEnabled == false) throw new InvalidOperationException("Shadowing is not enabled."); + if (_shadow != Guid.Empty) throw new InvalidOperationException("Already shadowing (" + _shadow + ")."); + _shadow = id; + + LogHelper.Debug("Shadow " + id + "."); + + ShadowFs(id, _macroPartialFileSystemWrapper, "Views/MacroPartials"); + ShadowFs(id, _partialViewsFileSystemWrapper, "Views/Partials"); + ShadowFs(id, _stylesheetsFileSystemWrapper, "css"); + ShadowFs(id, _scriptsFileSystemWrapper, "scripts"); + ShadowFs(id, _xsltFileSystemWrapper, "xslt"); + ShadowFs(id, _masterPagesFileSystemWrapper, "masterpages"); + ShadowFs(id, _mvcViewsFileSystemWrapper, "Views"); + + _shadowFs = _fs.ToArray(); + foreach (var fs in _shadowFs) + ShadowFs(id, fs, "stfs/" + fs.GetType().FullName); + } + } + + private static void ShadowFs(Guid id, FileSystemWrapper filesystem, string path) + { + var virt = "~/App_Data/Shadow/" + id + "/" + path; + var dir = HostingEnvironment.MapPath(virt); + if (dir == null) throw new InvalidOperationException("Could not map path."); + Directory.CreateDirectory(dir); + + // shadow filesystem pretends to be IFileSystem2 even though the inner filesystem + // is not, by invoking the GetSize extension method when needed. + var shadowFs = new ShadowFileSystem(filesystem.Wrapped, new PhysicalFileSystem(virt)); + filesystem.Wrapped = shadowFs; + } + + internal void UnShadow(bool complete) + { + lock (_shadowLocker) + { + if (_shadow == Guid.Empty) return; + + // copy and null before anything else + var shadow = _shadow; + var shadowFs = _shadowFs; + _shadow = Guid.Empty; + _shadowFs = null; + + LogHelper.Debug("UnShadow " + shadow + (complete?" (complete)":" (abort)") + "."); + + if (complete) + { + ((ShadowFileSystem) _macroPartialFileSystemWrapper.Wrapped).Complete(); + ((ShadowFileSystem) _partialViewsFileSystemWrapper.Wrapped).Complete(); + ((ShadowFileSystem) _stylesheetsFileSystemWrapper.Wrapped).Complete(); + ((ShadowFileSystem) _scriptsFileSystemWrapper.Wrapped).Complete(); + ((ShadowFileSystem) _xsltFileSystemWrapper.Wrapped).Complete(); + ((ShadowFileSystem) _masterPagesFileSystemWrapper.Wrapped).Complete(); + ((ShadowFileSystem) _mvcViewsFileSystemWrapper.Wrapped).Complete(); + + foreach (var fs in shadowFs) + ((ShadowFileSystem) fs.Wrapped).Complete(); + } + + UnShadowFs(_macroPartialFileSystemWrapper); + UnShadowFs(_partialViewsFileSystemWrapper); + UnShadowFs(_stylesheetsFileSystemWrapper); + UnShadowFs(_scriptsFileSystemWrapper); + UnShadowFs(_xsltFileSystemWrapper); + UnShadowFs(_masterPagesFileSystemWrapper); + UnShadowFs(_mvcViewsFileSystemWrapper); + + foreach (var fs in shadowFs) + UnShadowFs(fs); + } + } + + private static void UnShadowFs(FileSystemWrapper filesystem) + { + var inner = ((ShadowFileSystem) filesystem.Wrapped).Inner; + filesystem.Wrapped = inner; + } + + #endregion + + private class WeakSet + where T : class + { + private readonly HashSet> _set = new HashSet>(); + + public void Add(T item) + { + lock (_set) + { + _set.Add(new WeakReference(item)); + CollectLocked(); + } + } + + public T[] ToArray() + { + lock (_set) + { + CollectLocked(); + return _set.Select(x => + { + T target; + return x.TryGetTarget(out target) ? target : null; + }).WhereNotNull().ToArray(); + } + } + + private void CollectLocked() + { + _set.RemoveWhere(x => + { + T target; + return x.TryGetTarget(out target) == false; + }); + } } } } diff --git a/src/Umbraco.Core/IO/FileSystemWrapper.cs b/src/Umbraco.Core/IO/FileSystemWrapper.cs index db4ab115f6..5bac2afa6b 100644 --- a/src/Umbraco.Core/IO/FileSystemWrapper.cs +++ b/src/Umbraco.Core/IO/FileSystemWrapper.cs @@ -16,93 +16,103 @@ namespace Umbraco.Core.IO /// public abstract class FileSystemWrapper : IFileSystem { - private readonly IFileSystem _wrapped; - - protected FileSystemWrapper(IFileSystem wrapped) + protected FileSystemWrapper(IFileSystem wrapped) { - _wrapped = wrapped; + Wrapped = wrapped; } - internal IFileSystem Wrapped { get { return _wrapped; } } + internal IFileSystem Wrapped { get; set; } - public IEnumerable GetDirectories(string path) + public IEnumerable GetDirectories(string path) { - return _wrapped.GetDirectories(path); + return Wrapped.GetDirectories(path); } public void DeleteDirectory(string path) { - _wrapped.DeleteDirectory(path); + Wrapped.DeleteDirectory(path); } public void DeleteDirectory(string path, bool recursive) { - _wrapped.DeleteDirectory(path, recursive); + Wrapped.DeleteDirectory(path, recursive); } public bool DirectoryExists(string path) { - return _wrapped.DirectoryExists(path); + return Wrapped.DirectoryExists(path); } public void AddFile(string path, Stream stream) { - _wrapped.AddFile(path, stream); + Wrapped.AddFile(path, stream); } public void AddFile(string path, Stream stream, bool overrideExisting) { - _wrapped.AddFile(path, stream, overrideExisting); + Wrapped.AddFile(path, stream, overrideExisting); } public IEnumerable GetFiles(string path) { - return _wrapped.GetFiles(path); + return Wrapped.GetFiles(path); } public IEnumerable GetFiles(string path, string filter) { - return _wrapped.GetFiles(path, filter); + return Wrapped.GetFiles(path, filter); } public Stream OpenFile(string path) { - return _wrapped.OpenFile(path); + return Wrapped.OpenFile(path); } public void DeleteFile(string path) { - _wrapped.DeleteFile(path); + Wrapped.DeleteFile(path); } public bool FileExists(string path) { - return _wrapped.FileExists(path); + return Wrapped.FileExists(path); } public string GetRelativePath(string fullPathOrUrl) { - return _wrapped.GetRelativePath(fullPathOrUrl); + return Wrapped.GetRelativePath(fullPathOrUrl); } public string GetFullPath(string path) { - return _wrapped.GetFullPath(path); + return Wrapped.GetFullPath(path); } public string GetUrl(string path) { - return _wrapped.GetUrl(path); + return Wrapped.GetUrl(path); } public DateTimeOffset GetLastModified(string path) { - return _wrapped.GetLastModified(path); + return Wrapped.GetLastModified(path); } public DateTimeOffset GetCreated(string path) { - return _wrapped.GetCreated(path); + return Wrapped.GetCreated(path); } } + + public class FileSystemWrapper2 : FileSystemWrapper, IFileSystem2 + { + public FileSystemWrapper2(IFileSystem2 fs) + : base(fs) + { } + + public long GetSize(string path) + { + return ((IFileSystem2) Wrapped).GetSize(path); + } + } } \ No newline at end of file diff --git a/src/Umbraco.Core/IO/IFileSystem.cs b/src/Umbraco.Core/IO/IFileSystem.cs index 3e1c1e3527..063f63c437 100644 --- a/src/Umbraco.Core/IO/IFileSystem.cs +++ b/src/Umbraco.Core/IO/IFileSystem.cs @@ -41,4 +41,10 @@ namespace Umbraco.Core.IO DateTimeOffset GetCreated(string path); } + + // this should be part of IFileSystem but we don't want to change the interface + public interface IFileSystem2 : IFileSystem + { + long GetSize(string path); + } } diff --git a/src/Umbraco.Core/IO/MediaFileSystem.cs b/src/Umbraco.Core/IO/MediaFileSystem.cs index 9dd0a8fda1..63fcbbc82e 100644 --- a/src/Umbraco.Core/IO/MediaFileSystem.cs +++ b/src/Umbraco.Core/IO/MediaFileSystem.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Globalization; using System.IO; -using System.Linq; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Media; @@ -13,16 +12,16 @@ namespace Umbraco.Core.IO /// A custom file system provider for media /// [FileSystemProvider("media")] - public class MediaFileSystem : FileSystemWrapper + public class MediaFileSystem : FileSystemWrapper2 { private readonly IContentSection _contentConfig; - public MediaFileSystem(IFileSystem wrapped) + public MediaFileSystem(IFileSystem2 wrapped) : this(wrapped, UmbracoConfig.For.UmbracoSettings().Content) { } - public MediaFileSystem(IFileSystem wrapped, IContentSection contentConfig) : base(wrapped) + public MediaFileSystem(IFileSystem2 wrapped, IContentSection contentConfig) : base(wrapped) { _contentConfig = contentConfig; } diff --git a/src/Umbraco.Core/IO/PhysicalFileSystem.cs b/src/Umbraco.Core/IO/PhysicalFileSystem.cs index 6b07ff21f8..33e4dc71e3 100644 --- a/src/Umbraco.Core/IO/PhysicalFileSystem.cs +++ b/src/Umbraco.Core/IO/PhysicalFileSystem.cs @@ -6,7 +6,7 @@ using Umbraco.Core.Logging; namespace Umbraco.Core.IO { - public class PhysicalFileSystem : IFileSystem + public class PhysicalFileSystem : IFileSystem2 { // the rooted, filesystem path, using directory separator chars, NOT ending with a separator // eg "c:" or "c:\path\to\site" or "\\server\path" diff --git a/src/Umbraco.Core/IO/ShadowFileSystem.cs b/src/Umbraco.Core/IO/ShadowFileSystem.cs new file mode 100644 index 0000000000..db537ba2ca --- /dev/null +++ b/src/Umbraco.Core/IO/ShadowFileSystem.cs @@ -0,0 +1,265 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Umbraco.Core.IO +{ + // at the moment this is just a wrapper + + public class ShadowFileSystem : IFileSystem2 + { + private readonly IFileSystem _fs; + private readonly IFileSystem2 _sfs; + + public ShadowFileSystem(IFileSystem fs, IFileSystem2 sfs) + { + _fs = fs; + _sfs = sfs; + } + + public IFileSystem Inner { get { return _fs; } } + + public void Complete() + { + if (_nodes == null) return; + foreach (var kvp in _nodes) + { + if (kvp.Value.IsExist) + { + if (kvp.Value.IsFile) + { + using (var stream = _sfs.OpenFile(kvp.Key)) + _fs.AddFile(kvp.Key, stream, true); + } + } + else + { + if (kvp.Value.IsDir) + _fs.DeleteDirectory(kvp.Key, true); + else + _fs.DeleteFile(kvp.Key); + } + } + _nodes.Clear(); + } + + private Dictionary _nodes; + + private Dictionary Nodes { get { return _nodes ?? (_nodes = new Dictionary()); } } + + private class ShadowNode + { + public ShadowNode(bool isDelete, bool isdir) + { + IsDelete = isDelete; + IsDir = isdir; + } + + public bool IsDelete { get; private set; } + public bool IsDir { get; private set; } + + public bool IsExist { get { return IsDelete == false; } } + public bool IsFile { get { return IsDir == false; } } + } + + private static string NormPath(string path) + { + return path.ToLowerInvariant().Replace("\\", "/"); + } + + // values can be "" (root), "foo", "foo/bar"... + private static bool IsChild(string path, string input) + { + if (input.StartsWith(path) == false || input.Length < path.Length + 2) + return false; + if (path.Length > 0 && input[path.Length] != '/') return false; + var pos = input.IndexOf("/", path.Length + 1, StringComparison.OrdinalIgnoreCase); + return pos < 0; + } + + private static bool IsDescendant(string path, string input) + { + if (input.StartsWith(path) == false || input.Length < path.Length + 2) + return false; + return path.Length == 0 || input[path.Length] == '/'; + } + + public IEnumerable GetDirectories(string path) + { + var normPath = NormPath(path); + var shadows = Nodes.Where(kvp => IsChild(normPath, kvp.Key)).ToArray(); + var directories = _fs.GetDirectories(path); + return directories + .Except(shadows.Where(kvp => (kvp.Value.IsDir && kvp.Value.IsDelete) || (kvp.Value.IsFile && kvp.Value.IsExist)) + .Select(kvp => kvp.Key)) + .Union(shadows.Where(kvp => kvp.Value.IsDir && kvp.Value.IsExist).Select(kvp => kvp.Key)) + .Distinct(); + } + + public void DeleteDirectory(string path) + { + DeleteDirectory(path, false); + } + + public void DeleteDirectory(string path, bool recursive) + { + if (DirectoryExists(path) == false) return; + var normPath = NormPath(path); + if (recursive) + { + Nodes[normPath] = new ShadowNode(true, true); + var remove = Nodes.Where(x => IsDescendant(normPath, x.Key)).ToList(); + foreach (var kvp in remove) Nodes.Remove(kvp.Key); + Delete(path, true); + } + else + { + if (Nodes.Any(x => IsChild(normPath, x.Key) && x.Value.IsExist) // shadow content + || _fs.GetDirectories(path).Any() || _fs.GetFiles(path).Any()) // actual content + throw new InvalidOperationException("Directory is not empty."); + Nodes[path] = new ShadowNode(true, true); + var remove = Nodes.Where(x => IsChild(normPath, x.Key)).ToList(); + foreach (var kvp in remove) Nodes.Remove(kvp.Key); + Delete(path, false); + } + } + + private void Delete(string path, bool recurse) + { + foreach (var file in _fs.GetFiles(path)) + { + Nodes[NormPath(file)] = new ShadowNode(true, false); + } + foreach (var dir in _fs.GetDirectories(path)) + { + Nodes[NormPath(dir)] = new ShadowNode(true, true); + if (recurse) Delete(dir, true); + } + } + + public bool DirectoryExists(string path) + { + ShadowNode sf; + if (Nodes.TryGetValue(NormPath(path), out sf)) + return sf.IsDir && sf.IsExist; + return _fs.DirectoryExists(path); + } + + public void AddFile(string path, Stream stream) + { + AddFile(path, stream, true); + } + + public void AddFile(string path, Stream stream, bool overrideIfExists) + { + ShadowNode sf; + var normPath = NormPath(path); + if (Nodes.TryGetValue(normPath, out sf) && sf.IsExist && (sf.IsDir || overrideIfExists == false)) + throw new InvalidOperationException(string.Format("A file at path '{0}' already exists", path)); + + var parts = normPath.Split('/'); + for (var i = 0; i < parts.Length - 1; i++) + { + var dirPath = string.Join("/", parts.Take(i + 1)); + ShadowNode sd; + if (Nodes.TryGetValue(dirPath, out sd)) + { + if (sd.IsFile) throw new InvalidOperationException("Invalid path."); + if (sd.IsDelete) Nodes[dirPath] = new ShadowNode(false, true); + } + else + { + if (_fs.DirectoryExists(dirPath)) continue; + if (_fs.FileExists(dirPath)) throw new InvalidOperationException("Invalid path."); + Nodes[dirPath] = new ShadowNode(false, true); + } + } + + _sfs.AddFile(path, stream, overrideIfExists); + Nodes[normPath] = new ShadowNode(false, false); + } + + public IEnumerable GetFiles(string path) + { + var normPath = NormPath(path); + var shadows = Nodes.Where(kvp => IsChild(normPath, kvp.Key)).ToArray(); + var files = _fs.GetFiles(path); + return files + .Except(shadows.Where(kvp => (kvp.Value.IsFile && kvp.Value.IsDelete) || kvp.Value.IsDir) + .Select(kvp => kvp.Key)) + .Union(shadows.Where(kvp => kvp.Value.IsFile && kvp.Value.IsExist).Select(kvp => kvp.Key)) + .Distinct(); + } + + public IEnumerable GetFiles(string path, string filter) + { + return _fs.GetFiles(path, filter); + } + + public Stream OpenFile(string path) + { + ShadowNode sf; + if (Nodes.TryGetValue(NormPath(path), out sf)) + return sf.IsDir || sf.IsDelete ? null : _sfs.OpenFile(path); + return _fs.OpenFile(path); + } + + public void DeleteFile(string path) + { + if (FileExists(path) == false) return; + Nodes[NormPath(path)] = new ShadowNode(true, false); + } + + public bool FileExists(string path) + { + ShadowNode sf; + if (Nodes.TryGetValue(NormPath(path), out sf)) + return sf.IsFile && sf.IsExist; + return _fs.FileExists(path); + } + + public string GetRelativePath(string fullPathOrUrl) + { + return _fs.GetRelativePath(fullPathOrUrl); + } + + public string GetFullPath(string path) + { + return _fs.GetFullPath(path); + } + + public string GetUrl(string path) + { + return _fs.GetUrl(path); + } + + public DateTimeOffset GetLastModified(string path) + { + ShadowNode sf; + if (Nodes.TryGetValue(NormPath(path), out sf) == false) return _fs.GetLastModified(path); + if (sf.IsDelete) throw new InvalidOperationException("Invalid path."); + return _sfs.GetLastModified(path); + } + + public DateTimeOffset GetCreated(string path) + { + ShadowNode sf; + if (Nodes.TryGetValue(NormPath(path), out sf) == false) return _fs.GetCreated(path); + if (sf.IsDelete) throw new InvalidOperationException("Invalid path."); + return _sfs.GetCreated(path); + } + + public long GetSize(string path) + { + ShadowNode sf; + if (Nodes.TryGetValue(NormPath(path), out sf) == false) + { + var fs2 = _fs as IFileSystem2; + return fs2 == null ? _fs.GetSize(path) : fs2.GetSize(path); + } + if (sf.IsDelete || sf.IsDir) throw new InvalidOperationException("Invalid path."); + return _sfs.GetSize(path); + } + } +} diff --git a/src/Umbraco.Core/Persistence/Repositories/PartialViewMacroRepository.cs b/src/Umbraco.Core/Persistence/Repositories/PartialViewMacroRepository.cs index 277e0daf30..827bee68ef 100644 --- a/src/Umbraco.Core/Persistence/Repositories/PartialViewMacroRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/PartialViewMacroRepository.cs @@ -7,7 +7,7 @@ namespace Umbraco.Core.Persistence.Repositories internal class PartialViewMacroRepository : PartialViewRepository { public PartialViewMacroRepository(IUnitOfWork work) - : this(work, new PhysicalFileSystem(SystemDirectories.MacroPartials)) + : this(work, FileSystemProviderManager.Current.MacroPartialsFileSystem) { } diff --git a/src/Umbraco.Core/Persistence/Repositories/PartialViewRepository.cs b/src/Umbraco.Core/Persistence/Repositories/PartialViewRepository.cs index 6631adc13b..199adb51ba 100644 --- a/src/Umbraco.Core/Persistence/Repositories/PartialViewRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/PartialViewRepository.cs @@ -11,7 +11,7 @@ namespace Umbraco.Core.Persistence.Repositories internal class PartialViewRepository : FileRepository, IPartialViewRepository { public PartialViewRepository(IUnitOfWork work) - : this(work, new PhysicalFileSystem(SystemDirectories.PartialViews)) + : this(work, FileSystemProviderManager.Current.PartialViewsFileSystem) { } diff --git a/src/Umbraco.Core/Persistence/RepositoryFactory.cs b/src/Umbraco.Core/Persistence/RepositoryFactory.cs index dbb36f2cf9..d67139c713 100644 --- a/src/Umbraco.Core/Persistence/RepositoryFactory.cs +++ b/src/Umbraco.Core/Persistence/RepositoryFactory.cs @@ -212,7 +212,7 @@ namespace Umbraco.Core.Persistence public virtual IScriptRepository CreateScriptRepository(IUnitOfWork uow) { - return new ScriptRepository(uow, new PhysicalFileSystem(SystemDirectories.Scripts), _settings.Content); + return new ScriptRepository(uow, FileSystemProviderManager.Current.ScriptsFileSystem, _settings.Content); } internal virtual IPartialViewRepository CreatePartialViewRepository(IUnitOfWork uow) @@ -227,7 +227,7 @@ namespace Umbraco.Core.Persistence public virtual IStylesheetRepository CreateStylesheetRepository(IUnitOfWork uow, IDatabaseUnitOfWork db) { - return new StylesheetRepository(uow, new PhysicalFileSystem(SystemDirectories.Css)); + return new StylesheetRepository(uow, FileSystemProviderManager.Current.StylesheetsFileSystem); } public virtual ITemplateRepository CreateTemplateRepository(IDatabaseUnitOfWork uow) @@ -235,14 +235,14 @@ namespace Umbraco.Core.Persistence return new TemplateRepository(uow, _cacheHelper, _logger, _sqlSyntax, - new PhysicalFileSystem(SystemDirectories.Masterpages), - new PhysicalFileSystem(SystemDirectories.MvcViews), + FileSystemProviderManager.Current.MasterPagesFileSystem, + FileSystemProviderManager.Current.MvcViewsFileSystem, _settings.Templates); } public virtual IXsltFileRepository CreateXsltFileRepository(IUnitOfWork uow) { - return new XsltFileRepository(uow, new PhysicalFileSystem(SystemDirectories.Xslt)); + return new XsltFileRepository(uow, FileSystemProviderManager.Current.XsltFileSystem); } public virtual IMigrationEntryRepository CreateMigrationEntryRepository(IDatabaseUnitOfWork uow) @@ -350,7 +350,7 @@ namespace Umbraco.Core.Persistence internal IStylesheetRepository CreateStylesheetRepository(IDatabaseUnitOfWork uow) { - return new StylesheetRepository(uow, new PhysicalFileSystem(SystemDirectories.Css)); + return new StylesheetRepository(uow, FileSystemProviderManager.Current.StylesheetsFileSystem); } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 96e87d67f3..57ed4dcaec 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -357,6 +357,7 @@ + diff --git a/src/Umbraco.Tests/IO/ShadowFileSystemTests.cs b/src/Umbraco.Tests/IO/ShadowFileSystemTests.cs new file mode 100644 index 0000000000..ff1d3bf1d7 --- /dev/null +++ b/src/Umbraco.Tests/IO/ShadowFileSystemTests.cs @@ -0,0 +1,395 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using NUnit.Framework; +using Umbraco.Core.IO; + +namespace Umbraco.Tests.IO +{ + [TestFixture] + public class ShadowFileSystemTests + { + [SetUp] + public void SetUp() + { } + + [TearDown] + public void TearDown() + { + var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "FileSysTests"); + if (Directory.Exists(path) == false) return; + Directory.Delete(path, true); + } + + private static string NormPath(string path) + { + return path.ToLowerInvariant().Replace("\\", "/"); + } + + [Test] + public void ShadowDeleteDirectory() + { + var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "FileSysTests"); + Directory.CreateDirectory(path); + Directory.CreateDirectory(path + "/ShadowTests"); + Directory.CreateDirectory(path + "/ShadowSystem"); + + var fs = new PhysicalFileSystem(path + "/ShadowTests/", "ignore"); + var sfs = new PhysicalFileSystem(path + "/ShadowSystem/", "ignore"); + var ss = new ShadowFileSystem(fs, sfs); + + Directory.CreateDirectory(path + "/ShadowTests/d1"); + Directory.CreateDirectory(path + "/ShadowTests/d2"); + + var files = fs.GetFiles(""); + Assert.AreEqual(0, files.Count()); + + var dirs = fs.GetDirectories(""); + Assert.AreEqual(2, dirs.Count()); + Assert.IsTrue(dirs.Contains("d1")); + Assert.IsTrue(dirs.Contains("d2")); + + ss.DeleteDirectory("d1"); + + Assert.IsTrue(Directory.Exists(path + "/ShadowTests/d1")); + Assert.IsTrue(fs.DirectoryExists("d1")); + Assert.IsFalse(ss.DirectoryExists("d1")); + + dirs = ss.GetDirectories(""); + Assert.AreEqual(1, dirs.Count()); + Assert.IsTrue(dirs.Contains("d2")); + } + + [Test] + public void ShadowDeleteDirectoryInDir() + { + var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "FileSysTests"); + Directory.CreateDirectory(path); + Directory.CreateDirectory(path + "/ShadowTests"); + Directory.CreateDirectory(path + "/ShadowSystem"); + + var fs = new PhysicalFileSystem(path + "/ShadowTests/", "ignore"); + var sfs = new PhysicalFileSystem(path + "/ShadowSystem/", "ignore"); + var ss = new ShadowFileSystem(fs, sfs); + + Directory.CreateDirectory(path + "/ShadowTests/sub"); + Directory.CreateDirectory(path + "/ShadowTests/sub/d1"); + Directory.CreateDirectory(path + "/ShadowTests/sub/d2"); + + var files = fs.GetFiles(""); + Assert.AreEqual(0, files.Count()); + + var dirs = ss.GetDirectories(""); + Assert.AreEqual(1, dirs.Count()); + Assert.IsTrue(dirs.Contains("sub")); + + dirs = fs.GetDirectories("sub"); + Assert.AreEqual(2, dirs.Count()); + Assert.IsTrue(dirs.Contains("sub/d1")); + Assert.IsTrue(dirs.Contains("sub/d2")); + + dirs = ss.GetDirectories("sub"); + Assert.AreEqual(2, dirs.Count()); + Assert.IsTrue(dirs.Contains("sub/d1")); + Assert.IsTrue(dirs.Contains("sub/d2")); + + ss.DeleteDirectory("sub/d1"); + + Assert.IsTrue(Directory.Exists(path + "/ShadowTests/sub/d1")); + Assert.IsTrue(fs.DirectoryExists("sub/d1")); + Assert.IsFalse(ss.DirectoryExists("sub/d1")); + + dirs = fs.GetDirectories("sub"); + Assert.AreEqual(2, dirs.Count()); + Assert.IsTrue(dirs.Contains("sub/d1")); + Assert.IsTrue(dirs.Contains("sub/d2")); + + dirs = ss.GetDirectories("sub"); + Assert.AreEqual(1, dirs.Count()); + Assert.IsTrue(dirs.Contains("sub/d2")); + } + + [Test] + public void ShadowDeleteFile() + { + var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "FileSysTests"); + Directory.CreateDirectory(path); + Directory.CreateDirectory(path + "/ShadowTests"); + Directory.CreateDirectory(path + "/ShadowSystem"); + + var fs = new PhysicalFileSystem(path + "/ShadowTests/", "ignore"); + var sfs = new PhysicalFileSystem(path + "/ShadowSystem/", "ignore"); + var ss = new ShadowFileSystem(fs, sfs); + + File.WriteAllText(path + "/ShadowTests/f1.txt", "foo"); + File.WriteAllText(path + "/ShadowTests/f2.txt", "foo"); + + var files = fs.GetFiles(""); + Assert.AreEqual(2, files.Count()); + Assert.IsTrue(files.Contains("f1.txt")); + Assert.IsTrue(files.Contains("f2.txt")); + + files = ss.GetFiles(""); + Assert.AreEqual(2, files.Count()); + Assert.IsTrue(files.Contains("f1.txt")); + Assert.IsTrue(files.Contains("f2.txt")); + + var dirs = ss.GetDirectories(""); + Assert.AreEqual(0, dirs.Count()); + + ss.DeleteFile("f1.txt"); + + Assert.IsTrue(File.Exists(path + "/ShadowTests/f1.txt")); + Assert.IsTrue(fs.FileExists("f1.txt")); + Assert.IsFalse(ss.FileExists("f1.txt")); + + files = ss.GetFiles(""); + Assert.AreEqual(1, files.Count()); + Assert.IsTrue(files.Contains("f2.txt")); + } + + [Test] + public void ShadowDeleteFileInDir() + { + var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "FileSysTests"); + Directory.CreateDirectory(path); + Directory.CreateDirectory(path + "/ShadowTests"); + Directory.CreateDirectory(path + "/ShadowSystem"); + + var fs = new PhysicalFileSystem(path + "/ShadowTests/", "ignore"); + var sfs = new PhysicalFileSystem(path + "/ShadowSystem/", "ignore"); + var ss = new ShadowFileSystem(fs, sfs); + + Directory.CreateDirectory(path + "/ShadowTests/sub"); + File.WriteAllText(path + "/ShadowTests/sub/f1.txt", "foo"); + File.WriteAllText(path + "/ShadowTests/sub/f2.txt", "foo"); + + var files = fs.GetFiles(""); + Assert.AreEqual(0, files.Count()); + + files = fs.GetFiles("sub"); + Assert.AreEqual(2, files.Count()); + Assert.IsTrue(files.Contains("sub/f1.txt")); + Assert.IsTrue(files.Contains("sub/f2.txt")); + + files = ss.GetFiles(""); + Assert.AreEqual(0, files.Count()); + + var dirs = ss.GetDirectories(""); + Assert.AreEqual(1, dirs.Count()); + Assert.IsTrue(dirs.Contains("sub")); + + files = ss.GetFiles("sub"); + Assert.AreEqual(2, files.Count()); + Assert.IsTrue(files.Contains("sub/f1.txt")); + Assert.IsTrue(files.Contains("sub/f2.txt")); + + dirs = ss.GetDirectories("sub"); + Assert.AreEqual(0, dirs.Count()); + + ss.DeleteFile("sub/f1.txt"); + + Assert.IsTrue(File.Exists(path + "/ShadowTests/sub/f1.txt")); + Assert.IsTrue(fs.FileExists("sub/f1.txt")); + Assert.IsFalse(ss.FileExists("sub/f1.txt")); + + files = fs.GetFiles("sub"); + Assert.AreEqual(2, files.Count()); + Assert.IsTrue(files.Contains("sub/f1.txt")); + Assert.IsTrue(files.Contains("sub/f2.txt")); + + files = ss.GetFiles("sub"); + Assert.AreEqual(1, files.Count()); + Assert.IsTrue(files.Contains("sub/f2.txt")); + } + + [Test] + public void ShadowCantCreateFile() + { + var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "FileSysTests"); + Directory.CreateDirectory(path); + Directory.CreateDirectory(path + "/ShadowTests"); + Directory.CreateDirectory(path + "/ShadowSystem"); + + var fs = new PhysicalFileSystem(path + "/ShadowTests/", "ignore"); + var sfs = new PhysicalFileSystem(path + "/ShadowSystem/", "ignore"); + var ss = new ShadowFileSystem(fs, sfs); + + Assert.Throws(() => + { + using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo"))) + ss.AddFile("../../f1.txt", ms); + }); + } + + [Test] + public void ShadowCreateFile() + { + var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "FileSysTests"); + Directory.CreateDirectory(path); + Directory.CreateDirectory(path + "/ShadowTests"); + Directory.CreateDirectory(path + "/ShadowSystem"); + + var fs = new PhysicalFileSystem(path + "/ShadowTests/", "ignore"); + var sfs = new PhysicalFileSystem(path + "/ShadowSystem/", "ignore"); + var ss = new ShadowFileSystem(fs, sfs); + + File.WriteAllText(path + "/ShadowTests/f2.txt", "foo"); + + using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo"))) + ss.AddFile("f1.txt", ms); + + Assert.IsTrue(File.Exists(path + "/ShadowTests/f2.txt")); + Assert.IsFalse(File.Exists(path + "/ShadowSystem/f2.txt")); + Assert.IsTrue(fs.FileExists("f2.txt")); + Assert.IsTrue(ss.FileExists("f2.txt")); + + Assert.IsFalse(File.Exists(path + "/ShadowTests/f1.txt")); + Assert.IsTrue(File.Exists(path + "/ShadowSystem/f1.txt")); + Assert.IsFalse(fs.FileExists("f1.txt")); + Assert.IsTrue(ss.FileExists("f1.txt")); + + var files = ss.GetFiles(""); + Assert.AreEqual(2, files.Count()); + Assert.IsTrue(files.Contains("f1.txt")); + Assert.IsTrue(files.Contains("f2.txt")); + + string content; + using (var stream = ss.OpenFile("f1.txt")) + content = new StreamReader(stream).ReadToEnd(); + + Assert.AreEqual("foo", content); + } + + [Test] + public void ShadowCreateFileInDir() + { + var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "FileSysTests"); + Directory.CreateDirectory(path); + Directory.CreateDirectory(path + "/ShadowTests"); + Directory.CreateDirectory(path + "/ShadowSystem"); + + var fs = new PhysicalFileSystem(path + "/ShadowTests/", "ignore"); + var sfs = new PhysicalFileSystem(path + "/ShadowSystem/", "ignore"); + var ss = new ShadowFileSystem(fs, sfs); + + using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo"))) + ss.AddFile("sub/f1.txt", ms); + + Assert.IsFalse(File.Exists(path + "/ShadowTests/sub/f1.txt")); + Assert.IsTrue(File.Exists(path + "/ShadowSystem/sub/f1.txt")); + Assert.IsFalse(fs.FileExists("sub/f1.txt")); + Assert.IsTrue(ss.FileExists("sub/f1.txt")); + + Assert.IsFalse(fs.DirectoryExists("sub")); + Assert.IsTrue(ss.DirectoryExists("sub")); + + var dirs = fs.GetDirectories(""); + Assert.AreEqual(0, dirs.Count()); + + dirs = ss.GetDirectories(""); + Assert.AreEqual(1, dirs.Count()); + Assert.IsTrue(dirs.Contains("sub")); + + var files = ss.GetFiles("sub"); + Assert.AreEqual(1, files.Count()); + + string content; + using (var stream = ss.OpenFile("sub/f1.txt")) + content = new StreamReader(stream).ReadToEnd(); + + Assert.AreEqual("foo", content); + } + + [Test] + public void ShadowAbort() + { + var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "FileSysTests"); + Directory.CreateDirectory(path); + Directory.CreateDirectory(path + "/ShadowTests"); + Directory.CreateDirectory(path + "/ShadowSystem"); + + var fs = new PhysicalFileSystem(path + "/ShadowTests/", "ignore"); + var sfs = new PhysicalFileSystem(path + "/ShadowSystem/", "ignore"); + var ss = new ShadowFileSystem(fs, sfs); + + using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo"))) + ss.AddFile("path/to/some/dir/f1.txt", ms); + + Assert.IsTrue(File.Exists(path + "/ShadowSystem/path/to/some/dir/f1.txt")); + + // kill everything and let the shadow fs die + Directory.Delete(path + "/ShadowSystem", true); + } + + [Test] + public void ShadowComplete() + { + var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "FileSysTests"); + Directory.CreateDirectory(path); + Directory.CreateDirectory(path + "/ShadowTests"); + Directory.CreateDirectory(path + "/ShadowSystem"); + + var fs = new PhysicalFileSystem(path + "/ShadowTests/", "ignore"); + var sfs = new PhysicalFileSystem(path + "/ShadowSystem/", "ignore"); + var ss = new ShadowFileSystem(fs, sfs); + + Directory.CreateDirectory(path + "/ShadowTests/sub/sub"); + File.WriteAllText(path + "/ShadowTests/sub/sub/f2.txt", "foo"); + + using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo"))) + ss.AddFile("path/to/some/dir/f1.txt", ms); + ss.DeleteFile("sub/sub/f2.txt"); + + Assert.IsTrue(File.Exists(path + "/ShadowSystem/path/to/some/dir/f1.txt")); + + ss.Complete(); + + Assert.IsTrue(File.Exists(path + "/ShadowSystem/path/to/some/dir/f1.txt")); // *not* cleaning + Assert.IsTrue(File.Exists(path + "/ShadowTests/path/to/some/dir/f1.txt")); + Assert.IsFalse(File.Exists(path + "/ShadowTests/sub/sub/f2.txt")); + } + + [Test] + public void GetFilesReturnsChildrenOnly() + { + var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "FileSysTests"); + Directory.CreateDirectory(path); + File.WriteAllText(path + "/f1.txt", "foo"); + Directory.CreateDirectory(path + "/test"); + File.WriteAllText(path + "/test/f2.txt", "foo"); + Directory.CreateDirectory(path + "/test/inner"); + File.WriteAllText(path + "/test/inner/f3.txt", "foo"); + + path = NormPath(path); + var files = Directory.GetFiles(path); + Assert.AreEqual(1, files.Length); + files = Directory.GetFiles(path, "*", SearchOption.AllDirectories); + Assert.AreEqual(3, files.Length); + var efiles = Directory.EnumerateFiles(path); + Assert.AreEqual(1, efiles.Count()); + efiles = Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories); + Assert.AreEqual(3, efiles.Count()); + } + + [Test] + public void DeleteDirectoryAndFiles() + { + var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "FileSysTests"); + Directory.CreateDirectory(path); + File.WriteAllText(path + "/f1.txt", "foo"); + Directory.CreateDirectory(path + "/test"); + File.WriteAllText(path + "/test/f2.txt", "foo"); + Directory.CreateDirectory(path + "/test/inner"); + File.WriteAllText(path + "/test/inner/f3.txt", "foo"); + + path = NormPath(path); + Directory.Delete(path, true); + + Assert.IsFalse(File.Exists(path + "/test/inner/f3.txt")); + } + } +} diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 23f8b74d8d..d5f33151f4 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -176,6 +176,7 @@ + diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/loadTemplates.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/loadTemplates.cs index bf7c04bb27..ad03d99223 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/loadTemplates.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/loadTemplates.cs @@ -39,7 +39,7 @@ namespace umbraco { public loadTemplates(string application) : base(application) {} - private ViewHelper _viewHelper = new ViewHelper(new PhysicalFileSystem(SystemDirectories.MvcViews)); + private ViewHelper _viewHelper = new ViewHelper(FileSystemProviderManager.Current.MvcViewsFileSystem); protected override void CreateRootNode(ref XmlTreeNode rootNode) { diff --git a/src/umbraco.cms/businesslogic/template/Template.cs b/src/umbraco.cms/businesslogic/template/Template.cs index e78925bd60..823f6c873f 100644 --- a/src/umbraco.cms/businesslogic/template/Template.cs +++ b/src/umbraco.cms/businesslogic/template/Template.cs @@ -29,8 +29,8 @@ namespace umbraco.cms.businesslogic.template #region Private members - private readonly ViewHelper _viewHelper = new ViewHelper(new PhysicalFileSystem(SystemDirectories.MvcViews)); - private readonly MasterPageHelper _masterPageHelper = new MasterPageHelper(new PhysicalFileSystem(SystemDirectories.Masterpages)); + private readonly ViewHelper _viewHelper = new ViewHelper(FileSystemProviderManager.Current.MvcViewsFileSystem); + private readonly MasterPageHelper _masterPageHelper = new MasterPageHelper(FileSystemProviderManager.Current.MasterPagesFileSystem); internal ITemplate TemplateEntity; private int? _mastertemplate;