diff --git a/src/Umbraco.StorageProviders/Abstractions/IStorageProvider.cs b/src/Umbraco.StorageProviders/Abstractions/IStorageProvider.cs new file mode 100644 index 0000000..302b51c --- /dev/null +++ b/src/Umbraco.StorageProviders/Abstractions/IStorageProvider.cs @@ -0,0 +1,91 @@ +using Microsoft.Extensions.FileProviders; + +namespace Umbraco.StorageProviders; + +/// +/// A read/write storage provider abstraction. +/// +/// +public interface IStorageProvider : IFileProvider +{ + /// + /// Create a writable stream to store file contents. Caller should dispose the stream when complete. + /// + /// The path/name of the file. + /// true if the file can be overwritten; otherwise, false. + /// + /// The file stream. + /// + Stream? CreateWriteStream(string subpath, bool overwrite = false); + + /// + /// Moves the specified file to a new location, providing the option to specify a new file name. + /// + /// The path/name of the source file. + /// The path/name of the destination file. This cannot be a directory. + /// true if the destination file can be overwritten; otherwise, false. + /// The cancellation token. + /// + /// Returns a value indicating whether the operation succeeded. + /// + Task MoveAsync(string sourceSubpath, string destinationSubpath, bool overwrite = false, CancellationToken cancellationToken = default); + + /// + /// Moves the specified directory to a new location. + /// + /// The path/name of the source directory. + /// The path/name of the destination directory. + /// true if files in the destination directory can be overwritten; otherwise, false. + /// true to recursively move subdirectories and files; otherwise, false. + /// The cancellation token. + /// + /// Returns a value indicating whether the operation succeeded. + /// + Task MoveDirectoryAsync(string sourceSubpath, string destinationSubpath, bool overwrite = false, bool recursive = false, CancellationToken cancellationToken = default); + + /// + /// Copies an existing file to a new file, providing the option to specify a new file name. + /// + /// The path/name of the source directory. + /// The path/name of the destination file. This cannot be a directory. + /// true if the destination file can be overwritten; otherwise, false. + /// The cancellation token. + /// + /// Returns a value indicating whether the operation succeeded. + /// + Task CopyAsync(string sourceSubpath, string destinationSubpath, bool overwrite = false, CancellationToken cancellationToken = default); + + /// + /// Copies an existing directory to a new directory. + /// + /// The path/name of the source directory. + /// The path/name of the destination directory. + /// true if files in the destination directory can be overwritten; otherwise, false. + /// true to recursively copy subdirectories and files; otherwise, false. + /// The cancellation token. + /// + /// Returns a value indicating whether the operation succeeded. + /// + Task CopyDirectoryAsync(string sourceSubpath, string destinationSubpath, bool overwrite = false, bool recursive = false, CancellationToken cancellationToken = default); + + /// + /// Deletes the specified file. + /// + /// The path/name of the file. + /// The cancellation token. + /// + /// Returns a value indicating whether the operation succeeded. + /// + Task DeleteAsync(string subpath, CancellationToken cancellationToken = default); + + /// + /// Deletes the specified directory. + /// + /// The path/name of the directory. + /// true to recursively remove subdirectories and files; otherwise, false. + /// The cancellation token. + /// + /// Returns a value indicating whether the operation succeeded. + /// + Task DeleteDirectoryAsync(string subpath, bool recursive = false, CancellationToken cancellationToken = default); +} diff --git a/src/Umbraco.StorageProviders/Abstractions/StorageProviderBase.cs b/src/Umbraco.StorageProviders/Abstractions/StorageProviderBase.cs new file mode 100644 index 0000000..363b27c --- /dev/null +++ b/src/Umbraco.StorageProviders/Abstractions/StorageProviderBase.cs @@ -0,0 +1,146 @@ +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Primitives; + +namespace Umbraco.StorageProviders; + +/// +/// Provides a generic implementation for most storage provider actions. +/// +/// +/// The default implementation where moving is based on copy/delete and copying requires reading/writing the file contents. +/// +/// +public abstract class StorageProviderBase : IStorageProvider +{ + /// + public abstract IDirectoryContents GetDirectoryContents(string subpath); + + /// + public abstract IFileInfo GetFileInfo(string subpath); + + /// + public virtual IChangeToken Watch(string filter) => NullChangeToken.Singleton; + + /// + public abstract Stream CreateWriteStream(string subpath, bool overwrite = false); + + /// + public virtual async Task MoveAsync(string sourceSubpath, string destinationSubpath, bool overwrite = false, CancellationToken cancellationToken = default) + => await CopyAsync(sourceSubpath, destinationSubpath, overwrite, cancellationToken).ConfigureAwait(false) && + await DeleteAsync(sourceSubpath, cancellationToken).ConfigureAwait(false); + + /// + public virtual async Task MoveDirectoryAsync(string sourceSubpath, string destinationSubpath, bool overwrite = false, bool recursive = false, CancellationToken cancellationToken = default) + { + var sourceDirectoryContents = GetDirectoryContents(sourceSubpath); + if (sourceDirectoryContents is null || sourceDirectoryContents.Exists is false) + { + return false; + } + + foreach (var sourceFileInfo in sourceDirectoryContents) + { + var sourcePath = CombinePath(sourceSubpath, sourceFileInfo.Name); + var destinationPath = CombinePath(destinationSubpath, sourceFileInfo.Name); + + if (sourceFileInfo.IsDirectory is false) + { + await MoveAsync(sourcePath, destinationPath, overwrite, cancellationToken).ConfigureAwait(false); + } + else if (recursive) + { + await MoveDirectoryAsync(sourcePath, destinationPath, overwrite, recursive, cancellationToken).ConfigureAwait(false); + } + } + + return true; + } + + /// + public virtual async Task CopyAsync(string sourceSubpath, string destinationSubpath, bool overwrite = false, CancellationToken cancellationToken = default) + => await CopyAsync(GetFileInfo(sourceSubpath), destinationSubpath, overwrite, cancellationToken).ConfigureAwait(false); + + /// + protected virtual async Task CopyAsync(IFileInfo sourceFileInfo, string destinationSubpath, bool overwrite = false, CancellationToken cancellationToken = default) + { + if (sourceFileInfo is null || sourceFileInfo.Exists is false || sourceFileInfo.IsDirectory) + { + return false; + } + + using (var source = sourceFileInfo.CreateReadStream()) + using (var destination = CreateWriteStream(destinationSubpath, overwrite)) + { + await source.CopyToAsync(destination, cancellationToken).ConfigureAwait(false); + } + + return true; + } + + /// + public virtual async Task CopyDirectoryAsync(string sourceSubpath, string destinationSubpath, bool overwrite = false, bool recursive = false, CancellationToken cancellationToken = default) + { + var sourceDirectoryContents = GetDirectoryContents(sourceSubpath); + if (sourceDirectoryContents is null || sourceDirectoryContents.Exists is false) + { + return false; + } + + foreach (var sourceFileInfo in sourceDirectoryContents) + { + var destinationPath = CombinePath(destinationSubpath, sourceFileInfo.Name); + + if (sourceFileInfo.IsDirectory is false) + { + await CopyAsync(sourceFileInfo, destinationPath, overwrite, cancellationToken).ConfigureAwait(false); + } + else if (recursive) + { + var sourcePath = CombinePath(sourceSubpath, sourceFileInfo.Name); + + await CopyDirectoryAsync(sourcePath, destinationPath, overwrite, recursive, cancellationToken).ConfigureAwait(false); + } + } + + return true; + } + + /// + public abstract Task DeleteAsync(string subpath, CancellationToken cancellationToken = default); + + /// + public virtual async Task DeleteDirectoryAsync(string subpath, bool recursive = false, CancellationToken cancellationToken = default) + { + var directoryContents = GetDirectoryContents(subpath); + if (directoryContents is null || directoryContents.Exists is false) + { + return false; + } + + foreach (var fileInfo in directoryContents) + { + var path = CombinePath(subpath, fileInfo.Name); + + if (fileInfo.IsDirectory is false) + { + await DeleteAsync(path, cancellationToken).ConfigureAwait(false); + } + else if (recursive) + { + await DeleteDirectoryAsync(path, recursive, cancellationToken).ConfigureAwait(false); + } + } + + return true; + } + + /// + /// Combines the path and file name. + /// + /// The path. + /// The file name. + /// + /// The combined path and file name. + /// + protected virtual string CombinePath(string path, string fileName) => Path.Combine(path, fileName); +} diff --git a/src/Umbraco.StorageProviders/FileSystem/FileSystemDirectoryContents.cs b/src/Umbraco.StorageProviders/FileSystem/FileSystemDirectoryContents.cs new file mode 100644 index 0000000..3997f6e --- /dev/null +++ b/src/Umbraco.StorageProviders/FileSystem/FileSystemDirectoryContents.cs @@ -0,0 +1,56 @@ +using System.Collections; +using Microsoft.Extensions.FileProviders; +using Umbraco.Cms.Core.IO; + +namespace Umbraco.StorageProviders.FileSystem; + +/// +/// Represents the directory contents in an . +/// +/// +public class FileSystemDirectoryContents : IDirectoryContents +{ + private readonly IFileSystem _fileSystem; + private readonly string _subpath; + private IEnumerable _entries = null!; + + /// + public bool Exists => true; + + /// + /// Initializes a new instance of the class. + /// + /// The file system. + /// The subpath. + /// + /// fileSystem + /// or + /// subpath + /// + public FileSystemDirectoryContents(IFileSystem fileSystem, string subpath) + { + _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); + _subpath = subpath ?? throw new ArgumentNullException(nameof(subpath)); + } + + /// + public IEnumerator GetEnumerator() + { + EnsureInitialized(); + return _entries.GetEnumerator(); + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + EnsureInitialized(); + return _entries.GetEnumerator(); + } + + private void EnsureInitialized() + { + _entries = _fileSystem.GetDirectories(_subpath).Select(d => new FileSystemDirectoryInfo(_fileSystem, d)) + .Union(_fileSystem.GetFiles(_subpath).Select(f => new FileSystemFileInfo(_fileSystem, f))) + .ToList(); + } +} diff --git a/src/Umbraco.StorageProviders/FileSystem/FileSystemDirectoryInfo.cs b/src/Umbraco.StorageProviders/FileSystem/FileSystemDirectoryInfo.cs new file mode 100644 index 0000000..289cbee --- /dev/null +++ b/src/Umbraco.StorageProviders/FileSystem/FileSystemDirectoryInfo.cs @@ -0,0 +1,46 @@ +using Microsoft.Extensions.FileProviders; +using Umbraco.Cms.Core.IO; + +namespace Umbraco.StorageProviders.FileSystem; + +/// +/// Represents a directory in an . +/// +/// +public class FileSystemDirectoryInfo : IFileInfo +{ + private readonly IFileSystem _fileSystem; + private readonly string _subpath; + + /// + public bool Exists => true; + + /// + public bool IsDirectory => true; + + /// + public DateTimeOffset LastModified => _fileSystem.GetLastModified(_subpath); + + /// + public long Length => -1; + + /// + public string Name => _fileSystem.GetRelativePath(_subpath); + + /// + public string PhysicalPath => null!; + + /// + /// Initializes a new instance of the class. + /// + /// The file system. + /// The subpath. + public FileSystemDirectoryInfo(IFileSystem fileSystem, string subpath) + { + _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); + _subpath = subpath ?? throw new ArgumentNullException(nameof(subpath)); + } + + /// + public Stream CreateReadStream() => throw new InvalidOperationException("Cannot create a stream for a directory."); +} diff --git a/src/Umbraco.StorageProviders/FileSystem/FileSystemFileInfo.cs b/src/Umbraco.StorageProviders/FileSystem/FileSystemFileInfo.cs new file mode 100644 index 0000000..cbd5dfb --- /dev/null +++ b/src/Umbraco.StorageProviders/FileSystem/FileSystemFileInfo.cs @@ -0,0 +1,46 @@ +using Microsoft.Extensions.FileProviders; +using Umbraco.Cms.Core.IO; + +namespace Umbraco.StorageProviders.FileSystem; + +/// +/// Represents a file in an . +/// +/// +public class FileSystemFileInfo : IFileInfo +{ + private readonly IFileSystem _fileSystem; + private readonly string _subpath; + + /// + public bool Exists => true; + + /// + public bool IsDirectory => false; + + /// + public DateTimeOffset LastModified => _fileSystem.GetLastModified(_subpath); + + /// + public long Length => _fileSystem.GetSize(_subpath); + + /// + public string Name => _fileSystem.GetRelativePath(_subpath); + + /// + public string PhysicalPath => null!; + + /// + /// Initializes a new instance of the class. + /// + /// The file system. + /// The subpath. + public FileSystemFileInfo(IFileSystem fileSystem, string subpath) + { + _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); + _subpath = subpath ?? throw new ArgumentNullException(nameof(subpath)); + } + + /// + public Stream CreateReadStream() => _fileSystem.OpenFile(_subpath); +} diff --git a/src/Umbraco.StorageProviders/FileSystem/FileSystemFileProvider.cs b/src/Umbraco.StorageProviders/FileSystem/FileSystemFileProvider.cs new file mode 100644 index 0000000..9809db6 --- /dev/null +++ b/src/Umbraco.StorageProviders/FileSystem/FileSystemFileProvider.cs @@ -0,0 +1,62 @@ +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Primitives; +using Umbraco.Cms.Core.IO; +using Umbraco.StorageProviders.FileSystem; + +namespace Umbraco.StorageProviders; + +/// +/// Exposes an as an . +/// +/// +public class FileSystemFileProvider : IFileProvider +{ + /// + /// The file system. + /// + protected IFileSystem FileSystem { get; } + + /// + /// The path prefix. + /// + protected string? PathPrefix { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The file system. + /// The path prefix. + /// fileSystem + public FileSystemFileProvider(IFileSystem fileSystem, string? pathPrefix = null) + { + FileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); + PathPrefix = pathPrefix; + } + + /// + public IDirectoryContents GetDirectoryContents(string subpath) + { + var path = PathPrefix + subpath; + if (path == null || !FileSystem.DirectoryExists(path)) + { + return NotFoundDirectoryContents.Singleton; + } + + return new FileSystemDirectoryContents(FileSystem, path); + } + + /// + public IFileInfo GetFileInfo(string subpath) + { + var path = PathPrefix + subpath; + if (!FileSystem.FileExists(path)) + { + return new NotFoundFileInfo(path); + } + + return new FileSystemFileInfo(FileSystem, path); + } + + /// + public IChangeToken Watch(string filter) => NullChangeToken.Singleton; +} diff --git a/src/Umbraco.StorageProviders/FileSystem/FileSystemStorageProvider.cs b/src/Umbraco.StorageProviders/FileSystem/FileSystemStorageProvider.cs new file mode 100644 index 0000000..5a52974 --- /dev/null +++ b/src/Umbraco.StorageProviders/FileSystem/FileSystemStorageProvider.cs @@ -0,0 +1,90 @@ +using Umbraco.Cms.Core.IO; + +namespace Umbraco.StorageProviders; + +/// +/// Exposes an as an . +/// +/// +/// +public class FileSystemStorageProvider : FileSystemFileProvider, IStorageProvider +{ + /// + /// Initializes a new instance of the class. + /// + /// The file system. + /// The path prefix. + public FileSystemStorageProvider(IFileSystem fileSystem, string? pathPrefix = null) + : base(fileSystem, pathPrefix) + { } + + ///// + //public void CopyFile(string sourceSubpath, string destinationSubpath, bool overwrite = false) + //{ + // var sourcePath = PathPrefix + sourceSubpath; + // var destinationPath = PathPrefix + destinationSubpath; + // if (sourcePath != null && destinationPath != null) + // { + // using var stream = FileSystem.OpenFile(sourcePath); + // FileSystem.AddFile(destinationPath, stream, overwrite); + // } + //} + + ///// + //public void DeleteDirectory(string subpath, bool recursive = false) + //{ + // var path = PathPrefix + subpath; + // if (path != null) + // { + // FileSystem.DeleteDirectory(path, recursive); + // } + //} + + ///// + //public void DeleteFile(string subpath) + //{ + // var path = PathPrefix + subpath; + // if (path != null) + // { + // FileSystem.DeleteFile(path); + // } + //} + + ///// + //public void MoveFile(string sourceSubpath, string destinationSubpath, bool overwrite = false) + //{ + // CopyFile(sourceSubpath, destinationSubpath, overwrite); + // DeleteFile(sourceSubpath); + //} + + ///// + //public void StoreFile(string subpath, Stream stream, bool overwrite = false) + //{ + // var path = PathPrefix + subpath; + // if (path != null) + // { + // FileSystem.AddFile(path, stream, overwrite); + // } + //} + + /// + public Stream? CreateWriteStream(string subpath, bool overwrite = false) => throw new NotImplementedException(); + + /// + public Task MoveAsync(string sourceSubpath, string destinationSubpath, bool overwrite = false, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + + /// + public Task MoveDirectoryAsync(string sourceSubpath, string destinationSubpath, bool overwrite = false, bool recursive = false, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + + /// + public Task CopyAsync(string sourceSubpath, string destinationSubpath, bool overwrite = false, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + + /// + public Task CopyDirectoryAsync(string sourceSubpath, string destinationSubpath, bool overwrite = false, bool recursive = false, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + + /// + public Task DeleteAsync(string subpath, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + + /// + public Task DeleteDirectoryAsync(string subpath, bool recursive = false, CancellationToken cancellationToken = default) => throw new NotImplementedException(); +} diff --git a/src/Umbraco.StorageProviders/Internal/PathUtils.cs b/src/Umbraco.StorageProviders/Internal/PathUtils.cs new file mode 100644 index 0000000..07d8e43 --- /dev/null +++ b/src/Umbraco.StorageProviders/Internal/PathUtils.cs @@ -0,0 +1,49 @@ +using Microsoft.Extensions.Primitives; + +namespace Umbraco.StorageProviders.Internal; + +internal static class PathUtils +{ + private static readonly char[] _invalidFileNameChars = Path.GetInvalidFileNameChars() + .Where(c => c != Path.DirectorySeparatorChar && c != Path.AltDirectorySeparatorChar).ToArray(); + + private static readonly char[] _pathSeparators = new[] + { + Path.DirectorySeparatorChar, + Path.AltDirectorySeparatorChar + }; + + internal static bool HasInvalidPathChars(string path) + { + return path.IndexOfAny(_invalidFileNameChars) != -1; + } + + internal static bool PathNavigatesAboveRoot(string path) + { + var tokenizer = new StringTokenizer(path, _pathSeparators); + int depth = 0; + + foreach (StringSegment segment in tokenizer) + { + if (segment.Equals(".") || segment.Equals("")) + { + continue; + } + else if (segment.Equals("..")) + { + depth--; + + if (depth == -1) + { + return true; + } + } + else + { + depth++; + } + } + + return false; + } +} diff --git a/src/Umbraco.StorageProviders/Physical/PhysicalStorageProvider.cs b/src/Umbraco.StorageProviders/Physical/PhysicalStorageProvider.cs new file mode 100644 index 0000000..38e10ba --- /dev/null +++ b/src/Umbraco.StorageProviders/Physical/PhysicalStorageProvider.cs @@ -0,0 +1,440 @@ +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.FileProviders.Physical; +using Umbraco.StorageProviders.Internal; + +namespace Umbraco.StorageProviders; + +/// +/// Storage provider for the on-disk file system. +/// +/// +/// +public class PhysicalStorageProvider : PhysicalFileProvider, IStorageProvider +{ + private static readonly char[] _pathSeparators = new[] + { + Path.DirectorySeparatorChar, + Path.AltDirectorySeparatorChar + }; + + /// + /// Initializes a new instance of the class. + /// + /// The root directory. This should be an absolute path. + public PhysicalStorageProvider(string root) + : base(root) + { } + + /// + /// Initializes a new instance of the class. + /// + /// The root directory. This should be an absolute path. + /// Specifies which files or directories are excluded. + public PhysicalStorageProvider(string root, ExclusionFilters filters) + : base(root, filters) + { } + + /// + public Stream? CreateWriteStream(string subpath, bool overwrite = false) + { + if (TryGetFullPath(subpath, out string fullPath)) + { + EnsureDirectory(fullPath); + + return File.Open(fullPath, overwrite ? FileMode.Create : FileMode.CreateNew, FileAccess.Write, FileShare.None); + } + + return null; + } + + /// + public Task MoveAsync(string sourceSubpath, string destinationSubpath, bool overwrite = false, CancellationToken cancellationToken = default) + { + var fileInfo = GetFileInfo(sourceSubpath); + if (fileInfo.Exists && + fileInfo.PhysicalPath is string sourceFullPath && + TryGetFullPath(destinationSubpath, out string destinationFullPath)) + { + EnsureDirectory(destinationFullPath); + + File.Move(sourceFullPath, destinationFullPath, overwrite); + + return Task.FromResult(true); + } + + return Task.FromResult(false); + } + + /// + public Task MoveDirectoryAsync(string sourceSubpath, string destinationSubpath, bool overwrite = false, bool recursive = false, CancellationToken cancellationToken = default) + { + var directoryContents = GetDirectoryContents(sourceSubpath); + if (directoryContents.Exists && + TryGetFullPath(sourceSubpath, out string sourceFullPath) && + TryGetFullPath(destinationSubpath, out string destinationFullPath)) + { + EnsureDirectory(destinationFullPath); + + bool deleteSourceDirectory = true; + foreach (var fileInfo in directoryContents) + { + if (fileInfo.PhysicalPath is string sourceFilePath) + { + var relativeSourcePath = Path.GetRelativePath(sourceFullPath, sourceFilePath); + var destinationFilePath = Path.Combine(destinationFullPath, relativeSourcePath); + + if (!fileInfo.IsDirectory) + { + File.Move(sourceFilePath, destinationFilePath, overwrite); + } + else if (recursive) + { + MoveDirectoryAsync(sourceFilePath, destinationFilePath); + } + else + { + // We have subdirectories, but aren't recursively moving them, so can't delete source directory + deleteSourceDirectory = false; + } + } + + + } + + var directoryInfo = new DirectoryInfo(sourceFullPath); + if (directoryInfo.Exists) + { + EnsureDirectory(destinationSubpath); + + // Move files + foreach (var fileInfo in directoryInfo.EnumerateFiles()) + { + var destinationFilePath = Path.Combine(destinationFullPath, Path.GetRelativePath(sourceFullPath, fileInfo.FullName)); + + fileInfo.MoveTo(destinationFilePath, overwrite); + } + + if (recursive) + { + foreach (var subDirectoryInfo in directoryInfo.EnumerateDirectories()) + { + + } + } + + return Task.FromResult(true); + } + } + + return Task.FromResult(false); + } + + /// + public Task CopyAsync(string sourceSubpath, string destinationSubpath, bool overwrite = false, CancellationToken cancellationToken = default) + { + if (TryGetFullPath(sourceSubpath, out string sourceFullPath) && + TryGetFullPath(destinationSubpath, out string destinationFullPath)) + { + var fileInfo = new FileInfo(sourceFullPath); + if (fileInfo.Exists) + { + EnsureDirectory(destinationSubpath); + + fileInfo.CopyTo(destinationFullPath, overwrite); + + return Task.FromResult(true); + } + } + + return Task.FromResult(false); + } + + /// + public Task CopyDirectoryAsync(string sourceSubpath, string destinationSubpath, bool overwrite = false, bool recursive = false, CancellationToken cancellationToken = default) + { + if (TryGetFullPath(sourceSubpath, out string sourceFullPath) && + TryGetFullPath(destinationSubpath, out string destinationFullPath)) + { + var directoryInfo = new DirectoryInfo(sourceFullPath); + if (directoryInfo.Exists) + { + + + return Task.FromResult(true); + } + } + + return Task.FromResult(false); + } + + /// + public Task DeleteAsync(string subpath, CancellationToken cancellationToken = default) + { + if (TryGetFullPath(subpath, out string fullPath)) + { + var fileInfo = new FileInfo(subpath); + if (fileInfo.Exists) + { + fileInfo.Delete(); + + return Task.FromResult(true); + } + } + + return Task.FromResult(false); + } + + /// + public Task DeleteDirectoryAsync(string subpath, bool recursive = false, CancellationToken cancellationToken = default) + { + if (TryGetFullPath(subpath, out string fullPath)) + { + var directoryInfo = new DirectoryInfo(subpath); + if (directoryInfo.Exists) + { + directoryInfo.Delete(recursive); + + return Task.FromResult(true); + } + } + + return Task.FromResult(false); + } + + private bool TryGetFullPath(string subpath, out string fullPath) + { + if (string.IsNullOrEmpty(subpath) || PathUtils.HasInvalidPathChars(subpath)) + { + fullPath = null!; + return false; + } + + // Relative paths starting with leading slashes are okay + subpath = subpath.TrimStart(_pathSeparators); + + // Absolute paths not permitted + if (Path.IsPathRooted(subpath)) + { + fullPath = null!; + return false; + } + + if (PathUtils.PathNavigatesAboveRoot(subpath)) + { + fullPath = null!; + return false; + } + + try + { + fullPath = Path.GetFullPath(Path.Combine(Root, subpath)); + } + catch + { + fullPath = null!; + return false; + } + + if (!fullPath.StartsWith(Root, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + return true; + } + + private static void EnsureDirectory(string fullPath) + { + var directoryName = Path.GetDirectoryName(fullPath); + if (directoryName is not null && !Directory.Exists(directoryName)) + { + Directory.CreateDirectory(directoryName); + } + } +} + +//private class Temp +//{ +// private static readonly char[] _pathSeparators = new[] +// { +// Path.DirectorySeparatorChar, +// Path.AltDirectorySeparatorChar +// }; + +// /// +// public void CopyFile(string sourceSubpath, string destinationSubpath, bool overwrite = false) +// { +// if (string.IsNullOrEmpty(sourceSubpath) || PathUtils.HasInvalidPathChars(sourceSubpath) || +// string.IsNullOrEmpty(destinationSubpath) || PathUtils.HasInvalidPathChars(destinationSubpath)) +// { +// return; +// } + +// // Relative paths starting with leading slashes are okay +// sourceSubpath = sourceSubpath.TrimStart(_pathSeparators); +// destinationSubpath = destinationSubpath.TrimStart(_pathSeparators); + +// // Absolute paths not permitted. +// if (Path.IsPathRooted(sourceSubpath) || Path.IsPathRooted(destinationSubpath)) +// { +// return; +// } + +// string? sourceFullPath = GetFullPath(sourceSubpath); +// if (sourceFullPath == null) +// { +// return; +// } + +// string? destinationFullPath = GetFullPath(destinationSubpath); +// if (destinationFullPath == null) +// { +// return; +// } + +// File.Copy(sourceFullPath, destinationFullPath, overwrite); +// } + +// /// +// public void DeleteDirectory(string subpath, bool recursive = false) +// { +// if (subpath == null || PathUtils.HasInvalidPathChars(subpath)) +// { +// return; +// } + +// // Relative paths starting with leading slashes are okay +// subpath = subpath.TrimStart(_pathSeparators); + +// // Absolute paths not permitted. +// if (Path.IsPathRooted(subpath)) +// { +// return; +// } + +// string? fullPath = GetFullPath(subpath); +// if (fullPath == null || !Directory.Exists(fullPath)) +// { +// return; +// } + +// Directory.Delete(fullPath, recursive); +// } + +// /// +// public void DeleteFile(string subpath) +// { +// if (string.IsNullOrEmpty(subpath) || PathUtils.HasInvalidPathChars(subpath)) +// { +// return; +// } + +// // Relative paths starting with leading slashes are okay +// subpath = subpath.TrimStart(_pathSeparators); + +// // Absolute paths not permitted. +// if (Path.IsPathRooted(subpath)) +// { +// return; +// } + +// string? fullPath = GetFullPath(subpath); +// if (fullPath == null || !Directory.Exists(fullPath)) +// { +// return; +// } + +// File.Delete(fullPath); +// } + +// /// +// public void MoveFile(string sourceSubpath, string destinationSubpath, bool overwrite = false) +// { +// if (string.IsNullOrEmpty(sourceSubpath) || PathUtils.HasInvalidPathChars(sourceSubpath) || +// string.IsNullOrEmpty(destinationSubpath) || PathUtils.HasInvalidPathChars(destinationSubpath)) +// { +// return; +// } + +// // Relative paths starting with leading slashes are okay +// sourceSubpath = sourceSubpath.TrimStart(_pathSeparators); +// destinationSubpath = destinationSubpath.TrimStart(_pathSeparators); + +// // Absolute paths not permitted. +// if (Path.IsPathRooted(sourceSubpath) || Path.IsPathRooted(destinationSubpath)) +// { +// return; +// } + +// string? sourceFullPath = GetFullPath(sourceSubpath); +// if (sourceFullPath == null) +// { +// return; +// } + +// string? destinationFullPath = GetFullPath(destinationSubpath); +// if (destinationFullPath == null) +// { +// return; +// } + +// File.Move(sourceFullPath, destinationFullPath, overwrite); +// } + +// /// +// public void StoreFile(string subpath, Stream stream, bool overwrite = false) +// { +// if (string.IsNullOrEmpty(subpath) || PathUtils.HasInvalidPathChars(subpath) || stream is null) +// { +// return; +// } + +// // Relative paths starting with leading slashes are okay +// subpath = subpath.TrimStart(_pathSeparators); + +// // Absolute paths not permitted. +// if (Path.IsPathRooted(subpath)) +// { +// return; +// } + +// string? fullPath = GetFullPath(subpath); +// if (fullPath == null || !Directory.Exists(fullPath)) +// { +// return; +// } + +// using var fileStream = new FileStream(fullPath, overwrite ? FileMode.Create : FileMode.CreateNew, FileAccess.Write); +// stream.CopyTo(fileStream); +// } + +// private string? GetFullPath(string path) +// { +// if (PathUtils.PathNavigatesAboveRoot(path)) +// { +// return null; +// } + +// string fullPath; +// try +// { +// fullPath = Path.GetFullPath(Path.Combine(Root, path)); +// } +// catch +// { +// return null; +// } + +// if (!IsUnderneathRoot(fullPath)) +// { +// return null; +// } + +// return fullPath; +// } + +// private bool IsUnderneathRoot(string fullPath) +// { +// return fullPath.StartsWith(Root, StringComparison.OrdinalIgnoreCase); +// } +//}