From 0a505c401f1235349e1cfb8ae4dc8db8d6008d64 Mon Sep 17 00:00:00 2001 From: rameel Date: Sat, 16 Aug 2025 03:45:14 +0500 Subject: [PATCH 1/4] SubFileSystem: Passthrough pattern-based directory enumeration to wrapped file system --- src/Ramstack.FileSystem.Sub/SubDirectory.cs | 47 ++++++++++++++-- src/Ramstack.FileSystem.Sub/SubFileSystem.cs | 58 ++++++++++++++++---- 2 files changed, 87 insertions(+), 18 deletions(-) diff --git a/src/Ramstack.FileSystem.Sub/SubDirectory.cs b/src/Ramstack.FileSystem.Sub/SubDirectory.cs index bd0821e..474edfc 100644 --- a/src/Ramstack.FileSystem.Sub/SubDirectory.cs +++ b/src/Ramstack.FileSystem.Sub/SubDirectory.cs @@ -60,20 +60,55 @@ protected override async IAsyncEnumerable GetFileNodesCoreAsync([En /// protected override async IAsyncEnumerable 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); } } /// protected override async IAsyncEnumerable 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); + } + } + + /// + protected override async IAsyncEnumerable 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) + }; + } + } + + /// + protected override async IAsyncEnumerable 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); + } + } + + /// + protected override async IAsyncEnumerable 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); } } } diff --git a/src/Ramstack.FileSystem.Sub/SubFileSystem.cs b/src/Ramstack.FileSystem.Sub/SubFileSystem.cs index 7ecc346..77cce48 100644 --- a/src/Ramstack.FileSystem.Sub/SubFileSystem.cs +++ b/src/Ramstack.FileSystem.Sub/SubFileSystem.cs @@ -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. /// -[DebuggerDisplay("{_path,nq}")] +[DebuggerDisplay("{_root,nq}")] public sealed class SubFileSystem : IVirtualFileSystem { private readonly IVirtualFileSystem _fs; - private readonly string _path; + private readonly string _root; /// public bool IsReadOnly => _fs.IsReadOnly; @@ -25,13 +25,15 @@ public sealed class SubFileSystem : IVirtualFileSystem /// The path under the root directory of the . /// The underlying file system. public SubFileSystem(string path, IVirtualFileSystem fileSystem) => - (_path, _fs) = (VirtualPath.Normalize(path), fileSystem); + (_root, _fs) = (VirtualPath.Normalize(path), fileSystem); /// 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); } @@ -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); } @@ -50,17 +54,47 @@ public void Dispose() => _fs.Dispose(); /// - /// 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. + /// + /// The absolute path within the parent file system. + /// Must be normalized and start with this file system's root path. + /// + /// The corresponding path within this file system. + /// + /// + /// For example, converts "/app/assets/images/logo.png" to "/images/logo.png", + /// where "/app/assets" is this file system's root. + /// + 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)); + } + + /// + /// Converts a path from this file system to the corresponding absolute path in the underlying file system. /// - /// The path to resolve. + /// The path within this file system. + /// Must be normalized (e.g., "/" or "/images/logo.png"). /// - /// The resolved path in the underlying file system. + /// The absolute path in the underlying file system that corresponds to this file system path. /// - private string ResolvePath(string path) + /// + /// For example, converts "/images/logo.png" to "/app/assets/images/logo.png", + /// where "/app/assets" is this file system's root. + /// + 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); } } From a90bd9eb7a1155e415de93044d2727836c1aabcd Mon Sep 17 00:00:00 2001 From: rameel Date: Sat, 16 Aug 2025 03:45:41 +0500 Subject: [PATCH 2/4] PrefixedFileSystem: Passthrough pattern-based directory enumeration to wrapped file system --- .../PrefixedDirectory.cs | 47 ++++++++++++++++--- .../PrefixedFileSystem.cs | 35 ++++++++++---- 2 files changed, 66 insertions(+), 16 deletions(-) diff --git a/src/Ramstack.FileSystem.Prefixed/PrefixedDirectory.cs b/src/Ramstack.FileSystem.Prefixed/PrefixedDirectory.cs index 43708a3..16dc682 100644 --- a/src/Ramstack.FileSystem.Prefixed/PrefixedDirectory.cs +++ b/src/Ramstack.FileSystem.Prefixed/PrefixedDirectory.cs @@ -60,20 +60,55 @@ protected override async IAsyncEnumerable GetFileNodesCoreAsync([En /// protected override async IAsyncEnumerable 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); } } /// protected override async IAsyncEnumerable 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); + } + } + + /// + protected override async IAsyncEnumerable 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) + }; + } + } + + /// + protected override async IAsyncEnumerable 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); + } + } + + /// + protected override async IAsyncEnumerable 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); } } } diff --git a/src/Ramstack.FileSystem.Prefixed/PrefixedFileSystem.cs b/src/Ramstack.FileSystem.Prefixed/PrefixedFileSystem.cs index d720901..cd731e0 100644 --- a/src/Ramstack.FileSystem.Prefixed/PrefixedFileSystem.cs +++ b/src/Ramstack.FileSystem.Prefixed/PrefixedFileSystem.cs @@ -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)); @@ -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)); @@ -84,23 +84,38 @@ public void Dispose() => _fs.Dispose(); /// - /// 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. /// - /// The full path to match against the prefix. + /// The path relative to the wrapped file system (must be normalized). + /// + /// The full path including this file system's prefix. + /// + internal string WrapWithPrefix(string underlyingPath) + { + Debug.Assert(VirtualPath.IsNormalized(underlyingPath)); + + if (underlyingPath == "/") + return _prefix; + + return VirtualPath.Join(_prefix, underlyingPath); + } + + /// + /// Attempts to extract the underlying path by removing this file system's prefix from a full path. + /// + /// The full path that may include this file system's prefix (must be normalized). /// The prefix to compare against the path. /// - /// The relative path if the prefix matches; otherwise, null. + /// The underlying path relative to the wrapped file system if the prefix matches; + /// otherwise, if the path doesn't belong to this prefixed file system. /// - 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)); From 3b7c968f6a0a3940bd002b2d95dba21fa8b3ae97 Mon Sep 17 00:00:00 2001 From: rameel Date: Sat, 16 Aug 2025 03:46:08 +0500 Subject: [PATCH 3/4] ReadonlyFileSystem: Passthrough pattern-based directory enumeration to wrapped file system --- .../ReadonlyDirectory.cs | 35 ++++++++++++++++--- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/src/Ramstack.FileSystem.Readonly/ReadonlyDirectory.cs b/src/Ramstack.FileSystem.Readonly/ReadonlyDirectory.cs index 851bf2f..3fc4c9e 100644 --- a/src/Ramstack.FileSystem.Readonly/ReadonlyDirectory.cs +++ b/src/Ramstack.FileSystem.Readonly/ReadonlyDirectory.cs @@ -57,14 +57,41 @@ protected override async IAsyncEnumerable GetFileNodesCoreAsync([En /// protected override async IAsyncEnumerable 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); } /// protected override async IAsyncEnumerable 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); + } + + /// + protected override async IAsyncEnumerable 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) + }; + } + } + + /// + protected override async IAsyncEnumerable 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); + } + + /// + protected override async IAsyncEnumerable 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); } } From 5c9bc2cd497c6c65df2684b6f56347fbd23c41e4 Mon Sep 17 00:00:00 2001 From: rameel Date: Sat, 16 Aug 2025 03:46:21 +0500 Subject: [PATCH 4/4] GlobbingFileSystem: Passthrough pattern-based directory enumeration to wrapped file system --- .../GlobbingDirectory.cs | 45 +++++++++++++++++-- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/src/Ramstack.FileSystem.Globbing/GlobbingDirectory.cs b/src/Ramstack.FileSystem.Globbing/GlobbingDirectory.cs index 8598b85..3a0f47a 100644 --- a/src/Ramstack.FileSystem.Globbing/GlobbingDirectory.cs +++ b/src/Ramstack.FileSystem.Globbing/GlobbingDirectory.cs @@ -80,10 +80,8 @@ protected override async IAsyncEnumerable 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); - } } } @@ -93,10 +91,51 @@ protected override async IAsyncEnumerable 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); + } + } + + /// + protected override async IAsyncEnumerable 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); + } } } } + + /// + protected override async IAsyncEnumerable 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); + } + } + + /// + protected override async IAsyncEnumerable 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); + } + } }