AB3734 - Moved alot of stuff from Umbraco.Core.IO into abstractions

This commit is contained in:
Bjarke Berg
2019-11-19 07:52:40 +01:00
parent cd3a97ce75
commit 4f204543e6
37 changed files with 148 additions and 133 deletions

View File

@@ -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)
{
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,11 +0,0 @@
using Umbraco.Core.Composing;
namespace Umbraco.Core.IO
{
public class SupportingFileSystems : TargetedServiceFactory<IFileSystem>
{
public SupportingFileSystems(IFactory factory)
: base(factory)
{ }
}
}

View File

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

View File

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

View File

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