merge v10 to v11

This commit is contained in:
Bjarke Berg
2022-08-18 14:38:28 +02:00
4076 changed files with 320268 additions and 303657 deletions

View File

@@ -1,49 +1,33 @@
using System;
using System.Collections.Generic;
using System.IO;
namespace Umbraco.Cms.Core.IO;
namespace Umbraco.Cms.Core.IO
public class CleanFolderResult
{
public class CleanFolderResult
private CleanFolderResult()
{
private CleanFolderResult()
}
public CleanFolderResultStatus Status { get; private set; }
public IReadOnlyCollection<Error>? Errors { get; private set; }
public static CleanFolderResult Success() => new CleanFolderResult { Status = CleanFolderResultStatus.Success };
public static CleanFolderResult FailedAsDoesNotExist() =>
new CleanFolderResult { Status = CleanFolderResultStatus.FailedAsDoesNotExist };
public static CleanFolderResult FailedWithErrors(List<Error> errors) =>
new CleanFolderResult { Status = CleanFolderResultStatus.FailedWithException, Errors = errors.AsReadOnly() };
public class Error
{
public Error(Exception exception, FileInfo erroringFile)
{
Exception = exception;
ErroringFile = erroringFile;
}
public CleanFolderResultStatus Status { get; private set; }
public Exception Exception { get; set; }
public IReadOnlyCollection<Error>? Errors { get; private set; }
public static CleanFolderResult Success()
{
return new CleanFolderResult { Status = CleanFolderResultStatus.Success };
}
public static CleanFolderResult FailedAsDoesNotExist()
{
return new CleanFolderResult { Status = CleanFolderResultStatus.FailedAsDoesNotExist };
}
public static CleanFolderResult FailedWithErrors(List<Error> errors)
{
return new CleanFolderResult
{
Status = CleanFolderResultStatus.FailedWithException,
Errors = errors.AsReadOnly(),
};
}
public class Error
{
public Error(Exception exception, FileInfo erroringFile)
{
Exception = exception;
ErroringFile = erroringFile;
}
public Exception Exception { get; set; }
public FileInfo ErroringFile { get; set; }
}
public FileInfo ErroringFile { get; set; }
}
}

View File

@@ -1,9 +1,8 @@
namespace Umbraco.Cms.Core.IO
namespace Umbraco.Cms.Core.IO;
public enum CleanFolderResultStatus
{
public enum CleanFolderResultStatus
{
Success,
FailedAsDoesNotExist,
FailedWithException
}
Success,
FailedAsDoesNotExist,
FailedWithException,
}

View File

@@ -1,62 +1,66 @@
using System.Text;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.IO
namespace Umbraco.Cms.Core.IO;
public class DefaultViewContentProvider : IDefaultViewContentProvider
{
public class DefaultViewContentProvider : IDefaultViewContentProvider
public string GetDefaultFileContent(string? layoutPageAlias = null, string? modelClassName = null, string? modelNamespace = null, string? modelNamespaceAlias = null)
{
public string GetDefaultFileContent(string? layoutPageAlias = null, string? modelClassName = null, string? modelNamespace = null, string? modelNamespaceAlias = null)
var content = new StringBuilder();
if (string.IsNullOrWhiteSpace(modelNamespaceAlias))
{
var content = new StringBuilder();
if (string.IsNullOrWhiteSpace(modelNamespaceAlias))
modelNamespaceAlias = "ContentModels";
// either
// @inherits Umbraco.Web.Mvc.UmbracoViewPage
// @inherits Umbraco.Web.Mvc.UmbracoViewPage<ModelClass>
content.AppendLine("@using Umbraco.Cms.Web.Common.PublishedModels;");
content.Append("@inherits Umbraco.Cms.Web.Common.Views.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();
modelNamespaceAlias = "ContentModels";
}
// either
// @inherits Umbraco.Web.Mvc.UmbracoViewPage
// @inherits Umbraco.Web.Mvc.UmbracoViewPage<ModelClass>
content.AppendLine("@using Umbraco.Cms.Web.Common.PublishedModels;");
content.Append("@inherits Umbraco.Cms.Web.Common.Views.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();
}
}

View File

@@ -1,112 +1,109 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using Microsoft.Extensions.FileProviders;
using Umbraco.Cms.Core.IO;
namespace Umbraco.Extensions
namespace Umbraco.Extensions;
public static class FileSystemExtensions
{
public static class FileSystemExtensions
public static string GetStreamHash(this Stream fileStream)
{
public static string GetStreamHash(this Stream fileStream)
if (fileStream.CanSeek)
{
if (fileStream.CanSeek)
{
fileStream.Seek(0, SeekOrigin.Begin);
}
using HashAlgorithm alg = SHA1.Create();
// create a string output for the hash
var stringBuilder = new StringBuilder();
var hashedByteArray = alg.ComputeHash(fileStream);
foreach (var b in hashedByteArray)
{
stringBuilder.Append(b.ToString("x2"));
}
return stringBuilder.ToString();
fileStream.Seek(0, SeekOrigin.Begin);
}
/// <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;
using HashAlgorithm alg = SHA1.Create();
while (retries > 0)
// create a string output for the hash
var stringBuilder = new StringBuilder();
var hashedByteArray = alg.ComputeHash(fileStream);
foreach (var b in hashedByteArray)
{
stringBuilder.Append(b.ToString("x2"));
}
return stringBuilder.ToString();
}
/// <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
{
try
return File.OpenRead(file.FullName);
}
catch (IOException)
{
retries--;
if (retries == 0)
{
return File.OpenRead(file.FullName);
throw;
}
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 (Stream stream = fs.OpenFile(path))
{
fs.AddFile(newPath, stream);
Thread.Sleep(sleepPerRetryInMilliseconds);
}
}
public static string GetExtension(this IFileSystem fs, string path)
{
return Path.GetExtension(fs.GetFullPath(path));
}
throw new ArgumentException("Retries must be greater than zero");
}
public static string GetFileName(this IFileSystem fs, string path)
public static void CopyFile(this IFileSystem fs, string path, string newPath)
{
using (Stream stream = fs.OpenFile(path))
{
return Path.GetFileName(fs.GetFullPath(path));
}
// TODO: Currently this is the only way to do this
public 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>
/// Creates a new <see cref="IFileProvider" /> from the file system.
/// </summary>
/// <param name="fileSystem">The file system.</param>
/// <param name="fileProvider">When this method returns, contains an <see cref="IFileProvider"/> created from the file system.</param>
/// <returns>
/// <c>true</c> if the <see cref="IFileProvider" /> was successfully created; otherwise, <c>false</c>.
/// </returns>
public static bool TryCreateFileProvider(this IFileSystem fileSystem, [MaybeNullWhen(false)] out IFileProvider fileProvider)
{
fileProvider = fileSystem switch
{
IFileProviderFactory fileProviderFactory => fileProviderFactory.Create(),
_ => null
};
return fileProvider != null;
fs.AddFile(newPath, stream);
}
}
public static string GetExtension(this IFileSystem fs, string path) => Path.GetExtension(fs.GetFullPath(path));
public static string GetFileName(this IFileSystem fs, string path) => Path.GetFileName(fs.GetFullPath(path));
// TODO: Currently this is the only way to do this
public 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>
/// Creates a new <see cref="IFileProvider" /> from the file system.
/// </summary>
/// <param name="fileSystem">The file system.</param>
/// <param name="fileProvider">
/// When this method returns, contains an <see cref="IFileProvider" /> created from the file
/// system.
/// </param>
/// <returns>
/// <c>true</c> if the <see cref="IFileProvider" /> was successfully created; otherwise, <c>false</c>.
/// </returns>
public static bool TryCreateFileProvider(
this IFileSystem fileSystem,
[MaybeNullWhen(false)] out IFileProvider fileProvider)
{
fileProvider = fileSystem switch
{
IFileProviderFactory fileProviderFactory => fileProviderFactory.Create(),
_ => null,
};
return fileProvider != null;
}
}

View File

@@ -1,7 +1,4 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Threading;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
@@ -29,13 +26,13 @@ namespace Umbraco.Cms.Core.IO
private ShadowWrapper? _mvcViewsFileSystem;
// well-known file systems lazy initialization
private object _wkfsLock = new object();
private object _wkfsLock = new();
private bool _wkfsInitialized;
private object? _wkfsObject; // unused
// shadow support
private readonly List<ShadowWrapper> _shadowWrappers = new List<ShadowWrapper>();
private readonly object _shadowLocker = new object();
private readonly List<ShadowWrapper> _shadowWrappers = new();
private readonly object _shadowLocker = new();
private static string? _shadowCurrentId; // static - unique!!
#region Constructor
@@ -193,7 +190,7 @@ namespace Umbraco.Cms.Core.IO
// to the VirtualPath we get with CodeFileDisplay from the frontend.
try
{
var rootPath = fileSystem.GetFullPath("/css/");
fileSystem.GetFullPath("/css/");
}
catch (UnauthorizedAccessException exception)
{
@@ -201,7 +198,8 @@ namespace Umbraco.Cms.Core.IO
"Can't register the stylesheet filesystem, "
+ "this is most likely caused by using a PhysicalFileSystem with an incorrect "
+ "rootPath/rootUrl. RootPath must be <installation folder>\\wwwroot\\css"
+ " and rootUrl must be /css", exception);
+ " and rootUrl must be /css",
exception);
}
_stylesheetsFileSystem = CreateShadowWrapperInternal(fileSystem, "css");
@@ -213,7 +211,7 @@ namespace Umbraco.Cms.Core.IO
// but it does not really matter what we return - here, null
private object? CreateWellKnownFileSystems()
{
var logger = _loggerFactory.CreateLogger<PhysicalFileSystem>();
ILogger<PhysicalFileSystem> logger = _loggerFactory.CreateLogger<PhysicalFileSystem>();
//TODO this is fucked, why do PhysicalFileSystem has a root url? Mvc views cannot be accessed by url!
var macroPartialFileSystem = new PhysicalFileSystem(_ioHelper, _hostingEnvironment, logger, _hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.MacroPartials), _hostingEnvironment.ToAbsolute(Constants.SystemDirectories.MacroPartials));
@@ -228,7 +226,10 @@ namespace Umbraco.Cms.Core.IO
if (_stylesheetsFileSystem == null)
{
var stylesheetsFileSystem = new PhysicalFileSystem(_ioHelper, _hostingEnvironment, logger,
var stylesheetsFileSystem = new PhysicalFileSystem(
_ioHelper,
_hostingEnvironment,
logger,
_hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoCssPath),
_hostingEnvironment.ToAbsolute(_globalSettings.UmbracoCssPath));

View File

@@ -1,8 +1,6 @@
namespace Umbraco.Cms.Core.IO
namespace Umbraco.Cms.Core.IO;
public interface IDefaultViewContentProvider
{
public interface IDefaultViewContentProvider
{
string GetDefaultFileContent(string? layoutPageAlias = null, string? modelClassName = null,
string? modelNamespace = null, string? modelNamespaceAlias = null);
}
string GetDefaultFileContent(string? layoutPageAlias = null, string? modelClassName = null, string? modelNamespace = null, string? modelNamespaceAlias = null);
}

View File

@@ -1,18 +1,17 @@
using Microsoft.Extensions.FileProviders;
namespace Umbraco.Cms.Core.IO
namespace Umbraco.Cms.Core.IO;
/// <summary>
/// Factory for creating <see cref="IFileProvider" /> instances.
/// </summary>
public interface IFileProviderFactory
{
/// <summary>
/// Factory for creating <see cref="IFileProvider" /> instances.
/// Creates a new <see cref="IFileProvider" /> instance.
/// </summary>
public interface IFileProviderFactory
{
/// <summary>
/// Creates a new <see cref="IFileProvider" /> instance.
/// </summary>
/// <returns>
/// The newly created <see cref="IFileProvider" /> instance (or <c>null</c> if not supported).
/// </returns>
IFileProvider? Create();
}
/// <returns>
/// The newly created <see cref="IFileProvider" /> instance (or <c>null</c> if not supported).
/// </returns>
IFileProvider? Create();
}

View File

@@ -1,178 +1,177 @@
using System;
using System.Collections.Generic;
using System.IO;
namespace Umbraco.Cms.Core.IO;
namespace Umbraco.Cms.Core.IO
/// <summary>
/// Provides methods allowing the manipulation of files.
/// </summary>
public interface IFileSystem
{
/// <summary>
/// Provides methods allowing the manipulation of files.
/// Gets a value indicating whether the filesystem can add/copy
/// a file which is on a physical filesystem.
/// </summary>
public interface IFileSystem
{
/// <summary>
/// Gets all directories matching the given path.
/// </summary>
/// <param name="path">The path to the directories.</param>
/// <returns>
/// The <see cref="IEnumerable{String}"/> representing the matched directories.
/// </returns>
IEnumerable<string> GetDirectories(string path);
/// <remarks>
/// In other words, whether the filesystem can copy/move a file
/// that is on local disk, in a fast and efficient way.
/// </remarks>
bool CanAddPhysical { get; }
/// <summary>
/// Deletes the specified directory.
/// </summary>
/// <param name="path">The name of the directory to remove.</param>
void DeleteDirectory(string path);
/// <summary>
/// Gets all directories matching the given path.
/// </summary>
/// <param name="path">The path to the directories.</param>
/// <returns>
/// The <see cref="IEnumerable{String}" /> representing the matched directories.
/// </returns>
IEnumerable<string> GetDirectories(string path);
/// <summary>
/// Deletes the specified directory and, if indicated, any subdirectories and files in the directory.
/// </summary>
/// <remarks>Azure blob storage has no real concept of directories so deletion is always recursive.</remarks>
/// <param name="path">The name of the directory to remove.</param>
/// <param name="recursive">Whether to remove directories, subdirectories, and files in path.</param>
void DeleteDirectory(string path, bool recursive);
/// <summary>
/// Deletes the specified directory.
/// </summary>
/// <param name="path">The name of the directory to remove.</param>
void DeleteDirectory(string path);
/// <summary>
/// Determines whether the specified directory exists.
/// </summary>
/// <param name="path">The directory to check.</param>
/// <returns>
/// <c>True</c> if the directory exists and the user has permission to view it; otherwise <c>false</c>.
/// </returns>
bool DirectoryExists(string path);
/// <summary>
/// Deletes the specified directory and, if indicated, any subdirectories and files in the directory.
/// </summary>
/// <remarks>Azure blob storage has no real concept of directories so deletion is always recursive.</remarks>
/// <param name="path">The name of the directory to remove.</param>
/// <param name="recursive">Whether to remove directories, subdirectories, and files in path.</param>
void DeleteDirectory(string path, bool recursive);
/// <summary>
/// Adds a file to the file system.
/// </summary>
/// <param name="path">The path to the given file.</param>
/// <param name="stream">The <see cref="Stream"/> containing the file contents.</param>
void AddFile(string path, Stream stream);
/// <summary>
/// Determines whether the specified directory exists.
/// </summary>
/// <param name="path">The directory to check.</param>
/// <returns>
/// <c>True</c> if the directory exists and the user has permission to view it; otherwise <c>false</c>.
/// </returns>
bool DirectoryExists(string path);
/// <summary>
/// Adds a file to the file system.
/// </summary>
/// <param name="path">The path to the given file.</param>
/// <param name="stream">The <see cref="Stream"/> containing the file contents.</param>
/// <param name="overrideIfExists">Whether to override the file if it already exists.</param>
void AddFile(string path, Stream stream, bool overrideIfExists);
/// <summary>
/// Adds a file to the file system.
/// </summary>
/// <param name="path">The path to the given file.</param>
/// <param name="stream">The <see cref="Stream" /> containing the file contents.</param>
void AddFile(string path, Stream stream);
/// <summary>
/// Gets all files matching the given path.
/// </summary>
/// <param name="path">The path to the files.</param>
/// <returns>
/// The <see cref="IEnumerable{String}"/> representing the matched files.
/// </returns>
IEnumerable<string> GetFiles(string path);
/// <summary>
/// Adds a file to the file system.
/// </summary>
/// <param name="path">The path to the given file.</param>
/// <param name="stream">The <see cref="Stream" /> containing the file contents.</param>
/// <param name="overrideIfExists">Whether to override the file if it already exists.</param>
void AddFile(string path, Stream stream, bool overrideIfExists);
/// <summary>
/// Gets all files matching the given path and filter.
/// </summary>
/// <param name="path">The path to the files.</param>
/// <param name="filter">A filter that allows the querying of file extension. <example>*.jpg</example></param>
/// <returns>
/// The <see cref="IEnumerable{String}"/> representing the matched files.
/// </returns>
IEnumerable<string> GetFiles(string path, string filter);
/// <summary>
/// Gets all files matching the given path.
/// </summary>
/// <param name="path">The path to the files.</param>
/// <returns>
/// The <see cref="IEnumerable{String}" /> representing the matched files.
/// </returns>
IEnumerable<string> GetFiles(string path);
/// <summary>
/// Gets a <see cref="Stream"/> representing the file at the given path.
/// </summary>
/// <param name="path">The path to the file.</param>
/// <returns>
/// <see cref="Stream"/>.
/// </returns>
Stream OpenFile(string path);
/// <summary>
/// Gets all files matching the given path and filter.
/// </summary>
/// <param name="path">The path to the files.</param>
/// <param name="filter">A filter that allows the querying of file extension.
/// <example>*.jpg</example>
/// </param>
/// <returns>
/// The <see cref="IEnumerable{String}" /> representing the matched files.
/// </returns>
IEnumerable<string> GetFiles(string path, string filter);
/// <summary>
/// Deletes the specified file.
/// </summary>
/// <param name="path">The name of the file to remove.</param>
void DeleteFile(string path);
/// <summary>
/// Gets a <see cref="Stream" /> representing the file at the given path.
/// </summary>
/// <param name="path">The path to the file.</param>
/// <returns>
/// <see cref="Stream" />.
/// </returns>
Stream OpenFile(string path);
/// <summary>
/// Determines whether the specified file exists.
/// </summary>
/// <param name="path">The file to check.</param>
/// <returns>
/// <c>True</c> if the file exists and the user has permission to view it; otherwise <c>false</c>.
/// </returns>
bool FileExists(string path);
/// <summary>
/// Deletes the specified file.
/// </summary>
/// <param name="path">The name of the file to remove.</param>
void DeleteFile(string path);
/// <summary>
/// Returns the application relative path to the file.
/// </summary>
/// <param name="fullPathOrUrl">The full path or URL.</param>
/// <returns>
/// The <see cref="string"/> representing the relative path.
/// </returns>
string GetRelativePath(string fullPathOrUrl);
/// <summary>
/// Determines whether the specified file exists.
/// </summary>
/// <param name="path">The file to check.</param>
/// <returns>
/// <c>True</c> if the file exists and the user has permission to view it; otherwise <c>false</c>.
/// </returns>
bool FileExists(string path);
/// <summary>
/// Gets the full qualified path to the file.
/// </summary>
/// <param name="path">The file to return the full path for.</param>
/// <returns>
/// The <see cref="string"/> representing the full path.
/// </returns>
string GetFullPath(string path);
/// <summary>
/// Returns the application relative path to the file.
/// </summary>
/// <param name="fullPathOrUrl">The full path or URL.</param>
/// <returns>
/// The <see cref="string" /> representing the relative path.
/// </returns>
string GetRelativePath(string fullPathOrUrl);
/// <summary>
/// Returns the application relative URL to the file.
/// </summary>
/// <param name="path">The path to return the URL for.</param>
/// <returns>
/// <see cref="string"/> representing the relative URL.
/// </returns>
string GetUrl(string? path);
/// <summary>
/// Gets the full qualified path to the file.
/// </summary>
/// <param name="path">The file to return the full path for.</param>
/// <returns>
/// The <see cref="string" /> representing the full path.
/// </returns>
string GetFullPath(string path);
/// <summary>
/// Gets the last modified date/time of the file, expressed as a UTC value.
/// </summary>
/// <param name="path">The path to the file.</param>
/// <returns>
/// <see cref="DateTimeOffset"/>.
/// </returns>
DateTimeOffset GetLastModified(string path);
/// <summary>
/// Returns the application relative URL to the file.
/// </summary>
/// <param name="path">The path to return the URL for.</param>
/// <returns>
/// <see cref="string" /> representing the relative URL.
/// </returns>
string GetUrl(string? path);
/// <summary>
/// Gets the created date/time of the file, expressed as a UTC value.
/// </summary>
/// <param name="path">The path to the file.</param>
/// <returns>
/// <see cref="DateTimeOffset"/>.
/// </returns>
DateTimeOffset GetCreated(string path);
/// <summary>
/// Gets the last modified date/time of the file, expressed as a UTC value.
/// </summary>
/// <param name="path">The path to the file.</param>
/// <returns>
/// <see cref="DateTimeOffset" />.
/// </returns>
DateTimeOffset GetLastModified(string path);
/// <summary>
/// Gets the size of a file.
/// </summary>
/// <param name="path">The path to the file.</param>
/// <returns>The size (in bytes) of the file.</returns>
long GetSize(string path);
/// <summary>
/// Gets the created date/time of the file, expressed as a UTC value.
/// </summary>
/// <param name="path">The path to the file.</param>
/// <returns>
/// <see cref="DateTimeOffset" />.
/// </returns>
DateTimeOffset GetCreated(string path);
/// <summary>
/// Gets a value indicating whether the filesystem can add/copy
/// a file which is on a physical filesystem.
/// </summary>
/// <remarks>In other words, whether the filesystem can copy/move a file
/// that is on local disk, in a fast and efficient way.</remarks>
bool CanAddPhysical { get; }
/// <summary>
/// Gets the size of a file.
/// </summary>
/// <param name="path">The path to the file.</param>
/// <returns>The size (in bytes) of the file.</returns>
long GetSize(string path);
/// <summary>
/// Adds a file which is on a physical filesystem.
/// </summary>
/// <param name="path">The path to the file.</param>
/// <param name="physicalPath">The absolute physical path to the source file.</param>
/// <param name="overrideIfExists">A value indicating what to do if the file already exists.</param>
/// <param name="copy">A value indicating whether to move (default) or copy.</param>
void AddFile(string path, string physicalPath, bool overrideIfExists = true, bool copy = false);
/// <summary>
/// Adds a file which is on a physical filesystem.
/// </summary>
/// <param name="path">The path to the file.</param>
/// <param name="physicalPath">The absolute physical path to the source file.</param>
/// <param name="overrideIfExists">A value indicating what to do if the file already exists.</param>
/// <param name="copy">A value indicating whether to move (default) or copy.</param>
void AddFile(string path, string physicalPath, bool overrideIfExists = true, bool copy = false);
// TODO: implement these
//
//void CreateDirectory(string path);
//
//// move or rename, directory or file
//void Move(string source, string target);
}
// TODO: implement these
//
// void CreateDirectory(string path);
//
//// move or rename, directory or file
// void Move(string source, string target);
}

View File

@@ -1,73 +1,68 @@
using System;
using System.Collections.Generic;
using System.IO;
namespace Umbraco.Cms.Core.IO;
namespace Umbraco.Cms.Core.IO
public interface IIOHelper
{
public interface IIOHelper
{
string FindFile(string virtualPath);
string FindFile(string virtualPath);
[Obsolete("Use IHostingEnvironment.ToAbsolute instead")]
string ResolveUrl(string virtualPath);
[Obsolete("Use IHostingEnvironment.ToAbsolute instead")]
string ResolveUrl(string virtualPath);
/// <summary>
/// Maps a virtual path to a physical path in the content root folder (i.e. www)
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
[Obsolete("Use IHostingEnvironment.MapPathContentRoot or IHostingEnvironment.MapPathWebRoot instead")]
string MapPath(string path);
/// <summary>
/// Maps a virtual path to a physical path in the content root folder (i.e. www)
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
[Obsolete("Use IHostingEnvironment.MapPathContentRoot or IHostingEnvironment.MapPathWebRoot instead")]
string MapPath(string path);
/// <summary>
/// Verifies that the current filepath matches a directory where the user is allowed to edit a file.
/// </summary>
/// <param name="filePath">The filepath to validate.</param>
/// <param name="validDir">The valid directory.</param>
/// <returns>A value indicating whether the filepath is valid.</returns>
bool VerifyEditPath(string filePath, string validDir);
/// <summary>
/// Verifies that the current filepath matches a directory where the user is allowed to edit a file.
/// </summary>
/// <param name="filePath">The filepath to validate.</param>
/// <param name="validDir">The valid directory.</param>
/// <returns>A value indicating whether the filepath is valid.</returns>
bool VerifyEditPath(string filePath, string validDir);
/// <summary>
/// Verifies that the current filepath matches one of several directories where the user is allowed to edit a file.
/// </summary>
/// <param name="filePath">The filepath to validate.</param>
/// <param name="validDirs">The valid directories.</param>
/// <returns>A value indicating whether the filepath is valid.</returns>
bool VerifyEditPath(string filePath, IEnumerable<string> validDirs);
/// <summary>
/// Verifies that the current filepath matches one of several directories where the user is allowed to edit a file.
/// </summary>
/// <param name="filePath">The filepath to validate.</param>
/// <param name="validDirs">The valid directories.</param>
/// <returns>A value indicating whether the filepath is valid.</returns>
bool VerifyEditPath(string filePath, IEnumerable<string> validDirs);
/// <summary>
/// Verifies that the current filepath has one of several authorized extensions.
/// </summary>
/// <param name="filePath">The filepath to validate.</param>
/// <param name="validFileExtensions">The valid extensions.</param>
/// <returns>A value indicating whether the filepath is valid.</returns>
bool VerifyFileExtension(string filePath, IEnumerable<string> validFileExtensions);
/// <summary>
/// Verifies that the current filepath has one of several authorized extensions.
/// </summary>
/// <param name="filePath">The filepath to validate.</param>
/// <param name="validFileExtensions">The valid extensions.</param>
/// <returns>A value indicating whether the filepath is valid.</returns>
bool VerifyFileExtension(string filePath, IEnumerable<string> validFileExtensions);
bool PathStartsWith(string path, string root, params char[] separators);
bool PathStartsWith(string path, string root, params char[] separators);
void EnsurePathExists(string path);
void EnsurePathExists(string path);
/// <summary>
/// Get properly formatted relative path from an existing absolute or relative path
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
string GetRelativePath(string path);
/// <summary>
/// Get properly formatted relative path from an existing absolute or relative path
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
string GetRelativePath(string path);
/// <summary>
/// Retrieves array of temporary folders from the hosting environment.
/// </summary>
/// <returns>Array of <see cref="DirectoryInfo"/> instances.</returns>
DirectoryInfo[] GetTempFolders();
/// <summary>
/// Retrieves array of temporary folders from the hosting environment.
/// </summary>
/// <returns>Array of <see cref="DirectoryInfo" /> instances.</returns>
DirectoryInfo[] GetTempFolders();
/// <summary>
/// Cleans contents of a folder by deleting all files older that the provided age.
/// If deletition of any file errors (e.g. due to a file lock) the process will continue to try to delete all that it can.
/// </summary>
/// <param name="folder">Folder to clean.</param>
/// <param name="age">Age of files within folder to delete.</param>
/// <returns>Result of operation</returns>
CleanFolderResult CleanFolder(DirectoryInfo folder, TimeSpan age);
}
/// <summary>
/// Cleans contents of a folder by deleting all files older that the provided age.
/// If deletition of any file errors (e.g. due to a file lock) the process will continue to try to delete all that it
/// can.
/// </summary>
/// <param name="folder">Folder to clean.</param>
/// <param name="age">Age of files within folder to delete.</param>
/// <returns>Result of operation</returns>
CleanFolderResult CleanFolder(DirectoryInfo folder, TimeSpan age);
}

View File

@@ -1,33 +1,29 @@
using System;
namespace Umbraco.Cms.Core.IO;
namespace Umbraco.Cms.Core.IO
/// <summary>
/// Represents a media file path scheme.
/// </summary>
public interface IMediaPathScheme
{
/// <summary>
/// Represents a media file path scheme.
/// Gets a media file path.
/// </summary>
public interface IMediaPathScheme
{
/// <summary>
/// Gets a media file path.
/// </summary>
/// <param name="fileManager">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>
///
/// <returns>The filesystem-relative complete file path.</returns>
string GetFilePath(MediaFileManager fileManager, Guid itemGuid, Guid propertyGuid, string filename);
/// <param name="fileManager">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>
/// <returns>The filesystem-relative complete file path.</returns>
string GetFilePath(MediaFileManager fileManager, Guid itemGuid, Guid propertyGuid, string filename);
/// <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(MediaFileManager fileSystem, string filepath);
}
/// <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(MediaFileManager fileSystem, string filepath);
}

View File

@@ -1,232 +1,243 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using Umbraco.Cms.Core.Hosting;
using Umbraco.Cms.Core.Strings;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.IO
namespace Umbraco.Cms.Core.IO;
public abstract class IOHelper : IIOHelper
{
public abstract class IOHelper : IIOHelper
private readonly IHostingEnvironment _hostingEnvironment;
public IOHelper(IHostingEnvironment hostingEnvironment) => _hostingEnvironment =
hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment));
// static compiled regex for faster performance
// private static readonly Regex ResolveUrlPattern = new Regex("(=[\"\']?)(\\W?\\~(?:.(?![\"\']?\\s+(?:\\S+)=|[>\"\']))+.)[\"\']?", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace);
// helper to try and match the old path to a new virtual one
public string FindFile(string virtualPath)
{
private readonly IHostingEnvironment _hostingEnvironment;
var retval = virtualPath;
public IOHelper(IHostingEnvironment hostingEnvironment)
if (virtualPath.StartsWith("~"))
{
_hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment));
retval = virtualPath.Replace("~", _hostingEnvironment.ApplicationVirtualPath);
}
// static compiled regex for faster performance
//private static readonly Regex ResolveUrlPattern = new Regex("(=[\"\']?)(\\W?\\~(?:.(?![\"\']?\\s+(?:\\S+)=|[>\"\']))+.)[\"\']?", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace);
//helper to try and match the old path to a new virtual one
public string FindFile(string virtualPath)
if (virtualPath.StartsWith("/") && !PathStartsWith(virtualPath, _hostingEnvironment.ApplicationVirtualPath))
{
string retval = virtualPath;
if (virtualPath.StartsWith("~"))
retval = virtualPath.Replace("~", _hostingEnvironment.ApplicationVirtualPath);
if (virtualPath.StartsWith("/") && !PathStartsWith(virtualPath, _hostingEnvironment.ApplicationVirtualPath))
retval = _hostingEnvironment.ApplicationVirtualPath + "/" + virtualPath.TrimStart(Constants.CharArrays.ForwardSlash);
return retval;
retval = _hostingEnvironment.ApplicationVirtualPath + "/" +
virtualPath.TrimStart(Constants.CharArrays.ForwardSlash);
}
// TODO: This is the same as IHostingEnvironment.ToAbsolute - marked as obsolete in IIOHelper for now
public string ResolveUrl(string virtualPath)
{
if (string.IsNullOrWhiteSpace(virtualPath)) return virtualPath;
return _hostingEnvironment.ToAbsolute(virtualPath);
return retval;
}
// TODO: This is the same as IHostingEnvironment.ToAbsolute - marked as obsolete in IIOHelper for now
public string ResolveUrl(string virtualPath)
{
if (string.IsNullOrWhiteSpace(virtualPath))
{
return virtualPath;
}
public string MapPath(string path)
{
if (path == null) throw new ArgumentNullException(nameof(path));
return _hostingEnvironment.ToAbsolute(virtualPath);
}
// Check if the path is already mapped - TODO: This should be switched to Path.IsPathFullyQualified once we are on Net Standard 2.1
if (IsPathFullyQualified(path))
public string MapPath(string path)
{
if (path == null)
{
throw new ArgumentNullException(nameof(path));
}
// Check if the path is already mapped - TODO: This should be switched to Path.IsPathFullyQualified once we are on Net Standard 2.1
if (IsPathFullyQualified(path))
{
return path;
}
if (_hostingEnvironment.IsHosted)
{
var result = !string.IsNullOrEmpty(path) &&
(path.StartsWith("~") || PathStartsWith(path, _hostingEnvironment.ApplicationVirtualPath))
? _hostingEnvironment.MapPathWebRoot(path)
: _hostingEnvironment.MapPathWebRoot("~/" + path.TrimStart(Constants.CharArrays.ForwardSlash));
if (result != null)
{
return path;
return result;
}
}
var dirSepChar = Path.DirectorySeparatorChar;
var root = Assembly.GetExecutingAssembly().GetRootDirectorySafe();
var newPath = path.TrimStart(Constants.CharArrays.TildeForwardSlash).Replace('/', dirSepChar);
var retval = root + dirSepChar.ToString(CultureInfo.InvariantCulture) + newPath;
return retval;
}
/// <summary>
/// Verifies that the current filepath matches a directory where the user is allowed to edit a file.
/// </summary>
/// <param name="filePath">The filepath to validate.</param>
/// <param name="validDir">The valid directory.</param>
/// <returns>A value indicating whether the filepath is valid.</returns>
public bool VerifyEditPath(string filePath, string validDir) => VerifyEditPath(filePath, new[] { validDir });
/// <summary>
/// Verifies that the current filepath matches one of several directories where the user is allowed to edit a file.
/// </summary>
/// <param name="filePath">The filepath to validate.</param>
/// <param name="validDirs">The valid directories.</param>
/// <returns>A value indicating whether the filepath is valid.</returns>
public bool VerifyEditPath(string filePath, IEnumerable<string> validDirs)
{
// this is called from ScriptRepository, PartialViewRepository, etc.
// filePath is the fullPath (rooted, filesystem path, can be trusted)
// validDirs are virtual paths (eg ~/Views)
//
// except that for templates, filePath actually is a virtual path
// TODO: what's below is dirty, there are too many ways to get the root dir, etc.
// not going to fix everything today
var mappedRoot = MapPath(_hostingEnvironment.ApplicationVirtualPath);
if (!PathStartsWith(filePath, mappedRoot))
{
// TODO this is going to fail.. Scripts Stylesheets need to use WebRoot, PartialViews need to use ContentRoot
filePath = _hostingEnvironment.MapPathWebRoot(filePath);
}
// yes we can (see above)
//// don't trust what we get, it may contain relative segments
// filePath = Path.GetFullPath(filePath);
foreach (var dir in validDirs)
{
var validDir = dir;
if (!PathStartsWith(validDir, mappedRoot))
{
validDir = _hostingEnvironment.MapPathWebRoot(validDir);
}
if (_hostingEnvironment.IsHosted)
if (PathStartsWith(filePath, validDir))
{
var result = (!string.IsNullOrEmpty(path) && (path.StartsWith("~") || PathStartsWith(path, _hostingEnvironment.ApplicationVirtualPath)))
? _hostingEnvironment.MapPathWebRoot(path)
: _hostingEnvironment.MapPathWebRoot("~/" + path.TrimStart(Constants.CharArrays.ForwardSlash));
if (result != null) return result;
return true;
}
var dirSepChar = Path.DirectorySeparatorChar;
var root = Assembly.GetExecutingAssembly().GetRootDirectorySafe();
var newPath = path.TrimStart(Constants.CharArrays.TildeForwardSlash).Replace('/', dirSepChar);
var retval = root + dirSepChar.ToString(CultureInfo.InvariantCulture) + newPath;
return retval;
}
/// <summary>
/// Returns true if the path has a root, and is considered fully qualified for the OS it is on
/// See https://github.com/dotnet/runtime/blob/30769e8f31b20be10ca26e27ec279cd4e79412b9/src/libraries/System.Private.CoreLib/src/System/IO/Path.cs#L281 for the .NET Standard 2.1 version of this
/// </summary>
/// <param name="path">The path to check</param>
/// <returns>True if the path is fully qualified, false otherwise</returns>
public abstract bool IsPathFullyQualified(string path);
return false;
}
/// <summary>
/// Verifies that the current filepath has one of several authorized extensions.
/// </summary>
/// <param name="filePath">The filepath to validate.</param>
/// <param name="validFileExtensions">The valid extensions.</param>
/// <returns>A value indicating whether the filepath is valid.</returns>
public bool VerifyFileExtension(string filePath, IEnumerable<string> validFileExtensions)
{
var ext = Path.GetExtension(filePath);
return ext != null && validFileExtensions.Contains(ext.TrimStart(Constants.CharArrays.Period));
}
/// <summary>
/// Verifies that the current filepath matches a directory where the user is allowed to edit a file.
/// </summary>
/// <param name="filePath">The filepath to validate.</param>
/// <param name="validDir">The valid directory.</param>
/// <returns>A value indicating whether the filepath is valid.</returns>
public bool VerifyEditPath(string filePath, string validDir)
public abstract bool PathStartsWith(string path, string root, params char[] separators);
public void EnsurePathExists(string path)
{
var absolutePath = MapPath(path);
if (Directory.Exists(absolutePath) == false)
{
return VerifyEditPath(filePath, new[] { validDir });
}
/// <summary>
/// Verifies that the current filepath matches one of several directories where the user is allowed to edit a file.
/// </summary>
/// <param name="filePath">The filepath to validate.</param>
/// <param name="validDirs">The valid directories.</param>
/// <returns>A value indicating whether the filepath is valid.</returns>
public bool VerifyEditPath(string filePath, IEnumerable<string> validDirs)
{
// this is called from ScriptRepository, PartialViewRepository, etc.
// filePath is the fullPath (rooted, filesystem path, can be trusted)
// validDirs are virtual paths (eg ~/Views)
//
// except that for templates, filePath actually is a virtual path
// TODO: what's below is dirty, there are too many ways to get the root dir, etc.
// not going to fix everything today
var mappedRoot = MapPath(_hostingEnvironment.ApplicationVirtualPath);
if (!PathStartsWith(filePath, mappedRoot))
{
// TODO this is going to fail.. Scripts Stylesheets need to use WebRoot, PartialViews need to use ContentRoot
filePath = _hostingEnvironment.MapPathWebRoot(filePath);
}
// yes we can (see above)
//// don't trust what we get, it may contain relative segments
//filePath = Path.GetFullPath(filePath);
foreach (var dir in validDirs)
{
var validDir = dir;
if (!PathStartsWith(validDir, mappedRoot))
validDir = _hostingEnvironment.MapPathWebRoot(validDir);
if (PathStartsWith(filePath, validDir))
return true;
}
return false;
}
/// <summary>
/// Verifies that the current filepath has one of several authorized extensions.
/// </summary>
/// <param name="filePath">The filepath to validate.</param>
/// <param name="validFileExtensions">The valid extensions.</param>
/// <returns>A value indicating whether the filepath is valid.</returns>
public bool VerifyFileExtension(string filePath, IEnumerable<string> validFileExtensions)
{
var ext = Path.GetExtension(filePath);
return ext != null && validFileExtensions.Contains(ext.TrimStart(Constants.CharArrays.Period));
}
public abstract bool PathStartsWith(string path, string root, params char[] separators);
public void EnsurePathExists(string path)
{
var absolutePath = MapPath(path);
if (Directory.Exists(absolutePath) == false)
Directory.CreateDirectory(absolutePath);
}
/// <summary>
/// Get properly formatted relative path from an existing absolute or relative path
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
public string GetRelativePath(string path)
{
if (path.IsFullPath())
{
var rootDirectory = MapPath("~");
var relativePath = PathStartsWith(path, rootDirectory) ? path.Substring(rootDirectory.Length) : path;
path = relativePath;
}
return PathUtility.EnsurePathIsApplicationRootPrefixed(path);
}
/// <summary>
/// Retrieves array of temporary folders from the hosting environment.
/// </summary>
/// <returns>Array of <see cref="DirectoryInfo"/> instances.</returns>
public DirectoryInfo[] GetTempFolders()
{
var tempFolderPaths = new[]
{
_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.TempFileUploads)
};
foreach (var tempFolderPath in tempFolderPaths)
{
// Ensure it exists
Directory.CreateDirectory(tempFolderPath);
}
return tempFolderPaths.Select(x => new DirectoryInfo(x)).ToArray();
}
/// <summary>
/// Cleans contents of a folder by deleting all files older that the provided age.
/// If deletition of any file errors (e.g. due to a file lock) the process will continue to try to delete all that it can.
/// </summary>
/// <param name="folder">Folder to clean.</param>
/// <param name="age">Age of files within folder to delete.</param>
/// <returns>Result of operation.</returns>
public CleanFolderResult CleanFolder(DirectoryInfo folder, TimeSpan age)
{
folder.Refresh(); // In case it's changed during runtime.
if (!folder.Exists)
{
return CleanFolderResult.FailedAsDoesNotExist();
}
var files = folder.GetFiles("*.*", SearchOption.AllDirectories);
var errors = new List<CleanFolderResult.Error>();
foreach (var file in files)
{
if (DateTime.UtcNow - file.LastWriteTimeUtc > age)
{
try
{
file.IsReadOnly = false;
file.Delete();
}
catch (Exception ex)
{
errors.Add(new CleanFolderResult.Error(ex, file));
}
}
}
return errors.Any()
? CleanFolderResult.FailedWithErrors(errors)
: CleanFolderResult.Success();
Directory.CreateDirectory(absolutePath);
}
}
/// <summary>
/// Get properly formatted relative path from an existing absolute or relative path
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
public string GetRelativePath(string path)
{
if (path.IsFullPath())
{
var rootDirectory = MapPath("~");
var relativePath = PathStartsWith(path, rootDirectory) ? path[rootDirectory.Length..] : path;
path = relativePath;
}
return PathUtility.EnsurePathIsApplicationRootPrefixed(path);
}
/// <summary>
/// Retrieves array of temporary folders from the hosting environment.
/// </summary>
/// <returns>Array of <see cref="DirectoryInfo" /> instances.</returns>
public DirectoryInfo[] GetTempFolders()
{
var tempFolderPaths = new[]
{
_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.TempFileUploads),
};
foreach (var tempFolderPath in tempFolderPaths)
{
// Ensure it exists
Directory.CreateDirectory(tempFolderPath);
}
return tempFolderPaths.Select(x => new DirectoryInfo(x)).ToArray();
}
/// <summary>
/// Cleans contents of a folder by deleting all files older that the provided age.
/// If deletition of any file errors (e.g. due to a file lock) the process will continue to try to delete all that it
/// can.
/// </summary>
/// <param name="folder">Folder to clean.</param>
/// <param name="age">Age of files within folder to delete.</param>
/// <returns>Result of operation.</returns>
public CleanFolderResult CleanFolder(DirectoryInfo folder, TimeSpan age)
{
folder.Refresh(); // In case it's changed during runtime.
if (!folder.Exists)
{
return CleanFolderResult.FailedAsDoesNotExist();
}
FileInfo[] files = folder.GetFiles("*.*", SearchOption.AllDirectories);
var errors = new List<CleanFolderResult.Error>();
foreach (FileInfo file in files)
{
if (DateTime.UtcNow - file.LastWriteTimeUtc > age)
{
try
{
file.IsReadOnly = false;
file.Delete();
}
catch (Exception ex)
{
errors.Add(new CleanFolderResult.Error(ex, file));
}
}
}
return errors.Any()
? CleanFolderResult.FailedWithErrors(errors)
: CleanFolderResult.Success();
}
/// <summary>
/// Returns true if the path has a root, and is considered fully qualified for the OS it is on
/// See
/// https://github.com/dotnet/runtime/blob/30769e8f31b20be10ca26e27ec279cd4e79412b9/src/libraries/System.Private.CoreLib/src/System/IO/Path.cs#L281
/// for the .NET Standard 2.1 version of this
/// </summary>
/// <param name="path">The path to check</param>
/// <returns>True if the path is fully qualified, false otherwise</returns>
public abstract bool IsPathFullyQualified(string path);
}

View File

@@ -1,55 +1,54 @@
using System;
using System.IO;
using Umbraco.Cms.Core.IO;
namespace Umbraco.Extensions
namespace Umbraco.Extensions;
public static class IOHelperExtensions
{
public static class IOHelperExtensions
/// <summary>
/// Will resolve a virtual path URL to an absolute path, else if it is not a virtual path (i.e. starts with ~/) then
/// it will just return the path as-is (relative).
/// </summary>
/// <param name="ioHelper"></param>
/// <param name="path"></param>
/// <returns></returns>
public static string? ResolveRelativeOrVirtualUrl(this IIOHelper ioHelper, string? path)
{
/// <summary>
/// Will resolve a virtual path URL to an absolute path, else if it is not a virtual path (i.e. starts with ~/) then
/// it will just return the path as-is (relative).
/// </summary>
/// <param name="ioHelper"></param>
/// <param name="path"></param>
/// <returns></returns>
public static string? ResolveRelativeOrVirtualUrl(this IIOHelper ioHelper, string? path)
if (string.IsNullOrWhiteSpace(path))
{
if (string.IsNullOrWhiteSpace(path)) return path;
return path.StartsWith("~/") ? ioHelper.ResolveUrl(path) : path;
return path;
}
/// <summary>
/// Tries to create a directory.
/// </summary>
/// <param name="ioHelper">The IOHelper.</param>
/// <param name="dir">the directory path.</param>
/// <returns>true if the directory was created, false otherwise.</returns>
public static bool TryCreateDirectory(this IIOHelper ioHelper, string dir)
{
try
{
var dirPath = ioHelper.MapPath(dir);
if (Directory.Exists(dirPath) == false)
Directory.CreateDirectory(dirPath);
var filePath = dirPath + "/" + CreateRandomFileName(ioHelper) + ".tmp";
File.WriteAllText(filePath, "This is an Umbraco internal test file. It is safe to delete it.");
File.Delete(filePath);
return true;
}
catch
{
return false;
}
}
public static string CreateRandomFileName(this IIOHelper ioHelper)
{
return "umbraco-test." + Guid.NewGuid().ToString("N").Substring(0, 8);
}
return path.StartsWith("~/") ? ioHelper.ResolveUrl(path) : path;
}
/// <summary>
/// Tries to create a directory.
/// </summary>
/// <param name="ioHelper">The IOHelper.</param>
/// <param name="dir">the directory path.</param>
/// <returns>true if the directory was created, false otherwise.</returns>
public static bool TryCreateDirectory(this IIOHelper ioHelper, string dir)
{
try
{
var dirPath = ioHelper.MapPath(dir);
if (Directory.Exists(dirPath) == false)
{
Directory.CreateDirectory(dirPath);
}
var filePath = dirPath + "/" + CreateRandomFileName(ioHelper) + ".tmp";
File.WriteAllText(filePath, "This is an Umbraco internal test file. It is safe to delete it.");
File.Delete(filePath);
return true;
}
catch
{
return false;
}
}
public static string CreateRandomFileName(this IIOHelper ioHelper) =>
"umbraco-test." + Guid.NewGuid().ToString("N").Substring(0, 8);
}

View File

@@ -1,28 +1,40 @@
using System;
using System.IO;
using System.Linq;
using Umbraco.Cms.Core.Hosting;
namespace Umbraco.Cms.Core.IO
namespace Umbraco.Cms.Core.IO;
public class IOHelperLinux : IOHelper
{
public class IOHelperLinux : IOHelper
public IOHelperLinux(IHostingEnvironment hostingEnvironment)
: base(hostingEnvironment)
{
public IOHelperLinux(IHostingEnvironment hostingEnvironment) : base(hostingEnvironment)
}
public override bool IsPathFullyQualified(string path) => Path.IsPathRooted(path);
public override bool PathStartsWith(string path, string root, params char[] separators)
{
// either it is identical to root,
// or it is root + separator + anything
if (separators == null || separators.Length == 0)
{
separators = new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar };
}
public override bool IsPathFullyQualified(string path) => Path.IsPathRooted(path);
public override bool PathStartsWith(string path, string root, params char[] separators)
if (!path.StartsWith(root, StringComparison.Ordinal))
{
// either it is identical to root,
// or it is root + separator + anything
if (separators == null || separators.Length == 0) separators = new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar };
if (!path.StartsWith(root, StringComparison.Ordinal)) return false;
if (path.Length == root.Length) return true;
if (path.Length < root.Length) return false;
return separators.Contains(path[root.Length]);
return false;
}
if (path.Length == root.Length)
{
return true;
}
if (path.Length < root.Length)
{
return false;
}
return separators.Contains(path[root.Length]);
}
}

View File

@@ -1,28 +1,40 @@
using System;
using System.IO;
using System.Linq;
using Umbraco.Cms.Core.Hosting;
namespace Umbraco.Cms.Core.IO
namespace Umbraco.Cms.Core.IO;
public class IOHelperOSX : IOHelper
{
public class IOHelperOSX : IOHelper
public IOHelperOSX(IHostingEnvironment hostingEnvironment)
: base(hostingEnvironment)
{
public IOHelperOSX(IHostingEnvironment hostingEnvironment) : base(hostingEnvironment)
}
public override bool IsPathFullyQualified(string path) => Path.IsPathRooted(path);
public override bool PathStartsWith(string path, string root, params char[] separators)
{
// either it is identical to root,
// or it is root + separator + anything
if (separators == null || separators.Length == 0)
{
separators = new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar };
}
public override bool IsPathFullyQualified(string path) => Path.IsPathRooted(path);
public override bool PathStartsWith(string path, string root, params char[] separators)
if (!path.StartsWith(root, StringComparison.OrdinalIgnoreCase))
{
// either it is identical to root,
// or it is root + separator + anything
if (separators == null || separators.Length == 0) separators = new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar };
if (!path.StartsWith(root, StringComparison.OrdinalIgnoreCase)) return false;
if (path.Length == root.Length) return true;
if (path.Length < root.Length) return false;
return separators.Contains(path[root.Length]);
return false;
}
if (path.Length == root.Length)
{
return true;
}
if (path.Length < root.Length)
{
return false;
}
return separators.Contains(path[root.Length]);
}
}

View File

@@ -1,54 +1,67 @@
using System;
using System.IO;
using System.Linq;
using Umbraco.Cms.Core.Hosting;
namespace Umbraco.Cms.Core.IO
namespace Umbraco.Cms.Core.IO;
public class IOHelperWindows : IOHelper
{
public class IOHelperWindows : IOHelper
public IOHelperWindows(IHostingEnvironment hostingEnvironment)
: base(hostingEnvironment)
{
public IOHelperWindows(IHostingEnvironment hostingEnvironment) : base(hostingEnvironment)
}
public override bool IsPathFullyQualified(string path)
{
// TODO: This implementation is taken from the .NET Standard 2.1 implementation. We should switch to using Path.IsPathFullyQualified once we are on .NET Standard 2.1
if (path.Length < 2)
{
// It isn't fixed, it must be relative. There is no way to specify a fixed
// path with one character (or less).
return false;
}
public override bool IsPathFullyQualified(string path)
if (path[0] == Path.DirectorySeparatorChar || path[0] == Path.AltDirectorySeparatorChar)
{
// TODO: This implementation is taken from the .NET Standard 2.1 implementation. We should switch to using Path.IsPathFullyQualified once we are on .NET Standard 2.1
if (path.Length < 2)
{
// It isn't fixed, it must be relative. There is no way to specify a fixed
// path with one character (or less).
return false;
}
if (path[0] == Path.DirectorySeparatorChar || path[0] == Path.AltDirectorySeparatorChar)
{
// There is no valid way to specify a relative path with two initial slashes or
// \? as ? isn't valid for drive relative paths and \??\ is equivalent to \\?\
return path[1] == '?' || path[1] == Path.DirectorySeparatorChar || path[1] == Path.AltDirectorySeparatorChar;
}
// The only way to specify a fixed path that doesn't begin with two slashes
// is the drive, colon, slash format- i.e. C:\
return (path.Length >= 3)
&& (path[1] == Path.VolumeSeparatorChar)
&& (path[2] == Path.DirectorySeparatorChar || path[2] == Path.AltDirectorySeparatorChar)
// To match old behavior we'll check the drive character for validity as the path is technically
// not qualified if you don't have a valid drive. "=:\" is the "=" file's default data stream.
&& ((path[0] >= 'A' && path[0] <= 'Z') || (path[0] >= 'a' && path[0] <= 'z'));
// There is no valid way to specify a relative path with two initial slashes or
// \? as ? isn't valid for drive relative paths and \??\ is equivalent to \\?\
return path[1] == '?' || path[1] == Path.DirectorySeparatorChar ||
path[1] == Path.AltDirectorySeparatorChar;
}
public override bool PathStartsWith(string path, string root, params char[] separators)
{
// either it is identical to root,
// or it is root + separator + anything
// The only way to specify a fixed path that doesn't begin with two slashes
// is the drive, colon, slash format- i.e. C:\
return path.Length >= 3
&& path[1] == Path.VolumeSeparatorChar
&& (path[2] == Path.DirectorySeparatorChar || path[2] == Path.AltDirectorySeparatorChar)
if (separators == null || separators.Length == 0) separators = new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar };
if (!path.StartsWith(root, StringComparison.OrdinalIgnoreCase)) return false;
if (path.Length == root.Length) return true;
if (path.Length < root.Length) return false;
return separators.Contains(path[root.Length]);
// To match old behavior we'll check the drive character for validity as the path is technically
// not qualified if you don't have a valid drive. "=:\" is the "=" file's default data stream.
&& ((path[0] >= 'A' && path[0] <= 'Z') || (path[0] >= 'a' && path[0] <= 'z'));
}
public override bool PathStartsWith(string path, string root, params char[] separators)
{
// either it is identical to root,
// or it is root + separator + anything
if (separators == null || separators.Length == 0)
{
separators = new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar };
}
if (!path.StartsWith(root, StringComparison.OrdinalIgnoreCase))
{
return false;
}
if (path.Length == root.Length)
{
return true;
}
if (path.Length < root.Length)
{
return false;
}
return separators.Contains(path[root.Length]);
}
}

View File

@@ -1,13 +1,16 @@
using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Core.IO
namespace Umbraco.Cms.Core.IO;
public interface IViewHelper
{
public interface IViewHelper
{
bool ViewExists(ITemplate t);
string GetFileContents(ITemplate t);
string CreateView(ITemplate t, bool overWrite = false);
string? UpdateViewFile(ITemplate t, string? currentAlias = null);
string ViewPath(string alias);
}
bool ViewExists(ITemplate t);
string GetFileContents(ITemplate t);
string CreateView(ITemplate t, bool overWrite = false);
string? UpdateViewFile(ITemplate t, string? currentAlias = null);
string ViewPath(string alias);
}

View File

@@ -1,8 +1,3 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@@ -12,237 +7,246 @@ using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.Strings;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.IO
namespace Umbraco.Cms.Core.IO;
public sealed class MediaFileManager
{
public sealed class MediaFileManager
private readonly ILogger<MediaFileManager> _logger;
private readonly IMediaPathScheme _mediaPathScheme;
private readonly IServiceProvider _serviceProvider;
private readonly IShortStringHelper _shortStringHelper;
private MediaUrlGeneratorCollection? _mediaUrlGenerators;
public MediaFileManager(
IFileSystem fileSystem,
IMediaPathScheme mediaPathScheme,
ILogger<MediaFileManager> logger,
IShortStringHelper shortStringHelper,
IServiceProvider serviceProvider)
{
private readonly IMediaPathScheme _mediaPathScheme;
private readonly ILogger<MediaFileManager> _logger;
private readonly IShortStringHelper _shortStringHelper;
private readonly IServiceProvider _serviceProvider;
private MediaUrlGeneratorCollection? _mediaUrlGenerators;
public MediaFileManager(
IFileSystem fileSystem,
IMediaPathScheme mediaPathScheme,
ILogger<MediaFileManager> logger,
IShortStringHelper shortStringHelper,
IServiceProvider serviceProvider)
{
_mediaPathScheme = mediaPathScheme;
_logger = logger;
_shortStringHelper = shortStringHelper;
_serviceProvider = serviceProvider;
FileSystem = fileSystem;
}
[Obsolete("Use the ctr that doesn't include unused parameters.")]
public MediaFileManager(
IFileSystem fileSystem,
IMediaPathScheme mediaPathScheme,
ILogger<MediaFileManager> logger,
IShortStringHelper shortStringHelper,
IServiceProvider serviceProvider,
IOptions<ContentSettings> contentSettings)
: this(fileSystem, mediaPathScheme, logger, shortStringHelper, serviceProvider)
{ }
/// <summary>
/// Gets the media filesystem.
/// </summary>
public IFileSystem FileSystem { get; }
/// <summary>
/// Delete media files.
/// </summary>
/// <param name="files">Files to delete (filesystem-relative paths).</param>
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 (FileSystem.FileExists(file) == false)
{
return;
}
FileSystem.DeleteFile(file);
var directory = _mediaPathScheme.GetDeleteDirectory(this, file);
if (!directory.IsNullOrWhiteSpace())
{
FileSystem.DeleteDirectory(directory!, true);
}
}
catch (Exception e)
{
_logger.LogError(e, "Failed to delete media file '{File}'.", file);
}
});
}
#region Media Path
/// <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>
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 = _shortStringHelper.CleanStringForSafeFileName(filename.ToLowerInvariant());
return _mediaPathScheme.GetFilePath(this, cuid, puid, filename);
}
#endregion
#region Associated Media Files
/// <summary>
/// Returns a stream (file) for a content item (or a null stream if there is no file).
/// </summary>
/// <param name="content"></param>
/// <param name="mediaFilePath">The file path if a file was found</param>
/// <param name="propertyTypeAlias"></param>
/// <param name="variationContextAccessor"></param>
/// <returns></returns>
public Stream GetFile(
IContentBase content,
out string? mediaFilePath,
string propertyTypeAlias = Constants.Conventions.Media.File,
string? culture = null,
string? segment = null)
{
// TODO: If collections were lazy we could just inject them
if (_mediaUrlGenerators == null)
{
_mediaUrlGenerators = _serviceProvider.GetRequiredService<MediaUrlGeneratorCollection>();
}
if (!content.TryGetMediaPath(propertyTypeAlias, _mediaUrlGenerators!, out mediaFilePath, culture, segment))
{
return Stream.Null;
}
return FileSystem.OpenFile(mediaFilePath!);
}
/// <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>
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 (filename == null)
{
throw new ArgumentNullException(nameof(filename));
}
if (string.IsNullOrWhiteSpace(filename))
{
throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(filename));
}
if (filestream == null)
{
throw new ArgumentNullException(nameof(filestream));
}
// clear the old file, if any
if (string.IsNullOrWhiteSpace(oldpath) == false)
{
FileSystem.DeleteFile(oldpath!);
}
// get the filepath, store the data
var filepath = GetMediaPath(filename, content.Key, propertyType.Key);
FileSystem.AddFile(filepath, filestream);
return filepath;
}
/// <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>
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 (sourcepath == null)
{
throw new ArgumentNullException(nameof(sourcepath));
}
if (string.IsNullOrWhiteSpace(sourcepath))
{
throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(sourcepath));
}
// ensure we have a file to copy
if (FileSystem.FileExists(sourcepath) == false)
{
return null;
}
// get the filepath
var filename = Path.GetFileName(sourcepath);
var filepath = GetMediaPath(filename, content.Key, propertyType.Key);
FileSystem.CopyFile(sourcepath, filepath);
return filepath;
}
#endregion
_mediaPathScheme = mediaPathScheme;
_logger = logger;
_shortStringHelper = shortStringHelper;
_serviceProvider = serviceProvider;
FileSystem = fileSystem;
}
[Obsolete("Use the ctr that doesn't include unused parameters.")]
public MediaFileManager(
IFileSystem fileSystem,
IMediaPathScheme mediaPathScheme,
ILogger<MediaFileManager> logger,
IShortStringHelper shortStringHelper,
IServiceProvider serviceProvider,
IOptions<ContentSettings> contentSettings)
: this(fileSystem, mediaPathScheme, logger, shortStringHelper, serviceProvider)
{
}
/// <summary>
/// Gets the media filesystem.
/// </summary>
public IFileSystem FileSystem { get; }
/// <summary>
/// Delete media files.
/// </summary>
/// <param name="files">Files to delete (filesystem-relative paths).</param>
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 (FileSystem.FileExists(file) == false)
{
return;
}
FileSystem.DeleteFile(file);
var directory = _mediaPathScheme.GetDeleteDirectory(this, file);
if (!directory.IsNullOrWhiteSpace())
{
FileSystem.DeleteDirectory(directory!, true);
}
}
catch (Exception e)
{
_logger.LogError(e, "Failed to delete media file '{File}'.", file);
}
});
}
#region Media Path
/// <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>
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 = _shortStringHelper.CleanStringForSafeFileName(filename.ToLowerInvariant());
return _mediaPathScheme.GetFilePath(this, cuid, puid, filename);
}
#endregion
#region Associated Media Files
/// <summary>
/// Returns a stream (file) for a content item (or a null stream if there is no file).
/// </summary>
/// <param name="content"></param>
/// <param name="mediaFilePath">The file path if a file was found</param>
/// <param name="propertyTypeAlias"></param>
/// <param name="variationContextAccessor"></param>
/// <param name="culture"></param>
/// <param name="segment"></param>
/// <returns></returns>
public Stream GetFile(
IContentBase content,
out string? mediaFilePath,
string propertyTypeAlias = Constants.Conventions.Media.File,
string? culture = null,
string? segment = null)
{
// TODO: If collections were lazy we could just inject them
if (_mediaUrlGenerators == null)
{
_mediaUrlGenerators = _serviceProvider.GetRequiredService<MediaUrlGeneratorCollection>();
}
if (!content.TryGetMediaPath(propertyTypeAlias, _mediaUrlGenerators!, out mediaFilePath, culture, segment))
{
return Stream.Null;
}
return FileSystem.OpenFile(mediaFilePath!);
}
/// <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>
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 (filename == null)
{
throw new ArgumentNullException(nameof(filename));
}
if (string.IsNullOrWhiteSpace(filename))
{
throw new ArgumentException(
"Value can't be empty or consist only of white-space characters.",
nameof(filename));
}
if (filestream == null)
{
throw new ArgumentNullException(nameof(filestream));
}
// clear the old file, if any
if (string.IsNullOrWhiteSpace(oldpath) == false)
{
FileSystem.DeleteFile(oldpath);
}
// get the filepath, store the data
var filepath = GetMediaPath(filename, content.Key, propertyType.Key);
FileSystem.AddFile(filepath, filestream);
return filepath;
}
/// <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>
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 (sourcepath == null)
{
throw new ArgumentNullException(nameof(sourcepath));
}
if (string.IsNullOrWhiteSpace(sourcepath))
{
throw new ArgumentException(
"Value can't be empty or consist only of white-space characters.",
nameof(sourcepath));
}
// ensure we have a file to copy
if (FileSystem.FileExists(sourcepath) == false)
{
return null;
}
// get the filepath
var filename = Path.GetFileName(sourcepath);
var filepath = GetMediaPath(filename, content.Key, propertyType.Key);
FileSystem.CopyFile(sourcepath, filepath);
return filepath;
}
#endregion
}

View File

@@ -1,28 +1,25 @@
using System;
using System.IO;
namespace Umbraco.Cms.Core.IO.MediaPathSchemes;
namespace Umbraco.Cms.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
{
/// <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(MediaFileManager fileManager, Guid itemGuid, Guid propertyGuid, string filename)
{
/// <inheritdoc />
public string GetFilePath(MediaFileManager fileManager, Guid itemGuid, Guid propertyGuid, string filename)
{
// 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(MediaFileManager fileSystem, string filepath) => Path.GetDirectoryName(filepath)!;
// 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
Guid 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(MediaFileManager fileSystem, string filepath) => Path.GetDirectoryName(filepath)!;
}

View File

@@ -1,26 +1,17 @@
using System;
using System.IO;
namespace Umbraco.Cms.Core.IO.MediaPathSchemes;
namespace Umbraco.Cms.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
{
/// <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(MediaFileManager fileManager, Guid itemGuid, Guid propertyGuid, string filename)
{
return Path.Combine(itemGuid.ToString("N"), propertyGuid.ToString("N"), filename).Replace('\\', '/');
}
/// <inheritdoc />
public string GetFilePath(MediaFileManager fileManager, Guid itemGuid, Guid propertyGuid, string filename) =>
Path.Combine(itemGuid.ToString("N"), propertyGuid.ToString("N"), filename).Replace('\\', '/');
/// <inheritdoc />
public string GetDeleteDirectory(MediaFileManager fileManager, string filepath)
{
return Path.GetDirectoryName(filepath)!;
}
}
/// <inheritdoc />
public string GetDeleteDirectory(MediaFileManager fileManager, string filepath) => Path.GetDirectoryName(filepath)!;
}

View File

@@ -1,37 +1,37 @@
using System;
using System.IO;
namespace Umbraco.Cms.Core.IO.MediaPathSchemes;
namespace Umbraco.Cms.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
{
/// <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(MediaFileManager fileManager, Guid itemGuid, Guid propertyGuid, string filename)
{
private const int DirectoryLength = 8;
Guid combinedGuid = GuidUtils.Combine(itemGuid, propertyGuid);
var directory = GuidUtils.ToBase32String(combinedGuid, DirectoryLength);
/// <inheritdoc />
public string GetFilePath(MediaFileManager fileManager, Guid itemGuid, Guid propertyGuid, string filename)
{
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(MediaFileManager fileManager, string filepath) => null;
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(MediaFileManager fileManager, string filepath) => null;
}

View File

@@ -1,8 +1,3 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.Hosting;
@@ -36,11 +31,30 @@ namespace Umbraco.Cms.Core.IO
_ioHelper = ioHelper ?? throw new ArgumentNullException(nameof(ioHelper));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
if (rootPath == null) throw new ArgumentNullException(nameof(rootPath));
if (string.IsNullOrEmpty(rootPath)) throw new ArgumentException("Value can't be empty.", nameof(rootPath));
if (rootUrl == null) throw new ArgumentNullException(nameof(rootUrl));
if (string.IsNullOrEmpty(rootUrl)) throw new ArgumentException("Value can't be empty.", nameof(rootUrl));
if (rootPath.StartsWith("~/")) throw new ArgumentException("Value can't be a virtual path and start with '~/'.", nameof(rootPath));
if (rootPath == null)
{
throw new ArgumentNullException(nameof(rootPath));
}
if (string.IsNullOrEmpty(rootPath))
{
throw new ArgumentException("Value can't be empty.", nameof(rootPath));
}
if (rootUrl == null)
{
throw new ArgumentNullException(nameof(rootUrl));
}
if (string.IsNullOrEmpty(rootUrl))
{
throw new ArgumentException("Value can't be empty.", nameof(rootUrl));
}
if (rootPath.StartsWith("~/"))
{
throw new ArgumentException("Value can't be a virtual path and start with '~/'.", nameof(rootPath));
}
// rootPath should be... rooted, as in, it's a root path!
if (Path.IsPathRooted(rootPath) == false)
@@ -71,7 +85,9 @@ namespace Umbraco.Cms.Core.IO
try
{
if (Directory.Exists(fullPath))
{
return Directory.EnumerateDirectories(fullPath).Select(GetRelativePath);
}
}
catch (UnauthorizedAccessException ex)
{
@@ -103,7 +119,9 @@ namespace Umbraco.Cms.Core.IO
{
var fullPath = GetFullPath(path);
if (Directory.Exists(fullPath) == false)
{
return;
}
try
{
@@ -154,7 +172,11 @@ namespace Umbraco.Cms.Core.IO
}
var directory = Path.GetDirectoryName(fullPath);
if (directory == null) throw new InvalidOperationException("Could not get directory.");
if (directory == null)
{
throw new InvalidOperationException("Could not get directory.");
}
Directory.CreateDirectory(directory); // ensure it exists
if (stream.CanSeek)
@@ -191,7 +213,9 @@ namespace Umbraco.Cms.Core.IO
try
{
if (Directory.Exists(fullPath))
{
return Directory.EnumerateFiles(fullPath, filter).Select(GetRelativePath);
}
}
catch (UnauthorizedAccessException ex)
{
@@ -224,7 +248,9 @@ namespace Umbraco.Cms.Core.IO
{
var fullPath = GetFullPath(path);
if (File.Exists(fullPath) == false)
{
return;
}
try
{
@@ -265,12 +291,16 @@ namespace Umbraco.Cms.Core.IO
// eg "c:/websites/test/root/Media/1234/img.jpg" => "1234/img.jpg"
// or on unix systems "/var/wwwroot/test/Meia/1234/img.jpg"
if (_ioHelper.PathStartsWith(path, _rootPathFwd, '/'))
{
return path.Substring(_rootPathFwd.Length).TrimStart(Constants.CharArrays.ForwardSlash);
}
// 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 (_ioHelper.PathStartsWith(path, _rootUrl, '/'))
{
return path.Substring(_rootUrl.Length).TrimStart(Constants.CharArrays.ForwardSlash);
}
// unchanged - what else?
return path.TrimStart(Constants.CharArrays.ForwardSlash);
@@ -296,11 +326,15 @@ namespace Umbraco.Cms.Core.IO
// 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 (_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
@@ -315,7 +349,10 @@ namespace Umbraco.Cms.Core.IO
// 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;
}
@@ -384,18 +421,29 @@ namespace Umbraco.Cms.Core.IO
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.");
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
@@ -442,11 +490,17 @@ namespace Umbraco.Cms.Core.IO
// 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 (e.GetType() != typeof(IOException))
{
throw;
}
// if we have tried enough, throw, else swallow
// the exception and retry after a pause
if (i == count) throw;
if (i == count)
{
throw;
}
}
Thread.Sleep(pausems);

View File

@@ -1,386 +1,473 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
namespace Umbraco.Cms.Core.IO
namespace Umbraco.Cms.Core.IO;
internal class ShadowFileSystem : IFileSystem
{
internal class ShadowFileSystem : IFileSystem
private readonly IFileSystem _sfs;
private Dictionary<string, ShadowNode>? _nodes;
public ShadowFileSystem(IFileSystem fs, IFileSystem sfs)
{
private readonly IFileSystem _fs;
private readonly IFileSystem _sfs;
Inner = fs;
_sfs = sfs;
}
public ShadowFileSystem(IFileSystem fs, IFileSystem sfs)
public IFileSystem Inner { get; }
public bool CanAddPhysical => true;
private Dictionary<string, ShadowNode> Nodes => _nodes ??= new Dictionary<string, ShadowNode>();
public IEnumerable<string> GetDirectories(string path)
{
var normPath = NormPath(path);
KeyValuePair<string, ShadowNode>[] shadows = Nodes.Where(kvp => IsChild(normPath, kvp.Key)).ToArray();
IEnumerable<string> directories = Inner.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)
{
_fs = fs;
_sfs = sfs;
return;
}
public IFileSystem Inner => _fs;
public void Complete()
var normPath = NormPath(path);
if (recursive)
{
if (_nodes == null) return;
var exceptions = new List<Exception>();
foreach (var kvp in _nodes)
Nodes[normPath] = new ShadowNode(true, true);
var remove = Nodes.Where(x => IsDescendant(normPath, x.Key)).ToList();
foreach (KeyValuePair<string, ShadowNode> kvp in remove)
{
if (kvp.Value.IsExist)
{
if (kvp.Value.IsFile)
{
try
{
if (_fs.CanAddPhysical)
{
_fs.AddFile(kvp.Key, _sfs.GetFullPath(kvp.Key)); // overwrite, move
}
else
{
using (Stream 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;
Nodes.Remove(kvp.Key);
}
public bool IsDelete { get; }
public bool IsDir { get; }
public bool IsExist => IsDelete == false;
public bool IsFile => IsDir == false;
Delete(path, true);
}
private static string NormPath(string path)
else
{
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)
// actual content
if (Nodes.Any(x => IsChild(normPath, x.Key) && x.Value.IsExist) // shadow content
|| Inner.GetDirectories(path).Any() || Inner.GetFiles(path).Any())
{
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);
throw new InvalidOperationException("Directory is not empty.");
}
Nodes[path] = new ShadowNode(true, true);
var remove = Nodes.Where(x => IsChild(normPath, x.Key)).ToList();
foreach (KeyValuePair<string, ShadowNode> kvp in remove)
{
Nodes.Remove(kvp.Key);
}
Delete(path, false);
}
}
public bool DirectoryExists(string path)
{
if (Nodes.TryGetValue(NormPath(path), out ShadowNode? sf))
{
return sf.IsDir && sf.IsExist;
}
return Inner.DirectoryExists(path);
}
public void AddFile(string path, Stream stream) => AddFile(path, stream, true);
public void AddFile(string path, Stream stream, bool overrideIfExists)
{
var normPath = NormPath(path);
if (Nodes.TryGetValue(normPath, out ShadowNode? sf) && sf.IsExist && (sf.IsDir || overrideIfExists == false))
{
throw new InvalidOperationException(string.Format("A file at path '{0}' already exists", path));
}
var parts = normPath.Split(Constants.CharArrays.ForwardSlash);
for (var i = 0; i < parts.Length - 1; i++)
{
var dirPath = string.Join("/", parts.Take(i + 1));
if (Nodes.TryGetValue(dirPath, out ShadowNode? sd))
{
if (sd.IsFile)
{
throw new InvalidOperationException("Invalid path.");
}
if (sd.IsDelete)
{
Nodes[dirPath] = new ShadowNode(false, 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(Constants.CharArrays.ForwardSlash);
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 (Inner.DirectoryExists(dirPath))
{
if (sd.IsFile) throw new InvalidOperationException("Invalid path.");
if (sd.IsDelete) Nodes[dirPath] = new ShadowNode(false, true);
continue;
}
else
if (Inner.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) => GetFiles(path, null);
public IEnumerable<string> GetFiles(string path, string? filter)
{
var normPath = NormPath(path);
KeyValuePair<string, ShadowNode>[] shadows = Nodes.Where(kvp => IsChild(normPath, kvp.Key)).ToArray();
IEnumerable<string> files = filter != null ? Inner.GetFiles(path, filter) : Inner.GetFiles(path);
WildcardExpression? 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)
{
if (Nodes.TryGetValue(NormPath(path), out ShadowNode? sf))
{
return sf.IsDir || sf.IsDelete ? Stream.Null : _sfs.OpenFile(path);
}
return Inner.OpenFile(path);
}
public void DeleteFile(string path)
{
if (FileExists(path) == false)
{
return;
}
Nodes[NormPath(path)] = new ShadowNode(true, false);
}
public bool FileExists(string path)
{
if (Nodes.TryGetValue(NormPath(path), out ShadowNode? sf))
{
return sf.IsFile && sf.IsExist;
}
return Inner.FileExists(path);
}
public string GetRelativePath(string fullPathOrUrl) => Inner.GetRelativePath(fullPathOrUrl);
public string GetFullPath(string path)
{
if (Nodes.TryGetValue(NormPath(path), out ShadowNode? sf))
{
return sf.IsDir || sf.IsDelete ? string.Empty : _sfs.GetFullPath(path);
}
return Inner.GetFullPath(path);
}
public string GetUrl(string? path) => Inner.GetUrl(path);
public DateTimeOffset GetLastModified(string path)
{
if (Nodes.TryGetValue(NormPath(path), out ShadowNode? sf) == false)
{
return Inner.GetLastModified(path);
}
if (sf.IsDelete)
{
throw new InvalidOperationException("Invalid path.");
}
return _sfs.GetLastModified(path);
}
public DateTimeOffset GetCreated(string path)
{
if (Nodes.TryGetValue(NormPath(path), out ShadowNode? sf) == false)
{
return Inner.GetCreated(path);
}
if (sf.IsDelete)
{
throw new InvalidOperationException("Invalid path.");
}
return _sfs.GetCreated(path);
}
public long GetSize(string path)
{
if (Nodes.TryGetValue(NormPath(path), out ShadowNode? sf) == false)
{
return Inner.GetSize(path);
}
if (sf.IsDelete || sf.IsDir)
{
throw new InvalidOperationException("Invalid path.");
}
return _sfs.GetSize(path);
}
public void AddFile(string path, string physicalPath, bool overrideIfExists = true, bool copy = false)
{
var normPath = NormPath(path);
if (Nodes.TryGetValue(normPath, out ShadowNode? sf) && sf.IsExist && (sf.IsDir || overrideIfExists == false))
{
throw new InvalidOperationException(string.Format("A file at path '{0}' already exists", path));
}
var parts = normPath.Split(Constants.CharArrays.ForwardSlash);
for (var i = 0; i < parts.Length - 1; i++)
{
var dirPath = string.Join("/", parts.Take(i + 1));
if (Nodes.TryGetValue(dirPath, out ShadowNode? sd))
{
if (sd.IsFile)
{
throw new InvalidOperationException("Invalid path.");
}
if (sd.IsDelete)
{
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)
{
if (Nodes.TryGetValue(NormPath(path), out ShadowNode? sf))
else
{
return sf.IsDir || sf.IsDelete ? Stream.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 ? string.Empty : _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(Constants.CharArrays.ForwardSlash);
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 (Inner.DirectoryExists(dirPath))
{
if (sd.IsFile) throw new InvalidOperationException("Invalid path.");
if (sd.IsDelete) Nodes[dirPath] = new ShadowNode(false, true);
continue;
}
else
if (Inner.FileExists(dirPath))
{
if (_fs.DirectoryExists(dirPath)) continue;
if (_fs.FileExists(dirPath)) throw new InvalidOperationException("Invalid path.");
Nodes[dirPath] = new ShadowNode(false, true);
throw new InvalidOperationException("Invalid path.");
}
Nodes[dirPath] = new ShadowNode(false, true);
}
}
_sfs.AddFile(path, physicalPath, overrideIfExists, copy);
Nodes[normPath] = new ShadowNode(false, false);
}
public void Complete()
{
if (_nodes == null)
{
return;
}
var exceptions = new List<Exception>();
foreach (KeyValuePair<string, ShadowNode> kvp in _nodes)
{
if (kvp.Value.IsExist)
{
if (kvp.Value.IsFile)
{
try
{
if (Inner.CanAddPhysical)
{
Inner.AddFile(kvp.Key, _sfs.GetFullPath(kvp.Key)); // overwrite, move
}
else
{
using (Stream stream = _sfs.OpenFile(kvp.Key))
{
Inner.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)
{
Inner.DeleteDirectory(kvp.Key, true);
}
else
{
Inner.DeleteFile(kvp.Key);
}
}
catch (Exception e)
{
exceptions.Add(new Exception(
"Could not delete " + (kvp.Value.IsDir ? "directory" : "file") + " \"" + kvp.Key + "\".", e));
}
}
_sfs.AddFile(path, physicalPath, overrideIfExists, copy);
Nodes[normPath] = new ShadowNode(false, false);
}
// copied from System.Web.Util.Wildcard internal
internal class WildcardExpression
_nodes.Clear();
if (exceptions.Count == 0)
{
private readonly string _pattern;
private readonly bool _caseInsensitive;
private Regex? _regex;
return;
}
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("(?=[\\\\:])");
throw new AggregateException("Failed to apply all changes (see exceptions).", exceptions);
}
public WildcardExpression(string pattern, bool caseInsensitive = true)
private static string NormPath(string path) => 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] == '/';
}
private void Delete(string path, bool recurse)
{
foreach (var file in Inner.GetFiles(path))
{
Nodes[NormPath(file)] = new ShadowNode(true, false);
}
foreach (var dir in Inner.GetDirectories(path))
{
Nodes[NormPath(dir)] = new ShadowNode(true, true);
if (recurse)
{
_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) ?? false;
Delete(dir, true);
}
}
}
// copied from System.Web.Util.Wildcard internal
internal class WildcardExpression
{
private static readonly Regex MetaRegex = new("[\\+\\{\\\\\\[\\|\\(\\)\\.\\^\\$]");
private static readonly Regex QuestRegex = new("\\?");
private static readonly Regex StarRegex = new("\\*");
private static readonly Regex CommaRegex = new(",");
private static readonly Regex SlashRegex = new("(?=/)");
private static readonly Regex BackslashRegex = new("(?=[\\\\:])");
private readonly bool _caseInsensitive;
private readonly string _pattern;
private Regex? _regex;
public WildcardExpression(string pattern, bool caseInsensitive = true)
{
_pattern = pattern;
_caseInsensitive = caseInsensitive;
}
public bool IsMatch(string input)
{
EnsureRegex(_pattern);
return _regex?.IsMatch(input) ?? false;
}
private void EnsureRegex(string pattern)
{
if (_regex != null)
{
return;
}
RegexOptions 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);
}
}
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;
}
}

View File

@@ -1,34 +1,26 @@
namespace Umbraco.Cms.Core.IO
namespace Umbraco.Cms.Core.IO;
// shadow filesystems is definitively ... too convoluted
internal class ShadowFileSystems : ICompletable
{
// shadow filesystems is definitively ... too convoluted
private readonly FileSystems _fileSystems;
private bool _completed;
internal class ShadowFileSystems : ICompletable
// invoked by the filesystems when shadowing
public ShadowFileSystems(FileSystems fileSystems, string id)
{
private readonly FileSystems _fileSystems;
private bool _completed;
_fileSystems = fileSystems;
Id = id;
// 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);
}
_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,233 +1,186 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.Hosting;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.IO
namespace Umbraco.Cms.Core.IO;
internal class ShadowWrapper : IFileSystem, IFileProviderFactory
{
internal class ShadowWrapper : IFileSystem, IFileProviderFactory
private static readonly string ShadowFsPath = Constants.SystemDirectories.TempData.EnsureEndsWith('/') + "ShadowFs";
private readonly IHostingEnvironment _hostingEnvironment;
private readonly IIOHelper _ioHelper;
private readonly Func<bool?>? _isScoped;
private readonly ILoggerFactory _loggerFactory;
private readonly string _shadowPath;
private string? _shadowDir;
private ShadowFileSystem? _shadowFileSystem;
public ShadowWrapper(IFileSystem innerFileSystem, IIOHelper ioHelper, IHostingEnvironment hostingEnvironment, ILoggerFactory loggerFactory, string shadowPath, Func<bool?>? isScoped = null)
{
private static readonly string ShadowFsPath = Constants.SystemDirectories.TempData.EnsureEndsWith('/') + "ShadowFs";
InnerFileSystem = innerFileSystem;
_ioHelper = ioHelper ?? throw new ArgumentNullException(nameof(ioHelper));
_hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment));
_loggerFactory = loggerFactory;
_shadowPath = shadowPath;
_isScoped = isScoped;
}
private readonly Func<bool?>? _isScoped;
private readonly IFileSystem _innerFileSystem;
private readonly string _shadowPath;
private ShadowFileSystem? _shadowFileSystem;
private string? _shadowDir;
private readonly IIOHelper _ioHelper;
private readonly IHostingEnvironment _hostingEnvironment;
private readonly ILoggerFactory _loggerFactory;
public IFileSystem InnerFileSystem { get; }
public ShadowWrapper(IFileSystem innerFileSystem, IIOHelper ioHelper, IHostingEnvironment hostingEnvironment, ILoggerFactory loggerFactory, string shadowPath, Func<bool?>? isScoped = null)
public bool CanAddPhysical => FileSystem.CanAddPhysical;
private IFileSystem FileSystem
{
get
{
_innerFileSystem = innerFileSystem;
_ioHelper = ioHelper ?? throw new ArgumentNullException(nameof(ioHelper));
_hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment));
_loggerFactory = loggerFactory;
_shadowPath = shadowPath;
_isScoped = isScoped;
}
public static string CreateShadowId(IHostingEnvironment hostingEnvironment)
{
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++)
if (_isScoped is not null && _shadowFileSystem is not null)
{
var id = GuidUtils.ToBase32String(Guid.NewGuid(), idLength);
var isScoped = _isScoped!();
var virt = ShadowFsPath + "/" + id;
var shadowDir = hostingEnvironment.MapPathContentRoot(virt);
if (Directory.Exists(shadowDir))
continue;
// 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.HasValue && isScoped.Value && _shadowFileSystem == null)
{
throw new Exception("The filesystems are shadowing, but this filesystem is not.");
}
Directory.CreateDirectory(shadowDir);
return id;
return isScoped.HasValue && isScoped.Value
? _shadowFileSystem
: InnerFileSystem;
}
throw new Exception($"Could not get a shadow identifier (tried {retries} times)");
return InnerFileSystem;
}
}
/// <inheritdoc />
public IFileProvider? Create() =>
InnerFileSystem.TryCreateFileProvider(out IFileProvider? fileProvider) ? fileProvider : null;
public IEnumerable<string> GetDirectories(string path) => 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) => 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) => FileSystem.GetFiles(path);
public IEnumerable<string> GetFiles(string path, string filter) => FileSystem.GetFiles(path, filter);
public Stream OpenFile(string path) => FileSystem.OpenFile(path);
public void DeleteFile(string path) => FileSystem.DeleteFile(path);
public bool FileExists(string path) => FileSystem.FileExists(path);
public string GetRelativePath(string fullPathOrUrl) => FileSystem.GetRelativePath(fullPathOrUrl);
public string GetFullPath(string path) => FileSystem.GetFullPath(path);
public string GetUrl(string? path) => FileSystem.GetUrl(path);
public DateTimeOffset GetLastModified(string path) => FileSystem.GetLastModified(path);
public DateTimeOffset GetCreated(string path) => FileSystem.GetCreated(path);
public long GetSize(string path) => FileSystem.GetSize(path);
public static string CreateShadowId(IHostingEnvironment hostingEnvironment)
{
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 = hostingEnvironment.MapPathContentRoot(virt);
if (Directory.Exists(shadowDir))
{
continue;
}
Directory.CreateDirectory(shadowDir);
return id;
}
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
throw new Exception($"Could not get a shadow identifier (tried {retries} times)");
}
var virt = Path.Combine(ShadowFsPath , id , _shadowPath);
_shadowDir = _hostingEnvironment.MapPathContentRoot(virt);
Directory.CreateDirectory(_shadowDir);
var tempfs = new PhysicalFileSystem(_ioHelper, _hostingEnvironment, _loggerFactory.CreateLogger<PhysicalFileSystem>(), _shadowDir, _hostingEnvironment.ToAbsolute(virt));
_shadowFileSystem = new ShadowFileSystem(_innerFileSystem, tempfs);
public void AddFile(string path, string physicalPath, bool overrideIfExists = true, bool copy = false) =>
FileSystem.AddFile(path, physicalPath, overrideIfExists, copy);
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 = Path.Combine(ShadowFsPath, id, _shadowPath);
_shadowDir = _hostingEnvironment.MapPathContentRoot(virt);
Directory.CreateDirectory(_shadowDir);
var tempfs = new PhysicalFileSystem(_ioHelper, _hostingEnvironment, _loggerFactory.CreateLogger<PhysicalFileSystem>(), _shadowDir, _hostingEnvironment.ToAbsolute(virt));
_shadowFileSystem = new ShadowFileSystem(InnerFileSystem, tempfs);
}
internal void UnShadow(bool complete)
{
ShadowFileSystem? 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();
}
}
internal void UnShadow(bool complete)
finally
{
var shadowFileSystem = _shadowFileSystem;
var dir = _shadowDir;
_shadowFileSystem = null;
_shadowDir = null;
// in any case, cleanup
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);
Directory.Delete(dir!, true);
// shadowPath make be path/to/dir, remove each
dir = dir!.Replace('/', Path.DirectorySeparatorChar);
var min = _hostingEnvironment.MapPathContentRoot(ShadowFsPath).Length;
var pos = dir.LastIndexOf(Path.DirectorySeparatorChar);
while (pos > min)
// shadowPath make be path/to/dir, remove each
dir = dir!.Replace('/', Path.DirectorySeparatorChar);
var min = _hostingEnvironment.MapPathContentRoot(ShadowFsPath).Length;
var pos = dir.LastIndexOf(Path.DirectorySeparatorChar);
while (pos > min)
{
dir = dir.Substring(0, pos);
if (Directory.EnumerateFileSystemEntries(dir).Any() == false)
{
dir = dir.Substring(0, pos);
if (Directory.EnumerateFileSystemEntries(dir).Any() == false)
Directory.Delete(dir, true);
else
break;
pos = dir.LastIndexOf(Path.DirectorySeparatorChar);
Directory.Delete(dir, true);
}
}
catch
{
// ugly, isn't it? but if we cannot cleanup, bah, just leave it there
else
{
break;
}
pos = dir.LastIndexOf(Path.DirectorySeparatorChar);
}
}
}
public IFileSystem InnerFileSystem => _innerFileSystem;
private IFileSystem FileSystem
{
get
catch
{
if (_isScoped is not null && _shadowFileSystem is not null)
{
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.HasValue && isScoped.Value && _shadowFileSystem == null)
throw new Exception("The filesystems are shadowing, but this filesystem is not.");
return isScoped.HasValue && isScoped.Value
? _shadowFileSystem
: _innerFileSystem;
}
return _innerFileSystem;
// ugly, isn't it? but if we cannot cleanup, bah, just leave it there
}
}
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);
}
/// <inheritdoc />
public IFileProvider? Create() => _innerFileSystem.TryCreateFileProvider(out IFileProvider? fileProvider) ? fileProvider : null;
}
}

View File

@@ -1,117 +1,120 @@
using System;
using System.IO;
using System.Linq;
using System.Text;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Web.Common.DependencyInjection;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.IO
namespace Umbraco.Cms.Core.IO;
public class ViewHelper : IViewHelper
{
public class ViewHelper : IViewHelper
{
private readonly IFileSystem _viewFileSystem;
private readonly IDefaultViewContentProvider _defaultViewContentProvider;
private readonly IDefaultViewContentProvider _defaultViewContentProvider;
private readonly IFileSystem _viewFileSystem;
public ViewHelper(FileSystems fileSystems, IDefaultViewContentProvider defaultViewContentProvider)
{
_viewFileSystem = fileSystems.MvcViewsFileSystem ?? throw new ArgumentNullException(nameof(fileSystems));
_defaultViewContentProvider = defaultViewContentProvider ?? throw new ArgumentNullException(nameof(defaultViewContentProvider));
}
}[Obsolete("Inject IDefaultViewContentProvider instead")]
public static string GetDefaultFileContent(string? layoutPageAlias = null, string? modelClassName = null, string? modelNamespace = null, string? modelNamespaceAlias = null)
{
IDefaultViewContentProvider viewContentProvider =
StaticServiceProvider.Instance.GetRequiredService<IDefaultViewContentProvider>();
return viewContentProvider.GetDefaultFileContent(layoutPageAlias, modelClassName, modelNamespace, modelNamespaceAlias);
}
public bool ViewExists(ITemplate t) => t.Alias is not null && _viewFileSystem.FileExists(ViewPath(t.Alias));
public bool ViewExists(ITemplate t) => t.Alias is not null && _viewFileSystem.FileExists(ViewPath(t.Alias));
public string GetFileContents(ITemplate t)
{
var viewContent = string.Empty;
var path = ViewPath(t.Alias ?? string.Empty);
public string GetFileContents(ITemplate t)
if (_viewFileSystem.FileExists(path))
{
var viewContent = "";
var path = ViewPath(t.Alias ?? string.Empty);
if (_viewFileSystem.FileExists(path))
using (var tr = new StreamReader(_viewFileSystem.OpenFile(path)))
{
using (var tr = new StreamReader(_viewFileSystem.OpenFile(path)))
{
viewContent = tr.ReadToEnd();
tr.Close();
}
viewContent = tr.ReadToEnd();
tr.Close();
}
return viewContent;
}
public string CreateView(ITemplate t, bool overWrite = false)
return viewContent;
}
public string CreateView(ITemplate t, bool overWrite = false)
{
string viewContent;
var path = ViewPath(t.Alias);
if (_viewFileSystem.FileExists(path) == false || overWrite)
{
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;
viewContent = SaveTemplateToFile(t);
}
private string SaveTemplateToFile(ITemplate template)
else
{
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))
using (var tr = new StreamReader(_viewFileSystem.OpenFile(path)))
{
_viewFileSystem.AddFile(path, ms, true);
viewContent = tr.ReadToEnd();
tr.Close();
}
return design;
}
public string? UpdateViewFile(ITemplate t, string? currentAlias = null)
{
var path = ViewPath(t.Alias);
return viewContent;
}
if (string.IsNullOrEmpty(currentAlias) == false && currentAlias != t.Alias)
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))
{
//then kill the old file..
var oldFile = ViewPath(currentAlias);
if (_viewFileSystem.FileExists(oldFile))
_viewFileSystem.DeleteFile(oldFile);
_viewFileSystem.DeleteFile(oldFile);
}
var data = Encoding.UTF8.GetBytes(t.Content ?? string.Empty);
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)
var data = Encoding.UTF8.GetBytes(t.Content ?? string.Empty);
var withBom = Encoding.UTF8.GetPreamble().Concat(data).ToArray();
using (var ms = new MemoryStream(withBom))
{
return _viewFileSystem.GetRelativePath(alias.Replace(" ", "") + ".cshtml");
_viewFileSystem.AddFile(path, ms, true);
}
private string EnsureInheritedLayout(ITemplate template)
return t.Content;
}
public string ViewPath(string alias) => _viewFileSystem.GetRelativePath(alias.Replace(" ", string.Empty) + ".cshtml");
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))
{
var design = template.Content;
if (string.IsNullOrEmpty(design))
design = _defaultViewContentProvider.GetDefaultFileContent(template.MasterTemplateAlias);
return design;
_viewFileSystem.AddFile(path, ms, true);
}
return design;
}
private string EnsureInheritedLayout(ITemplate template)
{
var design = template.Content;
if (string.IsNullOrEmpty(design))
{
design = _defaultViewContentProvider.GetDefaultFileContent(template.MasterTemplateAlias);
}
return design;
}
}