diff --git a/src/Umbraco.Core/IO/FileSystemProviderManager.cs b/src/Umbraco.Core/IO/FileSystemProviderManager.cs index 63828fc1c3..1e076bb979 100644 --- a/src/Umbraco.Core/IO/FileSystemProviderManager.cs +++ b/src/Umbraco.Core/IO/FileSystemProviderManager.cs @@ -2,23 +2,16 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Configuration; -using System.IO; using System.Linq; using System.Reflection; -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; + private readonly WeakSet _wrappers = new WeakSet(); // actual well-known filesystems returned by properties private readonly IFileSystem2 _macroPartialFileSystem; @@ -30,13 +23,13 @@ namespace Umbraco.Core.IO 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; + private readonly ShadowWrapper _macroPartialFileSystemWrapper; + private readonly ShadowWrapper _partialViewsFileSystemWrapper; + private readonly ShadowWrapper _stylesheetsFileSystemWrapper; + private readonly ShadowWrapper _scriptsFileSystemWrapper; + private readonly ShadowWrapper _xsltFileSystemWrapper; + private readonly ShadowWrapper _masterPagesFileSystemWrapper; + private readonly ShadowWrapper _mvcViewsFileSystemWrapper; #region Singleton & Constructor @@ -59,31 +52,15 @@ namespace Umbraco.Core.IO _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 + _macroPartialFileSystem = _macroPartialFileSystemWrapper = new ShadowWrapper(_macroPartialFileSystem, "Views/MacroPartials"); + _partialViewsFileSystem = _partialViewsFileSystemWrapper = new ShadowWrapper(_partialViewsFileSystem, "Views/Partials"); + _stylesheetsFileSystem = _stylesheetsFileSystemWrapper = new ShadowWrapper(_stylesheetsFileSystem, "css"); + _scriptsFileSystem = _scriptsFileSystemWrapper = new ShadowWrapper(_scriptsFileSystem, "scripts"); + _xsltFileSystem = _xsltFileSystemWrapper = new ShadowWrapper(_xsltFileSystem, "xslt"); + _masterPagesFileSystem = _masterPagesFileSystemWrapper = new ShadowWrapper(_masterPagesFileSystem, "masterpages"); + _mvcViewsFileSystem = _mvcViewsFileSystemWrapper = new ShadowWrapper(_mvcViewsFileSystem, "Views"); + // filesystems obtained from GetFileSystemProvider are already wrapped and do not need to be wrapped again MediaFileSystem = GetFileSystemProvider(); } @@ -194,12 +171,14 @@ namespace Umbraco.Core.IO return attr.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 = GetUnderlyingFileSystemProvider(alias); + var shadowWrapper = new ShadowWrapper(innerFs, "typed/" + alias); var fs = (TFileSystem) Activator.CreateInstance(typeof (TFileSystem), innerFs); - if (_shadowEnabled) - _fs.Add(fs); + _wrappers.Add(shadowWrapper); // keeping a weak reference to the wrapper return fs; } @@ -225,88 +204,21 @@ namespace Umbraco.Core.IO // _shadowEnabled = true; //} - internal void Shadow(Guid id) + public ShadowFileSystemsScope 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; + var typed = _wrappers.ToArray(); + var wrappers = new ShadowWrapper[typed.Length + 7]; + var i = 0; + while (i < typed.Length) wrappers[i] = typed[i++]; + wrappers[i++] = _macroPartialFileSystemWrapper; + wrappers[i++] = _partialViewsFileSystemWrapper; + wrappers[i++] = _stylesheetsFileSystemWrapper; + wrappers[i++] = _scriptsFileSystemWrapper; + wrappers[i++] = _xsltFileSystemWrapper; + wrappers[i++] = _masterPagesFileSystemWrapper; + wrappers[i] = _mvcViewsFileSystemWrapper; - 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; + return ShadowFileSystemsScope.CreateScope(id, wrappers); } #endregion diff --git a/src/Umbraco.Core/IO/FileSystemWrapper.cs b/src/Umbraco.Core/IO/FileSystemWrapper.cs index 5bac2afa6b..27e08330ed 100644 --- a/src/Umbraco.Core/IO/FileSystemWrapper.cs +++ b/src/Umbraco.Core/IO/FileSystemWrapper.cs @@ -14,7 +14,7 @@ namespace Umbraco.Core.IO /// /// This abstract class just wraps the 'real' IFileSystem object passed in to its constructor. /// - public abstract class FileSystemWrapper : IFileSystem + public abstract class FileSystemWrapper : IFileSystem2 { protected FileSystemWrapper(IFileSystem wrapped) { @@ -102,17 +102,12 @@ namespace Umbraco.Core.IO { 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); - } + // explicitely implementing - not breaking + long IFileSystem2.GetSize(string path) + { + var wrapped2 = Wrapped as IFileSystem2; + return wrapped2 == null ? Wrapped.GetSize(path) : wrapped2.GetSize(path); + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/IO/MediaFileSystem.cs b/src/Umbraco.Core/IO/MediaFileSystem.cs index 63fcbbc82e..d0e1bbee53 100644 --- a/src/Umbraco.Core/IO/MediaFileSystem.cs +++ b/src/Umbraco.Core/IO/MediaFileSystem.cs @@ -12,7 +12,7 @@ namespace Umbraco.Core.IO /// A custom file system provider for media /// [FileSystemProvider("media")] - public class MediaFileSystem : FileSystemWrapper2 + public class MediaFileSystem : FileSystemWrapper { private readonly IContentSection _contentConfig; diff --git a/src/Umbraco.Core/IO/ShadowFileSystem.cs b/src/Umbraco.Core/IO/ShadowFileSystem.cs index db537ba2ca..276e058bcc 100644 --- a/src/Umbraco.Core/IO/ShadowFileSystem.cs +++ b/src/Umbraco.Core/IO/ShadowFileSystem.cs @@ -5,8 +5,6 @@ using System.Linq; namespace Umbraco.Core.IO { - // at the moment this is just a wrapper - public class ShadowFileSystem : IFileSystem2 { private readonly IFileSystem _fs; @@ -18,30 +16,51 @@ namespace Umbraco.Core.IO _sfs = sfs; } - public IFileSystem Inner { get { return _fs; } } + public IFileSystem Inner + { + get { return _fs; } + } public void Complete() { if (_nodes == null) return; + var exceptions = new List(); 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); + try + { + using (var stream = _sfs.OpenFile(kvp.Key)) + _fs.AddFile(kvp.Key, stream, true); + } + catch (Exception e) + { + exceptions.Add(new Exception("Could not save file \"" + kvp.Key + "\".", e)); + } } } else { - if (kvp.Value.IsDir) - _fs.DeleteDirectory(kvp.Key, true); - else - _fs.DeleteFile(kvp.Key); + try + { + if (kvp.Value.IsDir) + _fs.DeleteDirectory(kvp.Key, true); + else + _fs.DeleteFile(kvp.Key); + } + catch (Exception e) + { + exceptions.Add(new Exception("Could not delete " + (kvp.Value.IsDir ? "directory": "file") + " \"" + kvp.Key + "\".", e)); + } } } _nodes.Clear(); + + if (exceptions.Count == 0) return; + throw new AggregateException("Failed to apply all changes (see exceptions).", exceptions); } private Dictionary _nodes; @@ -255,6 +274,8 @@ namespace Umbraco.Core.IO ShadowNode sf; if (Nodes.TryGetValue(NormPath(path), out sf) == false) { + // the inner filesystem (_fs) can be IFileSystem2... or just IFileSystem + // figure it out and use the most effective GetSize method var fs2 = _fs as IFileSystem2; return fs2 == null ? _fs.GetSize(path) : fs2.GetSize(path); } diff --git a/src/Umbraco.Core/IO/ShadowFileSystemsScope.cs b/src/Umbraco.Core/IO/ShadowFileSystemsScope.cs new file mode 100644 index 0000000000..5e0280ddca --- /dev/null +++ b/src/Umbraco.Core/IO/ShadowFileSystemsScope.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.Runtime.Remoting.Messaging; +using Umbraco.Core.Logging; + +namespace Umbraco.Core.IO +{ + public class ShadowFileSystemsScope : IDisposable + { + // note: taking a reference to the _manager instead of using manager.Current + // to avoid using Current everywhere but really, we support only 1 scope at + // a time, not multiple scopes in case of multiple managers (not supported) + + private const string ItemKey = "Umbraco.Core.IO.ShadowFileSystemsScope"; + private static readonly object Locker = new object(); + private readonly Guid _id; + private readonly ShadowWrapper[] _wrappers; + + static ShadowFileSystemsScope() + { + SafeCallContext.Register( + () => + { + var scope = CallContext.LogicalGetData(ItemKey); + CallContext.FreeNamedDataSlot(ItemKey); + return scope; + }, + o => + { + if (CallContext.LogicalGetData(ItemKey) != null) throw new InvalidOperationException(); + if (o != null) CallContext.LogicalSetData(ItemKey, o); + }); + } + + private ShadowFileSystemsScope(Guid id, ShadowWrapper[] wrappers) + { + LogHelper.Debug("Shadow " + id + "."); + _id = id; + _wrappers = wrappers; + foreach (var wrapper in _wrappers) + wrapper.Shadow(id); + } + + // internal for tests + FileSystemProviderManager + // do NOT use otherwise + internal static ShadowFileSystemsScope CreateScope(Guid id, ShadowWrapper[] wrappers) + { + lock (Locker) + { + if (CallContext.LogicalGetData(ItemKey) != null) throw new InvalidOperationException("Already shadowing."); + CallContext.LogicalSetData(ItemKey, ItemKey); // value does not matter + } + return new ShadowFileSystemsScope(id, wrappers); + } + + internal static bool InScope + { + get { return NoScope == false; } + } + + internal static bool NoScope + { + get { return CallContext.LogicalGetData(ItemKey) == null; } + } + + public void Complete() + { + lock (Locker) + { + LogHelper.Debug("UnShadow " + _id + " (complete)."); + + var exceptions = new List(); + foreach (var wrapper in _wrappers) + { + try + { + // this may throw an AggregateException if some of the changes could not be applied + wrapper.UnShadow(true); + } + catch (AggregateException ae) + { + exceptions.Add(ae); + } + } + + if (exceptions.Count > 0) + throw new AggregateException("Failed to apply all changes (see exceptions).", exceptions); + + // last, & *only* if successful (otherwise we'll unshadow & cleanup as best as we can) + CallContext.FreeNamedDataSlot(ItemKey); + } + } + + public void Dispose() + { + lock (Locker) + { + if (CallContext.LogicalGetData(ItemKey) == null) return; + + try + { + LogHelper.Debug("UnShadow " + _id + " (abort)"); + foreach (var wrapper in _wrappers) + wrapper.UnShadow(false); // should not throw + } + finally + { + // last, & always + CallContext.FreeNamedDataSlot(ItemKey); + } + } + } + } +} diff --git a/src/Umbraco.Core/IO/ShadowWrapper.cs b/src/Umbraco.Core/IO/ShadowWrapper.cs new file mode 100644 index 0000000000..7f9bc61a66 --- /dev/null +++ b/src/Umbraco.Core/IO/ShadowWrapper.cs @@ -0,0 +1,159 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Web.Hosting; + +namespace Umbraco.Core.IO +{ + public class ShadowWrapper : IFileSystem2 + { + private readonly IFileSystem _innerFileSystem; + private readonly string _shadowPath; + private ShadowFileSystem _shadowFileSystem; + private string _shadowDir; + + public ShadowWrapper(IFileSystem innerFileSystem, string shadowPath) + { + _innerFileSystem = innerFileSystem; + _shadowPath = shadowPath; + } + + internal void Shadow(Guid id) + { + // note: no thread-safety here, because ShadowFs is thread-safe due to the check + // on ShadowFileSystemsScope.None - and if None is false then we should be running + // in a single thread anyways + + var virt = "~/App_Data/Shadow/" + id + "/" + _shadowPath; + _shadowDir = IOHelper.MapPath(virt); + Directory.CreateDirectory(_shadowDir); + var tempfs = new PhysicalFileSystem(virt); + _shadowFileSystem = new ShadowFileSystem(_innerFileSystem, tempfs); + } + + internal void UnShadow(bool complete) + { + var shadowFileSystem = _shadowFileSystem; + var dir = _shadowDir; + _shadowFileSystem = null; + _shadowDir = null; + + try + { + // this may throw an AggregateException if some of the changes could not be applied + if (complete) shadowFileSystem.Complete(); + } + finally + { + // in any case, cleanup + try + { + Directory.Delete(dir, true); + dir = dir.Substring(0, dir.Length - _shadowPath.Length - 1); + if (Directory.EnumerateFileSystemEntries(dir).Any() == false) + Directory.Delete(dir, true); + } + catch + { + // ugly, isn't it? but if we cannot cleanup, bah, just leave it there + } + } + } + + private IFileSystem FileSystem + { + get { return ShadowFileSystemsScope.NoScope ? _innerFileSystem : _shadowFileSystem; } + } + + public IEnumerable GetDirectories(string path) + { + return FileSystem.GetDirectories(path); + } + + public void DeleteDirectory(string path) + { + FileSystem.DeleteDirectory(path); + } + + public void DeleteDirectory(string path, bool recursive) + { + FileSystem.DeleteDirectory(path, recursive); + } + + public bool DirectoryExists(string path) + { + return FileSystem.DirectoryExists(path); + } + + public void AddFile(string path, Stream stream) + { + FileSystem.AddFile(path, stream); + } + + public void AddFile(string path, Stream stream, bool overrideExisting) + { + FileSystem.AddFile(path, stream, overrideExisting); + } + + public IEnumerable GetFiles(string path) + { + return FileSystem.GetFiles(path); + } + + public IEnumerable GetFiles(string path, string filter) + { + return FileSystem.GetFiles(path, filter); + } + + public Stream OpenFile(string path) + { + return FileSystem.OpenFile(path); + } + + public void DeleteFile(string path) + { + FileSystem.DeleteFile(path); + } + + public bool FileExists(string path) + { + return FileSystem.FileExists(path); + } + + public string GetRelativePath(string fullPathOrUrl) + { + return FileSystem.GetRelativePath(fullPathOrUrl); + } + + public string GetFullPath(string path) + { + return FileSystem.GetFullPath(path); + } + + public string GetUrl(string path) + { + return FileSystem.GetUrl(path); + } + + public DateTimeOffset GetLastModified(string path) + { + return FileSystem.GetLastModified(path); + } + + public DateTimeOffset GetCreated(string path) + { + return FileSystem.GetCreated(path); + } + + public long GetSize(string path) + { + var filesystem = FileSystem; // will be either a ShadowFileSystem OR the actual underlying IFileSystem + + // and the underlying filesystem can be IFileSystem2... or just IFileSystem + // figure it out and use the most effective GetSize method + var filesystem2 = filesystem as IFileSystem2; + return filesystem2 == null ? filesystem.GetSize(path) : filesystem2.GetSize(path); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/IO/UmbracoMediaFile.cs b/src/Umbraco.Core/IO/UmbracoMediaFile.cs index 27699baef4..b168ce89b4 100644 --- a/src/Umbraco.Core/IO/UmbracoMediaFile.cs +++ b/src/Umbraco.Core/IO/UmbracoMediaFile.cs @@ -2,7 +2,6 @@ using System.Drawing; using System.IO; using System.Web; -using Umbraco.Core.Configuration; using Umbraco.Core.Logging; using Umbraco.Core.Media; diff --git a/src/Umbraco.Core/Persistence/DefaultDatabaseFactory.cs b/src/Umbraco.Core/Persistence/DefaultDatabaseFactory.cs index d461801aa4..a9c7a976a3 100644 --- a/src/Umbraco.Core/Persistence/DefaultDatabaseFactory.cs +++ b/src/Umbraco.Core/Persistence/DefaultDatabaseFactory.cs @@ -181,15 +181,12 @@ namespace Umbraco.Core.Persistence if (HttpContext.Current == null) { if (NonContextValue != null) throw new InvalidOperationException(); - NonContextValue = database; + if (database != null) NonContextValue = database; } else { if (HttpContext.Current.Items[typeof(DefaultDatabaseFactory)] != null) throw new InvalidOperationException(); - if (database == null) - HttpContext.Current.Items.Remove(typeof(DefaultDatabaseFactory)); - else - HttpContext.Current.Items[typeof(DefaultDatabaseFactory)] = database; + if (database != null) HttpContext.Current.Items[typeof(DefaultDatabaseFactory)] = database; } } diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 57ed4dcaec..78e0cdc632 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -358,6 +358,8 @@ + + diff --git a/src/Umbraco.Tests/IO/ShadowFileSystemTests.cs b/src/Umbraco.Tests/IO/ShadowFileSystemTests.cs index ff1d3bf1d7..326e5e1369 100644 --- a/src/Umbraco.Tests/IO/ShadowFileSystemTests.cs +++ b/src/Umbraco.Tests/IO/ShadowFileSystemTests.cs @@ -1,10 +1,10 @@ using System; -using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; -using System.Threading.Tasks; +using System.Threading; using NUnit.Framework; +using Umbraco.Core; using Umbraco.Core.IO; namespace Umbraco.Tests.IO @@ -14,14 +14,32 @@ namespace Umbraco.Tests.IO { [SetUp] public void SetUp() - { } + { + ClearFiles(); + } [TearDown] public void TearDown() { - var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "FileSysTests"); - if (Directory.Exists(path) == false) return; - Directory.Delete(path, true); + ClearFiles(); + } + + private static void ClearFiles() + { + var path = IOHelper.MapPath("FileSysTests"); + if (Directory.Exists(path)) + { + foreach (var file in Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories)) + File.Delete(file); + Directory.Delete(path, true); + } + path = IOHelper.MapPath("App_Data"); + if (Directory.Exists(path)) + { + foreach (var file in Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories)) + File.Delete(file); + Directory.Delete(path, true); + } } private static string NormPath(string path) @@ -32,7 +50,7 @@ namespace Umbraco.Tests.IO [Test] public void ShadowDeleteDirectory() { - var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "FileSysTests"); + var path = IOHelper.MapPath("FileSysTests"); Directory.CreateDirectory(path); Directory.CreateDirectory(path + "/ShadowTests"); Directory.CreateDirectory(path + "/ShadowSystem"); @@ -66,7 +84,7 @@ namespace Umbraco.Tests.IO [Test] public void ShadowDeleteDirectoryInDir() { - var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "FileSysTests"); + var path = IOHelper.MapPath("FileSysTests"); Directory.CreateDirectory(path); Directory.CreateDirectory(path + "/ShadowTests"); Directory.CreateDirectory(path + "/ShadowSystem"); @@ -115,7 +133,7 @@ namespace Umbraco.Tests.IO [Test] public void ShadowDeleteFile() { - var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "FileSysTests"); + var path = IOHelper.MapPath("FileSysTests"); Directory.CreateDirectory(path); Directory.CreateDirectory(path + "/ShadowTests"); Directory.CreateDirectory(path + "/ShadowSystem"); @@ -154,7 +172,7 @@ namespace Umbraco.Tests.IO [Test] public void ShadowDeleteFileInDir() { - var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "FileSysTests"); + var path = IOHelper.MapPath("FileSysTests"); Directory.CreateDirectory(path); Directory.CreateDirectory(path + "/ShadowTests"); Directory.CreateDirectory(path + "/ShadowSystem"); @@ -209,7 +227,7 @@ namespace Umbraco.Tests.IO [Test] public void ShadowCantCreateFile() { - var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "FileSysTests"); + var path = IOHelper.MapPath("FileSysTests"); Directory.CreateDirectory(path); Directory.CreateDirectory(path + "/ShadowTests"); Directory.CreateDirectory(path + "/ShadowSystem"); @@ -228,7 +246,7 @@ namespace Umbraco.Tests.IO [Test] public void ShadowCreateFile() { - var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "FileSysTests"); + var path = IOHelper.MapPath("FileSysTests"); Directory.CreateDirectory(path); Directory.CreateDirectory(path + "/ShadowTests"); Directory.CreateDirectory(path + "/ShadowSystem"); @@ -267,7 +285,7 @@ namespace Umbraco.Tests.IO [Test] public void ShadowCreateFileInDir() { - var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "FileSysTests"); + var path = IOHelper.MapPath("FileSysTests"); Directory.CreateDirectory(path); Directory.CreateDirectory(path + "/ShadowTests"); Directory.CreateDirectory(path + "/ShadowSystem"); @@ -307,7 +325,7 @@ namespace Umbraco.Tests.IO [Test] public void ShadowAbort() { - var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "FileSysTests"); + var path = IOHelper.MapPath("FileSysTests"); Directory.CreateDirectory(path); Directory.CreateDirectory(path + "/ShadowTests"); Directory.CreateDirectory(path + "/ShadowSystem"); @@ -328,7 +346,7 @@ namespace Umbraco.Tests.IO [Test] public void ShadowComplete() { - var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "FileSysTests"); + var path = IOHelper.MapPath("FileSysTests"); Directory.CreateDirectory(path); Directory.CreateDirectory(path + "/ShadowTests"); Directory.CreateDirectory(path + "/ShadowSystem"); @@ -353,10 +371,183 @@ namespace Umbraco.Tests.IO Assert.IsFalse(File.Exists(path + "/ShadowTests/sub/sub/f2.txt")); } + [Test] + public void ShadowScopeComplete() + { + var path = IOHelper.MapPath("FileSysTests"); + var appdata = IOHelper.MapPath("App_Data"); + Directory.CreateDirectory(path); + + var fs = new PhysicalFileSystem(path, "ignore"); + var sw = new ShadowWrapper(fs, "shadow"); + var swa = new[] { sw }; + + using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo"))) + sw.AddFile("sub/f1.txt", ms); + Assert.IsTrue(fs.FileExists("sub/f1.txt")); + + Guid id; + + // explicit shadow without scope does not work + sw.Shadow(id = Guid.NewGuid()); + Assert.IsTrue(Directory.Exists(appdata + "/Shadow/" + id)); + using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo"))) + sw.AddFile("sub/f2.txt", ms); + Assert.IsTrue(fs.FileExists("sub/f2.txt")); + sw.UnShadow(true); + Assert.IsTrue(fs.FileExists("sub/f2.txt")); + Assert.IsFalse(Directory.Exists(appdata + "/Shadow/" + id)); + + // shadow with scope but no complete does not complete + var scope = ShadowFileSystemsScope.CreateScope(id = Guid.NewGuid(), swa); + Assert.IsTrue(Directory.Exists(appdata + "/Shadow/" + id)); + using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo"))) + sw.AddFile("sub/f3.txt", ms); + Assert.IsFalse(fs.FileExists("sub/f3.txt")); + Assert.AreEqual(1, Directory.GetDirectories(appdata + "/Shadow").Length); + scope.Dispose(); + Assert.IsFalse(fs.FileExists("sub/f3.txt")); + Assert.IsFalse(Directory.Exists(appdata + "/Shadow/" + id)); + + // shadow with scope and complete does complete + scope = ShadowFileSystemsScope.CreateScope(id = Guid.NewGuid(), swa); + Assert.IsTrue(Directory.Exists(appdata + "/Shadow/" + id)); + using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo"))) + sw.AddFile("sub/f4.txt", ms); + Assert.IsFalse(fs.FileExists("sub/f4.txt")); + Assert.AreEqual(1, Directory.GetDirectories(appdata + "/Shadow").Length); + scope.Complete(); + Assert.IsTrue(fs.FileExists("sub/f4.txt")); + Assert.AreEqual(0, Directory.GetDirectories(appdata + "/Shadow").Length); + scope.Dispose(); + Assert.IsTrue(fs.FileExists("sub/f4.txt")); + Assert.IsFalse(Directory.Exists(appdata + "/Shadow/" + id)); + + // test scope for "another thread" + + scope = ShadowFileSystemsScope.CreateScope(id = Guid.NewGuid(), swa); + Assert.IsTrue(Directory.Exists(appdata + "/Shadow/" + id)); + using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo"))) + sw.AddFile("sub/f5.txt", ms); + Assert.IsFalse(fs.FileExists("sub/f5.txt")); + using (new SafeCallContext()) // pretend we're another thread w/out scope + { + using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo"))) + sw.AddFile("sub/f6.txt", ms); + } + Assert.IsTrue(fs.FileExists("sub/f6.txt")); // other thread has written out to fs + scope.Complete(); + Assert.IsTrue(fs.FileExists("sub/f5.txt")); + scope.Dispose(); + Assert.IsTrue(fs.FileExists("sub/f5.txt")); + Assert.IsFalse(Directory.Exists(appdata + "/Shadow/" + id)); + } + + [Test] + public void ShadowScopeCompleteWithFileConflict() + { + var path = IOHelper.MapPath("FileSysTests"); + var appdata = IOHelper.MapPath("App_Data"); + Directory.CreateDirectory(path); + + var fs = new PhysicalFileSystem(path, "ignore"); + var sw = new ShadowWrapper(fs, "shadow"); + var swa = new[] { sw }; + + using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo"))) + sw.AddFile("sub/f1.txt", ms); + Assert.IsTrue(fs.FileExists("sub/f1.txt")); + + Guid id; + + var scope = ShadowFileSystemsScope.CreateScope(id = Guid.NewGuid(), swa); + Assert.IsTrue(Directory.Exists(appdata + "/Shadow/" + id)); + using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo"))) + sw.AddFile("sub/f2.txt", ms); + Assert.IsFalse(fs.FileExists("sub/f2.txt")); + using (new SafeCallContext()) // pretend we're another thread w/out scope + { + using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("bar"))) + sw.AddFile("sub/f2.txt", ms); + } + Assert.IsTrue(fs.FileExists("sub/f2.txt")); // other thread has written out to fs + scope.Complete(); + Assert.IsTrue(fs.FileExists("sub/f2.txt")); + scope.Dispose(); + Assert.IsTrue(fs.FileExists("sub/f2.txt")); + Assert.IsFalse(Directory.Exists(appdata + "/Shadow/" + id)); + + string text; + using (var s = fs.OpenFile("sub/f2.txt")) + using (var r = new StreamReader(s)) + text = r.ReadToEnd(); + + // the shadow filesystem will happily overwrite anything it can + Assert.AreEqual("foo", text); + } + + [Test] + public void ShadowScopeCompleteWithDirectoryConflict() + { + var path = IOHelper.MapPath("FileSysTests"); + var appdata = IOHelper.MapPath("App_Data"); + Directory.CreateDirectory(path); + + var fs = new PhysicalFileSystem(path, "ignore"); + var sw = new ShadowWrapper(fs, "shadow"); + var swa = new[] { sw }; + + using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo"))) + sw.AddFile("sub/f1.txt", ms); + Assert.IsTrue(fs.FileExists("sub/f1.txt")); + + Guid id; + + var scope = ShadowFileSystemsScope.CreateScope(id = Guid.NewGuid(), swa); + Assert.IsTrue(Directory.Exists(appdata + "/Shadow/" + id)); + using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo"))) + sw.AddFile("sub/f2.txt", ms); + Assert.IsFalse(fs.FileExists("sub/f2.txt")); + using (new SafeCallContext()) // pretend we're another thread w/out scope + { + using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("bar"))) + sw.AddFile("sub/f2.txt/f2.txt", ms); + } + Assert.IsTrue(fs.FileExists("sub/f2.txt/f2.txt")); // other thread has written out to fs + + using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo"))) + sw.AddFile("sub/f3.txt", ms); + Assert.IsFalse(fs.FileExists("sub/f3.txt")); + + try + { + // no way this can work since we're trying to write a file + // but there's now a directory with the same name on the real fs + scope.Complete(); + Assert.Fail("Expected AggregateException."); + } + catch (AggregateException ae) + { + Assert.AreEqual(1, ae.InnerExceptions.Count); + var e = ae.InnerExceptions[0]; + Assert.IsNotNull(e.InnerException); + Assert.IsInstanceOf(e); + ae = (AggregateException) e; + + Assert.AreEqual(1, ae.InnerExceptions.Count); + e = ae.InnerExceptions[0]; + Assert.IsNotNull(e.InnerException); + Assert.IsInstanceOf(e.InnerException); + } + + // still, the rest of the changes has been applied ok + Assert.IsTrue(fs.FileExists("sub/f3.txt")); + } + [Test] public void GetFilesReturnsChildrenOnly() { - var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "FileSysTests"); + var path = IOHelper.MapPath("FileSysTests"); Directory.CreateDirectory(path); File.WriteAllText(path + "/f1.txt", "foo"); Directory.CreateDirectory(path + "/test"); @@ -378,7 +569,7 @@ namespace Umbraco.Tests.IO [Test] public void DeleteDirectoryAndFiles() { - var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "FileSysTests"); + var path = IOHelper.MapPath("FileSysTests"); Directory.CreateDirectory(path); File.WriteAllText(path + "/f1.txt", "foo"); Directory.CreateDirectory(path + "/test");