Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add IStorageProvider implementation based on IFileProvider #26

Draft
wants to merge 11 commits into
base: develop
Choose a base branch
from
Draft
91 changes: 91 additions & 0 deletions src/Umbraco.StorageProviders/Abstractions/IStorageProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
using Microsoft.Extensions.FileProviders;

namespace Umbraco.StorageProviders;

/// <summary>
/// A read/write storage provider abstraction.
/// </summary>
/// <seealso cref="Microsoft.Extensions.FileProviders.IFileProvider" />
public interface IStorageProvider : IFileProvider
{
/// <summary>
/// Create a writable stream to store file contents. Caller should dispose the stream when complete.
/// </summary>
/// <param name="subpath">The path/name of the file.</param>
/// <param name="overwrite"><c>true</c> if the file can be overwritten; otherwise, <c>false</c>.</param>
/// <returns>
/// The file stream.
/// </returns>
Stream? CreateWriteStream(string subpath, bool overwrite = false);

/// <summary>
/// Moves the specified file to a new location, providing the option to specify a new file name.
/// </summary>
/// <param name="sourceSubpath">The path/name of the source file.</param>
/// <param name="destinationSubpath">The path/name of the destination file. This cannot be a directory.</param>
/// <param name="overwrite"><c>true</c> if the destination file can be overwritten; otherwise, <c>false</c>.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>
/// Returns a value indicating whether the operation succeeded.
/// </returns>
Task<bool> MoveAsync(string sourceSubpath, string destinationSubpath, bool overwrite = false, CancellationToken cancellationToken = default);

/// <summary>
/// Moves the specified directory to a new location.
/// </summary>
/// <param name="sourceSubpath">The path/name of the source directory.</param>
/// <param name="destinationSubpath">The path/name of the destination directory.</param>
/// <param name="overwrite"><c>true</c> if files in the destination directory can be overwritten; otherwise, <c>false</c>.</param>
/// <param name="recursive"><c>true</c> to recursively move subdirectories and files; otherwise, <c>false</c>.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>
/// Returns a value indicating whether the operation succeeded.
/// </returns>
Task<bool> MoveDirectoryAsync(string sourceSubpath, string destinationSubpath, bool overwrite = false, bool recursive = false, CancellationToken cancellationToken = default);

/// <summary>
/// Copies an existing file to a new file, providing the option to specify a new file name.
/// </summary>
/// <param name="sourceSubpath">The path/name of the source directory.</param>
/// <param name="destinationSubpath">The path/name of the destination file. This cannot be a directory.</param>
/// <param name="overwrite"><c>true</c> if the destination file can be overwritten; otherwise, <c>false</c>.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>
/// Returns a value indicating whether the operation succeeded.
/// </returns>
Task<bool> CopyAsync(string sourceSubpath, string destinationSubpath, bool overwrite = false, CancellationToken cancellationToken = default);

/// <summary>
/// Copies an existing directory to a new directory.
/// </summary>
/// <param name="sourceSubpath">The path/name of the source directory.</param>
/// <param name="destinationSubpath">The path/name of the destination directory.</param>
/// <param name="overwrite"><c>true</c> if files in the destination directory can be overwritten; otherwise, <c>false</c>.</param>
/// <param name="recursive"><c>true</c> to recursively copy subdirectories and files; otherwise, <c>false</c>.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>
/// Returns a value indicating whether the operation succeeded.
/// </returns>
Task<bool> CopyDirectoryAsync(string sourceSubpath, string destinationSubpath, bool overwrite = false, bool recursive = false, CancellationToken cancellationToken = default);

/// <summary>
/// Deletes the specified file.
/// </summary>
/// <param name="subpath">The path/name of the file.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>
/// Returns a value indicating whether the operation succeeded.
/// </returns>
Task<bool> DeleteAsync(string subpath, CancellationToken cancellationToken = default);

/// <summary>
/// Deletes the specified directory.
/// </summary>
/// <param name="subpath">The path/name of the directory.</param>
/// <param name="recursive"><c>true</c> to recursively remove subdirectories and files; otherwise, <c>false</c>.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>
/// Returns a value indicating whether the operation succeeded.
/// </returns>
Task<bool> DeleteDirectoryAsync(string subpath, bool recursive = false, CancellationToken cancellationToken = default);
}
146 changes: 146 additions & 0 deletions src/Umbraco.StorageProviders/Abstractions/StorageProviderBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Primitives;

namespace Umbraco.StorageProviders;

/// <summary>
/// Provides a generic implementation for most storage provider actions.
/// </summary>
/// <remarks>
/// The default implementation where moving is based on copy/delete and copying requires reading/writing the file contents.
/// </remarks>
/// <seealso cref="Umbraco.StorageProviders.IStorageProvider" />
public abstract class StorageProviderBase : IStorageProvider
{
/// <inheritdoc />
public abstract IDirectoryContents GetDirectoryContents(string subpath);

/// <inheritdoc />
public abstract IFileInfo GetFileInfo(string subpath);

/// <inheritdoc />
public virtual IChangeToken Watch(string filter) => NullChangeToken.Singleton;

/// <inheritdoc />
public abstract Stream CreateWriteStream(string subpath, bool overwrite = false);

/// <inheritdoc />
public virtual async Task<bool> 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);

/// <inheritdoc />
public virtual async Task<bool> 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;
}

/// <inheritdoc />
public virtual async Task<bool> CopyAsync(string sourceSubpath, string destinationSubpath, bool overwrite = false, CancellationToken cancellationToken = default)
=> await CopyAsync(GetFileInfo(sourceSubpath), destinationSubpath, overwrite, cancellationToken).ConfigureAwait(false);

/// <inheritdoc />
protected virtual async Task<bool> 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;
}

/// <inheritdoc />
public virtual async Task<bool> 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;
}

/// <inheritdoc />
public abstract Task<bool> DeleteAsync(string subpath, CancellationToken cancellationToken = default);

/// <inheritdoc />
public virtual async Task<bool> 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;
}

/// <summary>
/// Combines the path and file name.
/// </summary>
/// <param name="path">The path.</param>
/// <param name="fileName">The file name.</param>
/// <returns>
/// The combined path and file name.
/// </returns>
protected virtual string CombinePath(string path, string fileName) => Path.Combine(path, fileName);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using System.Collections;
using Microsoft.Extensions.FileProviders;
using Umbraco.Cms.Core.IO;

namespace Umbraco.StorageProviders.FileSystem;

/// <summary>
/// Represents the directory contents in an <see cref="IFileSystem" />.
/// </summary>
/// <seealso cref="Microsoft.Extensions.FileProviders.IDirectoryContents" />
public class FileSystemDirectoryContents : IDirectoryContents
{
private readonly IFileSystem _fileSystem;
private readonly string _subpath;
private IEnumerable<IFileInfo> _entries = null!;

/// <inheritdoc />
public bool Exists => true;

/// <summary>
/// Initializes a new instance of the <see cref="FileSystemDirectoryContents"/> class.
/// </summary>
/// <param name="fileSystem">The file system.</param>
/// <param name="subpath">The subpath.</param>
/// <exception cref="System.ArgumentNullException">
/// fileSystem
/// or
/// subpath
/// </exception>
public FileSystemDirectoryContents(IFileSystem fileSystem, string subpath)
{
_fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem));
_subpath = subpath ?? throw new ArgumentNullException(nameof(subpath));
}

/// <inheritdoc />
public IEnumerator<IFileInfo> GetEnumerator()
{
EnsureInitialized();
return _entries.GetEnumerator();
}

/// <inheritdoc />
IEnumerator IEnumerable.GetEnumerator()
{
EnsureInitialized();
return _entries.GetEnumerator();
}

private void EnsureInitialized()
{
_entries = _fileSystem.GetDirectories(_subpath).Select<string, IFileInfo>(d => new FileSystemDirectoryInfo(_fileSystem, d))
.Union(_fileSystem.GetFiles(_subpath).Select<string, IFileInfo>(f => new FileSystemFileInfo(_fileSystem, f)))
.ToList();
}
}
46 changes: 46 additions & 0 deletions src/Umbraco.StorageProviders/FileSystem/FileSystemDirectoryInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using Microsoft.Extensions.FileProviders;
using Umbraco.Cms.Core.IO;

namespace Umbraco.StorageProviders.FileSystem;

/// <summary>
/// Represents a directory in an <see cref="IFileSystem" />.
/// </summary>
/// <seealso cref="Microsoft.Extensions.FileProviders.IFileInfo" />
public class FileSystemDirectoryInfo : IFileInfo
{
private readonly IFileSystem _fileSystem;
private readonly string _subpath;

/// <inheritdoc />
public bool Exists => true;

/// <inheritdoc />
public bool IsDirectory => true;

/// <inheritdoc />
public DateTimeOffset LastModified => _fileSystem.GetLastModified(_subpath);

/// <inheritdoc />
public long Length => -1;

/// <inheritdoc />
public string Name => _fileSystem.GetRelativePath(_subpath);

/// <inheritdoc />
public string PhysicalPath => null!;

/// <summary>
/// Initializes a new instance of the <see cref="FileSystemDirectoryInfo" /> class.
/// </summary>
/// <param name="fileSystem">The file system.</param>
/// <param name="subpath">The subpath.</param>
public FileSystemDirectoryInfo(IFileSystem fileSystem, string subpath)
{
_fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem));
_subpath = subpath ?? throw new ArgumentNullException(nameof(subpath));
}

/// <inheritdoc />
public Stream CreateReadStream() => throw new InvalidOperationException("Cannot create a stream for a directory.");
}
46 changes: 46 additions & 0 deletions src/Umbraco.StorageProviders/FileSystem/FileSystemFileInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using Microsoft.Extensions.FileProviders;
using Umbraco.Cms.Core.IO;

namespace Umbraco.StorageProviders.FileSystem;

/// <summary>
/// Represents a file in an <see cref="IFileSystem" />.
/// </summary>
/// <seealso cref="Microsoft.Extensions.FileProviders.IFileInfo" />
public class FileSystemFileInfo : IFileInfo
{
private readonly IFileSystem _fileSystem;
private readonly string _subpath;

/// <inheritdoc />
public bool Exists => true;

/// <inheritdoc />
public bool IsDirectory => false;

/// <inheritdoc />
public DateTimeOffset LastModified => _fileSystem.GetLastModified(_subpath);

/// <inheritdoc />
public long Length => _fileSystem.GetSize(_subpath);

/// <inheritdoc />
public string Name => _fileSystem.GetRelativePath(_subpath);

/// <inheritdoc />
public string PhysicalPath => null!;

/// <summary>
/// Initializes a new instance of the <see cref="FileSystemFileInfo" /> class.
/// </summary>
/// <param name="fileSystem">The file system.</param>
/// <param name="subpath">The subpath.</param>
public FileSystemFileInfo(IFileSystem fileSystem, string subpath)
{
_fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem));
_subpath = subpath ?? throw new ArgumentNullException(nameof(subpath));
}

/// <inheritdoc />
public Stream CreateReadStream() => _fileSystem.OpenFile(_subpath);
}
Loading