Skip to content

Commit 56268c9

Browse files
authored
Merge pull request #40 from rameel/fs-glob-optimization
Optimize glob pattern matching with provider-specific overrides - New overloads in base `VirtualFileSystem` for efficient glob-based file/directory enumeration - Support for provider-specific pattern matching optimizations - Updated tests covering new and existing behavior - `GlobbingFileSystem`: Match directory paths against glob patterns correctly
2 parents 4c44903 + fe12a1c commit 56268c9

28 files changed

Lines changed: 1757 additions & 572 deletions

File tree

src/Ramstack.FileSystem.Abstractions/Null/NotFoundDirectory.cs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ namespace Ramstack.FileSystem.Null;
55
/// <summary>
66
/// Represents a non-existing directory.
77
/// </summary>
8-
public class NotFoundDirectory : VirtualDirectory
8+
public sealed class NotFoundDirectory : VirtualDirectory
99
{
1010
/// <inheritdoc />
1111
public override IVirtualFileSystem FileSystem { get; }
@@ -27,8 +27,11 @@ protected override ValueTask<bool> ExistsCoreAsync(CancellationToken cancellatio
2727
default;
2828

2929
/// <inheritdoc />
30-
protected override ValueTask CreateCoreAsync(CancellationToken cancellationToken) =>
31-
default;
30+
protected override ValueTask CreateCoreAsync(CancellationToken cancellationToken)
31+
{
32+
Error_UnauthorizedAccess(FullName);
33+
return default;
34+
}
3235

3336
/// <inheritdoc />
3437
protected override ValueTask DeleteCoreAsync(CancellationToken cancellationToken) =>
@@ -37,4 +40,8 @@ protected override ValueTask DeleteCoreAsync(CancellationToken cancellationToken
3740
/// <inheritdoc />
3841
protected override IAsyncEnumerable<VirtualNode> GetFileNodesCoreAsync(CancellationToken cancellationToken) =>
3942
Array.Empty<VirtualNode>().ToAsyncEnumerable();
43+
44+
[DoesNotReturn]
45+
private static void Error_UnauthorizedAccess(string path) =>
46+
throw new UnauthorizedAccessException($"Access to the path '{path}' is denied.");
4047
}

src/Ramstack.FileSystem.Abstractions/Null/NotFoundFile.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
using System.Diagnostics.CodeAnalysis;
2-
31
namespace Ramstack.FileSystem.Null;
42

53
/// <summary>
@@ -43,7 +41,7 @@ protected override ValueTask<Stream> OpenWriteCoreAsync(CancellationToken cancel
4341
/// <inheritdoc />
4442
protected override ValueTask WriteCoreAsync(Stream stream, bool overwrite, CancellationToken cancellationToken)
4543
{
46-
Error_FileNotFound(FullName);
44+
Error_UnauthorizedAccess(FullName);
4745
return default;
4846
}
4947

@@ -54,4 +52,8 @@ protected override ValueTask DeleteCoreAsync(CancellationToken cancellationToken
5452
[DoesNotReturn]
5553
private static void Error_FileNotFound(string path) =>
5654
throw new FileNotFoundException($"Unable to find file '{path}'.", path);
55+
56+
[DoesNotReturn]
57+
private static void Error_UnauthorizedAccess(string path) =>
58+
throw new UnauthorizedAccessException($"Access to the path '{path}' is denied.");
5759
}

src/Ramstack.FileSystem.Abstractions/Ramstack.FileSystem.Abstractions.csproj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@
3535
<GenerateDocumentationFile>true</GenerateDocumentationFile>
3636
</PropertyGroup>
3737

38+
<ItemGroup>
39+
<Using Include="System.Diagnostics" />
40+
<Using Include="System.Diagnostics.CodeAnalysis" />
41+
<Using Include="System.Runtime.CompilerServices" />
42+
</ItemGroup>
43+
3844
<ItemGroup>
3945
<PackageReference Include="Microsoft.SourceLink.GitHub">
4046
<PrivateAssets>all</PrivateAssets>

src/Ramstack.FileSystem.Abstractions/VirtualDirectory.cs

Lines changed: 74 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
using System.Diagnostics;
2-
using System.Runtime.CompilerServices;
3-
41
using Ramstack.Globbing.Traversal;
52

63
namespace Ramstack.FileSystem;
@@ -121,16 +118,7 @@ public IAsyncEnumerable<VirtualDirectory> GetDirectoriesAsync(CancellationToken
121118
public IAsyncEnumerable<VirtualNode> GetFileNodesAsync(string[] patterns, string[]? excludes, CancellationToken cancellationToken = default)
122119
{
123120
ArgumentNullException.ThrowIfNull(patterns);
124-
125-
return new FileTreeAsyncEnumerable<VirtualNode, VirtualNode>(this, cancellationToken)
126-
{
127-
Patterns = patterns,
128-
Excludes = excludes ?? [],
129-
FileNameSelector = node => node.FullName,
130-
ShouldRecursePredicate = node => node is VirtualDirectory,
131-
ChildrenSelector = (node, token) => ((VirtualDirectory)node).GetFileNodesCoreAsync(token),
132-
ResultSelector = node => node
133-
};
121+
return GetFileNodesCoreAsync(patterns, excludes, cancellationToken);
134122
}
135123

136124
/// <summary>
@@ -146,17 +134,7 @@ public IAsyncEnumerable<VirtualNode> GetFileNodesAsync(string[] patterns, string
146134
public IAsyncEnumerable<VirtualFile> GetFilesAsync(string[] patterns, string[]? excludes, CancellationToken cancellationToken = default)
147135
{
148136
ArgumentNullException.ThrowIfNull(patterns);
149-
150-
return new FileTreeAsyncEnumerable<VirtualNode, VirtualFile>(this, cancellationToken)
151-
{
152-
Patterns = patterns,
153-
Excludes = excludes ?? [],
154-
FileNameSelector = node => node.FullName,
155-
ShouldIncludePredicate = node => node is VirtualFile,
156-
ShouldRecursePredicate = node => node is VirtualDirectory,
157-
ChildrenSelector = (node, token) => ((VirtualDirectory)node).GetFileNodesCoreAsync(token),
158-
ResultSelector = node => (VirtualFile)node
159-
};
137+
return GetFilesCoreAsync(patterns, excludes, cancellationToken);
160138
}
161139

162140
/// <summary>
@@ -172,17 +150,7 @@ public IAsyncEnumerable<VirtualFile> GetFilesAsync(string[] patterns, string[]?
172150
public IAsyncEnumerable<VirtualDirectory> GetDirectoriesAsync(string[] patterns, string[]? excludes, CancellationToken cancellationToken = default)
173151
{
174152
ArgumentNullException.ThrowIfNull(patterns);
175-
176-
return new FileTreeAsyncEnumerable<VirtualNode, VirtualDirectory>(this, cancellationToken)
177-
{
178-
Patterns = patterns,
179-
Excludes = excludes ?? [],
180-
FileNameSelector = node => node.FullName,
181-
ShouldIncludePredicate = node => node is VirtualDirectory,
182-
ShouldRecursePredicate = node => node is VirtualDirectory,
183-
ChildrenSelector = (node, token) => ((VirtualDirectory)node).GetFileNodesCoreAsync(token),
184-
ResultSelector = node => (VirtualDirectory)node
185-
};
153+
return GetDirectoriesCoreAsync(patterns, excludes, cancellationToken);
186154
}
187155

188156
/// <summary>
@@ -243,4 +211,75 @@ protected virtual async IAsyncEnumerable<VirtualDirectory> GetDirectoriesCoreAsy
243211
yield return directory;
244212
}
245213
}
214+
215+
/// <summary>
216+
/// Core implementation for asynchronously returning an async-enumerable collection of file nodes (both directories and files)
217+
/// within the current directory that match any of the specified glob patterns.
218+
/// </summary>
219+
/// <param name="patterns">An array of glob patterns to match against the names of file nodes.</param>
220+
/// <param name="excludes">An optional array of glob patterns to exclude file nodes.</param>
221+
/// <param name="cancellationToken">An optional cancellation token to cancel the operation.</param>
222+
/// <returns>
223+
/// An async-enumerable collection of <see cref="VirtualNode"/> instances.
224+
/// </returns>
225+
protected virtual IAsyncEnumerable<VirtualNode> GetFileNodesCoreAsync(string[] patterns, string[]? excludes, CancellationToken cancellationToken)
226+
{
227+
return new FileTreeAsyncEnumerable<VirtualNode, VirtualNode>(this, cancellationToken)
228+
{
229+
Patterns = patterns,
230+
Excludes = excludes ?? [],
231+
FileNameSelector = node => node.Name,
232+
ShouldRecursePredicate = node => node is VirtualDirectory,
233+
ChildrenSelector = (node, token) => ((VirtualDirectory)node).GetFileNodesCoreAsync(token),
234+
ResultSelector = node => node
235+
};
236+
}
237+
238+
/// <summary>
239+
/// Core implementation for asynchronously returning an async-enumerable collection of files within the current directory
240+
/// that match any of the specified glob patterns.
241+
/// </summary>
242+
/// <param name="patterns">An array of glob patterns to match against the names of files.</param>
243+
/// <param name="excludes">An optional array of glob patterns to exclude files.</param>
244+
/// <param name="cancellationToken">An optional cancellation token to cancel the operation.</param>
245+
/// <returns>
246+
/// An async-enumerable collection of <see cref="VirtualFile"/> instances.
247+
/// </returns>
248+
protected virtual IAsyncEnumerable<VirtualFile> GetFilesCoreAsync(string[] patterns, string[]? excludes, CancellationToken cancellationToken)
249+
{
250+
return new FileTreeAsyncEnumerable<VirtualNode, VirtualFile>(this, cancellationToken)
251+
{
252+
Patterns = patterns,
253+
Excludes = excludes ?? [],
254+
FileNameSelector = node => node.Name,
255+
ShouldIncludePredicate = node => node is VirtualFile,
256+
ShouldRecursePredicate = node => node is VirtualDirectory,
257+
ChildrenSelector = (node, token) => ((VirtualDirectory)node).GetFileNodesCoreAsync(token),
258+
ResultSelector = node => (VirtualFile)node
259+
};
260+
}
261+
262+
/// <summary>
263+
/// Core implementation for asynchronously returning an async-enumerable collection of directories within the current directory
264+
/// that match any of the specified glob patterns.
265+
/// </summary>
266+
/// <param name="patterns">An array of glob patterns to match against the names of directories.</param>
267+
/// <param name="excludes">An optional array of glob patterns to exclude directories.</param>
268+
/// <param name="cancellationToken">An optional cancellation token to cancel the operation.</param>
269+
/// <returns>
270+
/// An async-enumerable collection of <see cref="VirtualDirectory"/> instances.
271+
/// </returns>
272+
protected virtual IAsyncEnumerable<VirtualDirectory> GetDirectoriesCoreAsync(string[] patterns, string[]? excludes, CancellationToken cancellationToken)
273+
{
274+
return new FileTreeAsyncEnumerable<VirtualNode, VirtualDirectory>(this, cancellationToken)
275+
{
276+
Patterns = patterns,
277+
Excludes = excludes ?? [],
278+
FileNameSelector = node => node.Name,
279+
ShouldIncludePredicate = node => node is VirtualDirectory,
280+
ShouldRecursePredicate = node => node is VirtualDirectory,
281+
ChildrenSelector = (node, token) => ((VirtualDirectory)node).GetFileNodesCoreAsync(token),
282+
ResultSelector = node => (VirtualDirectory)node
283+
};
284+
}
246285
}

src/Ramstack.FileSystem.Abstractions/VirtualFile.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
using System.Diagnostics;
2-
31
namespace Ramstack.FileSystem;
42

53
/// <summary>

src/Ramstack.FileSystem.Abstractions/VirtualNode.cs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
using System.Diagnostics;
2-
using System.Diagnostics.CodeAnalysis;
3-
41
namespace Ramstack.FileSystem;
52

63
/// <summary>

src/Ramstack.FileSystem.Abstractions/VirtualNodeProperties.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
using System.Diagnostics;
2-
31
namespace Ramstack.FileSystem;
42

53
/// <summary>

src/Ramstack.FileSystem.Abstractions/VirtualPath.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,8 @@ public static ReadOnlySpan<char> GetFileName(ReadOnlySpan<char> path)
118118
/// </summary>
119119
/// <param name="path">The path to retrieve the directory portion from.</param>
120120
/// <returns>
121-
/// Directory portion for <paramref name="path"/>, or an empty string if the path denotes a root directory.
121+
/// The directory portion of <paramref name="path"/>, or an empty span if <paramref name="path"/>
122+
/// is empty or denotes a root directory.
122123
/// </returns>
123124
public static string GetDirectoryName(string path)
124125
{
@@ -140,7 +141,8 @@ public static string GetDirectoryName(string path)
140141
/// </summary>
141142
/// <param name="path">The path to retrieve the directory portion from.</param>
142143
/// <returns>
143-
/// Directory portion for <paramref name="path"/>, or an empty string if path denotes a root directory.
144+
/// The directory portion of <paramref name="path"/>, or an empty span if <paramref name="path"/>
145+
/// is empty or denotes a root directory.
144146
/// </returns>
145147
public static ReadOnlySpan<char> GetDirectoryName(ReadOnlySpan<char> path)
146148
{

0 commit comments

Comments
 (0)