AB3734 - Moved alot of stuff from Umbraco.Core.IO into abstractions
This commit is contained in:
@@ -1,20 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Umbraco.Core.IO
|
||||
{
|
||||
public class FileSecurityException : Exception
|
||||
{
|
||||
public FileSecurityException()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public FileSecurityException(string message) : base(message)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
|
||||
namespace Umbraco.Core.IO
|
||||
{
|
||||
public static class FileSystemExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Attempts to open the file at <code>filePath</code> up to <code>maxRetries</code> times,
|
||||
/// with a thread sleep time of <code>sleepPerRetryInMilliseconds</code> between retries.
|
||||
/// </summary>
|
||||
public static FileStream OpenReadWithRetry(this FileInfo file, int maxRetries = 5, int sleepPerRetryInMilliseconds = 50)
|
||||
{
|
||||
var retries = maxRetries;
|
||||
|
||||
while (retries > 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
return File.OpenRead(file.FullName);
|
||||
}
|
||||
catch(IOException)
|
||||
{
|
||||
retries--;
|
||||
|
||||
if (retries == 0)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
|
||||
Thread.Sleep(sleepPerRetryInMilliseconds);
|
||||
}
|
||||
}
|
||||
|
||||
throw new ArgumentException("Retries must be greater than zero");
|
||||
}
|
||||
|
||||
public static void CopyFile(this IFileSystem fs, string path, string newPath)
|
||||
{
|
||||
using (var stream = fs.OpenFile(path))
|
||||
{
|
||||
fs.AddFile(newPath, stream);
|
||||
}
|
||||
}
|
||||
|
||||
public static string GetExtension(this IFileSystem fs, string path)
|
||||
{
|
||||
return Path.GetExtension(fs.GetFullPath(path));
|
||||
}
|
||||
|
||||
public static string GetFileName(this IFileSystem fs, string path)
|
||||
{
|
||||
return Path.GetFileName(fs.GetFullPath(path));
|
||||
}
|
||||
|
||||
// TODO: Currently this is the only way to do this
|
||||
internal static void CreateFolder(this IFileSystem fs, string folderPath)
|
||||
{
|
||||
var path = fs.GetRelativePath(folderPath);
|
||||
var tempFile = Path.Combine(path, Guid.NewGuid().ToString("N") + ".tmp");
|
||||
using (var s = new MemoryStream())
|
||||
{
|
||||
fs.AddFile(tempFile, s);
|
||||
}
|
||||
fs.DeleteFile(tempFile);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unwraps a filesystem.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>A filesystem can be wrapped in a <see cref="FileSystemWrapper"/> (public) or a <see cref="ShadowWrapper"/> (internal),
|
||||
/// and this method deals with the various wrappers and </para>
|
||||
/// </remarks>
|
||||
public static IFileSystem Unwrap(this IFileSystem filesystem)
|
||||
{
|
||||
var unwrapping = true;
|
||||
while (unwrapping)
|
||||
{
|
||||
switch (filesystem)
|
||||
{
|
||||
case FileSystemWrapper wrapper:
|
||||
filesystem = wrapper.InnerFileSystem;
|
||||
break;
|
||||
case ShadowWrapper shadow:
|
||||
filesystem = shadow.InnerFileSystem;
|
||||
break;
|
||||
default:
|
||||
unwrapping = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return filesystem;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
namespace Umbraco.Core.IO
|
||||
{
|
||||
/// <summary>
|
||||
/// All custom file systems that are based upon another IFileSystem should inherit from FileSystemWrapper
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// An IFileSystem is generally used as a base file system, for example like a PhysicalFileSystem or an S3FileSystem.
|
||||
/// Then, other custom file systems are wrapped upon these files systems like MediaFileSystem, etc... All of the custom
|
||||
/// file systems must inherit from FileSystemWrapper.
|
||||
///
|
||||
/// This abstract class just wraps the 'real' IFileSystem object passed in to its constructor.
|
||||
/// </remarks>
|
||||
public abstract class FileSystemWrapper : IFileSystem
|
||||
{
|
||||
protected FileSystemWrapper(IFileSystem innerFileSystem)
|
||||
{
|
||||
InnerFileSystem = innerFileSystem;
|
||||
}
|
||||
|
||||
internal IFileSystem InnerFileSystem { get; set; }
|
||||
|
||||
public IEnumerable<string> GetDirectories(string path)
|
||||
{
|
||||
return InnerFileSystem.GetDirectories(path);
|
||||
}
|
||||
|
||||
public void DeleteDirectory(string path)
|
||||
{
|
||||
InnerFileSystem.DeleteDirectory(path);
|
||||
}
|
||||
|
||||
public void DeleteDirectory(string path, bool recursive)
|
||||
{
|
||||
InnerFileSystem.DeleteDirectory(path, recursive);
|
||||
}
|
||||
|
||||
public bool DirectoryExists(string path)
|
||||
{
|
||||
return InnerFileSystem.DirectoryExists(path);
|
||||
}
|
||||
|
||||
public void AddFile(string path, Stream stream)
|
||||
{
|
||||
InnerFileSystem.AddFile(path, stream);
|
||||
}
|
||||
|
||||
public void AddFile(string path, Stream stream, bool overrideExisting)
|
||||
{
|
||||
InnerFileSystem.AddFile(path, stream, overrideExisting);
|
||||
}
|
||||
|
||||
public IEnumerable<string> GetFiles(string path)
|
||||
{
|
||||
return InnerFileSystem.GetFiles(path);
|
||||
}
|
||||
|
||||
public IEnumerable<string> GetFiles(string path, string filter)
|
||||
{
|
||||
return InnerFileSystem.GetFiles(path, filter);
|
||||
}
|
||||
|
||||
public Stream OpenFile(string path)
|
||||
{
|
||||
return InnerFileSystem.OpenFile(path);
|
||||
}
|
||||
|
||||
public void DeleteFile(string path)
|
||||
{
|
||||
InnerFileSystem.DeleteFile(path);
|
||||
}
|
||||
|
||||
public bool FileExists(string path)
|
||||
{
|
||||
return InnerFileSystem.FileExists(path);
|
||||
}
|
||||
|
||||
public string GetRelativePath(string fullPathOrUrl)
|
||||
{
|
||||
return InnerFileSystem.GetRelativePath(fullPathOrUrl);
|
||||
}
|
||||
|
||||
public string GetFullPath(string path)
|
||||
{
|
||||
return InnerFileSystem.GetFullPath(path);
|
||||
}
|
||||
|
||||
public string GetUrl(string path)
|
||||
{
|
||||
return InnerFileSystem.GetUrl(path);
|
||||
}
|
||||
|
||||
public DateTimeOffset GetLastModified(string path)
|
||||
{
|
||||
return InnerFileSystem.GetLastModified(path);
|
||||
}
|
||||
|
||||
public DateTimeOffset GetCreated(string path)
|
||||
{
|
||||
return InnerFileSystem.GetCreated(path);
|
||||
}
|
||||
|
||||
public long GetSize(string path)
|
||||
{
|
||||
return InnerFileSystem.GetSize(path);
|
||||
}
|
||||
|
||||
public bool CanAddPhysical => InnerFileSystem.CanAddPhysical;
|
||||
|
||||
public void AddFile(string path, string physicalPath, bool overrideIfExists = true, bool copy = false)
|
||||
{
|
||||
InnerFileSystem.AddFile(path, physicalPath, overrideIfExists, copy);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,287 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using Umbraco.Core.Logging;
|
||||
using Umbraco.Core.Composing;
|
||||
using Umbraco.Core.Configuration;
|
||||
|
||||
namespace Umbraco.Core.IO
|
||||
{
|
||||
public class FileSystems : IFileSystems
|
||||
{
|
||||
private readonly IFactory _container;
|
||||
private readonly ILogger _logger;
|
||||
private readonly IIOHelper _ioHelper;
|
||||
|
||||
private readonly ConcurrentDictionary<Type, Lazy<IFileSystem>> _filesystems = new ConcurrentDictionary<Type, Lazy<IFileSystem>>();
|
||||
|
||||
// wrappers for shadow support
|
||||
private ShadowWrapper _macroPartialFileSystem;
|
||||
private ShadowWrapper _partialViewsFileSystem;
|
||||
private ShadowWrapper _stylesheetsFileSystem;
|
||||
private ShadowWrapper _scriptsFileSystem;
|
||||
private ShadowWrapper _mvcViewsFileSystem;
|
||||
|
||||
// well-known file systems lazy initialization
|
||||
private object _wkfsLock = new object();
|
||||
private bool _wkfsInitialized;
|
||||
private object _wkfsObject; // unused
|
||||
|
||||
// shadow support
|
||||
private readonly List<ShadowWrapper> _shadowWrappers = new List<ShadowWrapper>();
|
||||
private readonly object _shadowLocker = new object();
|
||||
private static string _shadowCurrentId; // static - unique!!
|
||||
#region Constructor
|
||||
|
||||
// DI wants a public ctor
|
||||
public FileSystems(IFactory container, ILogger logger, IIOHelper ioHelper, IGlobalSettings globalSettings)
|
||||
{
|
||||
_container = container;
|
||||
_logger = logger;
|
||||
_ioHelper = ioHelper;
|
||||
_globalSettings = globalSettings;
|
||||
}
|
||||
|
||||
// for tests only, totally unsafe
|
||||
internal void Reset()
|
||||
{
|
||||
_shadowWrappers.Clear();
|
||||
_filesystems.Clear();
|
||||
Volatile.Write(ref _wkfsInitialized, false);
|
||||
_shadowCurrentId = null;
|
||||
}
|
||||
|
||||
// for tests only, totally unsafe
|
||||
internal static void ResetShadowId()
|
||||
{
|
||||
_shadowCurrentId = null;
|
||||
}
|
||||
|
||||
// set by the scope provider when taking control of filesystems
|
||||
internal Func<bool> IsScoped { get; set; } = () => false;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Well-Known FileSystems
|
||||
|
||||
/// <inheritdoc />
|
||||
public IFileSystem MacroPartialsFileSystem
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Volatile.Read(ref _wkfsInitialized) == false) EnsureWellKnownFileSystems();
|
||||
return _macroPartialFileSystem;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IFileSystem PartialViewsFileSystem
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Volatile.Read(ref _wkfsInitialized) == false) EnsureWellKnownFileSystems();
|
||||
return _partialViewsFileSystem;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IFileSystem StylesheetsFileSystem
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Volatile.Read(ref _wkfsInitialized) == false) EnsureWellKnownFileSystems();
|
||||
return _stylesheetsFileSystem;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IFileSystem ScriptsFileSystem
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Volatile.Read(ref _wkfsInitialized) == false) EnsureWellKnownFileSystems();
|
||||
return _scriptsFileSystem;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IFileSystem MvcViewsFileSystem
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Volatile.Read(ref _wkfsInitialized) == false) EnsureWellKnownFileSystems();
|
||||
return _mvcViewsFileSystem;
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureWellKnownFileSystems()
|
||||
{
|
||||
LazyInitializer.EnsureInitialized(ref _wkfsObject, ref _wkfsInitialized, ref _wkfsLock, CreateWellKnownFileSystems);
|
||||
}
|
||||
|
||||
// need to return something to LazyInitializer.EnsureInitialized
|
||||
// but it does not really matter what we return - here, null
|
||||
private object CreateWellKnownFileSystems()
|
||||
{
|
||||
var macroPartialFileSystem = new PhysicalFileSystem(Constants.SystemDirectories.MacroPartials);
|
||||
var partialViewsFileSystem = new PhysicalFileSystem(Constants.SystemDirectories.PartialViews);
|
||||
var stylesheetsFileSystem = new PhysicalFileSystem(_globalSettings.UmbracoCssPath);
|
||||
var scriptsFileSystem = new PhysicalFileSystem(_globalSettings.UmbracoScriptsPath);
|
||||
var mvcViewsFileSystem = new PhysicalFileSystem(Constants.SystemDirectories.MvcViews);
|
||||
|
||||
_macroPartialFileSystem = new ShadowWrapper(macroPartialFileSystem, "macro-partials", IsScoped);
|
||||
_partialViewsFileSystem = new ShadowWrapper(partialViewsFileSystem, "partials", IsScoped);
|
||||
_stylesheetsFileSystem = new ShadowWrapper(stylesheetsFileSystem, "css", IsScoped);
|
||||
_scriptsFileSystem = new ShadowWrapper(scriptsFileSystem, "scripts", IsScoped);
|
||||
_mvcViewsFileSystem = new ShadowWrapper(mvcViewsFileSystem, "views", IsScoped);
|
||||
|
||||
// TODO: do we need a lock here?
|
||||
_shadowWrappers.Add(_macroPartialFileSystem);
|
||||
_shadowWrappers.Add(_partialViewsFileSystem);
|
||||
_shadowWrappers.Add(_stylesheetsFileSystem);
|
||||
_shadowWrappers.Add(_scriptsFileSystem);
|
||||
_shadowWrappers.Add(_mvcViewsFileSystem);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Providers
|
||||
|
||||
private readonly Dictionary<Type, string> _paths = new Dictionary<Type, string>();
|
||||
private IGlobalSettings _globalSettings;
|
||||
|
||||
// internal for tests
|
||||
internal IReadOnlyDictionary<Type, string> Paths => _paths;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a strongly-typed filesystem.
|
||||
/// </summary>
|
||||
/// <typeparam name="TFileSystem">The type of the filesystem.</typeparam>
|
||||
/// <returns>A strongly-typed filesystem of the specified type.</returns>
|
||||
/// <remarks>
|
||||
/// <para>Note that any filesystem created by this method *after* shadowing begins, will *not* be
|
||||
/// shadowing (and an exception will be thrown by the ShadowWrapper).</para>
|
||||
/// </remarks>
|
||||
public TFileSystem GetFileSystem<TFileSystem>(IFileSystem supporting)
|
||||
where TFileSystem : FileSystemWrapper
|
||||
{
|
||||
if (Volatile.Read(ref _wkfsInitialized) == false) EnsureWellKnownFileSystems();
|
||||
|
||||
return (TFileSystem) _filesystems.GetOrAdd(typeof(TFileSystem), _ => new Lazy<IFileSystem>(() =>
|
||||
{
|
||||
var typeofTFileSystem = typeof(TFileSystem);
|
||||
|
||||
// path must be unique and not collide with paths used in CreateWellKnownFileSystems
|
||||
// for our well-known 'media' filesystem we can use the short 'media' path
|
||||
// for others, put them under 'x/' and use ... something
|
||||
string path;
|
||||
if (typeofTFileSystem == typeof(MediaFileSystem))
|
||||
{
|
||||
path = "media";
|
||||
}
|
||||
else
|
||||
{
|
||||
lock (_paths)
|
||||
{
|
||||
if (!_paths.TryGetValue(typeofTFileSystem, out path))
|
||||
{
|
||||
path = Guid.NewGuid().ToString("N").Substring(0, 6);
|
||||
while (_paths.ContainsValue(path)) // this can't loop forever, right?
|
||||
path = Guid.NewGuid().ToString("N").Substring(0, 6);
|
||||
_paths[typeofTFileSystem] = path;
|
||||
}
|
||||
}
|
||||
|
||||
path = "x/" + path;
|
||||
}
|
||||
|
||||
var shadowWrapper = CreateShadowWrapper(supporting, path);
|
||||
return _container.CreateInstance<TFileSystem>(shadowWrapper);
|
||||
})).Value;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Shadow
|
||||
|
||||
// note
|
||||
// shadowing is thread-safe, but entering and exiting shadow mode is not, and there is only one
|
||||
// global shadow for the entire application, so great care should be taken to ensure that the
|
||||
// application is *not* doing anything else when using a shadow.
|
||||
|
||||
internal ICompletable Shadow()
|
||||
{
|
||||
if (Volatile.Read(ref _wkfsInitialized) == false) EnsureWellKnownFileSystems();
|
||||
|
||||
var id = ShadowWrapper.CreateShadowId();
|
||||
return new ShadowFileSystems(this, id); // will invoke BeginShadow and EndShadow
|
||||
}
|
||||
|
||||
internal void BeginShadow(string id)
|
||||
{
|
||||
lock (_shadowLocker)
|
||||
{
|
||||
// if we throw here, it means that something very wrong happened.
|
||||
if (_shadowCurrentId != null)
|
||||
throw new InvalidOperationException("Already shadowing.");
|
||||
|
||||
_shadowCurrentId = id;
|
||||
|
||||
_logger.Debug<ShadowFileSystems>("Shadow '{ShadowId}'", _shadowCurrentId);
|
||||
|
||||
foreach (var wrapper in _shadowWrappers)
|
||||
wrapper.Shadow(_shadowCurrentId);
|
||||
}
|
||||
}
|
||||
|
||||
internal void EndShadow(string id, bool completed)
|
||||
{
|
||||
lock (_shadowLocker)
|
||||
{
|
||||
// if we throw here, it means that something very wrong happened.
|
||||
if (_shadowCurrentId == null)
|
||||
throw new InvalidOperationException("Not shadowing.");
|
||||
if (id != _shadowCurrentId)
|
||||
throw new InvalidOperationException("Not the current shadow.");
|
||||
|
||||
_logger.Debug<ShadowFileSystems>("UnShadow '{ShadowId}' {Status}", id, completed ? "complete" : "abort");
|
||||
|
||||
var exceptions = new List<Exception>();
|
||||
foreach (var wrapper in _shadowWrappers)
|
||||
{
|
||||
try
|
||||
{
|
||||
// this may throw an AggregateException if some of the changes could not be applied
|
||||
wrapper.UnShadow(completed);
|
||||
}
|
||||
catch (AggregateException ae)
|
||||
{
|
||||
exceptions.Add(ae);
|
||||
}
|
||||
}
|
||||
|
||||
_shadowCurrentId = null;
|
||||
|
||||
if (exceptions.Count > 0)
|
||||
throw new AggregateException(completed ? "Failed to apply all changes (see exceptions)." : "Failed to abort (see exceptions).", exceptions);
|
||||
}
|
||||
}
|
||||
|
||||
private ShadowWrapper CreateShadowWrapper(IFileSystem filesystem, string shadowPath)
|
||||
{
|
||||
lock (_shadowLocker)
|
||||
{
|
||||
var wrapper = new ShadowWrapper(filesystem, shadowPath, IsScoped);
|
||||
if (_shadowCurrentId != null)
|
||||
wrapper.Shadow(_shadowCurrentId);
|
||||
_shadowWrappers.Add(wrapper);
|
||||
return wrapper;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Umbraco.Core.Models;
|
||||
|
||||
namespace Umbraco.Core.IO
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides methods allowing the manipulation of media files.
|
||||
/// </summary>
|
||||
public interface IMediaFileSystem : IFileSystem
|
||||
{
|
||||
/// <summary>
|
||||
/// Delete media files.
|
||||
/// </summary>
|
||||
/// <param name="files">Files to delete (filesystem-relative paths).</param>
|
||||
void DeleteMediaFiles(IEnumerable<string> files);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the file path of a media file.
|
||||
/// </summary>
|
||||
/// <param name="filename">The file name.</param>
|
||||
/// <param name="cuid">The unique identifier of the content/media owning the file.</param>
|
||||
/// <param name="puid">The unique identifier of the property type owning the file.</param>
|
||||
/// <returns>The filesystem-relative path to the media file.</returns>
|
||||
/// <remarks>With the old media path scheme, this CREATES a new media path each time it is invoked.</remarks>
|
||||
string GetMediaPath(string filename, Guid cuid, Guid puid);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the file path of a media file.
|
||||
/// </summary>
|
||||
/// <param name="filename">The file name.</param>
|
||||
/// <param name="prevpath">A previous file path.</param>
|
||||
/// <param name="cuid">The unique identifier of the content/media owning the file.</param>
|
||||
/// <param name="puid">The unique identifier of the property type owning the file.</param>
|
||||
/// <returns>The filesystem-relative path to the media file.</returns>
|
||||
/// <remarks>In the old, legacy, number-based scheme, we try to re-use the media folder
|
||||
/// specified by <paramref name="prevpath"/>. Else, we CREATE a new one. Each time we are invoked.</remarks>
|
||||
string GetMediaPath(string filename, string prevpath, Guid cuid, Guid puid);
|
||||
|
||||
/// <summary>
|
||||
/// Stores a media file associated to a property of a content item.
|
||||
/// </summary>
|
||||
/// <param name="content">The content item owning the media file.</param>
|
||||
/// <param name="propertyType">The property type owning the media file.</param>
|
||||
/// <param name="filename">The media file name.</param>
|
||||
/// <param name="filestream">A stream containing the media bytes.</param>
|
||||
/// <param name="oldpath">An optional filesystem-relative filepath to the previous media file.</param>
|
||||
/// <returns>The filesystem-relative filepath to the media file.</returns>
|
||||
/// <remarks>
|
||||
/// <para>The file is considered "owned" by the content/propertyType.</para>
|
||||
/// <para>If an <paramref name="oldpath"/> is provided then that file (and associated thumbnails if any) is deleted
|
||||
/// before the new file is saved, and depending on the media path scheme, the folder may be reused for the new file.</para>
|
||||
/// </remarks>
|
||||
string StoreFile(IContentBase content, IPropertyType propertyType, string filename, Stream filestream, string oldpath);
|
||||
|
||||
/// <summary>
|
||||
/// Copies a media file as a new media file, associated to a property of a content item.
|
||||
/// </summary>
|
||||
/// <param name="content">The content item owning the copy of the media file.</param>
|
||||
/// <param name="propertyType">The property type owning the copy of the media file.</param>
|
||||
/// <param name="sourcepath">The filesystem-relative path to the source media file.</param>
|
||||
/// <returns>The filesystem-relative path to the copy of the media file.</returns>
|
||||
string CopyFile(IContentBase content, IPropertyType propertyType, string sourcepath);
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Umbraco.Core.IO
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a media file path scheme.
|
||||
/// </summary>
|
||||
public interface IMediaPathScheme
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a media file path.
|
||||
/// </summary>
|
||||
/// <param name="fileSystem">The media filesystem.</param>
|
||||
/// <param name="itemGuid">The (content, media) item unique identifier.</param>
|
||||
/// <param name="propertyGuid">The property type unique identifier.</param>
|
||||
/// <param name="filename">The file name.</param>
|
||||
/// <param name="previous">A previous filename.</param>
|
||||
/// <returns>The filesystem-relative complete file path.</returns>
|
||||
string GetFilePath(IMediaFileSystem fileSystem, Guid itemGuid, Guid propertyGuid, string filename, string previous = null);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the directory that can be deleted when the file is deleted.
|
||||
/// </summary>
|
||||
/// <param name="fileSystem">The media filesystem.</param>
|
||||
/// <param name="filepath">The filesystem-relative path of the file.</param>
|
||||
/// <returns>The filesystem-relative path of the directory.</returns>
|
||||
/// <remarks>
|
||||
/// <para>The directory, and anything below it, will be deleted.</para>
|
||||
/// <para>Can return null (or empty) when no directory should be deleted.</para>
|
||||
/// </remarks>
|
||||
string GetDeleteDirectory(IMediaFileSystem fileSystem, string filepath);
|
||||
}
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Configuration;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Umbraco.Core.Composing;
|
||||
using Umbraco.Core.Configuration;
|
||||
using Umbraco.Core.Configuration.UmbracoSettings;
|
||||
using Umbraco.Core.Exceptions;
|
||||
using Umbraco.Core.Logging;
|
||||
using Umbraco.Core.Media;
|
||||
using Umbraco.Core.Models;
|
||||
|
||||
namespace Umbraco.Core.IO
|
||||
{
|
||||
/// <summary>
|
||||
/// A custom file system provider for media
|
||||
/// </summary>
|
||||
public class MediaFileSystem : FileSystemWrapper, IMediaFileSystem
|
||||
{
|
||||
private readonly IMediaPathScheme _mediaPathScheme;
|
||||
private readonly IContentSection _contentConfig;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MediaFileSystem"/> class.
|
||||
/// </summary>
|
||||
public MediaFileSystem(IFileSystem innerFileSystem, IContentSection contentConfig, IMediaPathScheme mediaPathScheme, ILogger logger)
|
||||
: base(innerFileSystem)
|
||||
{
|
||||
_contentConfig = contentConfig;
|
||||
_mediaPathScheme = mediaPathScheme;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritoc />
|
||||
public void DeleteMediaFiles(IEnumerable<string> files)
|
||||
{
|
||||
files = files.Distinct();
|
||||
|
||||
// kinda try to keep things under control
|
||||
var options = new ParallelOptions { MaxDegreeOfParallelism = 20 };
|
||||
|
||||
Parallel.ForEach(files, options, file =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (file.IsNullOrWhiteSpace()) return;
|
||||
if (FileExists(file) == false) return;
|
||||
DeleteFile(file);
|
||||
|
||||
var directory = _mediaPathScheme.GetDeleteDirectory(this, file);
|
||||
if (!directory.IsNullOrWhiteSpace())
|
||||
DeleteDirectory(directory, true);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Error<MediaFileSystem>(e, "Failed to delete media file '{File}'.", file);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#region Media Path
|
||||
|
||||
/// <inheritoc />
|
||||
public string GetMediaPath(string filename, Guid cuid, Guid puid)
|
||||
{
|
||||
filename = Path.GetFileName(filename);
|
||||
if (filename == null) throw new ArgumentException("Cannot become a safe filename.", nameof(filename));
|
||||
filename = Current.IOHelper.SafeFileName(filename.ToLowerInvariant());
|
||||
|
||||
return _mediaPathScheme.GetFilePath(this, cuid, puid, filename);
|
||||
}
|
||||
|
||||
/// <inheritoc />
|
||||
public string GetMediaPath(string filename, string prevpath, Guid cuid, Guid puid)
|
||||
{
|
||||
filename = Path.GetFileName(filename);
|
||||
if (filename == null) throw new ArgumentException("Cannot become a safe filename.", nameof(filename));
|
||||
filename = Current.IOHelper.SafeFileName(filename.ToLowerInvariant());
|
||||
|
||||
return _mediaPathScheme.GetFilePath(this, cuid, puid, filename, prevpath);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Associated Media Files
|
||||
|
||||
/// <inheritoc />
|
||||
public string StoreFile(IContentBase content, IPropertyType propertyType, string filename, Stream filestream, string oldpath)
|
||||
{
|
||||
if (content == null) throw new ArgumentNullException(nameof(content));
|
||||
if (propertyType == null) throw new ArgumentNullException(nameof(propertyType));
|
||||
if (string.IsNullOrWhiteSpace(filename)) throw new ArgumentNullOrEmptyException(nameof(filename));
|
||||
if (filestream == null) throw new ArgumentNullException(nameof(filestream));
|
||||
|
||||
// clear the old file, if any
|
||||
if (string.IsNullOrWhiteSpace(oldpath) == false)
|
||||
DeleteFile(oldpath);
|
||||
|
||||
// get the filepath, store the data
|
||||
// use oldpath as "prevpath" to try and reuse the folder, in original number-based scheme
|
||||
var filepath = GetMediaPath(filename, oldpath, content.Key, propertyType.Key);
|
||||
AddFile(filepath, filestream);
|
||||
return filepath;
|
||||
}
|
||||
|
||||
/// <inheritoc />
|
||||
public string CopyFile(IContentBase content, IPropertyType propertyType, string sourcepath)
|
||||
{
|
||||
if (content == null) throw new ArgumentNullException(nameof(content));
|
||||
if (propertyType == null) throw new ArgumentNullException(nameof(propertyType));
|
||||
if (string.IsNullOrWhiteSpace(sourcepath)) throw new ArgumentNullOrEmptyException(nameof(sourcepath));
|
||||
|
||||
// ensure we have a file to copy
|
||||
if (FileExists(sourcepath) == false) return null;
|
||||
|
||||
// get the filepath
|
||||
var filename = Path.GetFileName(sourcepath);
|
||||
var filepath = GetMediaPath(filename, content.Key, propertyType.Key);
|
||||
this.CopyFile(sourcepath, filepath);
|
||||
return filepath;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace Umbraco.Core.IO.MediaPathSchemes
|
||||
{
|
||||
/// <summary>
|
||||
/// Implements a combined-guids media path scheme.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>Path is "{combinedGuid}/{filename}" where combinedGuid is a combination of itemGuid and propertyGuid.</para>
|
||||
/// </remarks>
|
||||
public class CombinedGuidsMediaPathScheme : IMediaPathScheme
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string GetFilePath(IMediaFileSystem fileSystem, Guid itemGuid, Guid propertyGuid, string filename, string previous = null)
|
||||
{
|
||||
// assumes that cuid and puid keys can be trusted - and that a single property type
|
||||
// for a single content cannot store two different files with the same name
|
||||
|
||||
var combinedGuid = GuidUtils.Combine(itemGuid, propertyGuid);
|
||||
var directory = HexEncoder.Encode(combinedGuid.ToByteArray()/*'/', 2, 4*/); // could use ext to fragment path eg 12/e4/f2/...
|
||||
return Path.Combine(directory, filename).Replace('\\', '/');
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetDeleteDirectory(IMediaFileSystem fileSystem, string filepath) => Path.GetDirectoryName(filepath);
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using Umbraco.Core.Composing;
|
||||
using Umbraco.Core.Configuration;
|
||||
|
||||
namespace Umbraco.Core.IO.MediaPathSchemes
|
||||
{
|
||||
/// <summary>
|
||||
/// Implements the original media path scheme.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>Path is "{number}/{filename}" or "{number}-{filename}" where number is an incremented counter.</para>
|
||||
/// <para>Use '/' or '-' depending on UploadAllowDirectories setting.</para>
|
||||
/// </remarks>
|
||||
// scheme: path is "<number>/<filename>" where number is an incremented counter
|
||||
public class OriginalMediaPathScheme : IMediaPathScheme
|
||||
{
|
||||
private readonly object _folderCounterLock = new object();
|
||||
private long _folderCounter;
|
||||
private bool _folderCounterInitialized;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetFilePath(IMediaFileSystem fileSystem, Guid itemGuid, Guid propertyGuid, string filename, string previous = null)
|
||||
{
|
||||
string directory;
|
||||
if (previous != null)
|
||||
{
|
||||
// old scheme, with a previous path
|
||||
// prevpath should be "<int>/<filename>" OR "<int>-<filename>"
|
||||
// and we want to reuse the "<int>" part, so try to find it
|
||||
|
||||
const string sep = "/";
|
||||
var pos = previous.IndexOf(sep, StringComparison.Ordinal);
|
||||
var s = pos > 0 ? previous.Substring(0, pos) : null;
|
||||
|
||||
directory = pos > 0 && int.TryParse(s, out _) ? s : GetNextDirectory(fileSystem);
|
||||
}
|
||||
else
|
||||
{
|
||||
directory = GetNextDirectory(fileSystem);
|
||||
}
|
||||
|
||||
if (directory == null)
|
||||
throw new InvalidOperationException("Cannot use a null directory.");
|
||||
|
||||
return Path.Combine(directory, filename).Replace('\\', '/');
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetDeleteDirectory(IMediaFileSystem fileSystem, string filepath)
|
||||
{
|
||||
return Path.GetDirectoryName(filepath);
|
||||
}
|
||||
|
||||
private string GetNextDirectory(IFileSystem fileSystem)
|
||||
{
|
||||
EnsureFolderCounterIsInitialized(fileSystem);
|
||||
return Interlocked.Increment(ref _folderCounter).ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
private void EnsureFolderCounterIsInitialized(IFileSystem fileSystem)
|
||||
{
|
||||
lock (_folderCounterLock)
|
||||
{
|
||||
if (_folderCounterInitialized) return;
|
||||
|
||||
_folderCounter = 1000; // seed
|
||||
var directories = fileSystem.GetDirectories("");
|
||||
foreach (var directory in directories)
|
||||
{
|
||||
if (long.TryParse(directory, out var folderNumber) && folderNumber > _folderCounter)
|
||||
_folderCounter = folderNumber;
|
||||
}
|
||||
|
||||
// note: not multi-domains ie LB safe as another domain could create directories
|
||||
// while we read and parse them - don't fix, move to new scheme eventually
|
||||
|
||||
_folderCounterInitialized = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace Umbraco.Core.IO.MediaPathSchemes
|
||||
{
|
||||
/// <summary>
|
||||
/// Implements a two-guids media path scheme.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>Path is "{itemGuid}/{propertyGuid}/{filename}".</para>
|
||||
/// </remarks>
|
||||
public class TwoGuidsMediaPathScheme : IMediaPathScheme
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string GetFilePath(IMediaFileSystem fileSystem, Guid itemGuid, Guid propertyGuid, string filename, string previous = null)
|
||||
{
|
||||
return Path.Combine(itemGuid.ToString("N"), propertyGuid.ToString("N"), filename).Replace('\\', '/');
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetDeleteDirectory(IMediaFileSystem fileSystem, string filepath)
|
||||
{
|
||||
return Path.GetDirectoryName(filepath);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace Umbraco.Core.IO.MediaPathSchemes
|
||||
{
|
||||
/// <summary>
|
||||
/// Implements a unique directory media path scheme.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>This scheme provides deterministic short paths, with potential collisions.</para>
|
||||
/// </remarks>
|
||||
public class UniqueMediaPathScheme : IMediaPathScheme
|
||||
{
|
||||
private const int DirectoryLength = 8;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetFilePath(IMediaFileSystem fileSystem, Guid itemGuid, Guid propertyGuid, string filename, string previous = null)
|
||||
{
|
||||
var combinedGuid = GuidUtils.Combine(itemGuid, propertyGuid);
|
||||
var directory = GuidUtils.ToBase32String(combinedGuid, DirectoryLength);
|
||||
|
||||
return Path.Combine(directory, filename).Replace('\\', '/');
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <remarks>
|
||||
/// <para>Returning null so that <see cref="MediaFileSystem.DeleteMediaFiles"/> does *not*
|
||||
/// delete any directory. This is because the above shortening of the Guid to 8 chars
|
||||
/// means we're increasing the risk of collision, and we don't want to delete files
|
||||
/// belonging to other media items.</para>
|
||||
/// <para>And, at the moment, we cannot delete directory "only if it is empty" because of
|
||||
/// race conditions. We'd need to implement locks in <see cref="MediaFileSystem"/> for
|
||||
/// this.</para>
|
||||
/// </remarks>
|
||||
public string GetDeleteDirectory(IMediaFileSystem fileSystem, string filepath) => null;
|
||||
}
|
||||
}
|
||||
@@ -1,451 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Umbraco.Core.Composing;
|
||||
using Umbraco.Core.Exceptions;
|
||||
using System.Threading;
|
||||
using Umbraco.Core.Logging;
|
||||
|
||||
namespace Umbraco.Core.IO
|
||||
{
|
||||
public class PhysicalFileSystem : IFileSystem
|
||||
{
|
||||
// the rooted, filesystem path, using directory separator chars, NOT ending with a separator
|
||||
// eg "c:" or "c:\path\to\site" or "\\server\path"
|
||||
private readonly string _rootPath;
|
||||
|
||||
// _rootPath, but with separators replaced by forward-slashes
|
||||
// eg "c:" or "c:/path/to/site" or "//server/path"
|
||||
// (is used in GetRelativePath)
|
||||
private readonly string _rootPathFwd;
|
||||
|
||||
// the relative url, using url separator chars, NOT ending with a separator
|
||||
// eg "" or "/Views" or "/Media" or "/<vpath>/Media" in case of a virtual path
|
||||
private readonly string _rootUrl;
|
||||
|
||||
// virtualRoot should be "~/path/to/root" eg "~/Views"
|
||||
// the "~/" is mandatory.
|
||||
public PhysicalFileSystem(string virtualRoot)
|
||||
{
|
||||
if (virtualRoot == null) throw new ArgumentNullException("virtualRoot");
|
||||
if (virtualRoot.StartsWith("~/") == false)
|
||||
throw new ArgumentException("The virtualRoot argument must be a virtual path and start with '~/'");
|
||||
|
||||
_rootPath = EnsureDirectorySeparatorChar(Current.IOHelper.MapPath(virtualRoot)).TrimEnd(Path.DirectorySeparatorChar);
|
||||
_rootPathFwd = EnsureUrlSeparatorChar(_rootPath);
|
||||
_rootUrl = EnsureUrlSeparatorChar(Current.IOHelper.ResolveUrl(virtualRoot)).TrimEnd('/');
|
||||
}
|
||||
|
||||
public PhysicalFileSystem(string rootPath, string rootUrl)
|
||||
{
|
||||
if (string.IsNullOrEmpty(rootPath)) throw new ArgumentNullOrEmptyException(nameof(rootPath));
|
||||
if (string.IsNullOrEmpty(rootUrl)) throw new ArgumentNullOrEmptyException(nameof(rootUrl));
|
||||
if (rootPath.StartsWith("~/")) throw new ArgumentException("The rootPath argument cannot be a virtual path and cannot start with '~/'");
|
||||
|
||||
// rootPath should be... rooted, as in, it's a root path!
|
||||
if (Path.IsPathRooted(rootPath) == false)
|
||||
{
|
||||
// but the test suite App.config cannot really "root" anything so we have to do it here
|
||||
var localRoot = Current.IOHelper.GetRootDirectorySafe();
|
||||
rootPath = Path.Combine(localRoot, rootPath);
|
||||
}
|
||||
|
||||
_rootPath = EnsureDirectorySeparatorChar(rootPath).TrimEnd(Path.DirectorySeparatorChar);
|
||||
_rootPathFwd = EnsureUrlSeparatorChar(_rootPath);
|
||||
_rootUrl = EnsureUrlSeparatorChar(rootUrl).TrimEnd('/');
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets directories in a directory.
|
||||
/// </summary>
|
||||
/// <param name="path">The filesystem-relative path to the directory.</param>
|
||||
/// <returns>The filesystem-relative path to the directories in the directory.</returns>
|
||||
/// <remarks>Filesystem-relative paths use forward-slashes as directory separators.</remarks>
|
||||
public IEnumerable<string> GetDirectories(string path)
|
||||
{
|
||||
var fullPath = GetFullPath(path);
|
||||
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(fullPath))
|
||||
return Directory.EnumerateDirectories(fullPath).Select(GetRelativePath);
|
||||
}
|
||||
catch (UnauthorizedAccessException ex)
|
||||
{
|
||||
Current.Logger.Error<PhysicalFileSystem>(ex, "Not authorized to get directories for '{Path}'", fullPath);
|
||||
}
|
||||
catch (DirectoryNotFoundException ex)
|
||||
{
|
||||
Current.Logger.Error<PhysicalFileSystem>(ex, "Directory not found for '{Path}'", fullPath);
|
||||
}
|
||||
|
||||
return Enumerable.Empty<string>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a directory.
|
||||
/// </summary>
|
||||
/// <param name="path">The filesystem-relative path of the directory.</param>
|
||||
public void DeleteDirectory(string path)
|
||||
{
|
||||
DeleteDirectory(path, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a directory.
|
||||
/// </summary>
|
||||
/// <param name="path">The filesystem-relative path of the directory.</param>
|
||||
/// <param name="recursive">A value indicating whether to recursively delete sub-directories.</param>
|
||||
public void DeleteDirectory(string path, bool recursive)
|
||||
{
|
||||
var fullPath = GetFullPath(path);
|
||||
if (Directory.Exists(fullPath) == false)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
WithRetry(() => Directory.Delete(fullPath, recursive));
|
||||
}
|
||||
catch (DirectoryNotFoundException ex)
|
||||
{
|
||||
Current.Logger.Error<PhysicalFileSystem>(ex, "Directory not found for '{Path}'", fullPath);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether a directory exists.
|
||||
/// </summary>
|
||||
/// <param name="path">The filesystem-relative path of the directory.</param>
|
||||
/// <returns>A value indicating whether a directory exists.</returns>
|
||||
public bool DirectoryExists(string path)
|
||||
{
|
||||
var fullPath = GetFullPath(path);
|
||||
return Directory.Exists(fullPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves a file.
|
||||
/// </summary>
|
||||
/// <param name="path">The filesystem-relative path of the file.</param>
|
||||
/// <param name="stream">A stream containing the file data.</param>
|
||||
/// <remarks>Overrides the existing file, if any.</remarks>
|
||||
public void AddFile(string path, Stream stream)
|
||||
{
|
||||
AddFile(path, stream, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves a file.
|
||||
/// </summary>
|
||||
/// <param name="path">The filesystem-relative path of the file.</param>
|
||||
/// <param name="stream">A stream containing the file data.</param>
|
||||
/// <param name="overrideExisting">A value indicating whether to override the existing file, if any.</param>
|
||||
/// <remarks>If a file exists and <paramref name="overrideExisting"/> is false, an exception is thrown.</remarks>
|
||||
public void AddFile(string path, Stream stream, bool overrideExisting)
|
||||
{
|
||||
var fullPath = GetFullPath(path);
|
||||
var exists = File.Exists(fullPath);
|
||||
if (exists && overrideExisting == false)
|
||||
throw new InvalidOperationException(string.Format("A file at path '{0}' already exists", path));
|
||||
|
||||
var directory = Path.GetDirectoryName(fullPath);
|
||||
if (directory == null) throw new InvalidOperationException("Could not get directory.");
|
||||
Directory.CreateDirectory(directory); // ensure it exists
|
||||
|
||||
if (stream.CanSeek) // TODO: what if we cannot?
|
||||
stream.Seek(0, 0);
|
||||
|
||||
using (var destination = (Stream) File.Create(fullPath))
|
||||
stream.CopyTo(destination);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets files in a directory.
|
||||
/// </summary>
|
||||
/// <param name="path">The filesystem-relative path of the directory.</param>
|
||||
/// <returns>The filesystem-relative path to the files in the directory.</returns>
|
||||
/// <remarks>Filesystem-relative paths use forward-slashes as directory separators.</remarks>
|
||||
public IEnumerable<string> GetFiles(string path)
|
||||
{
|
||||
return GetFiles(path, "*.*");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets files in a directory.
|
||||
/// </summary>
|
||||
/// <param name="path">The filesystem-relative path of the directory.</param>
|
||||
/// <param name="filter">A filter.</param>
|
||||
/// <returns>The filesystem-relative path to the matching files in the directory.</returns>
|
||||
/// <remarks>Filesystem-relative paths use forward-slashes as directory separators.</remarks>
|
||||
public IEnumerable<string> GetFiles(string path, string filter)
|
||||
{
|
||||
var fullPath = GetFullPath(path);
|
||||
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(fullPath))
|
||||
return Directory.EnumerateFiles(fullPath, filter).Select(GetRelativePath);
|
||||
}
|
||||
catch (UnauthorizedAccessException ex)
|
||||
{
|
||||
Current.Logger.Error<PhysicalFileSystem>(ex, "Not authorized to get directories for '{Path}'", fullPath);
|
||||
}
|
||||
catch (DirectoryNotFoundException ex)
|
||||
{
|
||||
Current.Logger.Error<PhysicalFileSystem>(ex, "Directory not found for '{FullPath}'", fullPath);
|
||||
}
|
||||
|
||||
return Enumerable.Empty<string>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens a file.
|
||||
/// </summary>
|
||||
/// <param name="path">The filesystem-relative path to the file.</param>
|
||||
/// <returns></returns>
|
||||
public Stream OpenFile(string path)
|
||||
{
|
||||
var fullPath = GetFullPath(path);
|
||||
return File.OpenRead(fullPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a file.
|
||||
/// </summary>
|
||||
/// <param name="path">The filesystem-relative path to the file.</param>
|
||||
public void DeleteFile(string path)
|
||||
{
|
||||
var fullPath = GetFullPath(path);
|
||||
if (File.Exists(fullPath) == false)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
WithRetry(() => File.Delete(fullPath));
|
||||
}
|
||||
catch (FileNotFoundException ex)
|
||||
{
|
||||
Current.Logger.Error<PhysicalFileSystem>(ex.InnerException, "DeleteFile failed with FileNotFoundException for '{Path}'", fullPath);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether a file exists.
|
||||
/// </summary>
|
||||
/// <param name="path">The filesystem-relative path to the file.</param>
|
||||
/// <returns>A value indicating whether the file exists.</returns>
|
||||
public bool FileExists(string path)
|
||||
{
|
||||
var fullpath = GetFullPath(path);
|
||||
return File.Exists(fullpath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the filesystem-relative path of a full path or of an url.
|
||||
/// </summary>
|
||||
/// <param name="fullPathOrUrl">The full path or url.</param>
|
||||
/// <returns>The path, relative to this filesystem's root.</returns>
|
||||
/// <remarks>
|
||||
/// <para>The relative path is relative to this filesystem's root, not starting with any
|
||||
/// directory separator. All separators are forward-slashes.</para>
|
||||
/// </remarks>
|
||||
public string GetRelativePath(string fullPathOrUrl)
|
||||
{
|
||||
// test url
|
||||
var path = fullPathOrUrl.Replace('\\', '/'); // ensure url separator char
|
||||
|
||||
// if it starts with the root url, strip it and trim the starting slash to make it relative
|
||||
// eg "/Media/1234/img.jpg" => "1234/img.jpg"
|
||||
if (Current.IOHelper.PathStartsWith(path, _rootUrl, '/'))
|
||||
return path.Substring(_rootUrl.Length).TrimStart('/');
|
||||
|
||||
// if it starts with the root path, strip it and trim the starting slash to make it relative
|
||||
// eg "c:/websites/test/root/Media/1234/img.jpg" => "1234/img.jpg"
|
||||
if (Current.IOHelper.PathStartsWith(path, _rootPathFwd, '/'))
|
||||
return path.Substring(_rootPathFwd.Length).TrimStart('/');
|
||||
|
||||
// unchanged - what else?
|
||||
return path;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the full path.
|
||||
/// </summary>
|
||||
/// <param name="path">The full or filesystem-relative path.</param>
|
||||
/// <returns>The full path.</returns>
|
||||
/// <remarks>
|
||||
/// <para>On the physical filesystem, the full path is the rooted (ie non-relative), safe (ie within this
|
||||
/// filesystem's root) path. All separators are Path.DirectorySeparatorChar.</para>
|
||||
/// </remarks>
|
||||
public string GetFullPath(string path)
|
||||
{
|
||||
// normalize
|
||||
var opath = path;
|
||||
path = EnsureDirectorySeparatorChar(path);
|
||||
|
||||
// FIXME: this part should go!
|
||||
// not sure what we are doing here - so if input starts with a (back) slash,
|
||||
// we assume it's not a FS relative path and we try to convert it... but it
|
||||
// really makes little sense?
|
||||
if (path.StartsWith(Path.DirectorySeparatorChar.ToString()))
|
||||
path = GetRelativePath(path);
|
||||
|
||||
// if not already rooted, combine with the root path
|
||||
if (Current.IOHelper.PathStartsWith(path, _rootPath, Path.DirectorySeparatorChar) == false)
|
||||
path = Path.Combine(_rootPath, path);
|
||||
|
||||
// sanitize - GetFullPath will take care of any relative
|
||||
// segments in path, eg '../../foo.tmp' - it may throw a SecurityException
|
||||
// if the combined path reaches illegal parts of the filesystem
|
||||
path = Path.GetFullPath(path);
|
||||
|
||||
// at that point, path is within legal parts of the filesystem, ie we have
|
||||
// permissions to reach that path, but it may nevertheless be outside of
|
||||
// our root path, due to relative segments, so better check
|
||||
if (Current.IOHelper.PathStartsWith(path, _rootPath, Path.DirectorySeparatorChar))
|
||||
{
|
||||
// this says that 4.7.2 supports long paths - but Windows does not
|
||||
// https://docs.microsoft.com/en-us/dotnet/api/system.io.pathtoolongexception?view=netframework-4.7.2
|
||||
if (path.Length > 260)
|
||||
throw new PathTooLongException($"Path {path} is too long.");
|
||||
return path;
|
||||
}
|
||||
|
||||
// nothing prevents us to reach the file, security-wise, yet it is outside
|
||||
// this filesystem's root - throw
|
||||
throw new FileSecurityException("File '" + opath + "' is outside this filesystem's root.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the url.
|
||||
/// </summary>
|
||||
/// <param name="path">The filesystem-relative path.</param>
|
||||
/// <returns>The url.</returns>
|
||||
/// <remarks>All separators are forward-slashes.</remarks>
|
||||
public string GetUrl(string path)
|
||||
{
|
||||
path = EnsureUrlSeparatorChar(path).Trim('/');
|
||||
return _rootUrl + "/" + path;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the last-modified date of a directory or file.
|
||||
/// </summary>
|
||||
/// <param name="path">The filesystem-relative path to the directory or the file.</param>
|
||||
/// <returns>The last modified date of the directory or the file.</returns>
|
||||
public DateTimeOffset GetLastModified(string path)
|
||||
{
|
||||
var fullpath = GetFullPath(path);
|
||||
return DirectoryExists(fullpath)
|
||||
? new DirectoryInfo(fullpath).LastWriteTimeUtc
|
||||
: new FileInfo(fullpath).LastWriteTimeUtc;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the created date of a directory or file.
|
||||
/// </summary>
|
||||
/// <param name="path">The filesystem-relative path to the directory or the file.</param>
|
||||
/// <returns>The created date of the directory or the file.</returns>
|
||||
public DateTimeOffset GetCreated(string path)
|
||||
{
|
||||
var fullpath = GetFullPath(path);
|
||||
return DirectoryExists(fullpath)
|
||||
? Directory.GetCreationTimeUtc(fullpath)
|
||||
: File.GetCreationTimeUtc(fullpath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the size of a file.
|
||||
/// </summary>
|
||||
/// <param name="path">The filesystem-relative path to the file.</param>
|
||||
/// <returns>The file of the size, in bytes.</returns>
|
||||
/// <remarks>If the file does not exist, returns -1.</remarks>
|
||||
public long GetSize(string path)
|
||||
{
|
||||
var fullPath = GetFullPath(path);
|
||||
var file = new FileInfo(fullPath);
|
||||
return file.Exists ? file.Length : -1;
|
||||
}
|
||||
|
||||
public bool CanAddPhysical => true;
|
||||
|
||||
public void AddFile(string path, string physicalPath, bool overrideIfExists = true, bool copy = false)
|
||||
{
|
||||
var fullPath = GetFullPath(path);
|
||||
|
||||
if (File.Exists(fullPath))
|
||||
{
|
||||
if (overrideIfExists == false)
|
||||
throw new InvalidOperationException($"A file at path '{path}' already exists");
|
||||
WithRetry(() => File.Delete(fullPath));
|
||||
}
|
||||
|
||||
var directory = Path.GetDirectoryName(fullPath);
|
||||
if (directory == null) throw new InvalidOperationException("Could not get directory.");
|
||||
Directory.CreateDirectory(directory); // ensure it exists
|
||||
|
||||
if (copy)
|
||||
WithRetry(() => File.Copy(physicalPath, fullPath));
|
||||
else
|
||||
WithRetry(() => File.Move(physicalPath, fullPath));
|
||||
}
|
||||
|
||||
#region Helper Methods
|
||||
|
||||
protected virtual void EnsureDirectory(string path)
|
||||
{
|
||||
path = GetFullPath(path);
|
||||
Directory.CreateDirectory(path);
|
||||
}
|
||||
|
||||
protected string EnsureTrailingSeparator(string path)
|
||||
{
|
||||
return path.EnsureEndsWith(Path.DirectorySeparatorChar);
|
||||
}
|
||||
|
||||
protected string EnsureDirectorySeparatorChar(string path)
|
||||
{
|
||||
path = path.Replace('/', Path.DirectorySeparatorChar);
|
||||
path = path.Replace('\\', Path.DirectorySeparatorChar);
|
||||
return path;
|
||||
}
|
||||
|
||||
protected string EnsureUrlSeparatorChar(string path)
|
||||
{
|
||||
path = path.Replace('\\', '/');
|
||||
return path;
|
||||
}
|
||||
|
||||
protected void WithRetry(Action action)
|
||||
{
|
||||
// 10 times 100ms is 1s
|
||||
const int count = 10;
|
||||
const int pausems = 100;
|
||||
|
||||
for (var i = 0;; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
action();
|
||||
break; // done
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
// if it's not *exactly* IOException then it could be
|
||||
// some inherited exception such as FileNotFoundException,
|
||||
// and then we don't want to retry
|
||||
if (e.GetType() != typeof(IOException)) throw;
|
||||
|
||||
// if we have tried enough, throw, else swallow
|
||||
// the exception and retry after a pause
|
||||
if (i == count) throw;
|
||||
}
|
||||
|
||||
Thread.Sleep(pausems);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,382 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Umbraco.Core.IO
|
||||
{
|
||||
internal class ShadowFileSystem : IFileSystem
|
||||
{
|
||||
private readonly IFileSystem _fs;
|
||||
private readonly IFileSystem _sfs;
|
||||
|
||||
public ShadowFileSystem(IFileSystem fs, IFileSystem sfs)
|
||||
{
|
||||
_fs = fs;
|
||||
_sfs = sfs;
|
||||
}
|
||||
|
||||
public IFileSystem Inner => _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)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_fs.CanAddPhysical)
|
||||
{
|
||||
_fs.AddFile(kvp.Key, _sfs.GetFullPath(kvp.Key)); // overwrite, move
|
||||
}
|
||||
else
|
||||
{
|
||||
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
|
||||
{
|
||||
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;
|
||||
|
||||
private Dictionary<string, ShadowNode> Nodes => _nodes ?? (_nodes = new Dictionary<string, ShadowNode>());
|
||||
|
||||
private class ShadowNode
|
||||
{
|
||||
public ShadowNode(bool isDelete, bool isdir)
|
||||
{
|
||||
IsDelete = isDelete;
|
||||
IsDir = isdir;
|
||||
}
|
||||
|
||||
public bool IsDelete { get; }
|
||||
public bool IsDir { get; }
|
||||
|
||||
public bool IsExist => IsDelete == false;
|
||||
public bool IsFile => 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<string> 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<string> GetFiles(string path)
|
||||
{
|
||||
return GetFiles(path, null);
|
||||
}
|
||||
|
||||
public IEnumerable<string> GetFiles(string path, string filter)
|
||||
{
|
||||
var normPath = NormPath(path);
|
||||
var shadows = Nodes.Where(kvp => IsChild(normPath, kvp.Key)).ToArray();
|
||||
var files = filter != null ? _fs.GetFiles(path, filter) : _fs.GetFiles(path);
|
||||
var wildcard = filter == null ? null : new WildcardExpression(filter);
|
||||
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 && (wildcard == null || wildcard.IsMatch(kvp.Key))).Select(kvp => kvp.Key))
|
||||
.Distinct();
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
ShadowNode sf;
|
||||
if (Nodes.TryGetValue(NormPath(path), out sf))
|
||||
return sf.IsDir || sf.IsDelete ? null : _sfs.GetFullPath(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)
|
||||
return _fs.GetSize(path);
|
||||
|
||||
if (sf.IsDelete || sf.IsDir) throw new InvalidOperationException("Invalid path.");
|
||||
return _sfs.GetSize(path);
|
||||
}
|
||||
|
||||
public bool CanAddPhysical { get { return true; } }
|
||||
|
||||
public void AddFile(string path, string physicalPath, bool overrideIfExists = true, bool copy = false)
|
||||
{
|
||||
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, physicalPath, overrideIfExists, copy);
|
||||
Nodes[normPath] = new ShadowNode(false, false);
|
||||
}
|
||||
|
||||
// copied from System.Web.Util.Wildcard internal
|
||||
internal class WildcardExpression
|
||||
{
|
||||
private readonly string _pattern;
|
||||
private readonly bool _caseInsensitive;
|
||||
private Regex _regex;
|
||||
|
||||
private static Regex metaRegex = new Regex("[\\+\\{\\\\\\[\\|\\(\\)\\.\\^\\$]");
|
||||
private static Regex questRegex = new Regex("\\?");
|
||||
private static Regex starRegex = new Regex("\\*");
|
||||
private static Regex commaRegex = new Regex(",");
|
||||
private static Regex slashRegex = new Regex("(?=/)");
|
||||
private static Regex backslashRegex = new Regex("(?=[\\\\:])");
|
||||
|
||||
public WildcardExpression(string pattern, bool caseInsensitive = true)
|
||||
{
|
||||
_pattern = pattern;
|
||||
_caseInsensitive = caseInsensitive;
|
||||
}
|
||||
|
||||
private void EnsureRegex(string pattern)
|
||||
{
|
||||
if (_regex != null) return;
|
||||
|
||||
var options = RegexOptions.None;
|
||||
|
||||
// match right-to-left (for speed) if the pattern starts with a *
|
||||
|
||||
if (pattern.Length > 0 && pattern[0] == '*')
|
||||
options = RegexOptions.RightToLeft | RegexOptions.Singleline;
|
||||
else
|
||||
options = RegexOptions.Singleline;
|
||||
|
||||
// case insensitivity
|
||||
|
||||
if (_caseInsensitive)
|
||||
options |= RegexOptions.IgnoreCase | RegexOptions.CultureInvariant;
|
||||
|
||||
// Remove regex metacharacters
|
||||
|
||||
pattern = metaRegex.Replace(pattern, "\\$0");
|
||||
|
||||
// Replace wildcard metacharacters with regex codes
|
||||
|
||||
pattern = questRegex.Replace(pattern, ".");
|
||||
pattern = starRegex.Replace(pattern, ".*");
|
||||
pattern = commaRegex.Replace(pattern, "\\z|\\A");
|
||||
|
||||
// anchor the pattern at beginning and end, and return the regex
|
||||
|
||||
_regex = new Regex("\\A" + pattern + "\\z", options);
|
||||
}
|
||||
|
||||
public bool IsMatch(string input)
|
||||
{
|
||||
EnsureRegex(_pattern);
|
||||
return _regex.IsMatch(input);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Umbraco.Core.IO
|
||||
{
|
||||
// shadow filesystems is definitively ... too convoluted
|
||||
|
||||
internal class ShadowFileSystems : ICompletable
|
||||
{
|
||||
private readonly FileSystems _fileSystems;
|
||||
private bool _completed;
|
||||
|
||||
// invoked by the filesystems when shadowing
|
||||
public ShadowFileSystems(FileSystems fileSystems, string id)
|
||||
{
|
||||
_fileSystems = fileSystems;
|
||||
Id = id;
|
||||
|
||||
_fileSystems.BeginShadow(id);
|
||||
}
|
||||
|
||||
// for tests
|
||||
public string Id { get; }
|
||||
|
||||
// invoked by the scope when exiting, if completed
|
||||
public void Complete()
|
||||
{
|
||||
_completed = true;
|
||||
}
|
||||
|
||||
// invoked by the scope when exiting
|
||||
public void Dispose()
|
||||
{
|
||||
_fileSystems.EndShadow(Id, _completed);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,216 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Umbraco.Core.Composing;
|
||||
|
||||
namespace Umbraco.Core.IO
|
||||
{
|
||||
internal class ShadowWrapper : IFileSystem
|
||||
{
|
||||
private static readonly string ShadowFsPath = Constants.SystemDirectories.TempData.EnsureEndsWith('/') + "ShadowFs";
|
||||
|
||||
private readonly Func<bool> _isScoped;
|
||||
private readonly IFileSystem _innerFileSystem;
|
||||
private readonly string _shadowPath;
|
||||
private ShadowFileSystem _shadowFileSystem;
|
||||
private string _shadowDir;
|
||||
|
||||
public ShadowWrapper(IFileSystem innerFileSystem, string shadowPath, Func<bool> isScoped = null)
|
||||
{
|
||||
_innerFileSystem = innerFileSystem;
|
||||
_shadowPath = shadowPath;
|
||||
_isScoped = isScoped;
|
||||
}
|
||||
|
||||
public static string CreateShadowId()
|
||||
{
|
||||
const int retries = 50; // avoid infinite loop
|
||||
const int idLength = 8; // 6 chars
|
||||
|
||||
// shorten a Guid to idLength chars, and see whether it collides
|
||||
// with an existing directory or not - if it does, try again, and
|
||||
// we should end up with a unique identifier eventually - but just
|
||||
// detect infinite loops (just in case)
|
||||
|
||||
for (var i = 0; i < retries; i++)
|
||||
{
|
||||
var id = GuidUtils.ToBase32String(Guid.NewGuid(), idLength);
|
||||
|
||||
var virt = ShadowFsPath + "/" + id;
|
||||
var shadowDir = Current.IOHelper.MapPath(virt);
|
||||
if (Directory.Exists(shadowDir))
|
||||
continue;
|
||||
|
||||
Directory.CreateDirectory(shadowDir);
|
||||
return id;
|
||||
}
|
||||
|
||||
throw new Exception($"Could not get a shadow identifier (tried {retries} times)");
|
||||
}
|
||||
|
||||
internal void Shadow(string 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 = ShadowFsPath + "/" + id + "/" + _shadowPath;
|
||||
_shadowDir = Current.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);
|
||||
|
||||
// shadowPath make be path/to/dir, remove each
|
||||
dir = dir.Replace("/", "\\");
|
||||
var min = Current.IOHelper.MapPath(ShadowFsPath).Length;
|
||||
var pos = dir.LastIndexOf("\\", StringComparison.OrdinalIgnoreCase);
|
||||
while (pos > min)
|
||||
{
|
||||
dir = dir.Substring(0, pos);
|
||||
if (Directory.EnumerateFileSystemEntries(dir).Any() == false)
|
||||
Directory.Delete(dir, true);
|
||||
else
|
||||
break;
|
||||
pos = dir.LastIndexOf("\\", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ugly, isn't it? but if we cannot cleanup, bah, just leave it there
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IFileSystem InnerFileSystem => _innerFileSystem;
|
||||
|
||||
private IFileSystem FileSystem
|
||||
{
|
||||
get
|
||||
{
|
||||
var isScoped = _isScoped();
|
||||
|
||||
// if the filesystem is created *after* shadowing starts, it won't be shadowing
|
||||
// better not ignore that situation and raised a meaningful (?) exception
|
||||
if (isScoped && _shadowFileSystem == null)
|
||||
throw new Exception("The filesystems are shadowing, but this filesystem is not.");
|
||||
|
||||
return isScoped
|
||||
? _shadowFileSystem
|
||||
: _innerFileSystem;
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
return FileSystem.GetSize(path);
|
||||
}
|
||||
|
||||
public bool CanAddPhysical => FileSystem.CanAddPhysical;
|
||||
|
||||
public void AddFile(string path, string physicalPath, bool overrideIfExists = true, bool copy = false)
|
||||
{
|
||||
FileSystem.AddFile(path, physicalPath, overrideIfExists, copy);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
using Umbraco.Core.Composing;
|
||||
|
||||
namespace Umbraco.Core.IO
|
||||
{
|
||||
public class SupportingFileSystems : TargetedServiceFactory<IFileSystem>
|
||||
{
|
||||
public SupportingFileSystems(IFactory factory)
|
||||
: base(factory)
|
||||
{ }
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
using System.Web;
|
||||
using Umbraco.Core.Composing;
|
||||
|
||||
namespace Umbraco.Core.IO
|
||||
{
|
||||
//all paths has a starting but no trailing /
|
||||
public class SystemDirectories
|
||||
{
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
using System.IO;
|
||||
using Umbraco.Core.Composing;
|
||||
using Umbraco.Core.Configuration;
|
||||
|
||||
namespace Umbraco.Core.IO
|
||||
{
|
||||
public class SystemFiles
|
||||
{
|
||||
public static string TinyMceConfig => Constants.SystemDirectories.Config + "/tinyMceConfig.config";
|
||||
|
||||
// TODO: Kill this off we don't have umbraco.config XML cache we now have NuCache
|
||||
public static string GetContentCacheXml(IGlobalSettings globalSettings)
|
||||
{
|
||||
return Path.Combine(globalSettings.LocalTempPath, "umbraco.config");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,168 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Umbraco.Core.Models;
|
||||
|
||||
namespace Umbraco.Core.IO
|
||||
{
|
||||
public class ViewHelper
|
||||
{
|
||||
private readonly IFileSystem _viewFileSystem;
|
||||
|
||||
public ViewHelper(IFileSystem viewFileSystem)
|
||||
{
|
||||
if (viewFileSystem == null) throw new ArgumentNullException(nameof(viewFileSystem));
|
||||
_viewFileSystem = viewFileSystem;
|
||||
}
|
||||
|
||||
internal bool ViewExists(ITemplate t)
|
||||
{
|
||||
return _viewFileSystem.FileExists(ViewPath(t.Alias));
|
||||
}
|
||||
|
||||
internal string GetFileContents(ITemplate t)
|
||||
{
|
||||
var viewContent = "";
|
||||
var path = ViewPath(t.Alias);
|
||||
|
||||
if (_viewFileSystem.FileExists(path))
|
||||
{
|
||||
using (var tr = new StreamReader(_viewFileSystem.OpenFile(path)))
|
||||
{
|
||||
viewContent = tr.ReadToEnd();
|
||||
tr.Close();
|
||||
}
|
||||
}
|
||||
|
||||
return viewContent;
|
||||
}
|
||||
|
||||
public string CreateView(ITemplate t, bool overWrite = false)
|
||||
{
|
||||
string viewContent;
|
||||
var path = ViewPath(t.Alias);
|
||||
|
||||
if (_viewFileSystem.FileExists(path) == false || overWrite)
|
||||
{
|
||||
viewContent = SaveTemplateToFile(t);
|
||||
}
|
||||
else
|
||||
{
|
||||
using (var tr = new StreamReader(_viewFileSystem.OpenFile(path)))
|
||||
{
|
||||
viewContent = tr.ReadToEnd();
|
||||
tr.Close();
|
||||
}
|
||||
}
|
||||
|
||||
return viewContent;
|
||||
}
|
||||
|
||||
public static string GetDefaultFileContent(string layoutPageAlias = null, string modelClassName = null, string modelNamespace = null, string modelNamespaceAlias = null)
|
||||
{
|
||||
var content = new StringBuilder();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(modelNamespaceAlias))
|
||||
modelNamespaceAlias = "ContentModels";
|
||||
|
||||
// either
|
||||
// @inherits Umbraco.Web.Mvc.UmbracoViewPage
|
||||
// @inherits Umbraco.Web.Mvc.UmbracoViewPage<ModelClass>
|
||||
content.Append("@inherits Umbraco.Web.Mvc.UmbracoViewPage");
|
||||
if (modelClassName.IsNullOrWhiteSpace() == false)
|
||||
{
|
||||
content.Append("<");
|
||||
if (modelNamespace.IsNullOrWhiteSpace() == false)
|
||||
{
|
||||
content.Append(modelNamespaceAlias);
|
||||
content.Append(".");
|
||||
}
|
||||
content.Append(modelClassName);
|
||||
content.Append(">");
|
||||
}
|
||||
content.Append("\r\n");
|
||||
|
||||
// if required, add
|
||||
// @using ContentModels = ModelNamespace;
|
||||
if (modelClassName.IsNullOrWhiteSpace() == false && modelNamespace.IsNullOrWhiteSpace() == false)
|
||||
{
|
||||
content.Append("@using ");
|
||||
content.Append(modelNamespaceAlias);
|
||||
content.Append(" = ");
|
||||
content.Append(modelNamespace);
|
||||
content.Append(";\r\n");
|
||||
}
|
||||
|
||||
// either
|
||||
// Layout = null;
|
||||
// Layout = "layoutPage.cshtml";
|
||||
content.Append("@{\r\n\tLayout = ");
|
||||
if (layoutPageAlias.IsNullOrWhiteSpace())
|
||||
{
|
||||
content.Append("null");
|
||||
}
|
||||
else
|
||||
{
|
||||
content.Append("\"");
|
||||
content.Append(layoutPageAlias);
|
||||
content.Append(".cshtml\"");
|
||||
}
|
||||
content.Append(";\r\n}");
|
||||
return content.ToString();
|
||||
}
|
||||
|
||||
private string SaveTemplateToFile(ITemplate template)
|
||||
{
|
||||
var design = template.Content.IsNullOrWhiteSpace() ? EnsureInheritedLayout(template) : template.Content;
|
||||
var path = ViewPath(template.Alias);
|
||||
|
||||
var data = Encoding.UTF8.GetBytes(design);
|
||||
var withBom = Encoding.UTF8.GetPreamble().Concat(data).ToArray();
|
||||
|
||||
using (var ms = new MemoryStream(withBom))
|
||||
{
|
||||
_viewFileSystem.AddFile(path, ms, true);
|
||||
}
|
||||
|
||||
return design;
|
||||
}
|
||||
|
||||
public string UpdateViewFile(ITemplate t, string currentAlias = null)
|
||||
{
|
||||
var path = ViewPath(t.Alias);
|
||||
|
||||
if (string.IsNullOrEmpty(currentAlias) == false && currentAlias != t.Alias)
|
||||
{
|
||||
//then kill the old file..
|
||||
var oldFile = ViewPath(currentAlias);
|
||||
if (_viewFileSystem.FileExists(oldFile))
|
||||
_viewFileSystem.DeleteFile(oldFile);
|
||||
}
|
||||
|
||||
var data = Encoding.UTF8.GetBytes(t.Content);
|
||||
var withBom = Encoding.UTF8.GetPreamble().Concat(data).ToArray();
|
||||
|
||||
using (var ms = new MemoryStream(withBom))
|
||||
{
|
||||
_viewFileSystem.AddFile(path, ms, true);
|
||||
}
|
||||
return t.Content;
|
||||
}
|
||||
|
||||
public string ViewPath(string alias)
|
||||
{
|
||||
return _viewFileSystem.GetRelativePath(alias.Replace(" ", "") + ".cshtml");
|
||||
}
|
||||
|
||||
private static string EnsureInheritedLayout(ITemplate template)
|
||||
{
|
||||
var design = template.Content;
|
||||
|
||||
if (string.IsNullOrEmpty(design))
|
||||
design = GetDefaultFileContent(template.MasterTemplateAlias);
|
||||
|
||||
return design;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user