Refactor Shadow FileSystems

This commit is contained in:
Stephan
2016-09-22 08:33:11 +02:00
parent 7176c99264
commit 111a3ca6ac
10 changed files with 557 additions and 167 deletions

View File

@@ -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<FileSystemWrapper> _fs = new WeakSet<FileSystemWrapper>();
private readonly bool _shadowEnabled;
private Guid _shadow = Guid.Empty;
private FileSystemWrapper[] _shadowFs;
private readonly WeakSet<ShadowWrapper> _wrappers = new WeakSet<ShadowWrapper>();
// 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<MediaFileSystem>();
}
@@ -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<FileSystemProviderManager>("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<FileSystemProviderManager>("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

View File

@@ -14,7 +14,7 @@ namespace Umbraco.Core.IO
///
/// This abstract class just wraps the 'real' IFileSystem object passed in to its constructor.
/// </remarks>
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);
}
}
}

View File

@@ -12,7 +12,7 @@ namespace Umbraco.Core.IO
/// A custom file system provider for media
/// </summary>
[FileSystemProvider("media")]
public class MediaFileSystem : FileSystemWrapper2
public class MediaFileSystem : FileSystemWrapper
{
private readonly IContentSection _contentConfig;

View File

@@ -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<Exception>();
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<string, ShadowNode> _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);
}

View File

@@ -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<ShadowFileSystemsScope>("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<ShadowFileSystemsScope>("UnShadow " + _id + " (complete).");
var exceptions = new List<Exception>();
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<ShadowFileSystemsScope>("UnShadow " + _id + " (abort)");
foreach (var wrapper in _wrappers)
wrapper.UnShadow(false); // should not throw
}
finally
{
// last, & always
CallContext.FreeNamedDataSlot(ItemKey);
}
}
}
}
}

View File

@@ -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<string> 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<string> GetFiles(string path)
{
return FileSystem.GetFiles(path);
}
public IEnumerable<string> 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);
}
}
}

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -358,6 +358,8 @@
<Compile Include="IApplicationEventHandler.cs" />
<Compile Include="IDisposeOnRequestEnd.cs" />
<Compile Include="IO\ShadowFileSystem.cs" />
<Compile Include="IO\ShadowFileSystemsScope.cs" />
<Compile Include="IO\ShadowWrapper.cs" />
<Compile Include="Logging\AsyncForwardingAppenderBase.cs" />
<Compile Include="Logging\ImageProcessorLogger.cs" />
<Compile Include="Logging\IQueue.cs" />

View File

@@ -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<AggregateException>(e);
ae = (AggregateException) e;
Assert.AreEqual(1, ae.InnerExceptions.Count);
e = ae.InnerExceptions[0];
Assert.IsNotNull(e.InnerException);
Assert.IsInstanceOf<UnauthorizedAccessException>(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");