merge v10 to v11
This commit is contained in:
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)!;
|
||||
}
|
||||
|
||||
@@ -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)!;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user