Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 42 additions & 3 deletions src/Ramstack.FileSystem.Globbing/GlobbingDirectory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,8 @@ protected override async IAsyncEnumerable<VirtualFile> GetFilesCoreAsync([Enumer
if (_included)
{
await foreach (var file in _directory.GetFilesAsync(cancellationToken).ConfigureAwait(false))
{
if (_fs.IsFileIncluded(file.FullName))
yield return new GlobbingFile(_fs, file, included: true);
}
}
}

Expand All @@ -93,10 +91,51 @@ protected override async IAsyncEnumerable<VirtualDirectory> GetDirectoriesCoreAs
if (_included)
{
await foreach (var directory in _directory.GetDirectoriesAsync(cancellationToken).ConfigureAwait(false))
{
if (_fs.IsDirectoryIncluded(directory.FullName))
yield return new GlobbingDirectory(_fs, directory, included: true);
}
}

/// <inheritdoc />
protected override async IAsyncEnumerable<VirtualNode> GetFileNodesCoreAsync(string[] patterns, string[]? excludes, [EnumeratorCancellation] CancellationToken cancellationToken)
{
if (_included)
{
await foreach (var node in _directory.GetFileNodesAsync(patterns, excludes, cancellationToken).ConfigureAwait(false))
{
if (node is VirtualFile file)
{
if (_fs.IsFileIncluded(file.FullName))
yield return new GlobbingFile(_fs, file, included: true);
}
else
{
if (_fs.IsDirectoryIncluded(node.FullName))
yield return new GlobbingDirectory(_fs, (VirtualDirectory)node, included: true);
}
}
}
}

/// <inheritdoc />
protected override async IAsyncEnumerable<VirtualFile> GetFilesCoreAsync(string[] patterns, string[]? excludes, [EnumeratorCancellation] CancellationToken cancellationToken)
{
if (_included)
{
await foreach (var file in _directory.GetFilesAsync(patterns, excludes, cancellationToken).ConfigureAwait(false))
if (_fs.IsFileIncluded(file.FullName))
yield return new GlobbingFile(_fs, file, included: true);
}
}

/// <inheritdoc />
protected override async IAsyncEnumerable<VirtualDirectory> GetDirectoriesCoreAsync(string[] patterns, string[]? excludes, [EnumeratorCancellation] CancellationToken cancellationToken)
{
if (_included)
{
await foreach (var directory in _directory.GetDirectoriesAsync(patterns, excludes, cancellationToken).ConfigureAwait(false))
if (_fs.IsDirectoryIncluded(directory.FullName))
yield return new GlobbingDirectory(_fs, directory, included: true);
}
}
}
47 changes: 41 additions & 6 deletions src/Ramstack.FileSystem.Prefixed/PrefixedDirectory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,20 +60,55 @@ protected override async IAsyncEnumerable<VirtualNode> GetFileNodesCoreAsync([En
/// <inheritdoc />
protected override async IAsyncEnumerable<VirtualFile> GetFilesCoreAsync([EnumeratorCancellation] CancellationToken cancellationToken)
{
await foreach (var node in _directory.GetFilesAsync(cancellationToken).ConfigureAwait(false))
await foreach (var file in _directory.GetFilesAsync(cancellationToken).ConfigureAwait(false))
{
var path = VirtualPath.Join(FullName, node.Name);
yield return new PrefixedFile(_fs, path, node);
var path = VirtualPath.Join(FullName, file.Name);
yield return new PrefixedFile(_fs, path, file);
}
}

/// <inheritdoc />
protected override async IAsyncEnumerable<VirtualDirectory> GetDirectoriesCoreAsync([EnumeratorCancellation] CancellationToken cancellationToken)
{
await foreach (var node in _directory.GetDirectoriesAsync(cancellationToken).ConfigureAwait(false))
await foreach (var directory in _directory.GetDirectoriesAsync(cancellationToken).ConfigureAwait(false))
{
var path = VirtualPath.Join(FullName, node.Name);
yield return new PrefixedDirectory(_fs, path, node);
var path = VirtualPath.Join(FullName, directory.Name);
yield return new PrefixedDirectory(_fs, path, directory);
}
}

/// <inheritdoc />
protected override async IAsyncEnumerable<VirtualNode> GetFileNodesCoreAsync(string[] patterns, string[]? excludes, [EnumeratorCancellation] CancellationToken cancellationToken)
{
await foreach (var node in _directory.GetFileNodesAsync(patterns, excludes, cancellationToken).ConfigureAwait(false))
{
var path = _fs.WrapWithPrefix(node.FullName);

yield return node switch
{
VirtualDirectory directory => new PrefixedDirectory(_fs, path, directory),
_ => new PrefixedFile(_fs, path, (VirtualFile)node)
};
}
}

/// <inheritdoc />
protected override async IAsyncEnumerable<VirtualFile> GetFilesCoreAsync(string[] patterns, string[]? excludes, [EnumeratorCancellation] CancellationToken cancellationToken)
{
await foreach (var file in _directory.GetFilesAsync(patterns, excludes, cancellationToken).ConfigureAwait(false))
{
var path = _fs.WrapWithPrefix(file.FullName);
yield return new PrefixedFile(_fs, path, file);
}
}

/// <inheritdoc />
protected override async IAsyncEnumerable<VirtualDirectory> GetDirectoriesCoreAsync(string[] patterns, string[]? excludes, [EnumeratorCancellation] CancellationToken cancellationToken)
{
await foreach (var directory in _directory.GetDirectoriesAsync(patterns, excludes, cancellationToken).ConfigureAwait(false))
{
var path = _fs.WrapWithPrefix(directory.FullName);
yield return new PrefixedDirectory(_fs, path, directory);
}
}
}
35 changes: 25 additions & 10 deletions src/Ramstack.FileSystem.Prefixed/PrefixedFileSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public VirtualFile GetFile(string path)
{
path = VirtualPath.Normalize(path);

var underlying = TryGetPath(path, _prefix);
var underlying = TryUnwrapPrefix(path, _prefix);
if (underlying is not null)
return new PrefixedFile(this, path, _fs.GetFile(underlying));

Expand All @@ -72,7 +72,7 @@ public VirtualDirectory GetDirectory(string path)
if (directory.FullName == path)
return directory;

var underlying = TryGetPath(path, _prefix);
var underlying = TryUnwrapPrefix(path, _prefix);
if (underlying is not null)
return new PrefixedDirectory(this, path, _fs.GetDirectory(underlying));

Expand All @@ -84,23 +84,38 @@ public void Dispose() =>
_fs.Dispose();

/// <summary>
/// Attempts to match a given path against the prefix. If successful, returns the remainder of the path relative to the prefix.
/// Converts a path from the wrapped file system to a full path in this prefixed file system.
/// </summary>
/// <param name="path">The full path to match against the prefix.</param>
/// <param name="underlyingPath">The path relative to the wrapped file system (must be normalized).</param>
/// <returns>
/// The full path including this file system's prefix.
/// </returns>
internal string WrapWithPrefix(string underlyingPath)
{
Debug.Assert(VirtualPath.IsNormalized(underlyingPath));

if (underlyingPath == "/")
return _prefix;

return VirtualPath.Join(_prefix, underlyingPath);
}

/// <summary>
/// Attempts to extract the underlying path by removing this file system's prefix from a full path.
/// </summary>
/// <param name="path">The full path that may include this file system's prefix (must be normalized).</param>
/// <param name="prefix">The prefix to compare against the path.</param>
/// <returns>
/// The relative path if the prefix matches; otherwise, null.
/// The underlying path relative to the wrapped file system if the prefix matches;
/// otherwise, <see langword="null"/> if the path doesn't belong to this prefixed file system.
/// </returns>
private static string? TryGetPath(string path, string prefix)
private static string? TryUnwrapPrefix(string path, string prefix)
{
Debug.Assert(path == VirtualPath.Normalize(path));
Debug.Assert(VirtualPath.IsNormalized(path));

if (path == prefix)
return "/";

// TODO: Consider adding support for different file casing options.
// FileSystemCasing? FilePathCasing?

if (path.StartsWith(prefix, StringComparison.Ordinal) && path[prefix.Length] == '/')
return new string(path.AsSpan(prefix.Length));

Expand Down
35 changes: 31 additions & 4 deletions src/Ramstack.FileSystem.Readonly/ReadonlyDirectory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,41 @@ protected override async IAsyncEnumerable<VirtualNode> GetFileNodesCoreAsync([En
/// <inheritdoc />
protected override async IAsyncEnumerable<VirtualFile> GetFilesCoreAsync([EnumeratorCancellation] CancellationToken cancellationToken)
{
await foreach (var node in _directory.GetFilesAsync(cancellationToken).ConfigureAwait(false))
yield return new ReadonlyFile(_fs, node);
await foreach (var file in _directory.GetFilesAsync(cancellationToken).ConfigureAwait(false))
yield return new ReadonlyFile(_fs, file);
}

/// <inheritdoc />
protected override async IAsyncEnumerable<VirtualDirectory> GetDirectoriesCoreAsync([EnumeratorCancellation] CancellationToken cancellationToken)
{
await foreach (var node in _directory.GetDirectoriesAsync(cancellationToken).ConfigureAwait(false))
yield return new ReadonlyDirectory(_fs, node);
await foreach (var directory in _directory.GetDirectoriesAsync(cancellationToken).ConfigureAwait(false))
yield return new ReadonlyDirectory(_fs, directory);
}

/// <inheritdoc />
protected override async IAsyncEnumerable<VirtualNode> GetFileNodesCoreAsync(string[] patterns, string[]? excludes, [EnumeratorCancellation] CancellationToken cancellationToken)
{
await foreach (var node in _directory.GetFileNodesAsync(patterns, excludes, cancellationToken).ConfigureAwait(false))
{
yield return node switch
{
VirtualDirectory directory => new ReadonlyDirectory(_fs, directory),
_ => new ReadonlyFile(_fs, (VirtualFile)node)
};
}
}

/// <inheritdoc />
protected override async IAsyncEnumerable<VirtualFile> GetFilesCoreAsync(string[] patterns, string[]? excludes, [EnumeratorCancellation] CancellationToken cancellationToken)
{
await foreach (var file in _directory.GetFilesAsync(patterns, excludes, cancellationToken).ConfigureAwait(false))
yield return new ReadonlyFile(_fs, file);
}

/// <inheritdoc />
protected override async IAsyncEnumerable<VirtualDirectory> GetDirectoriesCoreAsync(string[] patterns, string[]? excludes, [EnumeratorCancellation] CancellationToken cancellationToken)
{
await foreach (var directory in _directory.GetDirectoriesAsync(patterns, excludes, cancellationToken).ConfigureAwait(false))
yield return new ReadonlyDirectory(_fs, directory);
}
}
47 changes: 41 additions & 6 deletions src/Ramstack.FileSystem.Sub/SubDirectory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,20 +60,55 @@ protected override async IAsyncEnumerable<VirtualNode> GetFileNodesCoreAsync([En
/// <inheritdoc />
protected override async IAsyncEnumerable<VirtualFile> GetFilesCoreAsync([EnumeratorCancellation] CancellationToken cancellationToken)
{
await foreach (var node in _directory.GetFilesAsync(cancellationToken).ConfigureAwait(false))
await foreach (var file in _directory.GetFilesAsync(cancellationToken).ConfigureAwait(false))
{
var path = VirtualPath.Join(FullName, node.Name);
yield return new SubFile(_fs, path, node);
var path = VirtualPath.Join(FullName, file.Name);
yield return new SubFile(_fs, path, file);
}
}

/// <inheritdoc />
protected override async IAsyncEnumerable<VirtualDirectory> GetDirectoriesCoreAsync([EnumeratorCancellation] CancellationToken cancellationToken)
{
await foreach (var node in _directory.GetDirectoriesAsync(cancellationToken).ConfigureAwait(false))
await foreach (var directory in _directory.GetDirectoriesAsync(cancellationToken).ConfigureAwait(false))
{
var path = VirtualPath.Join(FullName, node.Name);
yield return new SubDirectory(_fs, path, node);
var path = VirtualPath.Join(FullName, directory.Name);
yield return new SubDirectory(_fs, path, directory);
}
}

/// <inheritdoc />
protected override async IAsyncEnumerable<VirtualNode> GetFileNodesCoreAsync(string[] patterns, string[]? excludes, [EnumeratorCancellation] CancellationToken cancellationToken)
{
await foreach (var node in _directory.GetFileNodesAsync(patterns, excludes, cancellationToken).ConfigureAwait(false))
{
var path = _fs.ConvertToSubPath(node.FullName);

yield return node switch
{
VirtualDirectory directory => new SubDirectory(_fs, path, directory),
_ => new SubFile(_fs, path, (VirtualFile)node)
};
}
}

/// <inheritdoc />
protected override async IAsyncEnumerable<VirtualFile> GetFilesCoreAsync(string[] patterns, string[]? excludes, [EnumeratorCancellation] CancellationToken cancellationToken)
{
await foreach (var file in _directory.GetFilesAsync(patterns, excludes, cancellationToken).ConfigureAwait(false))
{
var path = _fs.ConvertToSubPath(file.FullName);
yield return new SubFile(_fs, path, file);
}
}

/// <inheritdoc />
protected override async IAsyncEnumerable<VirtualDirectory> GetDirectoriesCoreAsync(string[] patterns, string[]? excludes, [EnumeratorCancellation] CancellationToken cancellationToken)
{
await foreach (var directory in _directory.GetDirectoriesAsync(patterns, excludes, cancellationToken).ConfigureAwait(false))
{
var path = _fs.ConvertToSubPath(directory.FullName);
yield return new SubDirectory(_fs, path, directory);
}
}
}
58 changes: 46 additions & 12 deletions src/Ramstack.FileSystem.Sub/SubFileSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ namespace Ramstack.FileSystem.Sub;
/// This class provides functionality to handle files and directories that are located under
/// a specific path within the root directory of the underlying file system.
/// </remarks>
[DebuggerDisplay("{_path,nq}")]
[DebuggerDisplay("{_root,nq}")]
public sealed class SubFileSystem : IVirtualFileSystem
{
private readonly IVirtualFileSystem _fs;
private readonly string _path;
private readonly string _root;

/// <inheritdoc />
public bool IsReadOnly => _fs.IsReadOnly;
Expand All @@ -25,13 +25,15 @@ public sealed class SubFileSystem : IVirtualFileSystem
/// <param name="path">The path under the root directory of the <paramref name="fileSystem"/>.</param>
/// <param name="fileSystem">The underlying file system.</param>
public SubFileSystem(string path, IVirtualFileSystem fileSystem) =>
(_path, _fs) = (VirtualPath.Normalize(path), fileSystem);
(_root, _fs) = (VirtualPath.Normalize(path), fileSystem);

/// <inheritdoc />
public VirtualFile GetFile(string path)
{
path = VirtualPath.Normalize(path);
var file = _fs.GetFile(ResolvePath(path));

var underlyingPath = ConvertToUnderlyingPath(path);
var file = _fs.GetFile(underlyingPath);

return new SubFile(this, path, file);
}
Expand All @@ -40,7 +42,9 @@ public VirtualFile GetFile(string path)
public VirtualDirectory GetDirectory(string path)
{
path = VirtualPath.Normalize(path);
var directory = _fs.GetDirectory(ResolvePath(path));

var underlyingPath = ConvertToUnderlyingPath(path);
var directory = _fs.GetDirectory(underlyingPath);

return new SubDirectory(this, path, directory);
}
Expand All @@ -50,17 +54,47 @@ public void Dispose() =>
_fs.Dispose();

/// <summary>
/// Resolves the specified path to the underlying file system.
/// Converts a path from the underlying file system to a path relative to this file system's root.
/// </summary>
/// <param name="underlyingPath">The absolute path within the parent file system.
/// Must be normalized and start with this file system's root path.</param>
/// <returns>
/// The corresponding path within this file system.
/// </returns>
/// <remarks>
/// For example, converts "/app/assets/images/logo.png" to "/images/logo.png",
/// where "/app/assets" is this file system's root.
/// </remarks>
internal string ConvertToSubPath(string underlyingPath)
{
Debug.Assert(VirtualPath.IsNormalized(underlyingPath));
Debug.Assert(underlyingPath.StartsWith(_root, StringComparison.Ordinal));

if (underlyingPath == _root)
return "/";

return new string(underlyingPath.AsSpan(_root.Length));
}

/// <summary>
/// Converts a path from this file system to the corresponding absolute path in the underlying file system.
/// </summary>
/// <param name="path">The path to resolve.</param>
/// <param name="path">The path within this file system.
/// Must be normalized (e.g., "/" or "/images/logo.png").</param>
/// <returns>
/// The resolved path in the underlying file system.
/// The absolute path in the underlying file system that corresponds to this file system path.
/// </returns>
private string ResolvePath(string path)
/// <remarks>
/// For example, converts "/images/logo.png" to "/app/assets/images/logo.png",
/// where "/app/assets" is this file system's root.
/// </remarks>
private string ConvertToUnderlyingPath(string path)
{
if (path.Length == 0 || path == "/")
return _path;
Debug.Assert(VirtualPath.IsNormalized(path));

if (path == "/")
return _root;

return VirtualPath.Join(_path, path);
return VirtualPath.Join(_root, path);
}
}
Loading