diff --git a/.editorconfig b/.editorconfig index 1a2db97..7960d07 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,74 +1,92 @@ -root = true - -[*] -charset = utf-8 -indent_size = 4 -indent_style = space -insert_final_newline = true -trim_trailing_whitespace = true - -[*.{yml,props,targets,csproj}] -indent_size = 2 - -# IDE0079: Remove unnecessary suppression -dotnet_remove_unnecessary_suppression_exclusions = category:ReSharper - -# IDE0079: Remove unnecessary suppression -# dotnet_diagnostic.IDE0079.severity = none - -# CA2255: The 'ModuleInitializer' attribute should not be used in libraries -dotnet_diagnostic.CA2255.severity = none - -# CS8600: Converting null literal or possible null value to non-nullable type. -dotnet_diagnostic.CS8600.severity = error - -# CS8601: Possible null reference assignment. -dotnet_diagnostic.CS8601.severity = error - -# CS8602: Dereference of a possibly null reference. -dotnet_diagnostic.CS8602.severity = error - -# CS8603: Possible null reference return. -dotnet_diagnostic.CS8603.severity = error - -# CS8604: Possible null reference argument. -dotnet_diagnostic.CS8604.severity = error - -# CS8610: Nullability of reference types in type of parameter doesn't match overridden member. -dotnet_diagnostic.CS8610.severity = error - -# CS8613: Nullability of reference types in return type doesn't match implicitly implemented member. -dotnet_diagnostic.CS8613.severity = error - -# CS8614: Nullability of reference types in type of parameter doesn't match implicitly implemented member. -dotnet_diagnostic.CS8614.severity = error - -# CS8618: Non-nullable field is uninitialized. Consider declaring as nullable. -dotnet_diagnostic.CS8618.severity = error - -# CS8619: Nullability of reference types in value doesn't match target type. -dotnet_diagnostic.CS8619.severity = error - -# CS8620: Argument cannot be used for parameter due to differences in the nullability of reference types. -dotnet_diagnostic.CS8620.severity = error - -# CS8625: Cannot convert null literal to non-nullable reference type. -dotnet_diagnostic.CS8625.severity = error - -# CS8634: The type cannot be used as type parameter in the generic type or method. Nullability of type argument doesn't match 'class' constraint. -dotnet_diagnostic.CS8634.severity = error - -# CS8643: Nullability of reference types in explicit interface specifier doesn't match interface implemented by the type. -dotnet_diagnostic.CS8643.severity = error - -# CS8653: A default expression introduces a null value for a type parameter. -dotnet_diagnostic.CS8653.severity = error - -# CS8714: The type cannot be used as type parameter in the generic type or method. Nullability of type argument doesn't match 'notnull' constraint. -dotnet_diagnostic.CS8714.severity = error - -# IDE0059: Unnecessary assigment of a value -dotnet_diagnostic.IDE0059.severity = none - -# IDE0060: Remove unused parameter -dotnet_diagnostic.IDE0060.severity = none +root = true + +[*] +charset = utf-8 +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{yml,yaml}] +indent_size = 2 + +[*.{slnx,csproj,proj,props,targets}] +indent_size = 2 + +[*.cs] +resharper_csharp_max_line_length = 160 + +# Using directive options +dotnet_sort_system_directives_first = true +dotnet_separate_import_directive_groups = true + +# Prefer var whenever possible +csharp_style_var_for_built_in_types = true:suggestion +csharp_style_var_when_type_is_apparent = true:suggestion +csharp_style_var_elsewhere = true:suggestion + +# Indentation preferences +csharp_indent_labels = no_change + +# IDE0079: Remove unnecessary suppression +dotnet_remove_unnecessary_suppression_exclusions = category:ReSharper + +# IDE0079: Remove unnecessary suppression +# dotnet_diagnostic.IDE0079.severity = none + +# CA2255: The 'ModuleInitializer' attribute should not be used in libraries +dotnet_diagnostic.CA2255.severity = none + +# CS8600: Converting null literal or possible null value to non-nullable type. +dotnet_diagnostic.CS8600.severity = error + +# CS8601: Possible null reference assignment. +dotnet_diagnostic.CS8601.severity = error + +# CS8602: Dereference of a possibly null reference. +dotnet_diagnostic.CS8602.severity = error + +# CS8603: Possible null reference return. +dotnet_diagnostic.CS8603.severity = error + +# CS8604: Possible null reference argument. +dotnet_diagnostic.CS8604.severity = error + +# CS8610: Nullability of reference types in type of parameter doesn't match overridden member. +dotnet_diagnostic.CS8610.severity = error + +# CS8613: Nullability of reference types in return type doesn't match implicitly implemented member. +dotnet_diagnostic.CS8613.severity = error + +# CS8614: Nullability of reference types in type of parameter doesn't match implicitly implemented member. +dotnet_diagnostic.CS8614.severity = error + +# CS8618: Non-nullable field is uninitialized. Consider declaring as nullable. +dotnet_diagnostic.CS8618.severity = error + +# CS8619: Nullability of reference types in value doesn't match target type. +dotnet_diagnostic.CS8619.severity = error + +# CS8620: Argument cannot be used for parameter due to differences in the nullability of reference types. +dotnet_diagnostic.CS8620.severity = error + +# CS8625: Cannot convert null literal to non-nullable reference type. +dotnet_diagnostic.CS8625.severity = error + +# CS8634: The type cannot be used as type parameter in the generic type or method. Nullability of type argument doesn't match 'class' constraint. +dotnet_diagnostic.CS8634.severity = error + +# CS8643: Nullability of reference types in explicit interface specifier doesn't match interface implemented by the type. +dotnet_diagnostic.CS8643.severity = error + +# CS8653: A default expression introduces a null value for a type parameter. +dotnet_diagnostic.CS8653.severity = error + +# CS8714: The type cannot be used as type parameter in the generic type or method. Nullability of type argument doesn't match 'notnull' constraint. +dotnet_diagnostic.CS8714.severity = error + +# IDE0059: Unnecessary assigment of a value +dotnet_diagnostic.IDE0059.severity = none + +# IDE0060: Remove unused parameter +dotnet_diagnostic.IDE0060.severity = none diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..176a458 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 6640c4f..f67e651 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,39 +1,39 @@ -name: Publish - -on: - push: - tags: - - "[0-9]+.[0-9]+.[0-9]+" - - "[0-9]+.[0-9]+.[0-9]+-[a-z]+.[0-9]+" - -permissions: - contents: write - -jobs: - publish: - name: Publish packages - runs-on: ubuntu-latest - - steps: - - name: Install .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: 9.0.x - - - name: Checkout - uses: actions/checkout@v4 - - - name: Build Solution - run: dotnet build -c Release - - - name: Create NuGet Packages - run: dotnet pack -c Release -o ./nuget --no-build - - - name: Publish NuGet Packages - run: dotnet nuget push ./nuget/*.nupkg --api-key ${{secrets.NUGET_API_KEY}} --skip-duplicate -s https://api.nuget.org/v3/index.json - - - name: Create GitHub Release - uses: softprops/action-gh-release@v2 - with: - draft: true - files: nuget/* +name: Publish + +on: + push: + tags: + - "[0-9]+.[0-9]+.[0-9]+" + - "[0-9]+.[0-9]+.[0-9]+-[a-z]+.[0-9]+" + +permissions: + contents: write + +jobs: + publish: + name: Publish packages + runs-on: ubuntu-latest + + steps: + - name: Install .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 9.0.x + + - name: Checkout + uses: actions/checkout@v4 + + - name: Build Solution + run: dotnet build -c Release + + - name: Create NuGet Packages + run: dotnet pack -c Release -o ./nuget --no-build + + - name: Publish NuGet Packages + run: dotnet nuget push ./nuget/*.nupkg --api-key ${{secrets.NUGET_API_KEY}} --skip-duplicate -s https://api.nuget.org/v3/index.json + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + draft: true + files: nuget/* diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ad9629e..925bfc4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,54 +1,54 @@ -name: Build & Test - -on: - pull_request: - branches: [main] - -jobs: - build-and-test: - name: "Test projects, ${{ matrix.os.name }}" - runs-on: ${{ matrix.os.id }} - strategy: - matrix: - os: - - id: windows-latest - name: Windows - - id: ubuntu-latest - name: Ubuntu - - id: ubuntu-24.04-arm - name: Ubuntu (ARM64) - - steps: - - name: Install .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: | - 6.x - 7.x - 8.x - 9.x - - - name: Checkout - uses: actions/checkout@v4 - - - name: Build (Debug) - run: dotnet build -c Debug - - - name: Build (Release) - run: dotnet build -c Release - - - name: Test (Debug) - run: dotnet test -c Debug --no-build - - - name: Test (Release) - run: dotnet test -c Release --no-build - - - name: Test (Release, AVX2=0) - env: - DOTNET_EnableAVX2: "0" - run: dotnet test -c Release --no-build - - - name: Test (Release, HWIntrinsic=0) - env: - DOTNET_EnableHWIntrinsic: "0" - run: dotnet test -c Release --no-build +name: Build & Test + +on: + pull_request: + branches: [main] + +jobs: + build-and-test: + name: "Test projects, ${{ matrix.os.name }}" + runs-on: ${{ matrix.os.id }} + strategy: + matrix: + os: + - id: windows-latest + name: Windows + - id: ubuntu-latest + name: Ubuntu + - id: ubuntu-24.04-arm + name: Ubuntu (ARM64) + + steps: + - name: Install .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 6.x + 7.x + 8.x + 9.x + + - name: Checkout + uses: actions/checkout@v4 + + - name: Build (Debug) + run: dotnet build -c Debug + + - name: Build (Release) + run: dotnet build -c Release + + - name: Test (Debug) + run: dotnet test -c Debug --no-build + + - name: Test (Release) + run: dotnet test -c Release --no-build + + - name: Test (Release, AVX2=0) + env: + DOTNET_EnableAVX2: "0" + run: dotnet test -c Release --no-build + + - name: Test (Release, HWIntrinsic=0) + env: + DOTNET_EnableHWIntrinsic: "0" + run: dotnet test -c Release --no-build diff --git a/.gitignore b/.gitignore index 4f81b1b..bd6adb1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,37 +1,37 @@ -#Ignore thumbnails created by windows -[Tt]humbs.db - -#Ignore files build by Visual Studio -*.obj -*.exe -*.pdb -*.user -*.patch -*.aps -*.pch -*.vspscc -*_i.c -*_p.c -*.ncb -*.suo -*.tlb -*.tlh -*.bak -*.cache -*.ilk -*.log -*.lib -*.sbr -*.cs.ide -[Dd]ebug*/ -[Rr]elease*/ -bin/ -obj/ -.vs/ -.vscode/ -*.ReSharper -*.ReSharper.user -[Tt]est[Rr]esult* - -#Ignore Rider/Idea files -.idea/ +#Ignore thumbnails created by windows +[Tt]humbs.db + +#Ignore files build by Visual Studio +*.obj +*.exe +*.pdb +*.user +*.patch +*.aps +*.pch +*.vspscc +*_i.c +*_p.c +*.ncb +*.suo +*.tlb +*.tlh +*.bak +*.cache +*.ilk +*.log +*.lib +*.sbr +*.cs.ide +[Dd]ebug*/ +[Rr]elease*/ +bin/ +obj/ +.vs/ +.vscode/ +*.ReSharper +*.ReSharper.user +[Tt]est[Rr]esult* + +#Ignore Rider/Idea files +.idea/ diff --git a/Directory.Build.props b/Directory.Build.props index bf61882..ed14ea7 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,5 +1,5 @@ - - - true - - + + + true + + diff --git a/Directory.Packages.props b/Directory.Packages.props index 6f2bf25..99899a3 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,17 +1,17 @@ - - - - true - - - - - - - - - - - - - + + + + true + + + + + + + + + + + + + diff --git a/LICENSE b/LICENSE index 6666765..92e3431 100644 --- a/LICENSE +++ b/LICENSE @@ -1,9 +1,9 @@ -MIT License - -Copyright 2024 Rameel (https://github.com/rameel) - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +MIT License + +Copyright 2024 Rameel (https://github.com/rameel) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index b93c21c..154ae64 100644 --- a/README.md +++ b/README.md @@ -1,297 +1,297 @@ -# Ramstack.FileProviders -[![NuGet](https://img.shields.io/nuget/v/Ramstack.FileProviders.svg)](https://nuget.org/packages/Ramstack.FileProviders) -[![MIT](https://img.shields.io/github/license/rameel/ramstack.fileproviders)](https://github.com/rameel/ramstack.fileproviders/blob/main/LICENSE) - -`Ramstack.FileProviders` is a collection of lightweight .NET libraries that enhance file handling capabilities in .NET applications, -building upon `Microsoft.Extensions.FileProviders`. - - -* [Ramstack.FileProviders](#ramstackfileproviders) - * [Projects](#projects) - * [Ramstack.FileProviders.Extensions](#ramstackfileprovidersextensions) - * [Ramstack.FileProviders](#ramstackfileproviders-1) - * [Ramstack.FileProviders.Globbing](#ramstackfileprovidersglobbing) - * [Ramstack.FileProviders.Composition](#ramstackfileproviderscomposition) - * [Overview](#overview) - * [Ramstack.FileProviders](#ramstackfileproviders-2) - * [PrefixedFileProvider](#prefixedfileprovider) - * [SubFileProvider](#subfileprovider) - * [Ramstack.FileProviders.Globbing](#ramstackfileprovidersglobbing-1) - * [Ramstack.FileProviders.Extensions](#ramstackfileprovidersextensions-1) - * [Ramstack.FileProviders.Composition](#ramstackfileproviderscomposition-1) - * [Flattening Providers](#flattening-providers) - * [Composing Providers](#composing-providers) - * [Flattening Change Tokens](#flattening-change-tokens) - * [NuGet Packages](#nuget-packages) - * [Supported versions](#supported-versions) - * [Contributions](#contributions) - * [License](#license) - - - -## Projects - -This repository contains the following projects: - -### Ramstack.FileProviders.Extensions -Offers useful and convenient extensions for `IFileProvider`, bringing its capabilities and experience -closer to what's provided by the `DirectoryInfo` and `FileInfo` classes. - -To install the `Ramstack.FileProviders.Extensions` [NuGet package](https://www.nuget.org/packages/Ramstack.FileProviders.Extensions) in your project, -run the following command: -```console -dotnet add package Ramstack.FileProviders.Extensions -``` - -### Ramstack.FileProviders -Provides additional implementations of `IFileProvider` including `PrefixedFileProvider` and `SubFileProvider`. - -To install the `Ramstack.FileProviders` [NuGet package](https://www.nuget.org/packages/Ramstack.FileProviders) in your project, -run the following command: -```console -dotnet add package Ramstack.FileProviders -``` - -### Ramstack.FileProviders.Globbing -Provides an implementation of the `IFileProvider` that filters files using include and/or exclude glob patterns -for flexible file visibility control. - -To install the `Ramstack.FileProviders.Globbing` [NuGet package](https://www.nuget.org/packages/Ramstack.FileProviders.Globbing) in your project, -run the following command: -```console -dotnet add package Ramstack.FileProviders.Globbing -``` - -### Ramstack.FileProviders.Composition -Provides a helper class for flattening and composing `IFileProvider` instances. - -To install the `Ramstack.FileProviders.Composition` [NuGet package](https://www.nuget.org/packages/Ramstack.FileProviders.Composition) in your project, -run the following command: -```console -dotnet add package Ramstack.FileProviders.Composition -``` - -## Overview - -### Ramstack.FileProviders - -This library offers additional implementations of the [IFileProvider](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.fileproviders.ifileprovider) interface: - -- `SubFileProvider` -- `PrefixedFileProvider` - -#### PrefixedFileProvider - -`PrefixedFileProvider` allows you to apply a prefix to the paths of files and directories. -This is useful when you need to organize files in a virtual hierarchy. - -Example: -```csharp -IFileProvider provider = new PrefixedFileProvider("/project/app", innerProvider); -IFileInfo file = provider.GetFileInfo("/project/app/docs/README"); -Console.WriteLine(file.Exists); -``` - -This is how you can add virtual directories to your project that are outside the project root: -```csharp -string packagesPath = Path.Combine(environment.ContentRootPath, "../Packages"); -string themesPath = Path.Combine(environment.ContentRootPath, "../Themes"); - -environment.ContentRootFileProvider = new CompositeFileProvider( - new PrefixedFileProvider("/Packages", new PhysicalFileProvider(packagesPath)), - new PrefixedFileProvider("/Themes", new PhysicalFileProvider(themesPath)), - environment.ContentRootFileProvider); -``` -The `Packages` and `Themes` directories are now available to the ASP.NET infrastructure under their respective names, -as if they were originally defined within your project. - -**Before:** -``` -/App -├── Controllers -├── Models -├── Views -└── wwwroot - -/Packages -├── package-1 -└── package-2 - -/Themes -├── theme-1 -└── theme-2 -``` - -**After:** -``` -/App -├── Controllers -├── Models -├── Views -├── Packages <-- (virtual) -│ ├── package-1 -│ └── package-2 -├── Themes <-- (virtual) -│ ├── theme-1 -│ └── theme-2 -└── wwwroot -``` - -#### SubFileProvider - -`SubFileProvider` lets you limit the view of the file system to a specific subdirectory, effectively creating a sandbox. - -Example: -```csharp -IFileProvider provider = new SubFileProvider("/docs", innerProvider); -IFileInfo file = provider.GetFileInfo("/README"); -Console.WriteLine(file.Exists); -``` - -### Ramstack.FileProviders.Globbing - -The `GlobbingFileProvider` class filters files using include and/or exclude glob patterns. Include patterns make only matching files visible, -while exclude patterns hide specific files. Both include and exclude patterns can be combined for flexible file visibility control. - -It relies on the [Ramstack.Globbing](https://www.nuget.org/packages/Ramstack.Globbing) package for its globbing capabilities. - -Example: -```csharp -IFileProvider provider = new GlobbingFileProvider(innerProvider, patterns: ["**/*.txt", "docs/*.md"], excludes: ["**/README.md"]); -foreach (IFileInfo file in provider.GetDirectoryContents("/")) - Console.WriteLine(file.Name); -``` - -### Ramstack.FileProviders.Extensions - -Provides useful extensions for `IFileProvider`, bringing its capabilities and experience closer to what's -provided by the `DirectoryInfo` and `FileInfo` classes. - -Simply stated, a `FileNode` knows which directory it is located in, and a directory represented by the `DirectoryNode` class can access -its parent directory and list all files within it, recursively. - -```csharp -using Ramstack.FileProviders; - -FileNode file = provider.GetFile("/docs/README"); - -// Prints the full path of the given file -Console.WriteLine($"Reading: {file.FullName}"); - -using StreamReader reader = file.OpenText(); -Console.WriteLine(reader.ReadToEnd()); -``` - -```csharp -DirectoryNode directory = provider.GetDirectory("/docs"); - -foreach (FileNode file in directory.EnumerateFiles()) - Console.WriteLine(file.FullName); -``` - -Furthermore, the methods for enumerating files (`EnumerateFiles`/`EnumerateDirectories`/`EnumerateFileNodes`) allow specifying glob patterns -to search for the desired files, as well as patterns to exclude files from the resulting list. - -```csharp -DirectoryNode directory = provider.GetDirectory("/project"); - -// Finds all *.md files and converts them to HTML -foreach (FileNode file in directory.EnumerateFiles(pattern: "**/*.md")) - RenderMarkdown(file); - -// Excludes files in a specific folder -foreach (FileNode file in directory.EnumerateFiles(pattern: "**/*.md", exclude: "vendors/**")) - RenderMarkdown(file); -``` -For convenience, many methods specific to `DirectoryNode` or `FileNode` are also available for `IFileProvider`. - -Thus, if we know the directory in which to look for files or the file to read, there is no need to obtain the -`DirectoryNode` or `FileNode` object. - -```csharp -using StreamReader reader = provider.OpenText("/docs/README", Encoding.UTF8); -Console.WriteLine(reader.ReadToEnd()); - -// Finds all *.md files and converts them to HTML -foreach (FileNode file in provider.EnumerateFiles("/project", pattern: "**/*.md")) - RenderMarkdown(file); -``` - -### Ramstack.FileProviders.Composition - -Provides a helper class `FileProviderComposer` for flattening and composing `IFileProvider` instances. - -#### Flattening Providers - -The `FlattenProvider` method attempts to flatten a given `IFileProvider` into a single list of file providers. - -This is especially useful when dealing with nested `CompositeFileProvider` instances, which might have been created during -different stages of a pipeline or configuration. Flattening helps in removing unnecessary indirectness and improving efficiency -by consolidating all file providers into a single level. - -```csharp -var builder = WebApplication.CreateBuilder(args); - -// Application pipeline configuration -... - -builder.Environment.ContentRootFileProvider = FileProviderComposer.FlattenProvider( - builder.Environment.ContentRootFileProvider); -``` - -#### Composing Providers - -The `ComposeProviders` method combines a list of `IFileProvider` instances into a single `IFileProvider`. -During this process, all encountered `CompositeFileProvider` instances are recursively flattened and merged into a single level. -This eliminates unnecessary indirectness and streamlines the file provider hierarchy. - -```csharp -string packagesPath = Path.Combine(environment.ContentRootPath, "../Packages"); -string themesPath = Path.Combine(environment.ContentRootPath, "../Themes"); - -environment.ContentRootFileProvider = FileProviderComposer.ComposeProviders( - // Inject external Packages directory - new PrefixedFileProvider("/Packages", new PhysicalFileProvider(packagesPath)), - - // Inject external Themes directory - new PrefixedFileProvider("/Themes", new PhysicalFileProvider(themesPath)), - - // Current provider - environment.ContentRootFileProvider); -``` - -In this example, the `ComposeProviders` method handles any unnecessary nesting that might occur, including when the current -`environment.ContentRootFileProvider` is a `CompositeFileProvider`. This ensures that all file providers are merged into a single -flat structure, avoiding unnecessary indirectness. - -#### Flattening Change Tokens -The `Flatten` extension method optimizes the structure of change token hierarchies by flattening nested `CompositeChangeToken` instances -and, most importantly, automatically filters out `NullChangeToken` instances from the hierarchy. Unlike standard `CompositeChangeToken` -behavior, which retains and processes `NullChangeToken` instances unnecessarily, this utility removes them completely, -resulting in improved performance and simplified change notification chains. - -```csharp -var changeToken = compositeFileProvider.Watch("**/*.json").Flatten(); -``` - - -## NuGet Packages -- [Ramstack.FileProviders.Extensions](https://www.nuget.org/packages/Ramstack.FileProviders.Extensions) — Useful and convenient extensions for `IFileProvider`, bringing its capabilities and experience closer to what's provided by the `DirectoryInfo` and `FileInfo` classes. -- [Ramstack.FileProviders](https://www.nuget.org/packages/Ramstack.FileProviders) — Additional file providers, including `PrefixedFileProvider` and `SubFileProvider`. -- [Ramstack.FileProviders.Globbing](https://www.nuget.org/packages/Ramstack.FileProviders.Globbing) — A file provider that filters files using include and/or exclude glob patterns. Include patterns make only matching files visible, while exclude patterns hide specific files. Both include and exclude patterns can be combined for flexible file visibility control. -- [Ramstack.FileProviders.Composition](https://www.nuget.org/packages/Ramstack.FileProviders.Composition) — Provides a helper class for flattening and composing `IFileProvider`. - -## Supported versions - -| | Version | -|------|----------------| -| .NET | 6, 7, 8, 9, 10 | - -## Contributions - -Bug reports and contributions are welcome. - -## License - -This project is released as open source under the **MIT License**. -See the [LICENSE](https://github.com/rameel/ramstack.fileproviders/blob/main/LICENSE) file for more details. +# Ramstack.FileProviders +[![NuGet](https://img.shields.io/nuget/v/Ramstack.FileProviders.svg)](https://nuget.org/packages/Ramstack.FileProviders) +[![MIT](https://img.shields.io/github/license/rameel/ramstack.fileproviders)](https://github.com/rameel/ramstack.fileproviders/blob/main/LICENSE) + +`Ramstack.FileProviders` is a collection of lightweight .NET libraries that enhance file handling capabilities in .NET applications, +building upon `Microsoft.Extensions.FileProviders`. + + +* [Ramstack.FileProviders](#ramstackfileproviders) + * [Projects](#projects) + * [Ramstack.FileProviders.Extensions](#ramstackfileprovidersextensions) + * [Ramstack.FileProviders](#ramstackfileproviders-1) + * [Ramstack.FileProviders.Globbing](#ramstackfileprovidersglobbing) + * [Ramstack.FileProviders.Composition](#ramstackfileproviderscomposition) + * [Overview](#overview) + * [Ramstack.FileProviders](#ramstackfileproviders-2) + * [PrefixedFileProvider](#prefixedfileprovider) + * [SubFileProvider](#subfileprovider) + * [Ramstack.FileProviders.Globbing](#ramstackfileprovidersglobbing-1) + * [Ramstack.FileProviders.Extensions](#ramstackfileprovidersextensions-1) + * [Ramstack.FileProviders.Composition](#ramstackfileproviderscomposition-1) + * [Flattening Providers](#flattening-providers) + * [Composing Providers](#composing-providers) + * [Flattening Change Tokens](#flattening-change-tokens) + * [NuGet Packages](#nuget-packages) + * [Supported versions](#supported-versions) + * [Contributions](#contributions) + * [License](#license) + + + +## Projects + +This repository contains the following projects: + +### Ramstack.FileProviders.Extensions +Offers useful and convenient extensions for `IFileProvider`, bringing its capabilities and experience +closer to what's provided by the `DirectoryInfo` and `FileInfo` classes. + +To install the `Ramstack.FileProviders.Extensions` [NuGet package](https://www.nuget.org/packages/Ramstack.FileProviders.Extensions) in your project, +run the following command: +```console +dotnet add package Ramstack.FileProviders.Extensions +``` + +### Ramstack.FileProviders +Provides additional implementations of `IFileProvider` including `PrefixedFileProvider` and `SubFileProvider`. + +To install the `Ramstack.FileProviders` [NuGet package](https://www.nuget.org/packages/Ramstack.FileProviders) in your project, +run the following command: +```console +dotnet add package Ramstack.FileProviders +``` + +### Ramstack.FileProviders.Globbing +Provides an implementation of the `IFileProvider` that filters files using include and/or exclude glob patterns +for flexible file visibility control. + +To install the `Ramstack.FileProviders.Globbing` [NuGet package](https://www.nuget.org/packages/Ramstack.FileProviders.Globbing) in your project, +run the following command: +```console +dotnet add package Ramstack.FileProviders.Globbing +``` + +### Ramstack.FileProviders.Composition +Provides a helper class for flattening and composing `IFileProvider` instances. + +To install the `Ramstack.FileProviders.Composition` [NuGet package](https://www.nuget.org/packages/Ramstack.FileProviders.Composition) in your project, +run the following command: +```console +dotnet add package Ramstack.FileProviders.Composition +``` + +## Overview + +### Ramstack.FileProviders + +This library offers additional implementations of the [IFileProvider](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.fileproviders.ifileprovider) interface: + +- `SubFileProvider` +- `PrefixedFileProvider` + +#### PrefixedFileProvider + +`PrefixedFileProvider` allows you to apply a prefix to the paths of files and directories. +This is useful when you need to organize files in a virtual hierarchy. + +Example: +```csharp +IFileProvider provider = new PrefixedFileProvider("/project/app", innerProvider); +IFileInfo file = provider.GetFileInfo("/project/app/docs/README"); +Console.WriteLine(file.Exists); +``` + +This is how you can add virtual directories to your project that are outside the project root: +```csharp +string packagesPath = Path.Combine(environment.ContentRootPath, "../Packages"); +string themesPath = Path.Combine(environment.ContentRootPath, "../Themes"); + +environment.ContentRootFileProvider = new CompositeFileProvider( + new PrefixedFileProvider("/Packages", new PhysicalFileProvider(packagesPath)), + new PrefixedFileProvider("/Themes", new PhysicalFileProvider(themesPath)), + environment.ContentRootFileProvider); +``` +The `Packages` and `Themes` directories are now available to the ASP.NET infrastructure under their respective names, +as if they were originally defined within your project. + +**Before:** +``` +/App +├── Controllers +├── Models +├── Views +└── wwwroot + +/Packages +├── package-1 +└── package-2 + +/Themes +├── theme-1 +└── theme-2 +``` + +**After:** +``` +/App +├── Controllers +├── Models +├── Views +├── Packages <-- (virtual) +│ ├── package-1 +│ └── package-2 +├── Themes <-- (virtual) +│ ├── theme-1 +│ └── theme-2 +└── wwwroot +``` + +#### SubFileProvider + +`SubFileProvider` lets you limit the view of the file system to a specific subdirectory, effectively creating a sandbox. + +Example: +```csharp +IFileProvider provider = new SubFileProvider("/docs", innerProvider); +IFileInfo file = provider.GetFileInfo("/README"); +Console.WriteLine(file.Exists); +``` + +### Ramstack.FileProviders.Globbing + +The `GlobbingFileProvider` class filters files using include and/or exclude glob patterns. Include patterns make only matching files visible, +while exclude patterns hide specific files. Both include and exclude patterns can be combined for flexible file visibility control. + +It relies on the [Ramstack.Globbing](https://www.nuget.org/packages/Ramstack.Globbing) package for its globbing capabilities. + +Example: +```csharp +IFileProvider provider = new GlobbingFileProvider(innerProvider, patterns: ["**/*.txt", "docs/*.md"], excludes: ["**/README.md"]); +foreach (IFileInfo file in provider.GetDirectoryContents("/")) + Console.WriteLine(file.Name); +``` + +### Ramstack.FileProviders.Extensions + +Provides useful extensions for `IFileProvider`, bringing its capabilities and experience closer to what's +provided by the `DirectoryInfo` and `FileInfo` classes. + +Simply stated, a `FileNode` knows which directory it is located in, and a directory represented by the `DirectoryNode` class can access +its parent directory and list all files within it, recursively. + +```csharp +using Ramstack.FileProviders; + +FileNode file = provider.GetFile("/docs/README"); + +// Prints the full path of the given file +Console.WriteLine($"Reading: {file.FullName}"); + +using StreamReader reader = file.OpenText(); +Console.WriteLine(reader.ReadToEnd()); +``` + +```csharp +DirectoryNode directory = provider.GetDirectory("/docs"); + +foreach (FileNode file in directory.EnumerateFiles()) + Console.WriteLine(file.FullName); +``` + +Furthermore, the methods for enumerating files (`EnumerateFiles`/`EnumerateDirectories`/`EnumerateFileNodes`) allow specifying glob patterns +to search for the desired files, as well as patterns to exclude files from the resulting list. + +```csharp +DirectoryNode directory = provider.GetDirectory("/project"); + +// Finds all *.md files and converts them to HTML +foreach (FileNode file in directory.EnumerateFiles(pattern: "**/*.md")) + RenderMarkdown(file); + +// Excludes files in a specific folder +foreach (FileNode file in directory.EnumerateFiles(pattern: "**/*.md", exclude: "vendors/**")) + RenderMarkdown(file); +``` +For convenience, many methods specific to `DirectoryNode` or `FileNode` are also available for `IFileProvider`. + +Thus, if we know the directory in which to look for files or the file to read, there is no need to obtain the +`DirectoryNode` or `FileNode` object. + +```csharp +using StreamReader reader = provider.OpenText("/docs/README", Encoding.UTF8); +Console.WriteLine(reader.ReadToEnd()); + +// Finds all *.md files and converts them to HTML +foreach (FileNode file in provider.EnumerateFiles("/project", pattern: "**/*.md")) + RenderMarkdown(file); +``` + +### Ramstack.FileProviders.Composition + +Provides a helper class `FileProviderComposer` for flattening and composing `IFileProvider` instances. + +#### Flattening Providers + +The `FlattenProvider` method attempts to flatten a given `IFileProvider` into a single list of file providers. + +This is especially useful when dealing with nested `CompositeFileProvider` instances, which might have been created during +different stages of a pipeline or configuration. Flattening helps in removing unnecessary indirectness and improving efficiency +by consolidating all file providers into a single level. + +```csharp +var builder = WebApplication.CreateBuilder(args); + +// Application pipeline configuration +... + +builder.Environment.ContentRootFileProvider = FileProviderComposer.FlattenProvider( + builder.Environment.ContentRootFileProvider); +``` + +#### Composing Providers + +The `ComposeProviders` method combines a list of `IFileProvider` instances into a single `IFileProvider`. +During this process, all encountered `CompositeFileProvider` instances are recursively flattened and merged into a single level. +This eliminates unnecessary indirectness and streamlines the file provider hierarchy. + +```csharp +string packagesPath = Path.Combine(environment.ContentRootPath, "../Packages"); +string themesPath = Path.Combine(environment.ContentRootPath, "../Themes"); + +environment.ContentRootFileProvider = FileProviderComposer.ComposeProviders( + // Inject external Packages directory + new PrefixedFileProvider("/Packages", new PhysicalFileProvider(packagesPath)), + + // Inject external Themes directory + new PrefixedFileProvider("/Themes", new PhysicalFileProvider(themesPath)), + + // Current provider + environment.ContentRootFileProvider); +``` + +In this example, the `ComposeProviders` method handles any unnecessary nesting that might occur, including when the current +`environment.ContentRootFileProvider` is a `CompositeFileProvider`. This ensures that all file providers are merged into a single +flat structure, avoiding unnecessary indirectness. + +#### Flattening Change Tokens +The `Flatten` extension method optimizes the structure of change token hierarchies by flattening nested `CompositeChangeToken` instances +and, most importantly, automatically filters out `NullChangeToken` instances from the hierarchy. Unlike standard `CompositeChangeToken` +behavior, which retains and processes `NullChangeToken` instances unnecessarily, this utility removes them completely, +resulting in improved performance and simplified change notification chains. + +```csharp +var changeToken = compositeFileProvider.Watch("**/*.json").Flatten(); +``` + + +## NuGet Packages +- [Ramstack.FileProviders.Extensions](https://www.nuget.org/packages/Ramstack.FileProviders.Extensions) — Useful and convenient extensions for `IFileProvider`, bringing its capabilities and experience closer to what's provided by the `DirectoryInfo` and `FileInfo` classes. +- [Ramstack.FileProviders](https://www.nuget.org/packages/Ramstack.FileProviders) — Additional file providers, including `PrefixedFileProvider` and `SubFileProvider`. +- [Ramstack.FileProviders.Globbing](https://www.nuget.org/packages/Ramstack.FileProviders.Globbing) — A file provider that filters files using include and/or exclude glob patterns. Include patterns make only matching files visible, while exclude patterns hide specific files. Both include and exclude patterns can be combined for flexible file visibility control. +- [Ramstack.FileProviders.Composition](https://www.nuget.org/packages/Ramstack.FileProviders.Composition) — Provides a helper class for flattening and composing `IFileProvider`. + +## Supported versions + +| | Version | +|------|----------------| +| .NET | 6, 7, 8, 9, 10 | + +## Contributions + +Bug reports and contributions are welcome. + +## License + +This project is released as open source under the **MIT License**. +See the [LICENSE](https://github.com/rameel/ramstack.fileproviders/blob/main/LICENSE) file for more details. diff --git a/Ramstack.FileProviders.slnx b/Ramstack.FileProviders.slnx index bb3a3d4..a8047e9 100644 --- a/Ramstack.FileProviders.slnx +++ b/Ramstack.FileProviders.slnx @@ -1,6 +1,7 @@ + diff --git a/src/Ramstack.FileProviders.Composition/ChangeTokenComposer.cs b/src/Ramstack.FileProviders.Composition/ChangeTokenComposer.cs index c26325f..b16bc5f 100644 --- a/src/Ramstack.FileProviders.Composition/ChangeTokenComposer.cs +++ b/src/Ramstack.FileProviders.Composition/ChangeTokenComposer.cs @@ -1,156 +1,156 @@ -using Microsoft.Extensions.FileProviders; -using Microsoft.Extensions.Primitives; - -namespace Ramstack.FileProviders.Composition; - -/// -/// Provides helper methods for the . -/// -public static class ChangeTokenComposer -{ - /// - /// Attempts to flatten the specified into a flat list of change tokens. - /// - /// - /// If the is not a , - /// the same instance of the is returned. - /// - /// The to flatten. - /// - /// An representing the flattened version of the specified . - /// - public static IChangeToken Flatten(this IChangeToken changeToken) => - FlattenChangeToken(changeToken); - - /// - /// Attempts to flatten the specified into a flat list of change tokens. - /// - /// - /// If the is not a , - /// the same instance of the is returned. - /// - /// The to flatten. - /// - /// An representing the flattened version of the specified . - /// - public static IChangeToken FlattenChangeToken(IChangeToken changeToken) - { - while (changeToken is CompositeChangeToken composite) - { - var changeTokens = composite.ChangeTokens; - if (changeTokens.Count == 0) - return NullChangeToken.Singleton; - - if (changeTokens.Count == 1) - { - changeToken = changeTokens[0]; - continue; - } - - for (int i = 0, count = changeTokens.Count; i < count; i++) - if (changeTokens[i] is CompositeChangeToken or NullChangeToken) - return ComposeChangeTokens(changeTokens); - - break; - } - - return changeToken; - } - - /// - /// Creates a change token from the specified list of instances and flattens it into a flat list of change tokens. - /// - /// - /// This method returns a if more than one token remains after flattening. - /// - /// The list of instances to compose and flatten. - /// - /// An representing the flattened version of the specified list of tokens. - /// - public static IChangeToken ComposeChangeTokens(params IChangeToken[] changeTokens) => - ComposeChangeTokens((IReadOnlyList)changeTokens); - - /// - /// Creates a change token from the specified list of instances and flattens it into a flat list of change tokens. - /// - /// - /// This method returns a if more than one token remains after flattening. - /// - /// The list of instances to compose and flatten. - /// - /// An representing the flattened version of the specified list of tokens. - /// - public static IChangeToken ComposeChangeTokens(IReadOnlyList changeTokens) - { - var queue = new Queue(); - var collection = new List(); - - for (int i = 0, count = changeTokens.Count; i < count; i++) - { - queue.Enqueue(changeTokens[i]); - - while (queue.TryDequeue(out var current)) - { - if (current is CompositeChangeToken composite) - { - var list = composite.ChangeTokens; - for (int j = 0, len = list.Count; j < len; j++) - queue.Enqueue(list[j]); - } - else if (current is not NullChangeToken) - { - collection.Add(current); - } - } - } - - return collection.Count switch - { - 0 => NullChangeToken.Singleton, - 1 => collection[0], - _ => new CompositeChangeToken(collection.ToArray()) - }; - } - - /// - /// Creates a change token from the specified list of instances and flattens it into a flat list of change tokens. - /// - /// - /// This method returns a if more than one token remains after flattening. - /// - /// The list of instances to compose and flatten. - /// - /// An representing the flattened version of the specified list of tokens. - /// - public static IChangeToken ComposeChangeTokens(IEnumerable changeTokens) - { - var queue = new Queue(); - var collection = new List(); - - foreach (var changeToken in changeTokens) - { - queue.Enqueue(changeToken); - - while (queue.TryDequeue(out var current)) - { - if (current is CompositeChangeToken composite) - { - var list = composite.ChangeTokens; - for (int i = 0, count = list.Count; i < count; i++) - queue.Enqueue(composite.ChangeTokens[i]); - } - else if (current is not NullChangeToken) - { - collection.Add(current); - } - } - } - - return collection.Count switch - { - 0 => NullChangeToken.Singleton, - 1 => collection[0], - _ => new CompositeChangeToken(collection.ToArray()) - }; - } -} +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Primitives; + +namespace Ramstack.FileProviders.Composition; + +/// +/// Provides helper methods for the . +/// +public static class ChangeTokenComposer +{ + /// + /// Attempts to flatten the specified into a flat list of change tokens. + /// + /// + /// If the is not a , + /// the same instance of the is returned. + /// + /// The to flatten. + /// + /// An representing the flattened version of the specified . + /// + public static IChangeToken Flatten(this IChangeToken changeToken) => + FlattenChangeToken(changeToken); + + /// + /// Attempts to flatten the specified into a flat list of change tokens. + /// + /// + /// If the is not a , + /// the same instance of the is returned. + /// + /// The to flatten. + /// + /// An representing the flattened version of the specified . + /// + public static IChangeToken FlattenChangeToken(IChangeToken changeToken) + { + while (changeToken is CompositeChangeToken composite) + { + var changeTokens = composite.ChangeTokens; + if (changeTokens.Count == 0) + return NullChangeToken.Singleton; + + if (changeTokens.Count == 1) + { + changeToken = changeTokens[0]; + continue; + } + + for (int i = 0, count = changeTokens.Count; i < count; i++) + if (changeTokens[i] is CompositeChangeToken or NullChangeToken) + return ComposeChangeTokens(changeTokens); + + break; + } + + return changeToken; + } + + /// + /// Creates a change token from the specified list of instances and flattens it into a flat list of change tokens. + /// + /// + /// This method returns a if more than one token remains after flattening. + /// + /// The list of instances to compose and flatten. + /// + /// An representing the flattened version of the specified list of tokens. + /// + public static IChangeToken ComposeChangeTokens(params IChangeToken[] changeTokens) => + ComposeChangeTokens((IReadOnlyList)changeTokens); + + /// + /// Creates a change token from the specified list of instances and flattens it into a flat list of change tokens. + /// + /// + /// This method returns a if more than one token remains after flattening. + /// + /// The list of instances to compose and flatten. + /// + /// An representing the flattened version of the specified list of tokens. + /// + public static IChangeToken ComposeChangeTokens(IReadOnlyList changeTokens) + { + var queue = new Queue(); + var collection = new List(); + + for (int i = 0, count = changeTokens.Count; i < count; i++) + { + queue.Enqueue(changeTokens[i]); + + while (queue.TryDequeue(out var current)) + { + if (current is CompositeChangeToken composite) + { + var list = composite.ChangeTokens; + for (int j = 0, len = list.Count; j < len; j++) + queue.Enqueue(list[j]); + } + else if (current is not NullChangeToken) + { + collection.Add(current); + } + } + } + + return collection.Count switch + { + 0 => NullChangeToken.Singleton, + 1 => collection[0], + _ => new CompositeChangeToken(collection.ToArray()) + }; + } + + /// + /// Creates a change token from the specified list of instances and flattens it into a flat list of change tokens. + /// + /// + /// This method returns a if more than one token remains after flattening. + /// + /// The list of instances to compose and flatten. + /// + /// An representing the flattened version of the specified list of tokens. + /// + public static IChangeToken ComposeChangeTokens(IEnumerable changeTokens) + { + var queue = new Queue(); + var collection = new List(); + + foreach (var changeToken in changeTokens) + { + queue.Enqueue(changeToken); + + while (queue.TryDequeue(out var current)) + { + if (current is CompositeChangeToken composite) + { + var list = composite.ChangeTokens; + for (int i = 0, count = list.Count; i < count; i++) + queue.Enqueue(composite.ChangeTokens[i]); + } + else if (current is not NullChangeToken) + { + collection.Add(current); + } + } + } + + return collection.Count switch + { + 0 => NullChangeToken.Singleton, + 1 => collection[0], + _ => new CompositeChangeToken(collection.ToArray()) + }; + } +} diff --git a/src/Ramstack.FileProviders.Composition/FileProviderComposer.cs b/src/Ramstack.FileProviders.Composition/FileProviderComposer.cs index e33afc1..5713c00 100644 --- a/src/Ramstack.FileProviders.Composition/FileProviderComposer.cs +++ b/src/Ramstack.FileProviders.Composition/FileProviderComposer.cs @@ -1,112 +1,112 @@ -using Microsoft.Extensions.FileProviders; - -namespace Ramstack.FileProviders.Composition; - -/// -/// Provides helper methods for the . -/// -public static class FileProviderComposer -{ - /// - /// Attempts to flatten the specified into a flat list of file providers. - /// - /// - /// If the is not a , - /// the same instance of the is returned. - /// - /// The to flatten. - /// - /// An representing the flattened version of the specified . - /// - public static IFileProvider Flatten(this IFileProvider provider) => - FlattenProvider(provider); - - /// - /// Attempts to flatten the specified into a flat list of file providers. - /// - /// - /// If the is not a , - /// the same instance of the is returned. - /// - /// The to flatten. - /// - /// An representing the flattened version of the specified . - /// - public static IFileProvider FlattenProvider(IFileProvider provider) - { - while (provider is CompositeFileProvider composite) - { - var providers = composite.FileProviders as IFileProvider[] ?? composite.FileProviders.ToArray(); - if (providers.Length == 0) - return new NullFileProvider(); - - if (providers.Length == 1) - { - provider = providers[0]; - continue; - } - - foreach (var p in providers) - if (p is CompositeFileProvider or NullFileProvider) - return ComposeProviders(providers); - - break; - } - - return provider; - } - - /// - /// Creates a provider from the specified list of instances and flattens it into a flat list of file providers. - /// - /// - /// This method returns a if more than one provider remains after flattening. - /// - /// The list of instances to compose and flatten. - /// - /// An representing the flattened version of the specified list of providers. - /// - public static IFileProvider ComposeProviders(params IFileProvider[] providers) => - ComposeProviders(providers.AsEnumerable()); - - /// - /// Creates a provider from the specified list of instances and flattens it into a flat list of file providers. - /// - /// - /// This method returns a if more than one provider remains after flattening. - /// - /// The list of instances to compose and flatten. - /// - /// An representing the flattened version of the specified list of providers. - /// - public static IFileProvider ComposeProviders(IEnumerable providers) - { - var queue = new Queue(); - var collection = new List(); - - foreach (var provider in providers) - { - queue.Enqueue(provider); - - while (queue.TryDequeue(out var current)) - { - if (current is CompositeFileProvider composite) - { - foreach (var p in composite.FileProviders) - queue.Enqueue(p); - } - else if (current is not NullFileProvider) - { - collection.Add(current); - } - } - } - - return collection.Count switch - { - 0 => new NullFileProvider(), - 1 => collection[0], - _ => new CompositeFileProvider(collection.ToArray()) - }; - } -} +using Microsoft.Extensions.FileProviders; + +namespace Ramstack.FileProviders.Composition; + +/// +/// Provides helper methods for the . +/// +public static class FileProviderComposer +{ + /// + /// Attempts to flatten the specified into a flat list of file providers. + /// + /// + /// If the is not a , + /// the same instance of the is returned. + /// + /// The to flatten. + /// + /// An representing the flattened version of the specified . + /// + public static IFileProvider Flatten(this IFileProvider provider) => + FlattenProvider(provider); + + /// + /// Attempts to flatten the specified into a flat list of file providers. + /// + /// + /// If the is not a , + /// the same instance of the is returned. + /// + /// The to flatten. + /// + /// An representing the flattened version of the specified . + /// + public static IFileProvider FlattenProvider(IFileProvider provider) + { + while (provider is CompositeFileProvider composite) + { + var providers = composite.FileProviders as IFileProvider[] ?? composite.FileProviders.ToArray(); + if (providers.Length == 0) + return new NullFileProvider(); + + if (providers.Length == 1) + { + provider = providers[0]; + continue; + } + + foreach (var p in providers) + if (p is CompositeFileProvider or NullFileProvider) + return ComposeProviders(providers); + + break; + } + + return provider; + } + + /// + /// Creates a provider from the specified list of instances and flattens it into a flat list of file providers. + /// + /// + /// This method returns a if more than one provider remains after flattening. + /// + /// The list of instances to compose and flatten. + /// + /// An representing the flattened version of the specified list of providers. + /// + public static IFileProvider ComposeProviders(params IFileProvider[] providers) => + ComposeProviders(providers.AsEnumerable()); + + /// + /// Creates a provider from the specified list of instances and flattens it into a flat list of file providers. + /// + /// + /// This method returns a if more than one provider remains after flattening. + /// + /// The list of instances to compose and flatten. + /// + /// An representing the flattened version of the specified list of providers. + /// + public static IFileProvider ComposeProviders(IEnumerable providers) + { + var queue = new Queue(); + var collection = new List(); + + foreach (var provider in providers) + { + queue.Enqueue(provider); + + while (queue.TryDequeue(out var current)) + { + if (current is CompositeFileProvider composite) + { + foreach (var p in composite.FileProviders) + queue.Enqueue(p); + } + else if (current is not NullFileProvider) + { + collection.Add(current); + } + } + } + + return collection.Count switch + { + 0 => new NullFileProvider(), + 1 => collection[0], + _ => new CompositeFileProvider(collection.ToArray()) + }; + } +} diff --git a/src/Ramstack.FileProviders.Composition/README.md b/src/Ramstack.FileProviders.Composition/README.md index c080ae5..6482e6f 100644 --- a/src/Ramstack.FileProviders.Composition/README.md +++ b/src/Ramstack.FileProviders.Composition/README.md @@ -1,83 +1,83 @@ -# Ramstack.FileProviders.Composition -[![NuGet](https://img.shields.io/nuget/v/Ramstack.FileProviders.Composition.svg)](https://nuget.org/packages/Ramstack.FileProviders.Composition) -[![MIT](https://img.shields.io/github/license/rameel/ramstack.fileproviders)](https://github.com/rameel/ramstack.fileproviders/blob/main/LICENSE) - -Represents a .NET library that provides a helper class for flattening and composing `IFileProvider` instances. - -## Getting Started - -To install the `Ramstack.FileProviders.Composition` [NuGet package](https://www.nuget.org/packages/Ramstack.FileProviders.Composition) -in your project, run the following command: -```console -dotnet add package Ramstack.FileProviders.Composition -``` - -## Flattening Providers -The `FlattenProvider` method attempts to flatten a given `IFileProvider` into a single list of file providers. - -This is especially useful when dealing with nested `CompositeFileProvider` instances, which might have been created during -different stages of a pipeline or configuration. Flattening helps in removing unnecessary indirectness and improving efficiency -by consolidating all file providers into a single level. - -```csharp -var builder = WebApplication.CreateBuilder(args); - -// Application pipeline configuration -... - -builder.Environment.ContentRootFileProvider = FileProviderComposer.FlattenProvider( - builder.Environment.ContentRootFileProvider); -``` - -## Composing Providers -The `ComposeProviders` method combines a list of `IFileProvider` instances into a single `IFileProvider`. -During this process, all encountered `CompositeFileProvider` instances are recursively flattened and merged into a single level. -This eliminates unnecessary indirectness and streamlines the file provider hierarchy. - -```csharp -string packagesPath = Path.Combine(environment.ContentRootPath, "../Packages"); -string themesPath = Path.Combine(environment.ContentRootPath, "../Themes"); - -environment.ContentRootFileProvider = FileProviderComposer.ComposeProviders( - // Inject external Packages directory - new PrefixedFileProvider("/Packages", new PhysicalFileProvider(packagesPath)), - - // Inject external Themes directory - new PrefixedFileProvider("/Themes", new PhysicalFileProvider(themesPath)), - - // Current provider - environment.ContentRootFileProvider); -``` - -In this example, the `ComposeProviders` method handles any unnecessary nesting that might occur, including when the current -`environment.ContentRootFileProvider` is a `CompositeFileProvider`. This ensures that all file providers are merged into a single -flat structure, avoiding unnecessary indirectness. - -## Flattening Change Tokens -The `Flatten` extension method optimizes the structure of change token hierarchies by flattening nested `CompositeChangeToken` instances -and, most importantly, automatically filters out `NullChangeToken` instances from the hierarchy. Unlike standard `CompositeChangeToken` -behavior, which retains and processes `NullChangeToken` instances unnecessarily, this utility removes them completely, -resulting in improved performance and simplified change notification chains. - -```csharp -var changeToken = compositeFileProvider.Watch("**/*.json").Flatten(); -``` - -## Related Packages -- [Ramstack.FileProviders.Extensions](https://www.nuget.org/packages/Ramstack.FileProviders.Extensions) — Useful and convenient extensions for `IFileProvider`, bringing its capabilities and experience closer to what's provided by the `DirectoryInfo` and `FileInfo` classes. -- [Ramstack.FileProviders](https://www.nuget.org/packages/Ramstack.FileProviders) — Additional file providers, including `PrefixedFileProvider` and `SubFileProvider`. -- [Ramstack.FileProviders.Globbing](https://www.nuget.org/packages/Ramstack.FileProviders.Globbing) — A file provider that filters files using include and/or exclude glob patterns. Include patterns make only matching files visible, while exclude patterns hide specific files. Both include and exclude patterns can be combined for flexible file visibility control. - -## Supported versions - -| | Version | -|------|----------------| -| .NET | 6, 7, 8, 9, 10 | - -## Contributions - -Bug reports and contributions are welcome. - -## License -This package is released as open source under the **MIT License**. -See the [LICENSE](https://github.com/rameel/ramstack.fileproviders/blob/main/LICENSE) file for more details. +# Ramstack.FileProviders.Composition +[![NuGet](https://img.shields.io/nuget/v/Ramstack.FileProviders.Composition.svg)](https://nuget.org/packages/Ramstack.FileProviders.Composition) +[![MIT](https://img.shields.io/github/license/rameel/ramstack.fileproviders)](https://github.com/rameel/ramstack.fileproviders/blob/main/LICENSE) + +Represents a .NET library that provides a helper class for flattening and composing `IFileProvider` instances. + +## Getting Started + +To install the `Ramstack.FileProviders.Composition` [NuGet package](https://www.nuget.org/packages/Ramstack.FileProviders.Composition) +in your project, run the following command: +```console +dotnet add package Ramstack.FileProviders.Composition +``` + +## Flattening Providers +The `FlattenProvider` method attempts to flatten a given `IFileProvider` into a single list of file providers. + +This is especially useful when dealing with nested `CompositeFileProvider` instances, which might have been created during +different stages of a pipeline or configuration. Flattening helps in removing unnecessary indirectness and improving efficiency +by consolidating all file providers into a single level. + +```csharp +var builder = WebApplication.CreateBuilder(args); + +// Application pipeline configuration +... + +builder.Environment.ContentRootFileProvider = FileProviderComposer.FlattenProvider( + builder.Environment.ContentRootFileProvider); +``` + +## Composing Providers +The `ComposeProviders` method combines a list of `IFileProvider` instances into a single `IFileProvider`. +During this process, all encountered `CompositeFileProvider` instances are recursively flattened and merged into a single level. +This eliminates unnecessary indirectness and streamlines the file provider hierarchy. + +```csharp +string packagesPath = Path.Combine(environment.ContentRootPath, "../Packages"); +string themesPath = Path.Combine(environment.ContentRootPath, "../Themes"); + +environment.ContentRootFileProvider = FileProviderComposer.ComposeProviders( + // Inject external Packages directory + new PrefixedFileProvider("/Packages", new PhysicalFileProvider(packagesPath)), + + // Inject external Themes directory + new PrefixedFileProvider("/Themes", new PhysicalFileProvider(themesPath)), + + // Current provider + environment.ContentRootFileProvider); +``` + +In this example, the `ComposeProviders` method handles any unnecessary nesting that might occur, including when the current +`environment.ContentRootFileProvider` is a `CompositeFileProvider`. This ensures that all file providers are merged into a single +flat structure, avoiding unnecessary indirectness. + +## Flattening Change Tokens +The `Flatten` extension method optimizes the structure of change token hierarchies by flattening nested `CompositeChangeToken` instances +and, most importantly, automatically filters out `NullChangeToken` instances from the hierarchy. Unlike standard `CompositeChangeToken` +behavior, which retains and processes `NullChangeToken` instances unnecessarily, this utility removes them completely, +resulting in improved performance and simplified change notification chains. + +```csharp +var changeToken = compositeFileProvider.Watch("**/*.json").Flatten(); +``` + +## Related Packages +- [Ramstack.FileProviders.Extensions](https://www.nuget.org/packages/Ramstack.FileProviders.Extensions) — Useful and convenient extensions for `IFileProvider`, bringing its capabilities and experience closer to what's provided by the `DirectoryInfo` and `FileInfo` classes. +- [Ramstack.FileProviders](https://www.nuget.org/packages/Ramstack.FileProviders) — Additional file providers, including `PrefixedFileProvider` and `SubFileProvider`. +- [Ramstack.FileProviders.Globbing](https://www.nuget.org/packages/Ramstack.FileProviders.Globbing) — A file provider that filters files using include and/or exclude glob patterns. Include patterns make only matching files visible, while exclude patterns hide specific files. Both include and exclude patterns can be combined for flexible file visibility control. + +## Supported versions + +| | Version | +|------|----------------| +| .NET | 6, 7, 8, 9, 10 | + +## Contributions + +Bug reports and contributions are welcome. + +## License +This package is released as open source under the **MIT License**. +See the [LICENSE](https://github.com/rameel/ramstack.fileproviders/blob/main/LICENSE) file for more details. diff --git a/src/Ramstack.FileProviders.Composition/Ramstack.FileProviders.Composition.csproj b/src/Ramstack.FileProviders.Composition/Ramstack.FileProviders.Composition.csproj index a6a7435..985d282 100644 --- a/src/Ramstack.FileProviders.Composition/Ramstack.FileProviders.Composition.csproj +++ b/src/Ramstack.FileProviders.Composition/Ramstack.FileProviders.Composition.csproj @@ -1,65 +1,65 @@ - - - - net6.0 - A .NET library providing helper classes for flattening and composing IFileProvider instances. - - enable - enable - preview - true - true - - - - 649;1591 - CS8600;CS8601;CS8602;CS8603;CS8604;CS8610;CS8613;CS8614;CS8618;CS8619;CS8620;CS8625;CS8643;CS8653;CS8714 - - - - https://github.com/rameel/ramstack.fileproviders - Rameel - fileprovider;files - https://github.com/rameel/ramstack.fileproviders#readme - README.md - MIT - true - snupkg - true - true - true - true - - - - true - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - Properties\AssemblyInfo.cs - - - - - - Properties\README.md - True - \ - - - - + + + + net6.0 + A .NET library providing helper classes for flattening and composing IFileProvider instances. + + enable + enable + preview + true + true + + + + 649;1591 + CS8600;CS8601;CS8602;CS8603;CS8604;CS8610;CS8613;CS8614;CS8618;CS8619;CS8620;CS8625;CS8643;CS8653;CS8714 + + + + https://github.com/rameel/ramstack.fileproviders + Rameel + fileprovider;files + https://github.com/rameel/ramstack.fileproviders#readme + README.md + MIT + true + snupkg + true + true + true + true + + + + true + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + Properties\AssemblyInfo.cs + + + + + + Properties\README.md + True + \ + + + + diff --git a/src/Ramstack.FileProviders.Extensions/DirectoryNode.cs b/src/Ramstack.FileProviders.Extensions/DirectoryNode.cs index 62eb40a..1c8692e 100644 --- a/src/Ramstack.FileProviders.Extensions/DirectoryNode.cs +++ b/src/Ramstack.FileProviders.Extensions/DirectoryNode.cs @@ -1,181 +1,181 @@ -namespace Ramstack.FileProviders; - -/// -/// Represents a directory in the specified file provider. -/// -public sealed class DirectoryNode : FileNodeBase -{ - private readonly IFileInfo? _file; - private IDirectoryContents? _directory; - - /// - /// Gets a value indicating whether the is a root. - /// - public bool IsRoot => FullName == "/"; - - /// - /// Gets a instance representing the root directory. - /// - public DirectoryNode Root - { - get - { - if (IsRoot) - return this; - - var directory = Provider.GetDirectoryContents("/"); - return new DirectoryNode(Provider, "/", directory); - } - } - - /// - /// Gets a instance representing the parent directory. - /// - public DirectoryNode? Parent - { - get - { - if (IsRoot) - return null; - - var parent = FilePath.GetDirectoryName(FullName); - var directory = Provider.GetDirectoryContents(parent); - return new DirectoryNode(Provider, parent, directory); - } - } - - /// - public override bool Exists => _file?.Exists ?? _directory!.Exists; - - /// - /// Initializes a new instance of the class. - /// - /// The associated with this directory. - /// The full path of the directory within the provider. - /// The associated with this directory. - internal DirectoryNode(IFileProvider provider, string path, IFileInfo file) : base(provider, path) - { - Debug.Assert(file.IsDirectory); - - _file = file; - // https://github.com/dotnet/aspnetcore/pull/50586 - _directory = _file as IDirectoryContents; - } - - /// - /// Initializes a new instance of the class. - /// - /// The associated with this directory. - /// The full path of the directory within the provider. - /// The associated with this directory. - internal DirectoryNode(IFileProvider provider, string path, IDirectoryContents directory) : base(provider, path) => - _directory = directory; - - /// - public override IFileInfo ToFileInfo() => - _file ?? new DirectoryFileInfoContents(this); - - /// - /// Returns the for the current instance. - /// - /// - /// The instance. - /// - public IDirectoryContents ToDirectoryContents() => - _directory ?? new DirectoryFileInfoContents(this); - - /// - /// Returns an enumerable collection of files in the current directory. - /// - /// - /// An enumerable collection of files in the current directory. - /// - public IEnumerable EnumerateFiles() - { - foreach (var fi in _directory ??= Provider.GetDirectoryContents(FullName)) - { - if (fi.IsDirectory) - continue; - - var path = FilePath.Join(FullName, fi.Name); - yield return new FileNode(Provider, path, fi); - } - } - - /// - /// Returns an enumerable collection of directories in the current directory. - /// - /// - /// An enumerable collection of directories in the current directory. - /// - public IEnumerable EnumerateDirectories() - { - foreach (var fi in _directory ??= Provider.GetDirectoryContents(FullName)) - { - if (fi.IsDirectory) - { - var path = FilePath.Join(FullName, fi.Name); - yield return new DirectoryNode(Provider, path, fi); - } - } - } - - /// - /// Returns an enumerable collection of file nodes in the current directory. - /// - /// - /// An enumerable collection of file nodes in the current directory. - /// - public IEnumerable EnumerateFileNodes() - { - foreach (var fi in _directory ??= Provider.GetDirectoryContents(FullName)) - { - var path = FilePath.Join(FullName, fi.Name); - yield return fi.IsDirectory - ? new DirectoryNode(Provider, path, fi) - : new FileNode(Provider, path, fi); - } - } - - #region Inner type: DirectoryFileInfoContents - - /// - /// Represents the contents of a directory as an implementation and provides - /// information about a directory as an . - /// - /// The directory node representing the directory. - private sealed class DirectoryFileInfoContents(DirectoryNode directory) : IFileInfo, IDirectoryContents - { - /// - public bool Exists => directory.Exists; - - /// - public long Length => -1; - - /// - public string? PhysicalPath => directory._file?.PhysicalPath; - - /// - public string Name => directory.Name; - - /// - public DateTimeOffset LastModified => default; - - /// - public bool IsDirectory => true; - - /// - public Stream CreateReadStream() => - throw new NotSupportedException("Cannot create a read stream for a directory."); - - /// - public IEnumerator GetEnumerator() => - (directory._directory ??= directory.Provider.GetDirectoryContents(directory.FullName)).GetEnumerator(); - - /// - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => - GetEnumerator(); - } - - #endregion -} +namespace Ramstack.FileProviders; + +/// +/// Represents a directory in the specified file provider. +/// +public sealed class DirectoryNode : FileNodeBase +{ + private readonly IFileInfo? _file; + private IDirectoryContents? _directory; + + /// + /// Gets a value indicating whether the is a root. + /// + public bool IsRoot => FullName == "/"; + + /// + /// Gets a instance representing the root directory. + /// + public DirectoryNode Root + { + get + { + if (IsRoot) + return this; + + var directory = Provider.GetDirectoryContents("/"); + return new DirectoryNode(Provider, "/", directory); + } + } + + /// + /// Gets a instance representing the parent directory. + /// + public DirectoryNode? Parent + { + get + { + if (IsRoot) + return null; + + var parent = FilePath.GetDirectoryName(FullName); + var directory = Provider.GetDirectoryContents(parent); + return new DirectoryNode(Provider, parent, directory); + } + } + + /// + public override bool Exists => _file?.Exists ?? _directory!.Exists; + + /// + /// Initializes a new instance of the class. + /// + /// The associated with this directory. + /// The full path of the directory within the provider. + /// The associated with this directory. + internal DirectoryNode(IFileProvider provider, string path, IFileInfo file) : base(provider, path) + { + Debug.Assert(file.IsDirectory); + + _file = file; + // https://github.com/dotnet/aspnetcore/pull/50586 + _directory = _file as IDirectoryContents; + } + + /// + /// Initializes a new instance of the class. + /// + /// The associated with this directory. + /// The full path of the directory within the provider. + /// The associated with this directory. + internal DirectoryNode(IFileProvider provider, string path, IDirectoryContents directory) : base(provider, path) => + _directory = directory; + + /// + public override IFileInfo ToFileInfo() => + _file ?? new DirectoryFileInfoContents(this); + + /// + /// Returns the for the current instance. + /// + /// + /// The instance. + /// + public IDirectoryContents ToDirectoryContents() => + _directory ?? new DirectoryFileInfoContents(this); + + /// + /// Returns an enumerable collection of files in the current directory. + /// + /// + /// An enumerable collection of files in the current directory. + /// + public IEnumerable EnumerateFiles() + { + foreach (var fi in _directory ??= Provider.GetDirectoryContents(FullName)) + { + if (fi.IsDirectory) + continue; + + var path = FilePath.Join(FullName, fi.Name); + yield return new FileNode(Provider, path, fi); + } + } + + /// + /// Returns an enumerable collection of directories in the current directory. + /// + /// + /// An enumerable collection of directories in the current directory. + /// + public IEnumerable EnumerateDirectories() + { + foreach (var fi in _directory ??= Provider.GetDirectoryContents(FullName)) + { + if (fi.IsDirectory) + { + var path = FilePath.Join(FullName, fi.Name); + yield return new DirectoryNode(Provider, path, fi); + } + } + } + + /// + /// Returns an enumerable collection of file nodes in the current directory. + /// + /// + /// An enumerable collection of file nodes in the current directory. + /// + public IEnumerable EnumerateFileNodes() + { + foreach (var fi in _directory ??= Provider.GetDirectoryContents(FullName)) + { + var path = FilePath.Join(FullName, fi.Name); + yield return fi.IsDirectory + ? new DirectoryNode(Provider, path, fi) + : new FileNode(Provider, path, fi); + } + } + + #region Inner type: DirectoryFileInfoContents + + /// + /// Represents the contents of a directory as an implementation and provides + /// information about a directory as an . + /// + /// The directory node representing the directory. + private sealed class DirectoryFileInfoContents(DirectoryNode directory) : IFileInfo, IDirectoryContents + { + /// + public bool Exists => directory.Exists; + + /// + public long Length => -1; + + /// + public string? PhysicalPath => directory._file?.PhysicalPath; + + /// + public string Name => directory.Name; + + /// + public DateTimeOffset LastModified => default; + + /// + public bool IsDirectory => true; + + /// + public Stream CreateReadStream() => + throw new NotSupportedException("Cannot create a read stream for a directory."); + + /// + public IEnumerator GetEnumerator() => + (directory._directory ??= directory.Provider.GetDirectoryContents(directory.FullName)).GetEnumerator(); + + /// + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => + GetEnumerator(); + } + + #endregion +} diff --git a/src/Ramstack.FileProviders.Extensions/DirectoryNodeExtensions.cs b/src/Ramstack.FileProviders.Extensions/DirectoryNodeExtensions.cs index 3ceef8a..333ab38 100644 --- a/src/Ramstack.FileProviders.Extensions/DirectoryNodeExtensions.cs +++ b/src/Ramstack.FileProviders.Extensions/DirectoryNodeExtensions.cs @@ -1,409 +1,409 @@ -using Ramstack.Globbing; -using Ramstack.Globbing.Traversal; - -namespace Ramstack.FileProviders; - -/// -/// Provides extension methods for the class. -/// -public static class DirectoryNodeExtensions -{ - /// - /// Returns an enumerable collection of files in the specified directory that match the given glob pattern. - /// - /// The instance representing the directory to search. - /// The glob pattern to match against the names of files. - /// Optional glob pattern to exclude files. - /// - /// An enumerable collection of files in the specified directory that match the given glob pattern. - /// - /// - /// Glob pattern: - /// - /// - /// - /// Supported meta-characters include '*', '?', '\' and '[', ']'. - /// And inside character classes '-', '!' and ']'. - /// - /// - /// - /// - /// The '.' and '..' symbols do not have any special treatment and are processed - /// as regular characters for matching. - /// - /// - /// - /// - /// Character classes can be negated by prefixing them with '!', such as [!0-9], - /// which matches all characters except digits. - /// - /// - /// - /// - /// Brace patterns are supported, including nested brace patterns: - /// {file,dir,name}, {file-1.{c,cpp},file-2.{cs,f}} - /// - /// - /// - /// - /// An empty pattern in brace expansion {} is allowed, as well as variations - /// like {.cs,}, {name,,file}, or {,.cs}. - /// - /// - /// - /// Leading and trailing separators are ignored. - /// - /// - /// Consecutive separators are counted as one. - /// - /// - /// - /// The '**' sequence in the glob pattern can be used to match zero or more directories and subdirectories. - /// It can be used at the beginning, middle, or end of a pattern, for example, - /// "**/file.txt", "dir/**/*.txt", "dir/**". - /// - /// - /// - /// - public static IEnumerable EnumerateFiles(this DirectoryNode directory, string pattern, string? exclude = null) => - EnumerateFiles(directory, [pattern], ToExcludes(exclude)); - - /// - /// Returns an enumerable collection of files in the specified directory that match any of the given glob patterns. - /// - /// The instance representing the directory to search. - /// An array of glob patterns to match against the names of files. - /// An optional array of glob patterns to exclude files. - /// - /// An enumerable collection of files in the specified directory that match any of the given glob patterns. - /// - /// - /// Glob pattern: - /// - /// - /// - /// Supported meta-characters include '*', '?', '\' and '[', ']'. - /// And inside character classes '-', '!' and ']'. - /// - /// - /// - /// - /// The '.' and '..' symbols do not have any special treatment and are processed - /// as regular characters for matching. - /// - /// - /// - /// - /// Character classes can be negated by prefixing them with '!', such as [!0-9], - /// which matches all characters except digits. - /// - /// - /// - /// - /// Brace patterns are supported, including nested brace patterns: - /// {file,dir,name}, {file-1.{c,cpp},file-2.{cs,f}} - /// - /// - /// - /// - /// An empty pattern in brace expansion {} is allowed, as well as variations - /// like {.cs,}, {name,,file}, or {,.cs}. - /// - /// - /// - /// Leading and trailing separators are ignored. - /// - /// - /// Consecutive separators are counted as one. - /// - /// - /// - /// The '**' sequence in the glob pattern can be used to match zero or more directories and subdirectories. - /// It can be used at the beginning, middle, or end of a pattern, for example, - /// "**/file.txt", "dir/**/*.txt", "dir/**". - /// - /// - /// - /// - public static IEnumerable EnumerateFiles(this DirectoryNode directory, string[] patterns, string[]? excludes = null) - { - return new FileTreeEnumerable(directory) - { - Patterns = patterns, - Excludes = excludes ?? [], - Flags = MatchFlags.Unix, - FileNameSelector = node => node.Name, - ShouldRecursePredicate = node => node is DirectoryNode, - ShouldIncludePredicate = node => node is FileNode, - ChildrenSelector = node => ((DirectoryNode)node).EnumerateFileNodes(), - ResultSelector = node => (FileNode)node - }; - } - - /// - /// Returns an enumerable collection of directories in the specified directory that match the given glob pattern. - /// - /// The instance representing the directory to search. - /// The glob pattern to match against the names of directories. - /// Optional glob pattern to exclude directories. - /// - /// An enumerable collection of directories in the specified directory that match the given glob pattern. - /// - /// - /// Glob pattern: - /// - /// - /// - /// Supported meta-characters include '*', '?', '\' and '[', ']'. - /// And inside character classes '-', '!' and ']'. - /// - /// - /// - /// - /// The '.' and '..' symbols do not have any special treatment and are processed - /// as regular characters for matching. - /// - /// - /// - /// - /// Character classes can be negated by prefixing them with '!', such as [!0-9], - /// which matches all characters except digits. - /// - /// - /// - /// - /// Brace patterns are supported, including nested brace patterns: - /// {file,dir,name}, {file-1.{c,cpp},file-2.{cs,f}} - /// - /// - /// - /// - /// An empty pattern in brace expansion {} is allowed, as well as variations - /// like {.cs,}, {name,,file}, or {,.cs}. - /// - /// - /// - /// Leading and trailing separators are ignored. - /// - /// - /// Consecutive separators are counted as one. - /// - /// - /// - /// The '**' sequence in the glob pattern can be used to match zero or more directories and subdirectories. - /// It can be used at the beginning, middle, or end of a pattern, for example, - /// "**/file.txt", "dir/**/*.txt", "dir/**". - /// - /// - /// - /// - public static IEnumerable EnumerateDirectories(this DirectoryNode directory, string pattern, string? exclude = null) => - EnumerateDirectories(directory, [pattern], ToExcludes(exclude)); - - /// - /// Returns an enumerable collection of directories in the specified directory that match any of the given glob patterns. - /// - /// The instance representing the directory to search. - /// An array of glob patterns to match against the names of directories. - /// An optional array of glob patterns to exclude directories. - /// - /// An enumerable collection of directories in the specified directory that match any of the given glob patterns. - /// - /// - /// Glob pattern: - /// - /// - /// - /// Supported meta-characters include '*', '?', '\' and '[', ']'. - /// And inside character classes '-', '!' and ']'. - /// - /// - /// - /// - /// The '.' and '..' symbols do not have any special treatment and are processed - /// as regular characters for matching. - /// - /// - /// - /// - /// Character classes can be negated by prefixing them with '!', such as [!0-9], - /// which matches all characters except digits. - /// - /// - /// - /// - /// Brace patterns are supported, including nested brace patterns: - /// {file,dir,name}, {file-1.{c,cpp},file-2.{cs,f}} - /// - /// - /// - /// - /// An empty pattern in brace expansion {} is allowed, as well as variations - /// like {.cs,}, {name,,file}, or {,.cs}. - /// - /// - /// - /// Leading and trailing separators are ignored. - /// - /// - /// Consecutive separators are counted as one. - /// - /// - /// - /// The '**' sequence in the glob pattern can be used to match zero or more directories and subdirectories. - /// It can be used at the beginning, middle, or end of a pattern, for example, - /// "**/file.txt", "dir/**/*.txt", "dir/**". - /// - /// - /// - /// - public static IEnumerable EnumerateDirectories(this DirectoryNode directory, string[] patterns, string[]? excludes = null) - { - return new FileTreeEnumerable(directory) - { - Patterns = patterns, - Excludes = excludes ?? [], - Flags = MatchFlags.Unix, - FileNameSelector = node => node.Name, - ShouldRecursePredicate = node => node is DirectoryNode, - ShouldIncludePredicate = node => node is DirectoryNode, - ChildrenSelector = node => ((DirectoryNode)node).EnumerateFileNodes(), - ResultSelector = node => (DirectoryNode)node - }; - } - - /// - /// Returns an enumerable collection of file nodes (both files and directories) in the specified directory - /// that match the given glob pattern. - /// - /// The instance representing the directory to search. - /// The glob pattern to match against the names of file nodes in the path. - /// Optional glob pattern to exclude file nodes. - /// - /// An enumerable collection of file nodes in the specified directory that match the given glob pattern. - /// - /// - /// Glob pattern: - /// - /// - /// - /// Supported meta-characters include '*', '?', '\' and '[', ']'. - /// And inside character classes '-', '!' and ']'. - /// - /// - /// - /// - /// The '.' and '..' symbols do not have any special treatment and are processed - /// as regular characters for matching. - /// - /// - /// - /// - /// Character classes can be negated by prefixing them with '!', such as [!0-9], - /// which matches all characters except digits. - /// - /// - /// - /// - /// Brace patterns are supported, including nested brace patterns: - /// {file,dir,name}, {file-1.{c,cpp},file-2.{cs,f}} - /// - /// - /// - /// - /// An empty pattern in brace expansion {} is allowed, as well as variations - /// like {.cs,}, {name,,file}, or {,.cs}. - /// - /// - /// - /// Leading and trailing separators are ignored. - /// - /// - /// Consecutive separators are counted as one. - /// - /// - /// - /// The '**' sequence in the glob pattern can be used to match zero or more directories and subdirectories. - /// It can be used at the beginning, middle, or end of a pattern, for example, - /// "**/file.txt", "dir/**/*.txt", "dir/**". - /// - /// - /// - /// - public static IEnumerable EnumerateFileNodes(this DirectoryNode directory, string pattern, string? exclude = null) => - EnumerateFileNodes(directory, [pattern], ToExcludes(exclude)); - - /// - /// Returns an enumerable collection of file nodes in the specified directory that match any of the given glob patterns. - /// - /// The instance representing the directory to search. - /// An array of glob patterns to match against the names of file nodes. - /// An optional array of glob patterns to exclude file nodes. - /// - /// An enumerable collection of file nodes in the specified directory that match any of the given glob patterns. - /// - /// - /// Glob pattern: - /// - /// - /// - /// Supported meta-characters include '*', '?', '\' and '[', ']'. - /// And inside character classes '-', '!' and ']'. - /// - /// - /// - /// - /// The '.' and '..' symbols do not have any special treatment and are processed - /// as regular characters for matching. - /// - /// - /// - /// - /// Character classes can be negated by prefixing them with '!', such as [!0-9], - /// which matches all characters except digits. - /// - /// - /// - /// - /// Brace patterns are supported, including nested brace patterns: - /// {file,dir,name}, {file-1.{c,cpp},file-2.{cs,f}} - /// - /// - /// - /// - /// An empty pattern in brace expansion {} is allowed, as well as variations - /// like {.cs,}, {name,,file}, or {,.cs}. - /// - /// - /// - /// Leading and trailing separators are ignored. - /// - /// - /// Consecutive separators are counted as one. - /// - /// - /// - /// The '**' sequence in the glob pattern can be used to match zero or more directories and subdirectories. - /// It can be used at the beginning, middle, or end of a pattern, for example, - /// "**/file.txt", "dir/**/*.txt", "dir/**". - /// - /// - /// - /// - public static IEnumerable EnumerateFileNodes(this DirectoryNode directory, string[] patterns, string[]? excludes = null) - { - return new FileTreeEnumerable(directory) - { - Patterns = patterns, - Excludes = excludes ?? [], - Flags = MatchFlags.Unix, - FileNameSelector = node => node.Name, - ShouldRecursePredicate = node => node is DirectoryNode, - ChildrenSelector = node => ((DirectoryNode)node).EnumerateFileNodes(), - ResultSelector = node => node - }; - } - - private static string[] ToExcludes(string? exclude) => - exclude is not null ? [exclude] : []; -} +using Ramstack.Globbing; +using Ramstack.Globbing.Traversal; + +namespace Ramstack.FileProviders; + +/// +/// Provides extension methods for the class. +/// +public static class DirectoryNodeExtensions +{ + /// + /// Returns an enumerable collection of files in the specified directory that match the given glob pattern. + /// + /// The instance representing the directory to search. + /// The glob pattern to match against the names of files. + /// Optional glob pattern to exclude files. + /// + /// An enumerable collection of files in the specified directory that match the given glob pattern. + /// + /// + /// Glob pattern: + /// + /// + /// + /// Supported meta-characters include '*', '?', '\' and '[', ']'. + /// And inside character classes '-', '!' and ']'. + /// + /// + /// + /// + /// The '.' and '..' symbols do not have any special treatment and are processed + /// as regular characters for matching. + /// + /// + /// + /// + /// Character classes can be negated by prefixing them with '!', such as [!0-9], + /// which matches all characters except digits. + /// + /// + /// + /// + /// Brace patterns are supported, including nested brace patterns: + /// {file,dir,name}, {file-1.{c,cpp},file-2.{cs,f}} + /// + /// + /// + /// + /// An empty pattern in brace expansion {} is allowed, as well as variations + /// like {.cs,}, {name,,file}, or {,.cs}. + /// + /// + /// + /// Leading and trailing separators are ignored. + /// + /// + /// Consecutive separators are counted as one. + /// + /// + /// + /// The '**' sequence in the glob pattern can be used to match zero or more directories and subdirectories. + /// It can be used at the beginning, middle, or end of a pattern, for example, + /// "**/file.txt", "dir/**/*.txt", "dir/**". + /// + /// + /// + /// + public static IEnumerable EnumerateFiles(this DirectoryNode directory, string pattern, string? exclude = null) => + EnumerateFiles(directory, [pattern], ToExcludes(exclude)); + + /// + /// Returns an enumerable collection of files in the specified directory that match any of the given glob patterns. + /// + /// The instance representing the directory to search. + /// An array of glob patterns to match against the names of files. + /// An optional array of glob patterns to exclude files. + /// + /// An enumerable collection of files in the specified directory that match any of the given glob patterns. + /// + /// + /// Glob pattern: + /// + /// + /// + /// Supported meta-characters include '*', '?', '\' and '[', ']'. + /// And inside character classes '-', '!' and ']'. + /// + /// + /// + /// + /// The '.' and '..' symbols do not have any special treatment and are processed + /// as regular characters for matching. + /// + /// + /// + /// + /// Character classes can be negated by prefixing them with '!', such as [!0-9], + /// which matches all characters except digits. + /// + /// + /// + /// + /// Brace patterns are supported, including nested brace patterns: + /// {file,dir,name}, {file-1.{c,cpp},file-2.{cs,f}} + /// + /// + /// + /// + /// An empty pattern in brace expansion {} is allowed, as well as variations + /// like {.cs,}, {name,,file}, or {,.cs}. + /// + /// + /// + /// Leading and trailing separators are ignored. + /// + /// + /// Consecutive separators are counted as one. + /// + /// + /// + /// The '**' sequence in the glob pattern can be used to match zero or more directories and subdirectories. + /// It can be used at the beginning, middle, or end of a pattern, for example, + /// "**/file.txt", "dir/**/*.txt", "dir/**". + /// + /// + /// + /// + public static IEnumerable EnumerateFiles(this DirectoryNode directory, string[] patterns, string[]? excludes = null) + { + return new FileTreeEnumerable(directory) + { + Patterns = patterns, + Excludes = excludes ?? [], + Flags = MatchFlags.Unix, + FileNameSelector = node => node.Name, + ShouldRecursePredicate = node => node is DirectoryNode, + ShouldIncludePredicate = node => node is FileNode, + ChildrenSelector = node => ((DirectoryNode)node).EnumerateFileNodes(), + ResultSelector = node => (FileNode)node + }; + } + + /// + /// Returns an enumerable collection of directories in the specified directory that match the given glob pattern. + /// + /// The instance representing the directory to search. + /// The glob pattern to match against the names of directories. + /// Optional glob pattern to exclude directories. + /// + /// An enumerable collection of directories in the specified directory that match the given glob pattern. + /// + /// + /// Glob pattern: + /// + /// + /// + /// Supported meta-characters include '*', '?', '\' and '[', ']'. + /// And inside character classes '-', '!' and ']'. + /// + /// + /// + /// + /// The '.' and '..' symbols do not have any special treatment and are processed + /// as regular characters for matching. + /// + /// + /// + /// + /// Character classes can be negated by prefixing them with '!', such as [!0-9], + /// which matches all characters except digits. + /// + /// + /// + /// + /// Brace patterns are supported, including nested brace patterns: + /// {file,dir,name}, {file-1.{c,cpp},file-2.{cs,f}} + /// + /// + /// + /// + /// An empty pattern in brace expansion {} is allowed, as well as variations + /// like {.cs,}, {name,,file}, or {,.cs}. + /// + /// + /// + /// Leading and trailing separators are ignored. + /// + /// + /// Consecutive separators are counted as one. + /// + /// + /// + /// The '**' sequence in the glob pattern can be used to match zero or more directories and subdirectories. + /// It can be used at the beginning, middle, or end of a pattern, for example, + /// "**/file.txt", "dir/**/*.txt", "dir/**". + /// + /// + /// + /// + public static IEnumerable EnumerateDirectories(this DirectoryNode directory, string pattern, string? exclude = null) => + EnumerateDirectories(directory, [pattern], ToExcludes(exclude)); + + /// + /// Returns an enumerable collection of directories in the specified directory that match any of the given glob patterns. + /// + /// The instance representing the directory to search. + /// An array of glob patterns to match against the names of directories. + /// An optional array of glob patterns to exclude directories. + /// + /// An enumerable collection of directories in the specified directory that match any of the given glob patterns. + /// + /// + /// Glob pattern: + /// + /// + /// + /// Supported meta-characters include '*', '?', '\' and '[', ']'. + /// And inside character classes '-', '!' and ']'. + /// + /// + /// + /// + /// The '.' and '..' symbols do not have any special treatment and are processed + /// as regular characters for matching. + /// + /// + /// + /// + /// Character classes can be negated by prefixing them with '!', such as [!0-9], + /// which matches all characters except digits. + /// + /// + /// + /// + /// Brace patterns are supported, including nested brace patterns: + /// {file,dir,name}, {file-1.{c,cpp},file-2.{cs,f}} + /// + /// + /// + /// + /// An empty pattern in brace expansion {} is allowed, as well as variations + /// like {.cs,}, {name,,file}, or {,.cs}. + /// + /// + /// + /// Leading and trailing separators are ignored. + /// + /// + /// Consecutive separators are counted as one. + /// + /// + /// + /// The '**' sequence in the glob pattern can be used to match zero or more directories and subdirectories. + /// It can be used at the beginning, middle, or end of a pattern, for example, + /// "**/file.txt", "dir/**/*.txt", "dir/**". + /// + /// + /// + /// + public static IEnumerable EnumerateDirectories(this DirectoryNode directory, string[] patterns, string[]? excludes = null) + { + return new FileTreeEnumerable(directory) + { + Patterns = patterns, + Excludes = excludes ?? [], + Flags = MatchFlags.Unix, + FileNameSelector = node => node.Name, + ShouldRecursePredicate = node => node is DirectoryNode, + ShouldIncludePredicate = node => node is DirectoryNode, + ChildrenSelector = node => ((DirectoryNode)node).EnumerateFileNodes(), + ResultSelector = node => (DirectoryNode)node + }; + } + + /// + /// Returns an enumerable collection of file nodes (both files and directories) in the specified directory + /// that match the given glob pattern. + /// + /// The instance representing the directory to search. + /// The glob pattern to match against the names of file nodes in the path. + /// Optional glob pattern to exclude file nodes. + /// + /// An enumerable collection of file nodes in the specified directory that match the given glob pattern. + /// + /// + /// Glob pattern: + /// + /// + /// + /// Supported meta-characters include '*', '?', '\' and '[', ']'. + /// And inside character classes '-', '!' and ']'. + /// + /// + /// + /// + /// The '.' and '..' symbols do not have any special treatment and are processed + /// as regular characters for matching. + /// + /// + /// + /// + /// Character classes can be negated by prefixing them with '!', such as [!0-9], + /// which matches all characters except digits. + /// + /// + /// + /// + /// Brace patterns are supported, including nested brace patterns: + /// {file,dir,name}, {file-1.{c,cpp},file-2.{cs,f}} + /// + /// + /// + /// + /// An empty pattern in brace expansion {} is allowed, as well as variations + /// like {.cs,}, {name,,file}, or {,.cs}. + /// + /// + /// + /// Leading and trailing separators are ignored. + /// + /// + /// Consecutive separators are counted as one. + /// + /// + /// + /// The '**' sequence in the glob pattern can be used to match zero or more directories and subdirectories. + /// It can be used at the beginning, middle, or end of a pattern, for example, + /// "**/file.txt", "dir/**/*.txt", "dir/**". + /// + /// + /// + /// + public static IEnumerable EnumerateFileNodes(this DirectoryNode directory, string pattern, string? exclude = null) => + EnumerateFileNodes(directory, [pattern], ToExcludes(exclude)); + + /// + /// Returns an enumerable collection of file nodes in the specified directory that match any of the given glob patterns. + /// + /// The instance representing the directory to search. + /// An array of glob patterns to match against the names of file nodes. + /// An optional array of glob patterns to exclude file nodes. + /// + /// An enumerable collection of file nodes in the specified directory that match any of the given glob patterns. + /// + /// + /// Glob pattern: + /// + /// + /// + /// Supported meta-characters include '*', '?', '\' and '[', ']'. + /// And inside character classes '-', '!' and ']'. + /// + /// + /// + /// + /// The '.' and '..' symbols do not have any special treatment and are processed + /// as regular characters for matching. + /// + /// + /// + /// + /// Character classes can be negated by prefixing them with '!', such as [!0-9], + /// which matches all characters except digits. + /// + /// + /// + /// + /// Brace patterns are supported, including nested brace patterns: + /// {file,dir,name}, {file-1.{c,cpp},file-2.{cs,f}} + /// + /// + /// + /// + /// An empty pattern in brace expansion {} is allowed, as well as variations + /// like {.cs,}, {name,,file}, or {,.cs}. + /// + /// + /// + /// Leading and trailing separators are ignored. + /// + /// + /// Consecutive separators are counted as one. + /// + /// + /// + /// The '**' sequence in the glob pattern can be used to match zero or more directories and subdirectories. + /// It can be used at the beginning, middle, or end of a pattern, for example, + /// "**/file.txt", "dir/**/*.txt", "dir/**". + /// + /// + /// + /// + public static IEnumerable EnumerateFileNodes(this DirectoryNode directory, string[] patterns, string[]? excludes = null) + { + return new FileTreeEnumerable(directory) + { + Patterns = patterns, + Excludes = excludes ?? [], + Flags = MatchFlags.Unix, + FileNameSelector = node => node.Name, + ShouldRecursePredicate = node => node is DirectoryNode, + ChildrenSelector = node => ((DirectoryNode)node).EnumerateFileNodes(), + ResultSelector = node => node + }; + } + + private static string[] ToExcludes(string? exclude) => + exclude is not null ? [exclude] : []; +} diff --git a/src/Ramstack.FileProviders.Extensions/FileInfoExtensions.cs b/src/Ramstack.FileProviders.Extensions/FileInfoExtensions.cs index 646ef04..f499c3b 100644 --- a/src/Ramstack.FileProviders.Extensions/FileInfoExtensions.cs +++ b/src/Ramstack.FileProviders.Extensions/FileInfoExtensions.cs @@ -1,279 +1,279 @@ -using System.Buffers; -using System.Text; - -namespace Ramstack.FileProviders; - -/// -/// Provides extension methods for the . -/// -public static class FileInfoExtensions -{ - /// - /// Returns file contents as a read-only stream. - /// - /// The . - /// - /// The file contents as a read-only stream. - /// - public static Stream OpenRead(this IFileInfo file) => - file.CreateReadStream(); - - /// - /// Returns a with the specified character encoding that reads from the text file. - /// - /// The . - /// The character encoding to use. - /// - /// A new with the specified character encoding. - /// - public static StreamReader OpenText(this IFileInfo file, Encoding? encoding) => - new StreamReader(file.CreateReadStream(), encoding, detectEncodingFromByteOrderMarks: true, bufferSize: -1, leaveOpen: false); - - /// - /// Reads all the text in the file with the specified encoding. - /// - /// The file from which to read the entire text content. - /// The encoding applied to the contents. - /// - /// A string containing all text in the file. - /// - public static string ReadAllText(this IFileInfo file, Encoding? encoding = null) - { - using var reader = file.OpenText(encoding); - return reader.ReadToEnd(); - } - - /// - /// Reads all lines of the file with the specified encoding. - /// - /// The file to read from. - /// The encoding applied to the contents. - /// - /// A string array containing all lines of the file. - /// - public static string[] ReadAllLines(this IFileInfo file, Encoding? encoding = null) - { - var stream = file.OpenRead(); - using var reader = new StreamReader(stream, encoding!); - - var list = new List(); - while (reader.ReadLine() is { } line) - list.Add(line); - - return list.ToArray(); - } - - /// - /// Reads the entire contents of the current file into a byte array. - /// - /// The file to read from. - /// - /// A byte array containing the contents of the file. - /// - public static byte[] ReadAllBytes(this IFileInfo file) - { - // ReSharper disable once UseAwaitUsing - using var stream = file.OpenRead(); - - var length = stream.CanSeek ? stream.Length : 0; - if (length > Array.MaxLength) - throw new IOException("The file is too large."); - - // https://github.com/dotnet/runtime/blob/5535e31a712343a63f5d7d796cd874e563e5ac14/src/libraries/System.Private.CoreLib/src/System/IO/File.cs#L660 - // Some file systems (e.g. procfs on Linux) return 0 for length even when there's content - // Thus we need to assume 0 doesn't mean empty. - return length <= 0 - ? ReadAllBytesUnknownLengthImpl(stream) - : ReadAllBytesImpl(stream); - - static byte[] ReadAllBytesImpl(Stream stream) - { - var bytes = new byte[stream.Length]; - var total = 0; - do - { - var count = stream.Read(bytes.AsSpan(total)); - if (count == 0) - Error_EndOfStream(); - - total += count; - } while (total < bytes.Length); - - return bytes; - } - - static byte[] ReadAllBytesUnknownLengthImpl(Stream stream) - { - var bytes = ArrayPool.Shared.Rent(512); - var total = 0; - - while (true) - { - if (total == bytes.Length) - bytes = ResizeBuffer(bytes); - - var count = stream.Read(bytes.AsSpan(total)); - if (count == 0) - break; - - total += count; - } - - var result = bytes.AsSpan(0, total).ToArray(); - ArrayPool.Shared.Return(bytes); - return result; - } - } - - /// - /// Asynchronously reads all the text in the current file. - /// - /// The file from which to read the entire text content. - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation, - /// containing the full text from the current file. - /// - public static ValueTask ReadAllTextAsync(this IFileInfo file, CancellationToken cancellationToken = default) => - ReadAllTextAsync(file, encoding: null, cancellationToken); - - /// - /// Asynchronously reads all the text in the current file with the specified encoding. - /// - /// The file from which to read the entire text content. - /// The encoding applied to the contents. - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation, - /// containing the full text from the current file. - /// - public static async ValueTask ReadAllTextAsync(this IFileInfo file, Encoding? encoding, CancellationToken cancellationToken = default) - { - // ReSharper disable once UseAwaitUsing - using var stream = file.OpenRead(); - using var reader = new StreamReader(stream, encoding!); - - return await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false); - } - - /// - /// Asynchronously reads all lines of the current file. - /// - /// The file to read from. - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation, - /// containing an array of all lines in the current file. - /// - public static ValueTask ReadAllLinesAsync(this IFileInfo file, CancellationToken cancellationToken = default) => - ReadAllLinesAsync(file, encoding: null, cancellationToken); - - /// - /// Asynchronously reads all lines of the current file with the specified encoding. - /// - /// The file to read from. - /// The encoding applied to the contents. - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation, - /// containing an array of all lines in the current file. - /// - public static async ValueTask ReadAllLinesAsync(this IFileInfo file, Encoding? encoding, CancellationToken cancellationToken = default) - { - // ReSharper disable once UseAwaitUsing - using var stream = file.OpenRead(); - using var reader = new StreamReader(stream, encoding!); - - var list = new List(); - while (await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false) is { } line) - list.Add(line); - - return list.ToArray(); - } - - /// - /// Asynchronously reads the entire contents of the current file into a byte array. - /// - /// The file to read from. - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation, - /// containing an array of the file's bytes. - /// - public static async ValueTask ReadAllBytesAsync(this IFileInfo file, CancellationToken cancellationToken = default) - { - // ReSharper disable once UseAwaitUsing - using var stream = file.OpenRead(); - - var length = stream.CanSeek ? stream.Length : 0; - if (length > Array.MaxLength) - throw new IOException("The file is too large."); - - // https://github.com/dotnet/runtime/blob/5535e31a712343a63f5d7d796cd874e563e5ac14/src/libraries/System.Private.CoreLib/src/System/IO/File.cs#L660 - // Some file systems (e.g. procfs on Linux) return 0 for length even when there's content. - // Thus we need to assume 0 doesn't mean empty. - var task = length <= 0 - ? ReadAllBytesUnknownLengthImplAsync(stream, cancellationToken) - : ReadAllBytesImplAsync(stream, cancellationToken); - - return await task.ConfigureAwait(false); - - static async ValueTask ReadAllBytesImplAsync(Stream stream, CancellationToken cancellationToken) - { - var bytes = new byte[stream.Length]; - var total = 0; - - do - { - var count = await stream.ReadAsync(bytes.AsMemory(total), cancellationToken).ConfigureAwait(false); - if (count == 0) - Error_EndOfStream(); - - total += count; - } while (total < bytes.Length); - - return bytes; - } - - static async ValueTask ReadAllBytesUnknownLengthImplAsync(Stream stream, CancellationToken cancellationToken) - { - var bytes = ArrayPool.Shared.Rent(512); - var total = 0; - - while (true) - { - if (total == bytes.Length) - bytes = ResizeBuffer(bytes); - - var count = await stream - .ReadAsync(bytes.AsMemory(total), cancellationToken) - .ConfigureAwait(false); - - if (count == 0) - break; - - total += count; - } - - var result = bytes.AsSpan(0, total).ToArray(); - ArrayPool.Shared.Return(bytes); - return result; - } - } - - private static byte[] ResizeBuffer(byte[] oldArray) - { - var length = oldArray.Length * 2; - if ((uint)length > (uint)Array.MaxLength) - length = Math.Max(Array.MaxLength, oldArray.Length + 1); - - var newArray = ArrayPool.Shared.Rent(length); - oldArray.AsSpan().TryCopyTo(newArray); - - ArrayPool.Shared.Return(oldArray); - return newArray; - } - - private static void Error_EndOfStream() => - throw new EndOfStreamException(); -} +using System.Buffers; +using System.Text; + +namespace Ramstack.FileProviders; + +/// +/// Provides extension methods for the . +/// +public static class FileInfoExtensions +{ + /// + /// Returns file contents as a read-only stream. + /// + /// The . + /// + /// The file contents as a read-only stream. + /// + public static Stream OpenRead(this IFileInfo file) => + file.CreateReadStream(); + + /// + /// Returns a with the specified character encoding that reads from the text file. + /// + /// The . + /// The character encoding to use. + /// + /// A new with the specified character encoding. + /// + public static StreamReader OpenText(this IFileInfo file, Encoding? encoding) => + new StreamReader(file.CreateReadStream(), encoding, detectEncodingFromByteOrderMarks: true, bufferSize: -1, leaveOpen: false); + + /// + /// Reads all the text in the file with the specified encoding. + /// + /// The file from which to read the entire text content. + /// The encoding applied to the contents. + /// + /// A string containing all text in the file. + /// + public static string ReadAllText(this IFileInfo file, Encoding? encoding = null) + { + using var reader = file.OpenText(encoding); + return reader.ReadToEnd(); + } + + /// + /// Reads all lines of the file with the specified encoding. + /// + /// The file to read from. + /// The encoding applied to the contents. + /// + /// A string array containing all lines of the file. + /// + public static string[] ReadAllLines(this IFileInfo file, Encoding? encoding = null) + { + var stream = file.OpenRead(); + using var reader = new StreamReader(stream, encoding!); + + var list = new List(); + while (reader.ReadLine() is { } line) + list.Add(line); + + return list.ToArray(); + } + + /// + /// Reads the entire contents of the current file into a byte array. + /// + /// The file to read from. + /// + /// A byte array containing the contents of the file. + /// + public static byte[] ReadAllBytes(this IFileInfo file) + { + // ReSharper disable once UseAwaitUsing + using var stream = file.OpenRead(); + + var length = stream.CanSeek ? stream.Length : 0; + if (length > Array.MaxLength) + throw new IOException("The file is too large."); + + // https://github.com/dotnet/runtime/blob/5535e31a712343a63f5d7d796cd874e563e5ac14/src/libraries/System.Private.CoreLib/src/System/IO/File.cs#L660 + // Some file systems (e.g. procfs on Linux) return 0 for length even when there's content + // Thus we need to assume 0 doesn't mean empty. + return length <= 0 + ? ReadAllBytesUnknownLengthImpl(stream) + : ReadAllBytesImpl(stream); + + static byte[] ReadAllBytesImpl(Stream stream) + { + var bytes = new byte[stream.Length]; + var total = 0; + do + { + var count = stream.Read(bytes.AsSpan(total)); + if (count == 0) + Error_EndOfStream(); + + total += count; + } while (total < bytes.Length); + + return bytes; + } + + static byte[] ReadAllBytesUnknownLengthImpl(Stream stream) + { + var bytes = ArrayPool.Shared.Rent(512); + var total = 0; + + while (true) + { + if (total == bytes.Length) + bytes = ResizeBuffer(bytes); + + var count = stream.Read(bytes.AsSpan(total)); + if (count == 0) + break; + + total += count; + } + + var result = bytes.AsSpan(0, total).ToArray(); + ArrayPool.Shared.Return(bytes); + return result; + } + } + + /// + /// Asynchronously reads all the text in the current file. + /// + /// The file from which to read the entire text content. + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation, + /// containing the full text from the current file. + /// + public static ValueTask ReadAllTextAsync(this IFileInfo file, CancellationToken cancellationToken = default) => + ReadAllTextAsync(file, encoding: null, cancellationToken); + + /// + /// Asynchronously reads all the text in the current file with the specified encoding. + /// + /// The file from which to read the entire text content. + /// The encoding applied to the contents. + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation, + /// containing the full text from the current file. + /// + public static async ValueTask ReadAllTextAsync(this IFileInfo file, Encoding? encoding, CancellationToken cancellationToken = default) + { + // ReSharper disable once UseAwaitUsing + using var stream = file.OpenRead(); + using var reader = new StreamReader(stream, encoding!); + + return await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false); + } + + /// + /// Asynchronously reads all lines of the current file. + /// + /// The file to read from. + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation, + /// containing an array of all lines in the current file. + /// + public static ValueTask ReadAllLinesAsync(this IFileInfo file, CancellationToken cancellationToken = default) => + ReadAllLinesAsync(file, encoding: null, cancellationToken); + + /// + /// Asynchronously reads all lines of the current file with the specified encoding. + /// + /// The file to read from. + /// The encoding applied to the contents. + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation, + /// containing an array of all lines in the current file. + /// + public static async ValueTask ReadAllLinesAsync(this IFileInfo file, Encoding? encoding, CancellationToken cancellationToken = default) + { + // ReSharper disable once UseAwaitUsing + using var stream = file.OpenRead(); + using var reader = new StreamReader(stream, encoding!); + + var list = new List(); + while (await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false) is { } line) + list.Add(line); + + return list.ToArray(); + } + + /// + /// Asynchronously reads the entire contents of the current file into a byte array. + /// + /// The file to read from. + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation, + /// containing an array of the file's bytes. + /// + public static async ValueTask ReadAllBytesAsync(this IFileInfo file, CancellationToken cancellationToken = default) + { + // ReSharper disable once UseAwaitUsing + using var stream = file.OpenRead(); + + var length = stream.CanSeek ? stream.Length : 0; + if (length > Array.MaxLength) + throw new IOException("The file is too large."); + + // https://github.com/dotnet/runtime/blob/5535e31a712343a63f5d7d796cd874e563e5ac14/src/libraries/System.Private.CoreLib/src/System/IO/File.cs#L660 + // Some file systems (e.g. procfs on Linux) return 0 for length even when there's content. + // Thus we need to assume 0 doesn't mean empty. + var task = length <= 0 + ? ReadAllBytesUnknownLengthImplAsync(stream, cancellationToken) + : ReadAllBytesImplAsync(stream, cancellationToken); + + return await task.ConfigureAwait(false); + + static async ValueTask ReadAllBytesImplAsync(Stream stream, CancellationToken cancellationToken) + { + var bytes = new byte[stream.Length]; + var total = 0; + + do + { + var count = await stream.ReadAsync(bytes.AsMemory(total), cancellationToken).ConfigureAwait(false); + if (count == 0) + Error_EndOfStream(); + + total += count; + } while (total < bytes.Length); + + return bytes; + } + + static async ValueTask ReadAllBytesUnknownLengthImplAsync(Stream stream, CancellationToken cancellationToken) + { + var bytes = ArrayPool.Shared.Rent(512); + var total = 0; + + while (true) + { + if (total == bytes.Length) + bytes = ResizeBuffer(bytes); + + var count = await stream + .ReadAsync(bytes.AsMemory(total), cancellationToken) + .ConfigureAwait(false); + + if (count == 0) + break; + + total += count; + } + + var result = bytes.AsSpan(0, total).ToArray(); + ArrayPool.Shared.Return(bytes); + return result; + } + } + + private static byte[] ResizeBuffer(byte[] oldArray) + { + var length = oldArray.Length * 2; + if ((uint)length > (uint)Array.MaxLength) + length = Math.Max(Array.MaxLength, oldArray.Length + 1); + + var newArray = ArrayPool.Shared.Rent(length); + oldArray.AsSpan().TryCopyTo(newArray); + + ArrayPool.Shared.Return(oldArray); + return newArray; + } + + private static void Error_EndOfStream() => + throw new EndOfStreamException(); +} diff --git a/src/Ramstack.FileProviders.Extensions/FileNode.cs b/src/Ramstack.FileProviders.Extensions/FileNode.cs index cb19b80..d51c851 100644 --- a/src/Ramstack.FileProviders.Extensions/FileNode.cs +++ b/src/Ramstack.FileProviders.Extensions/FileNode.cs @@ -1,99 +1,99 @@ -using System.Text; - -namespace Ramstack.FileProviders; - -/// -/// Represents a file in the specified file provider. -/// -public sealed class FileNode : FileNodeBase -{ - private readonly IFileInfo _file; - - /// - public override bool Exists => _file.Exists; - - /// - /// Gets the string representing the extension part of the file. - /// - public string Extension => FilePath.GetExtension(FullName); - - /// - /// Gets the full path of the directory containing the file. - /// - public string DirectoryName => FilePath.GetDirectoryName(FullName); - - /// - /// Gets the last modified time of the current file. - /// - public DateTimeOffset LastWriteTime => _file.LastModified; - - /// - /// Gets a instance representing the parent directory. - /// - public DirectoryNode Directory - { - get - { - var path = FilePath.GetDirectoryName(FullName); - var directory = Provider.GetDirectoryContents(path); - return new DirectoryNode(Provider, path, directory); - } - } - - /// - /// Gets the length of the file in bytes, or -1 for non-existing files. - /// - public long Length => _file.Length; - - /// - /// Initializes a new instance of the class. - /// - /// The associated with this file. - /// The full path of the file within the provider. - /// The associated with this file. - internal FileNode(IFileProvider provider, string path, IFileInfo file) : base(provider, path) - { - if (file.IsDirectory) - Error_FileExpected(); - - _file = file; - } - - /// - /// Returns a read-only stream of the current file. - /// - /// - /// A read-only stream. - /// - public Stream OpenRead() => - _file.CreateReadStream(); - - /// - /// Returns a with the specified character encoding that reads from the current text file. - /// - /// The character encoding to use. - /// - /// A new with the specified character encoding. - /// - public StreamReader OpenText(Encoding? encoding = null) => - new StreamReader(_file.CreateReadStream(), encoding, detectEncodingFromByteOrderMarks: true, bufferSize: -1, leaveOpen: false); - - /// - /// Returns the for the current instance. - /// - /// - /// The instance. - /// - [Obsolete("This method is obsolete and will be removed in a future release. Use the ToFileInfo method instead.", error: false)] - [EditorBrowsable(EditorBrowsableState.Never)] - public IFileInfo GetFileInfo() => - _file; - - /// - public override IFileInfo ToFileInfo() => - _file; - - [DoesNotReturn] - private static void Error_FileExpected() => - throw new ArgumentException("The specified path refers to a directory, not a file."); -} +using System.Text; + +namespace Ramstack.FileProviders; + +/// +/// Represents a file in the specified file provider. +/// +public sealed class FileNode : FileNodeBase +{ + private readonly IFileInfo _file; + + /// + public override bool Exists => _file.Exists; + + /// + /// Gets the string representing the extension part of the file. + /// + public string Extension => FilePath.GetExtension(FullName); + + /// + /// Gets the full path of the directory containing the file. + /// + public string DirectoryName => FilePath.GetDirectoryName(FullName); + + /// + /// Gets the last modified time of the current file. + /// + public DateTimeOffset LastWriteTime => _file.LastModified; + + /// + /// Gets a instance representing the parent directory. + /// + public DirectoryNode Directory + { + get + { + var path = FilePath.GetDirectoryName(FullName); + var directory = Provider.GetDirectoryContents(path); + return new DirectoryNode(Provider, path, directory); + } + } + + /// + /// Gets the length of the file in bytes, or -1 for non-existing files. + /// + public long Length => _file.Length; + + /// + /// Initializes a new instance of the class. + /// + /// The associated with this file. + /// The full path of the file within the provider. + /// The associated with this file. + internal FileNode(IFileProvider provider, string path, IFileInfo file) : base(provider, path) + { + if (file.IsDirectory) + Error_FileExpected(); + + _file = file; + } + + /// + /// Returns a read-only stream of the current file. + /// + /// + /// A read-only stream. + /// + public Stream OpenRead() => + _file.CreateReadStream(); + + /// + /// Returns a with the specified character encoding that reads from the current text file. + /// + /// The character encoding to use. + /// + /// A new with the specified character encoding. + /// + public StreamReader OpenText(Encoding? encoding = null) => + new StreamReader(_file.CreateReadStream(), encoding, detectEncodingFromByteOrderMarks: true, bufferSize: -1, leaveOpen: false); + + /// + /// Returns the for the current instance. + /// + /// + /// The instance. + /// + [Obsolete("This method is obsolete and will be removed in a future release. Use the ToFileInfo method instead.", error: false)] + [EditorBrowsable(EditorBrowsableState.Never)] + public IFileInfo GetFileInfo() => + _file; + + /// + public override IFileInfo ToFileInfo() => + _file; + + [DoesNotReturn] + private static void Error_FileExpected() => + throw new ArgumentException("The specified path refers to a directory, not a file."); +} diff --git a/src/Ramstack.FileProviders.Extensions/FileNodeBase.cs b/src/Ramstack.FileProviders.Extensions/FileNodeBase.cs index 9788603..d0da42e 100644 --- a/src/Ramstack.FileProviders.Extensions/FileNodeBase.cs +++ b/src/Ramstack.FileProviders.Extensions/FileNodeBase.cs @@ -1,44 +1,44 @@ -namespace Ramstack.FileProviders; - -/// -/// Represents a base class for both and objects. -/// -[DebuggerDisplay("{FullName,nq}")] -public abstract class FileNodeBase -{ - /// - /// Gets the file provider associated with this file node. - /// - public IFileProvider Provider { get; } - - /// - /// Gets the full path of the directory or file. - /// - public string FullName { get; } - - /// - /// Gets the name of the file or directory. - /// - public string Name => FilePath.GetFileName(FullName); - - /// - /// Gets a value indicating whether the file or directory exists. - /// - public abstract bool Exists { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The associated with this file node. - /// The path of the file node within the provider. - internal FileNodeBase(IFileProvider provider, string path) => - (Provider, FullName) = (provider, path); - - /// - /// Returns the for the current instance. - /// - /// - /// The instance. - /// - public abstract IFileInfo ToFileInfo(); -} +namespace Ramstack.FileProviders; + +/// +/// Represents a base class for both and objects. +/// +[DebuggerDisplay("{FullName,nq}")] +public abstract class FileNodeBase +{ + /// + /// Gets the file provider associated with this file node. + /// + public IFileProvider Provider { get; } + + /// + /// Gets the full path of the directory or file. + /// + public string FullName { get; } + + /// + /// Gets the name of the file or directory. + /// + public string Name => FilePath.GetFileName(FullName); + + /// + /// Gets a value indicating whether the file or directory exists. + /// + public abstract bool Exists { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The associated with this file node. + /// The path of the file node within the provider. + internal FileNodeBase(IFileProvider provider, string path) => + (Provider, FullName) = (provider, path); + + /// + /// Returns the for the current instance. + /// + /// + /// The instance. + /// + public abstract IFileInfo ToFileInfo(); +} diff --git a/src/Ramstack.FileProviders.Extensions/FileNodeExtensions.cs b/src/Ramstack.FileProviders.Extensions/FileNodeExtensions.cs index bc8efd0..ca6bafb 100644 --- a/src/Ramstack.FileProviders.Extensions/FileNodeExtensions.cs +++ b/src/Ramstack.FileProviders.Extensions/FileNodeExtensions.cs @@ -1,103 +1,103 @@ -using System.Text; - -namespace Ramstack.FileProviders; - -/// -/// Provides extension methods for the . -/// -public static class FileNodeExtensions -{ - /// - /// Reads all the text in the file with the specified encoding. - /// - /// The file from which to read the entire text content. - /// The encoding applied to the contents. - /// - /// A string containing all text in the file. - /// - public static string ReadAllText(this FileNode file, Encoding? encoding = null) => - file.ToFileInfo().ReadAllText(encoding); - - /// - /// Reads all lines of the file with the specified encoding. - /// - /// The file to read from. - /// The encoding applied to the contents. - /// - /// A string array containing all lines of the file. - /// - public static string[] ReadAllLines(this FileNode file, Encoding? encoding = null) => - file.ToFileInfo().ReadAllLines(encoding); - - /// - /// Reads the entire contents of the current file into a byte array. - /// - /// The file to read from. - /// - /// A byte array containing the contents of the file. - /// - public static byte[] ReadAllBytes(this FileNode file) => - file.ToFileInfo().ReadAllBytes(); - - /// - /// Asynchronously reads all the text in the current file. - /// - /// The file from which to read the entire text content. - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation, - /// containing the full text from the current file. - /// - public static ValueTask ReadAllTextAsync(this FileNode file, CancellationToken cancellationToken = default) => - file.ToFileInfo().ReadAllTextAsync(cancellationToken); - - /// - /// Asynchronously reads all the text in the current file with the specified encoding. - /// - /// The file from which to read the entire text content. - /// The encoding applied to the contents. - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation, - /// containing the full text from the current file. - /// - public static ValueTask ReadAllTextAsync(this FileNode file, Encoding? encoding, CancellationToken cancellationToken = default) => - file.ToFileInfo().ReadAllTextAsync(encoding, cancellationToken); - - /// - /// Asynchronously reads all lines of the current file. - /// - /// The file to read from. - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation, - /// containing an array of all lines in the current file. - /// - public static ValueTask ReadAllLinesAsync(this FileNode file, CancellationToken cancellationToken = default) => - file.ToFileInfo().ReadAllLinesAsync(cancellationToken); - - /// - /// Asynchronously reads all lines of the current file with the specified encoding. - /// - /// The file to read from. - /// The encoding applied to the contents. - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation, - /// containing an array of all lines in the current file. - /// - public static ValueTask ReadAllLinesAsync(this FileNode file, Encoding? encoding, CancellationToken cancellationToken = default) => - file.ToFileInfo().ReadAllLinesAsync(encoding, cancellationToken); - - /// - /// Asynchronously reads the entire contents of the current file into a byte array. - /// - /// The file to read from. - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation, - /// containing an array of the file's bytes. - /// - public static ValueTask ReadAllBytesAsync(this FileNode file, CancellationToken cancellationToken = default) => - file.ToFileInfo().ReadAllBytesAsync(cancellationToken); -} +using System.Text; + +namespace Ramstack.FileProviders; + +/// +/// Provides extension methods for the . +/// +public static class FileNodeExtensions +{ + /// + /// Reads all the text in the file with the specified encoding. + /// + /// The file from which to read the entire text content. + /// The encoding applied to the contents. + /// + /// A string containing all text in the file. + /// + public static string ReadAllText(this FileNode file, Encoding? encoding = null) => + file.ToFileInfo().ReadAllText(encoding); + + /// + /// Reads all lines of the file with the specified encoding. + /// + /// The file to read from. + /// The encoding applied to the contents. + /// + /// A string array containing all lines of the file. + /// + public static string[] ReadAllLines(this FileNode file, Encoding? encoding = null) => + file.ToFileInfo().ReadAllLines(encoding); + + /// + /// Reads the entire contents of the current file into a byte array. + /// + /// The file to read from. + /// + /// A byte array containing the contents of the file. + /// + public static byte[] ReadAllBytes(this FileNode file) => + file.ToFileInfo().ReadAllBytes(); + + /// + /// Asynchronously reads all the text in the current file. + /// + /// The file from which to read the entire text content. + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation, + /// containing the full text from the current file. + /// + public static ValueTask ReadAllTextAsync(this FileNode file, CancellationToken cancellationToken = default) => + file.ToFileInfo().ReadAllTextAsync(cancellationToken); + + /// + /// Asynchronously reads all the text in the current file with the specified encoding. + /// + /// The file from which to read the entire text content. + /// The encoding applied to the contents. + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation, + /// containing the full text from the current file. + /// + public static ValueTask ReadAllTextAsync(this FileNode file, Encoding? encoding, CancellationToken cancellationToken = default) => + file.ToFileInfo().ReadAllTextAsync(encoding, cancellationToken); + + /// + /// Asynchronously reads all lines of the current file. + /// + /// The file to read from. + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation, + /// containing an array of all lines in the current file. + /// + public static ValueTask ReadAllLinesAsync(this FileNode file, CancellationToken cancellationToken = default) => + file.ToFileInfo().ReadAllLinesAsync(cancellationToken); + + /// + /// Asynchronously reads all lines of the current file with the specified encoding. + /// + /// The file to read from. + /// The encoding applied to the contents. + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation, + /// containing an array of all lines in the current file. + /// + public static ValueTask ReadAllLinesAsync(this FileNode file, Encoding? encoding, CancellationToken cancellationToken = default) => + file.ToFileInfo().ReadAllLinesAsync(encoding, cancellationToken); + + /// + /// Asynchronously reads the entire contents of the current file into a byte array. + /// + /// The file to read from. + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation, + /// containing an array of the file's bytes. + /// + public static ValueTask ReadAllBytesAsync(this FileNode file, CancellationToken cancellationToken = default) => + file.ToFileInfo().ReadAllBytesAsync(cancellationToken); +} diff --git a/src/Ramstack.FileProviders.Extensions/FilePath.cs b/src/Ramstack.FileProviders.Extensions/FilePath.cs index c6031e4..0ce725a 100644 --- a/src/Ramstack.FileProviders.Extensions/FilePath.cs +++ b/src/Ramstack.FileProviders.Extensions/FilePath.cs @@ -1,390 +1,390 @@ -using System.Buffers; - -namespace Ramstack.FileProviders; - -/// -/// Provides utility methods for working with virtual paths. -/// -/// -/// -/// For compatibility across different implementations of -/// and operating systems, directory separators are unified to use both -/// backslashes and forward slashes ("/" and "\"). -/// This approach will be reviewed once a better solution is found. -/// -/// -/// When normalizing paths by using the method , -/// backslashes ("\") will be replaced with forward slashes ("/") forcibly. -/// -/// -#if EXPOSE_FILEPATH -public -#else -internal -#endif - static class FilePath -{ - /// - /// The threshold size in characters for using stack allocation. - /// - private const int StackallocThreshold = 256; - - /// - /// Returns an extension (including the period ".") of the specified path string. - /// - /// The path string from which to get the extension. - /// - /// The extension of the specified path (including the period "."), - /// or an empty string if no extension is present. - /// - /// - /// returns an empty string ("") - /// if the extension consists solely of a period (e.g., "file."), which differs from - /// , which returns "." in this case. - /// This method follows the behavior of . - /// - public static string GetExtension(string path) - { - _ = path.Length; - return GetExtension(path.AsSpan()).ToString(); - } - - /// - /// Returns an extension (including the period ".") of the specified path string. - /// - /// The path string from which to get the extension. - /// - /// The extension of the specified path (including the period "."), - /// or an empty string if no extension is present. - /// - /// - /// returns an empty string ("") - /// if the extension consists solely of a period (e.g., "file."), which differs from - /// , which returns "." in this case. - /// This method follows the behavior of . - /// - public static ReadOnlySpan GetExtension(ReadOnlySpan path) - { - for (var i = path.Length - 1; i >= 0; i--) - { - if (path[i] == '.') - { - if (i == path.Length - 1) - break; - - return path.Slice(i); - } - - if (path[i] == '/' || path[i] == '\\') - break; - } - - return default; - } - - /// - /// Returns the file name and extension for the specified path. - /// - /// The path from which to get the file name and extension. - /// - /// The file name and extension for the . - /// - public static string GetFileName(string path) - { - var length = path.Length; - - var fileName = GetFileName(path.AsSpan()); - if (fileName.Length != length) - return fileName.ToString(); - - return path; - } - - /// - /// Returns the file name and extension for the specified path. - /// - /// The path from which to get the file name and extension. - /// - /// The file name and extension for the . - /// - public static ReadOnlySpan GetFileName(ReadOnlySpan path) - { - var index = path.LastIndexOfAny('/', '\\'); - - return index >= 0 - ? path.Slice(index + 1) - : path; - } - - /// - /// Returns the directory portion for the specified path. - /// - /// The path to retrieve the directory portion from. - /// - /// Directory portion for , or an empty string if the path denotes a root directory. - /// - public static string GetDirectoryName(string path) - { - _ = path.Length; - - var offset = GetDirectoryNameOffset(path); - - if (offset > 0 && (uint)offset < (uint)path.Length) - return path[..offset]; - - if (offset == 0) - return "/"; - - return ""; - } - - /// - /// Returns the directory portion for the specified path. - /// - /// The path to retrieve the directory portion from. - /// - /// Directory portion for , or an empty string if the path denotes a root directory. - /// - public static ReadOnlySpan GetDirectoryName(ReadOnlySpan path) - { - var offset = GetDirectoryNameOffset(path); - - if (offset > 0 && (uint)offset < (uint)path.Length) - return path[..offset]; - - return offset == 0 ? "/" : ""; - } - - /// - /// Determines whether the specified path is in a normalized form. - /// - /// The path to test. - /// - /// if the path is in a normalized form; - /// otherwise, . - /// - public static bool IsNormalized(ReadOnlySpan path) - { - if (path is ['/', ..]) - { - var prior = path[0]; - - for (var j = 1; (uint)j < (uint)path.Length; j++) - { - var ch = path[j]; - if (ch == '\\' || ch == '/' && prior == '/') - return false; - - if (ch == '.' && prior == '/') - { - if ((uint)j + 1 >= (uint)path.Length) - return false; - - if (path[j + 1] is '/' or '\\') - return false; - - if (path[j + 1] == '.') - { - if ((uint)j + 2 >= (uint)path.Length) - return false; - - if (path[j + 2] is '/' or '\\') - return false; - } - } - - prior = ch; - } - - if (prior != '/' || path.Length == 1) - return true; - } - - return false; - } - - /// - /// Normalizes the specified path by resolving relative segments and applying formatting. - /// - /// The file or directory path to normalize. - /// - /// The fully normalized and absolute form of . - /// - /// - /// The normalization process includes the following steps: - /// - /// Resolves relative segments (e.g., ".", ".."). - /// Removes consecutive slashes. - /// Replaces backslashes with forward slashes. - /// Ensures the path starts with a leading slash. - /// Removes any trailing slash. - /// - /// - public static string Normalize(string path) - { - if (IsNormalized(path)) - return path; - - char[]? rented = null; - - var buffer = path.Length + 1 <= StackallocThreshold - ? stackalloc char[StackallocThreshold] - : rented = ArrayPool.Shared.Rent(path.Length + 1); - - var index = 0; - - foreach (var s in PathTokenizer.Tokenize(path)) - { - if (s.Length == 0 || s is ['.']) - continue; - - if (s is ['.', '.']) - { - // Unwind back to the last separator - index = buffer[..index].LastIndexOf('/'); - - // If no separator is found, reset to start - // (mimics Path.GetFullPath behavior) - if (index < 0) - index = 0; - } - else - { - buffer[index] = '/'; - s.CopyTo(buffer.Slice(index + 1)); - index += s.Length + 1; - } - } - - var result = index != 0 - ? buffer[..index].ToString() - : "/"; - - if (rented is not null) - ArrayPool.Shared.Return(rented); - - return result; - } - - /// - /// Concatenates two paths into a single path. - /// - /// The path to join. - /// The path to join. - /// - /// The concatenated path. - /// - public static string Join(string path1, string path2) - { - if (path1.Length == 0) - return path2; - - if (path2.Length == 0) - return path1; - - if (HasTrailingSlash(path1) || HasLeadingSlash(path2)) - return string.Concat(path1, path2); - - return string.Concat(path1, "/", path2); - } - - /// - /// Concatenates two paths into a single path. - /// - /// The path to join. - /// The path to join. - /// - /// The concatenated path. - /// - public static string Join(ReadOnlySpan path1, ReadOnlySpan path2) - { - if (path1.Length == 0) - return path2.ToString(); - - if (path2.Length == 0) - return path1.ToString(); - - if (HasTrailingSlash(path1) || HasLeadingSlash(path2)) - return string.Concat(path1, path2); - - return string.Concat(path1, "/", path2); - } - - /// - /// Determines whether the specified path string starts with a directory separator. - /// - /// The path to test. - /// - /// if the path has a leading directory separator; - /// otherwise, . - /// - public static bool HasLeadingSlash(string path) => - path.StartsWith('/') || path.StartsWith('\\'); - - /// - /// Determines whether the specified path string ends in a directory separator. - /// - /// The path to test. - /// - /// if the path has a trailing directory separator; - /// otherwise, . - /// - public static bool HasTrailingSlash(string path) => - path.EndsWith('/') || path.EndsWith('\\'); - - /// - /// Determines whether the specified path string starts with a directory separator. - /// - /// The path to test. - /// - /// if the path has a leading directory separator; - /// otherwise, . - /// - public static bool HasLeadingSlash(ReadOnlySpan path) - { - if (path.Length != 0) - { - var ch = path[0]; - if (ch == '/' || ch == '\\') - return true; - } - - return false; - } - - /// - /// Determines whether the specified path string ends in a directory separator. - /// - /// The path to test. - /// - /// if the path has a trailing directory separator; - /// otherwise, . - /// - public static bool HasTrailingSlash(ReadOnlySpan path) - { - if (path.Length != 0) - { - var ch = path[^1]; - if (ch == '/' || ch == '\\') - return true; - } - - return false; - } - - private static int GetDirectoryNameOffset(ReadOnlySpan path) - { - var lastIndex = path.LastIndexOfAny('/', '\\'); - var index = lastIndex; - - // Process consecutive separators - while ((uint)index - 1 < (uint)path.Length && (path[index - 1] == '/' || path[index - 1] == '\\')) - index--; - - // Case where the path consists of separators only - if (index == 0 && lastIndex + 1 == path.Length) - index = -1; - - return index; - } -} +using System.Buffers; + +namespace Ramstack.FileProviders; + +/// +/// Provides utility methods for working with virtual paths. +/// +/// +/// +/// For compatibility across different implementations of +/// and operating systems, directory separators are unified to use both +/// backslashes and forward slashes ("/" and "\"). +/// This approach will be reviewed once a better solution is found. +/// +/// +/// When normalizing paths by using the method , +/// backslashes ("\") will be replaced with forward slashes ("/") forcibly. +/// +/// +#if EXPOSE_FILEPATH +public +#else +internal +#endif + static class FilePath +{ + /// + /// The threshold size in characters for using stack allocation. + /// + private const int StackallocThreshold = 256; + + /// + /// Returns an extension (including the period ".") of the specified path string. + /// + /// The path string from which to get the extension. + /// + /// The extension of the specified path (including the period "."), + /// or an empty string if no extension is present. + /// + /// + /// returns an empty string ("") + /// if the extension consists solely of a period (e.g., "file."), which differs from + /// , which returns "." in this case. + /// This method follows the behavior of . + /// + public static string GetExtension(string path) + { + _ = path.Length; + return GetExtension(path.AsSpan()).ToString(); + } + + /// + /// Returns an extension (including the period ".") of the specified path string. + /// + /// The path string from which to get the extension. + /// + /// The extension of the specified path (including the period "."), + /// or an empty string if no extension is present. + /// + /// + /// returns an empty string ("") + /// if the extension consists solely of a period (e.g., "file."), which differs from + /// , which returns "." in this case. + /// This method follows the behavior of . + /// + public static ReadOnlySpan GetExtension(ReadOnlySpan path) + { + for (var i = path.Length - 1; i >= 0; i--) + { + if (path[i] == '.') + { + if (i == path.Length - 1) + break; + + return path.Slice(i); + } + + if (path[i] == '/' || path[i] == '\\') + break; + } + + return default; + } + + /// + /// Returns the file name and extension for the specified path. + /// + /// The path from which to get the file name and extension. + /// + /// The file name and extension for the . + /// + public static string GetFileName(string path) + { + var length = path.Length; + + var fileName = GetFileName(path.AsSpan()); + if (fileName.Length != length) + return fileName.ToString(); + + return path; + } + + /// + /// Returns the file name and extension for the specified path. + /// + /// The path from which to get the file name and extension. + /// + /// The file name and extension for the . + /// + public static ReadOnlySpan GetFileName(ReadOnlySpan path) + { + var index = path.LastIndexOfAny('/', '\\'); + + return index >= 0 + ? path.Slice(index + 1) + : path; + } + + /// + /// Returns the directory portion for the specified path. + /// + /// The path to retrieve the directory portion from. + /// + /// Directory portion for , or an empty string if the path denotes a root directory. + /// + public static string GetDirectoryName(string path) + { + _ = path.Length; + + var offset = GetDirectoryNameOffset(path); + + if (offset > 0 && (uint)offset < (uint)path.Length) + return path[..offset]; + + if (offset == 0) + return "/"; + + return ""; + } + + /// + /// Returns the directory portion for the specified path. + /// + /// The path to retrieve the directory portion from. + /// + /// Directory portion for , or an empty string if the path denotes a root directory. + /// + public static ReadOnlySpan GetDirectoryName(ReadOnlySpan path) + { + var offset = GetDirectoryNameOffset(path); + + if (offset > 0 && (uint)offset < (uint)path.Length) + return path[..offset]; + + return offset == 0 ? "/" : ""; + } + + /// + /// Determines whether the specified path is in a normalized form. + /// + /// The path to test. + /// + /// if the path is in a normalized form; + /// otherwise, . + /// + public static bool IsNormalized(ReadOnlySpan path) + { + if (path is ['/', ..]) + { + var prior = path[0]; + + for (var j = 1; (uint)j < (uint)path.Length; j++) + { + var ch = path[j]; + if (ch == '\\' || ch == '/' && prior == '/') + return false; + + if (ch == '.' && prior == '/') + { + if ((uint)j + 1 >= (uint)path.Length) + return false; + + if (path[j + 1] is '/' or '\\') + return false; + + if (path[j + 1] == '.') + { + if ((uint)j + 2 >= (uint)path.Length) + return false; + + if (path[j + 2] is '/' or '\\') + return false; + } + } + + prior = ch; + } + + if (prior != '/' || path.Length == 1) + return true; + } + + return false; + } + + /// + /// Normalizes the specified path by resolving relative segments and applying formatting. + /// + /// The file or directory path to normalize. + /// + /// The fully normalized and absolute form of . + /// + /// + /// The normalization process includes the following steps: + /// + /// Resolves relative segments (e.g., ".", ".."). + /// Removes consecutive slashes. + /// Replaces backslashes with forward slashes. + /// Ensures the path starts with a leading slash. + /// Removes any trailing slash. + /// + /// + public static string Normalize(string path) + { + if (IsNormalized(path)) + return path; + + char[]? rented = null; + + var buffer = path.Length + 1 <= StackallocThreshold + ? stackalloc char[StackallocThreshold] + : rented = ArrayPool.Shared.Rent(path.Length + 1); + + var index = 0; + + foreach (var s in PathTokenizer.Tokenize(path)) + { + if (s.Length == 0 || s is ['.']) + continue; + + if (s is ['.', '.']) + { + // Unwind back to the last separator + index = buffer[..index].LastIndexOf('/'); + + // If no separator is found, reset to start + // (mimics Path.GetFullPath behavior) + if (index < 0) + index = 0; + } + else + { + buffer[index] = '/'; + s.CopyTo(buffer.Slice(index + 1)); + index += s.Length + 1; + } + } + + var result = index != 0 + ? buffer[..index].ToString() + : "/"; + + if (rented is not null) + ArrayPool.Shared.Return(rented); + + return result; + } + + /// + /// Concatenates two paths into a single path. + /// + /// The path to join. + /// The path to join. + /// + /// The concatenated path. + /// + public static string Join(string path1, string path2) + { + if (path1.Length == 0) + return path2; + + if (path2.Length == 0) + return path1; + + if (HasTrailingSlash(path1) || HasLeadingSlash(path2)) + return string.Concat(path1, path2); + + return string.Concat(path1, "/", path2); + } + + /// + /// Concatenates two paths into a single path. + /// + /// The path to join. + /// The path to join. + /// + /// The concatenated path. + /// + public static string Join(ReadOnlySpan path1, ReadOnlySpan path2) + { + if (path1.Length == 0) + return path2.ToString(); + + if (path2.Length == 0) + return path1.ToString(); + + if (HasTrailingSlash(path1) || HasLeadingSlash(path2)) + return string.Concat(path1, path2); + + return string.Concat(path1, "/", path2); + } + + /// + /// Determines whether the specified path string starts with a directory separator. + /// + /// The path to test. + /// + /// if the path has a leading directory separator; + /// otherwise, . + /// + public static bool HasLeadingSlash(string path) => + path.StartsWith('/') || path.StartsWith('\\'); + + /// + /// Determines whether the specified path string ends in a directory separator. + /// + /// The path to test. + /// + /// if the path has a trailing directory separator; + /// otherwise, . + /// + public static bool HasTrailingSlash(string path) => + path.EndsWith('/') || path.EndsWith('\\'); + + /// + /// Determines whether the specified path string starts with a directory separator. + /// + /// The path to test. + /// + /// if the path has a leading directory separator; + /// otherwise, . + /// + public static bool HasLeadingSlash(ReadOnlySpan path) + { + if (path.Length != 0) + { + var ch = path[0]; + if (ch == '/' || ch == '\\') + return true; + } + + return false; + } + + /// + /// Determines whether the specified path string ends in a directory separator. + /// + /// The path to test. + /// + /// if the path has a trailing directory separator; + /// otherwise, . + /// + public static bool HasTrailingSlash(ReadOnlySpan path) + { + if (path.Length != 0) + { + var ch = path[^1]; + if (ch == '/' || ch == '\\') + return true; + } + + return false; + } + + private static int GetDirectoryNameOffset(ReadOnlySpan path) + { + var lastIndex = path.LastIndexOfAny('/', '\\'); + var index = lastIndex; + + // Process consecutive separators + while ((uint)index - 1 < (uint)path.Length && (path[index - 1] == '/' || path[index - 1] == '\\')) + index--; + + // Case where the path consists of separators only + if (index == 0 && lastIndex + 1 == path.Length) + index = -1; + + return index; + } +} diff --git a/src/Ramstack.FileProviders.Extensions/FileProviderExtensions.Enumeration.cs b/src/Ramstack.FileProviders.Extensions/FileProviderExtensions.Enumeration.cs index e42ab74..725eb98 100644 --- a/src/Ramstack.FileProviders.Extensions/FileProviderExtensions.Enumeration.cs +++ b/src/Ramstack.FileProviders.Extensions/FileProviderExtensions.Enumeration.cs @@ -1,403 +1,403 @@ -namespace Ramstack.FileProviders; - -partial class FileProviderExtensions -{ - /// - /// Returns an enumerable collection of files in the directory at the specified path. - /// - /// The file provider to use. - /// The path to the directory to enumerate. - /// - /// An enumerable collection of files in the specified directory. - /// - public static IEnumerable EnumerateFiles(this IFileProvider provider, string path) => - provider.GetDirectory(path).EnumerateFiles(); - - /// - /// Returns an enumerable collection of directories in the directory at the specified path. - /// - /// The file provider to use. - /// The path to the directory to enumerate. - /// - /// An enumerable collection of directories in the specified directory. - /// - public static IEnumerable EnumerateDirectories(this IFileProvider provider, string path) => - provider.GetDirectory(path).EnumerateDirectories(); - - /// - /// Returns an enumerable collection of file nodes in the directory at the specified path. - /// - /// The file provider to use. - /// The path to the directory to enumerate. - /// - /// An enumerable collection of file nodes in the specified directory. - /// - public static IEnumerable EnumerateFileNodes(this IFileProvider provider, string path) => - provider.GetDirectory(path).EnumerateFileNodes(); - - /// - /// Returns an enumerable collection of files in the specified directory that match the given glob pattern. - /// - /// The file provider to use. - /// The path to the directory to search. - /// The glob pattern to match against the names of files. - /// Optional glob pattern to exclude files. - /// - /// An enumerable collection of files in the directory that match the given glob pattern. - /// - /// - /// Glob pattern: - /// - /// - /// - /// Supported meta-characters include '*', '?', '\' and '[', ']'. - /// And inside character classes '-', '!' and ']'. - /// - /// - /// - /// - /// The '.' and '..' symbols do not have any special treatment and are processed - /// as regular characters for matching. - /// - /// - /// - /// - /// Character classes can be negated by prefixing them with '!', such as [!0-9], - /// which matches all characters except digits. - /// - /// - /// - /// - /// Brace patterns are supported, including nested brace patterns: - /// {file,dir,name}, {file-1.{c,cpp},file-2.{cs,f}} - /// - /// - /// - /// - /// An empty pattern in brace expansion {} is allowed, as well as variations - /// like {.cs,}, {name,,file}, or {,.cs}. - /// - /// - /// - /// Leading and trailing separators are ignored. - /// - /// - /// Consecutive separators are counted as one. - /// - /// - /// - /// The '**' sequence in the glob pattern can be used to match zero or more directories and subdirectories. - /// It can be used at the beginning, middle, or end of a pattern, for example, - /// "**/file.txt", "dir/**/*.txt", "dir/**". - /// - /// - /// - /// - public static IEnumerable EnumerateFiles(this IFileProvider provider, string path, string pattern, string? exclude = null) => - provider.GetDirectory(path).EnumerateFiles(pattern, exclude); - - /// - /// Returns an enumerable collection of files in the specified directory that match any of the given glob patterns. - /// - /// The file provider to use. - /// The path to the directory to search. - /// An array of glob patterns to match against the names of files. - /// An optional array of glob patterns to exclude files. - /// - /// An enumerable collection of files in the directory that match any of the given glob patterns. - /// - /// - /// Glob pattern: - /// - /// - /// - /// Supported meta-characters include '*', '?', '\' and '[', ']'. - /// And inside character classes '-', '!' and ']'. - /// - /// - /// - /// - /// The '.' and '..' symbols do not have any special treatment and are processed - /// as regular characters for matching. - /// - /// - /// - /// - /// Character classes can be negated by prefixing them with '!', such as [!0-9], - /// which matches all characters except digits. - /// - /// - /// - /// - /// Brace patterns are supported, including nested brace patterns: - /// {file,dir,name}, {file-1.{c,cpp},file-2.{cs,f}} - /// - /// - /// - /// - /// An empty pattern in brace expansion {} is allowed, as well as variations - /// like {.cs,}, {name,,file}, or {,.cs}. - /// - /// - /// - /// Leading and trailing separators are ignored. - /// - /// - /// Consecutive separators are counted as one. - /// - /// - /// - /// The '**' sequence in the glob pattern can be used to match zero or more directories and subdirectories. - /// It can be used at the beginning, middle, or end of a pattern, for example, - /// "**/file.txt", "dir/**/*.txt", "dir/**". - /// - /// - /// - /// - public static IEnumerable EnumerateFiles(this IFileProvider provider, string path, string[] patterns, string[]? excludes = null) => - provider.GetDirectory(path).EnumerateFiles(patterns, excludes); - - /// - /// Returns an enumerable collection of directories in the specified directory that match the given glob pattern. - /// - /// The file provider to use. - /// The path to the directory to search. - /// The glob pattern to match against the names of directories. - /// Optional glob pattern to exclude directories. - /// - /// An enumerable collection of directories in the directory that match the given glob pattern. - /// - /// - /// Glob pattern: - /// - /// - /// - /// Supported meta-characters include '*', '?', '\' and '[', ']'. - /// And inside character classes '-', '!' and ']'. - /// - /// - /// - /// - /// The '.' and '..' symbols do not have any special treatment and are processed - /// as regular characters for matching. - /// - /// - /// - /// - /// Character classes can be negated by prefixing them with '!', such as [!0-9], - /// which matches all characters except digits. - /// - /// - /// - /// - /// Brace patterns are supported, including nested brace patterns: - /// {file,dir,name}, {file-1.{c,cpp},file-2.{cs,f}} - /// - /// - /// - /// - /// An empty pattern in brace expansion {} is allowed, as well as variations - /// like {.cs,}, {name,,file}, or {,.cs}. - /// - /// - /// - /// Leading and trailing separators are ignored. - /// - /// - /// Consecutive separators are counted as one. - /// - /// - /// - /// The '**' sequence in the glob pattern can be used to match zero or more directories and subdirectories. - /// It can be used at the beginning, middle, or end of a pattern, for example, - /// "**/file.txt", "dir/**/*.txt", "dir/**". - /// - /// - /// - /// - public static IEnumerable EnumerateDirectories(this IFileProvider provider, string path, string pattern, string? exclude = null) => - provider.GetDirectory(path).EnumerateDirectories(pattern, exclude); - - /// - /// Returns an enumerable collection of directories in the specified directory that match any of the given glob patterns. - /// - /// The file provider to use. - /// The path to the directory to search. - /// An array of glob patterns to match against the names of directories. - /// An optional array of glob patterns to exclude directories. - /// - /// An enumerable collection of directories in the directory that match any of the given glob patterns. - /// - /// - /// Glob pattern: - /// - /// - /// - /// Supported meta-characters include '*', '?', '\' and '[', ']'. - /// And inside character classes '-', '!' and ']'. - /// - /// - /// - /// - /// The '.' and '..' symbols do not have any special treatment and are processed - /// as regular characters for matching. - /// - /// - /// - /// - /// Character classes can be negated by prefixing them with '!', such as [!0-9], - /// which matches all characters except digits. - /// - /// - /// - /// - /// Brace patterns are supported, including nested brace patterns: - /// {file,dir,name}, {file-1.{c,cpp},file-2.{cs,f}} - /// - /// - /// - /// - /// An empty pattern in brace expansion {} is allowed, as well as variations - /// like {.cs,}, {name,,file}, or {,.cs}. - /// - /// - /// - /// Leading and trailing separators are ignored. - /// - /// - /// Consecutive separators are counted as one. - /// - /// - /// - /// The '**' sequence in the glob pattern can be used to match zero or more directories and subdirectories. - /// It can be used at the beginning, middle, or end of a pattern, for example, - /// "**/file.txt", "dir/**/*.txt", "dir/**". - /// - /// - /// - /// - public static IEnumerable EnumerateDirectories(this IFileProvider provider, string path, string[] patterns, string[]? excludes = null) => - provider.GetDirectory(path).EnumerateDirectories(patterns, excludes); - - /// - /// Returns an enumerable collection of file nodes in the specified directory that match the given glob pattern. - /// - /// The file provider to use. - /// The path to the directory to search. - /// The glob pattern to match against the names of file nodes. - /// Optional glob pattern to exclude file nodes. - /// - /// An enumerable collection of file nodes in the directory that match the given glob pattern. - /// - /// - /// Glob pattern: - /// - /// - /// - /// Supported meta-characters include '*', '?', '\' and '[', ']'. - /// And inside character classes '-', '!' and ']'. - /// - /// - /// - /// - /// The '.' and '..' symbols do not have any special treatment and are processed - /// as regular characters for matching. - /// - /// - /// - /// - /// Character classes can be negated by prefixing them with '!', such as [!0-9], - /// which matches all characters except digits. - /// - /// - /// - /// - /// Brace patterns are supported, including nested brace patterns: - /// {file,dir,name}, {file-1.{c,cpp},file-2.{cs,f}} - /// - /// - /// - /// - /// An empty pattern in brace expansion {} is allowed, as well as variations - /// like {.cs,}, {name,,file}, or {,.cs}. - /// - /// - /// - /// Leading and trailing separators are ignored. - /// - /// - /// Consecutive separators are counted as one. - /// - /// - /// - /// The '**' sequence in the glob pattern can be used to match zero or more directories and subdirectories. - /// It can be used at the beginning, middle, or end of a pattern, for example, - /// "**/file.txt", "dir/**/*.txt", "dir/**". - /// - /// - /// - /// - public static IEnumerable EnumerateFileNodes(this IFileProvider provider, string path, string pattern, string? exclude = null) => - provider.GetDirectory(path).EnumerateFileNodes(pattern, exclude); - - /// - /// Returns an enumerable collection of file nodes in the specified directory that match any of the given glob patterns. - /// - /// The file provider to use. - /// The path of the directory from which to retrieve file nodes. - /// An array of glob patterns to match against the names of file nodes. - /// An optional array of glob patterns to exclude file nodes. - /// - /// An enumerable collection of file nodes in the directory that match any of the given glob patterns. - /// - /// - /// Glob pattern: - /// - /// - /// - /// Supported meta-characters include '*', '?', '\' and '[', ']'. - /// And inside character classes '-', '!' and ']'. - /// - /// - /// - /// - /// The '.' and '..' symbols do not have any special treatment and are processed - /// as regular characters for matching. - /// - /// - /// - /// - /// Character classes can be negated by prefixing them with '!', such as [!0-9], - /// which matches all characters except digits. - /// - /// - /// - /// - /// Brace patterns are supported, including nested brace patterns: - /// {file,dir,name}, {file-1.{c,cpp},file-2.{cs,f}} - /// - /// - /// - /// - /// An empty pattern in brace expansion {} is allowed, as well as variations - /// like {.cs,}, {name,,file}, or {,.cs}. - /// - /// - /// - /// Leading and trailing separators are ignored. - /// - /// - /// Consecutive separators are counted as one. - /// - /// - /// - /// The '**' sequence in the glob pattern can be used to match zero or more directories and subdirectories. - /// It can be used at the beginning, middle, or end of a pattern, for example, - /// "**/file.txt", "dir/**/*.txt", "dir/**". - /// - /// - /// - /// - public static IEnumerable EnumerateFileNodes(this IFileProvider provider, string path, string[] patterns, string[]? excludes = null) => - provider.GetDirectory(path).EnumerateFileNodes(patterns, excludes); -} +namespace Ramstack.FileProviders; + +partial class FileProviderExtensions +{ + /// + /// Returns an enumerable collection of files in the directory at the specified path. + /// + /// The file provider to use. + /// The path to the directory to enumerate. + /// + /// An enumerable collection of files in the specified directory. + /// + public static IEnumerable EnumerateFiles(this IFileProvider provider, string path) => + provider.GetDirectory(path).EnumerateFiles(); + + /// + /// Returns an enumerable collection of directories in the directory at the specified path. + /// + /// The file provider to use. + /// The path to the directory to enumerate. + /// + /// An enumerable collection of directories in the specified directory. + /// + public static IEnumerable EnumerateDirectories(this IFileProvider provider, string path) => + provider.GetDirectory(path).EnumerateDirectories(); + + /// + /// Returns an enumerable collection of file nodes in the directory at the specified path. + /// + /// The file provider to use. + /// The path to the directory to enumerate. + /// + /// An enumerable collection of file nodes in the specified directory. + /// + public static IEnumerable EnumerateFileNodes(this IFileProvider provider, string path) => + provider.GetDirectory(path).EnumerateFileNodes(); + + /// + /// Returns an enumerable collection of files in the specified directory that match the given glob pattern. + /// + /// The file provider to use. + /// The path to the directory to search. + /// The glob pattern to match against the names of files. + /// Optional glob pattern to exclude files. + /// + /// An enumerable collection of files in the directory that match the given glob pattern. + /// + /// + /// Glob pattern: + /// + /// + /// + /// Supported meta-characters include '*', '?', '\' and '[', ']'. + /// And inside character classes '-', '!' and ']'. + /// + /// + /// + /// + /// The '.' and '..' symbols do not have any special treatment and are processed + /// as regular characters for matching. + /// + /// + /// + /// + /// Character classes can be negated by prefixing them with '!', such as [!0-9], + /// which matches all characters except digits. + /// + /// + /// + /// + /// Brace patterns are supported, including nested brace patterns: + /// {file,dir,name}, {file-1.{c,cpp},file-2.{cs,f}} + /// + /// + /// + /// + /// An empty pattern in brace expansion {} is allowed, as well as variations + /// like {.cs,}, {name,,file}, or {,.cs}. + /// + /// + /// + /// Leading and trailing separators are ignored. + /// + /// + /// Consecutive separators are counted as one. + /// + /// + /// + /// The '**' sequence in the glob pattern can be used to match zero or more directories and subdirectories. + /// It can be used at the beginning, middle, or end of a pattern, for example, + /// "**/file.txt", "dir/**/*.txt", "dir/**". + /// + /// + /// + /// + public static IEnumerable EnumerateFiles(this IFileProvider provider, string path, string pattern, string? exclude = null) => + provider.GetDirectory(path).EnumerateFiles(pattern, exclude); + + /// + /// Returns an enumerable collection of files in the specified directory that match any of the given glob patterns. + /// + /// The file provider to use. + /// The path to the directory to search. + /// An array of glob patterns to match against the names of files. + /// An optional array of glob patterns to exclude files. + /// + /// An enumerable collection of files in the directory that match any of the given glob patterns. + /// + /// + /// Glob pattern: + /// + /// + /// + /// Supported meta-characters include '*', '?', '\' and '[', ']'. + /// And inside character classes '-', '!' and ']'. + /// + /// + /// + /// + /// The '.' and '..' symbols do not have any special treatment and are processed + /// as regular characters for matching. + /// + /// + /// + /// + /// Character classes can be negated by prefixing them with '!', such as [!0-9], + /// which matches all characters except digits. + /// + /// + /// + /// + /// Brace patterns are supported, including nested brace patterns: + /// {file,dir,name}, {file-1.{c,cpp},file-2.{cs,f}} + /// + /// + /// + /// + /// An empty pattern in brace expansion {} is allowed, as well as variations + /// like {.cs,}, {name,,file}, or {,.cs}. + /// + /// + /// + /// Leading and trailing separators are ignored. + /// + /// + /// Consecutive separators are counted as one. + /// + /// + /// + /// The '**' sequence in the glob pattern can be used to match zero or more directories and subdirectories. + /// It can be used at the beginning, middle, or end of a pattern, for example, + /// "**/file.txt", "dir/**/*.txt", "dir/**". + /// + /// + /// + /// + public static IEnumerable EnumerateFiles(this IFileProvider provider, string path, string[] patterns, string[]? excludes = null) => + provider.GetDirectory(path).EnumerateFiles(patterns, excludes); + + /// + /// Returns an enumerable collection of directories in the specified directory that match the given glob pattern. + /// + /// The file provider to use. + /// The path to the directory to search. + /// The glob pattern to match against the names of directories. + /// Optional glob pattern to exclude directories. + /// + /// An enumerable collection of directories in the directory that match the given glob pattern. + /// + /// + /// Glob pattern: + /// + /// + /// + /// Supported meta-characters include '*', '?', '\' and '[', ']'. + /// And inside character classes '-', '!' and ']'. + /// + /// + /// + /// + /// The '.' and '..' symbols do not have any special treatment and are processed + /// as regular characters for matching. + /// + /// + /// + /// + /// Character classes can be negated by prefixing them with '!', such as [!0-9], + /// which matches all characters except digits. + /// + /// + /// + /// + /// Brace patterns are supported, including nested brace patterns: + /// {file,dir,name}, {file-1.{c,cpp},file-2.{cs,f}} + /// + /// + /// + /// + /// An empty pattern in brace expansion {} is allowed, as well as variations + /// like {.cs,}, {name,,file}, or {,.cs}. + /// + /// + /// + /// Leading and trailing separators are ignored. + /// + /// + /// Consecutive separators are counted as one. + /// + /// + /// + /// The '**' sequence in the glob pattern can be used to match zero or more directories and subdirectories. + /// It can be used at the beginning, middle, or end of a pattern, for example, + /// "**/file.txt", "dir/**/*.txt", "dir/**". + /// + /// + /// + /// + public static IEnumerable EnumerateDirectories(this IFileProvider provider, string path, string pattern, string? exclude = null) => + provider.GetDirectory(path).EnumerateDirectories(pattern, exclude); + + /// + /// Returns an enumerable collection of directories in the specified directory that match any of the given glob patterns. + /// + /// The file provider to use. + /// The path to the directory to search. + /// An array of glob patterns to match against the names of directories. + /// An optional array of glob patterns to exclude directories. + /// + /// An enumerable collection of directories in the directory that match any of the given glob patterns. + /// + /// + /// Glob pattern: + /// + /// + /// + /// Supported meta-characters include '*', '?', '\' and '[', ']'. + /// And inside character classes '-', '!' and ']'. + /// + /// + /// + /// + /// The '.' and '..' symbols do not have any special treatment and are processed + /// as regular characters for matching. + /// + /// + /// + /// + /// Character classes can be negated by prefixing them with '!', such as [!0-9], + /// which matches all characters except digits. + /// + /// + /// + /// + /// Brace patterns are supported, including nested brace patterns: + /// {file,dir,name}, {file-1.{c,cpp},file-2.{cs,f}} + /// + /// + /// + /// + /// An empty pattern in brace expansion {} is allowed, as well as variations + /// like {.cs,}, {name,,file}, or {,.cs}. + /// + /// + /// + /// Leading and trailing separators are ignored. + /// + /// + /// Consecutive separators are counted as one. + /// + /// + /// + /// The '**' sequence in the glob pattern can be used to match zero or more directories and subdirectories. + /// It can be used at the beginning, middle, or end of a pattern, for example, + /// "**/file.txt", "dir/**/*.txt", "dir/**". + /// + /// + /// + /// + public static IEnumerable EnumerateDirectories(this IFileProvider provider, string path, string[] patterns, string[]? excludes = null) => + provider.GetDirectory(path).EnumerateDirectories(patterns, excludes); + + /// + /// Returns an enumerable collection of file nodes in the specified directory that match the given glob pattern. + /// + /// The file provider to use. + /// The path to the directory to search. + /// The glob pattern to match against the names of file nodes. + /// Optional glob pattern to exclude file nodes. + /// + /// An enumerable collection of file nodes in the directory that match the given glob pattern. + /// + /// + /// Glob pattern: + /// + /// + /// + /// Supported meta-characters include '*', '?', '\' and '[', ']'. + /// And inside character classes '-', '!' and ']'. + /// + /// + /// + /// + /// The '.' and '..' symbols do not have any special treatment and are processed + /// as regular characters for matching. + /// + /// + /// + /// + /// Character classes can be negated by prefixing them with '!', such as [!0-9], + /// which matches all characters except digits. + /// + /// + /// + /// + /// Brace patterns are supported, including nested brace patterns: + /// {file,dir,name}, {file-1.{c,cpp},file-2.{cs,f}} + /// + /// + /// + /// + /// An empty pattern in brace expansion {} is allowed, as well as variations + /// like {.cs,}, {name,,file}, or {,.cs}. + /// + /// + /// + /// Leading and trailing separators are ignored. + /// + /// + /// Consecutive separators are counted as one. + /// + /// + /// + /// The '**' sequence in the glob pattern can be used to match zero or more directories and subdirectories. + /// It can be used at the beginning, middle, or end of a pattern, for example, + /// "**/file.txt", "dir/**/*.txt", "dir/**". + /// + /// + /// + /// + public static IEnumerable EnumerateFileNodes(this IFileProvider provider, string path, string pattern, string? exclude = null) => + provider.GetDirectory(path).EnumerateFileNodes(pattern, exclude); + + /// + /// Returns an enumerable collection of file nodes in the specified directory that match any of the given glob patterns. + /// + /// The file provider to use. + /// The path of the directory from which to retrieve file nodes. + /// An array of glob patterns to match against the names of file nodes. + /// An optional array of glob patterns to exclude file nodes. + /// + /// An enumerable collection of file nodes in the directory that match any of the given glob patterns. + /// + /// + /// Glob pattern: + /// + /// + /// + /// Supported meta-characters include '*', '?', '\' and '[', ']'. + /// And inside character classes '-', '!' and ']'. + /// + /// + /// + /// + /// The '.' and '..' symbols do not have any special treatment and are processed + /// as regular characters for matching. + /// + /// + /// + /// + /// Character classes can be negated by prefixing them with '!', such as [!0-9], + /// which matches all characters except digits. + /// + /// + /// + /// + /// Brace patterns are supported, including nested brace patterns: + /// {file,dir,name}, {file-1.{c,cpp},file-2.{cs,f}} + /// + /// + /// + /// + /// An empty pattern in brace expansion {} is allowed, as well as variations + /// like {.cs,}, {name,,file}, or {,.cs}. + /// + /// + /// + /// Leading and trailing separators are ignored. + /// + /// + /// Consecutive separators are counted as one. + /// + /// + /// + /// The '**' sequence in the glob pattern can be used to match zero or more directories and subdirectories. + /// It can be used at the beginning, middle, or end of a pattern, for example, + /// "**/file.txt", "dir/**/*.txt", "dir/**". + /// + /// + /// + /// + public static IEnumerable EnumerateFileNodes(this IFileProvider provider, string path, string[] patterns, string[]? excludes = null) => + provider.GetDirectory(path).EnumerateFileNodes(patterns, excludes); +} diff --git a/src/Ramstack.FileProviders.Extensions/FileProviderExtensions.cs b/src/Ramstack.FileProviders.Extensions/FileProviderExtensions.cs index 6377474..e9fdaa8 100644 --- a/src/Ramstack.FileProviders.Extensions/FileProviderExtensions.cs +++ b/src/Ramstack.FileProviders.Extensions/FileProviderExtensions.cs @@ -1,166 +1,166 @@ -using System.Text; - -namespace Ramstack.FileProviders; - -/// -/// Provides extension methods for the . -/// -public static partial class FileProviderExtensions -{ - /// - /// Returns file contents as a read-only stream. - /// - /// The . - /// The path of the file to open. - /// - /// The file contents as a read-only stream. - /// - public static Stream OpenRead(this IFileProvider provider, string path) => - provider.GetFileInfo(path).OpenRead(); - - /// - /// Returns a with the specified character encoding that reads from the current text file. - /// - /// The . - /// The path of the file to open. - /// The character encoding to use. - /// - /// A new with the specified character encoding. - /// - public static StreamReader OpenText(this IFileProvider provider, string path, Encoding? encoding = null) => - provider.GetFileInfo(path).OpenText(encoding); - - /// - /// Reads all the text in the file with the specified encoding. - /// - /// The . - /// The path of the file to read from. - /// The encoding applied to the contents. - /// - /// A string containing all text in the file. - /// - public static string ReadAllText(this IFileProvider provider, string path, Encoding? encoding = null) => - provider.GetFileInfo(path).ReadAllText(encoding); - - /// - /// Reads all lines of the file with the specified encoding. - /// - /// The . - /// The path of the file to read from. - /// The encoding applied to the contents. - /// - /// A string array containing all lines of the file. - /// - public static string[] ReadAllLines(this IFileProvider provider, string path, Encoding? encoding = null) => - provider.GetFileInfo(path).ReadAllLines(encoding); - - /// - /// Reads the entire contents of the current file into a byte array. - /// - /// The . - /// The path of the file to read from. - /// - /// A byte array containing the contents of the file. - /// - public static byte[] ReadAllBytes(this IFileProvider provider, string path) => - provider.GetFileInfo(path).ReadAllBytes(); - - /// - /// Asynchronously reads all the text in the current file. - /// - /// The . - /// The path of the file to read from. - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation, - /// containing the full text from the current file. - /// - public static ValueTask ReadAllTextAsync(this IFileProvider provider, string path, CancellationToken cancellationToken = default) => - provider.GetFileInfo(path).ReadAllTextAsync(cancellationToken); - - /// - /// Asynchronously reads all the text in the current file with the specified encoding. - /// - /// The . - /// The path of the file to read from. - /// The encoding applied to the contents. - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation, - /// containing the full text from the current file. - /// - public static ValueTask ReadAllTextAsync(this IFileProvider provider, string path, Encoding? encoding, CancellationToken cancellationToken = default) => - provider.GetFileInfo(path).ReadAllTextAsync(encoding, cancellationToken); - - /// - /// Asynchronously reads all lines of the current file. - /// - /// The . - /// The path of the file to read from. - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation, - /// containing an array of all lines in the current file. - /// - public static ValueTask ReadAllLinesAsync(this IFileProvider provider, string path, CancellationToken cancellationToken = default) => - provider.GetFileInfo(path).ReadAllLinesAsync(cancellationToken); - - /// - /// Asynchronously reads all lines of the current file with the specified encoding. - /// - /// The . - /// The path of the file to read from. - /// The encoding applied to the contents. - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation, - /// containing an array of all lines in the current file. - /// - public static ValueTask ReadAllLinesAsync(this IFileProvider provider, string path, Encoding? encoding, CancellationToken cancellationToken = default) => - provider.GetFileInfo(path).ReadAllLinesAsync(encoding, cancellationToken); - - /// - /// Asynchronously reads the entire contents of the current file into a byte array. - /// - /// The . - /// The path of the file to read from. - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation, - /// containing an array of the file's bytes. - /// - public static ValueTask ReadAllBytesAsync(this IFileProvider provider, string path, CancellationToken cancellationToken = default) => - provider.GetFileInfo(path).ReadAllBytesAsync(cancellationToken); - - /// - /// Returns a directory with the specified path. - /// - /// The . - /// The path that identifies the directory. - /// - /// The representing the desired directory. - /// - public static DirectoryNode GetDirectory(this IFileProvider provider, string path) - { - path = FilePath.Normalize(path); - - var directory = provider.GetDirectoryContents(path); - return new DirectoryNode(provider, path, directory); - } - - /// - /// Returns a file with the specified path. - /// - /// The . - /// The path that identifies the file. - /// - /// The representing the desired file. - /// - public static FileNode GetFile(this IFileProvider provider, string path) - { - path = FilePath.Normalize(path); - - var file = provider.GetFileInfo(path); - return new FileNode(provider, path, file); - } -} +using System.Text; + +namespace Ramstack.FileProviders; + +/// +/// Provides extension methods for the . +/// +public static partial class FileProviderExtensions +{ + /// + /// Returns file contents as a read-only stream. + /// + /// The . + /// The path of the file to open. + /// + /// The file contents as a read-only stream. + /// + public static Stream OpenRead(this IFileProvider provider, string path) => + provider.GetFileInfo(path).OpenRead(); + + /// + /// Returns a with the specified character encoding that reads from the current text file. + /// + /// The . + /// The path of the file to open. + /// The character encoding to use. + /// + /// A new with the specified character encoding. + /// + public static StreamReader OpenText(this IFileProvider provider, string path, Encoding? encoding = null) => + provider.GetFileInfo(path).OpenText(encoding); + + /// + /// Reads all the text in the file with the specified encoding. + /// + /// The . + /// The path of the file to read from. + /// The encoding applied to the contents. + /// + /// A string containing all text in the file. + /// + public static string ReadAllText(this IFileProvider provider, string path, Encoding? encoding = null) => + provider.GetFileInfo(path).ReadAllText(encoding); + + /// + /// Reads all lines of the file with the specified encoding. + /// + /// The . + /// The path of the file to read from. + /// The encoding applied to the contents. + /// + /// A string array containing all lines of the file. + /// + public static string[] ReadAllLines(this IFileProvider provider, string path, Encoding? encoding = null) => + provider.GetFileInfo(path).ReadAllLines(encoding); + + /// + /// Reads the entire contents of the current file into a byte array. + /// + /// The . + /// The path of the file to read from. + /// + /// A byte array containing the contents of the file. + /// + public static byte[] ReadAllBytes(this IFileProvider provider, string path) => + provider.GetFileInfo(path).ReadAllBytes(); + + /// + /// Asynchronously reads all the text in the current file. + /// + /// The . + /// The path of the file to read from. + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation, + /// containing the full text from the current file. + /// + public static ValueTask ReadAllTextAsync(this IFileProvider provider, string path, CancellationToken cancellationToken = default) => + provider.GetFileInfo(path).ReadAllTextAsync(cancellationToken); + + /// + /// Asynchronously reads all the text in the current file with the specified encoding. + /// + /// The . + /// The path of the file to read from. + /// The encoding applied to the contents. + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation, + /// containing the full text from the current file. + /// + public static ValueTask ReadAllTextAsync(this IFileProvider provider, string path, Encoding? encoding, CancellationToken cancellationToken = default) => + provider.GetFileInfo(path).ReadAllTextAsync(encoding, cancellationToken); + + /// + /// Asynchronously reads all lines of the current file. + /// + /// The . + /// The path of the file to read from. + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation, + /// containing an array of all lines in the current file. + /// + public static ValueTask ReadAllLinesAsync(this IFileProvider provider, string path, CancellationToken cancellationToken = default) => + provider.GetFileInfo(path).ReadAllLinesAsync(cancellationToken); + + /// + /// Asynchronously reads all lines of the current file with the specified encoding. + /// + /// The . + /// The path of the file to read from. + /// The encoding applied to the contents. + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation, + /// containing an array of all lines in the current file. + /// + public static ValueTask ReadAllLinesAsync(this IFileProvider provider, string path, Encoding? encoding, CancellationToken cancellationToken = default) => + provider.GetFileInfo(path).ReadAllLinesAsync(encoding, cancellationToken); + + /// + /// Asynchronously reads the entire contents of the current file into a byte array. + /// + /// The . + /// The path of the file to read from. + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation, + /// containing an array of the file's bytes. + /// + public static ValueTask ReadAllBytesAsync(this IFileProvider provider, string path, CancellationToken cancellationToken = default) => + provider.GetFileInfo(path).ReadAllBytesAsync(cancellationToken); + + /// + /// Returns a directory with the specified path. + /// + /// The . + /// The path that identifies the directory. + /// + /// The representing the desired directory. + /// + public static DirectoryNode GetDirectory(this IFileProvider provider, string path) + { + path = FilePath.Normalize(path); + + var directory = provider.GetDirectoryContents(path); + return new DirectoryNode(provider, path, directory); + } + + /// + /// Returns a file with the specified path. + /// + /// The . + /// The path that identifies the file. + /// + /// The representing the desired file. + /// + public static FileNode GetFile(this IFileProvider provider, string path) + { + path = FilePath.Normalize(path); + + var file = provider.GetFileInfo(path); + return new FileNode(provider, path, file); + } +} diff --git a/src/Ramstack.FileProviders.Extensions/PathTokenizer.cs b/src/Ramstack.FileProviders.Extensions/PathTokenizer.cs index 0fb7b10..aaf7e58 100644 --- a/src/Ramstack.FileProviders.Extensions/PathTokenizer.cs +++ b/src/Ramstack.FileProviders.Extensions/PathTokenizer.cs @@ -1,97 +1,97 @@ -using System.Runtime.CompilerServices; - -namespace Ramstack.FileProviders; - -/// -/// Tokenizes a file path into its constituent components. -/// -/// The path of the file to tokenize. -internal readonly struct PathTokenizer(string path) -{ - /// - /// Returns an enumerator that iterates through the collection. - /// - /// - /// An enumerator to iterate through the collection. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Enumerator GetEnumerator() => - new Enumerator(path); - - /// - /// Tokenizes the specified path into a collection of the path components. - /// - /// The file path to tokenize. - /// - /// The . - /// - public static PathTokenizer Tokenize(string path) => - new PathTokenizer(path); - - #region Inner type: Enumerator - - /// - /// Represents the enumerator. - /// - public struct Enumerator - { - private readonly string _path; - private int _start; - private int _count; - - /// - /// Initializes a new instance of the structure. - /// - /// The file path to tokenize. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal Enumerator(string path) => - (_path, _count) = (path, -1); - - /// - /// Gets the current path component. - /// - public ReadOnlySpan Current - { - // - // Using AsSpan(_start) followed by slicing is more efficient - // than AsSpan(_start, _count) because: - // 1) MoveNext already validated _start bounds - // 2) We only need to check _count <= length (simpler than checking start+count) - // - // The alternative AsSpan(start, count) does a combined bounds check - // which the JIT can't optimize away: - // (ulong)(uint)_start + (ulong)(uint)_count <= (ulong)(uint)Length - // - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => _path.AsSpan(_start)[.._count]; - } - - /// - /// Advances the enumerator to the next path component. - /// - /// - /// if the enumerator was successfully advanced - /// to the next path component; otherwise, . - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool MoveNext() - { - _start = _start + _count + 1; - - if ((uint)_start < (uint)_path.Length) - { - var s = _path.AsSpan(_start); - - _count = s.IndexOfAny('/', '\\'); - if (_count < 0) - _count = s.Length; - - return true; - } - - return false; - } - } - - #endregion -} +using System.Runtime.CompilerServices; + +namespace Ramstack.FileProviders; + +/// +/// Tokenizes a file path into its constituent components. +/// +/// The path of the file to tokenize. +internal readonly struct PathTokenizer(string path) +{ + /// + /// Returns an enumerator that iterates through the collection. + /// + /// + /// An enumerator to iterate through the collection. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Enumerator GetEnumerator() => + new Enumerator(path); + + /// + /// Tokenizes the specified path into a collection of the path components. + /// + /// The file path to tokenize. + /// + /// The . + /// + public static PathTokenizer Tokenize(string path) => + new PathTokenizer(path); + + #region Inner type: Enumerator + + /// + /// Represents the enumerator. + /// + public struct Enumerator + { + private readonly string _path; + private int _start; + private int _count; + + /// + /// Initializes a new instance of the structure. + /// + /// The file path to tokenize. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal Enumerator(string path) => + (_path, _count) = (path, -1); + + /// + /// Gets the current path component. + /// + public ReadOnlySpan Current + { + // + // Using AsSpan(_start) followed by slicing is more efficient + // than AsSpan(_start, _count) because: + // 1) MoveNext already validated _start bounds + // 2) We only need to check _count <= length (simpler than checking start+count) + // + // The alternative AsSpan(start, count) does a combined bounds check + // which the JIT can't optimize away: + // (ulong)(uint)_start + (ulong)(uint)_count <= (ulong)(uint)Length + // + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _path.AsSpan(_start)[.._count]; + } + + /// + /// Advances the enumerator to the next path component. + /// + /// + /// if the enumerator was successfully advanced + /// to the next path component; otherwise, . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool MoveNext() + { + _start = _start + _count + 1; + + if ((uint)_start < (uint)_path.Length) + { + var s = _path.AsSpan(_start); + + _count = s.IndexOfAny('/', '\\'); + if (_count < 0) + _count = s.Length; + + return true; + } + + return false; + } + } + + #endregion +} diff --git a/src/Ramstack.FileProviders.Extensions/README.md b/src/Ramstack.FileProviders.Extensions/README.md index b3eb069..c7ad8c8 100644 --- a/src/Ramstack.FileProviders.Extensions/README.md +++ b/src/Ramstack.FileProviders.Extensions/README.md @@ -1,88 +1,88 @@ -# Ramstack.FileProviders.Extensions -[![NuGet](https://img.shields.io/nuget/v/Ramstack.FileProviders.Extensions.svg)](https://nuget.org/packages/Ramstack.FileProviders.Extensions) -[![MIT](https://img.shields.io/github/license/rameel/ramstack.fileproviders)](https://github.com/rameel/ramstack.fileproviders/blob/main/LICENSE) - -Represents a lightweight .NET library of useful and convenient extensions for `Microsoft.Extensions.FileProviders` -that enhances file handling capabilities in .NET applications. - -## Getting Started - -To install the `Ramstack.FileProviders.Extensions` [NuGet package](https://www.nuget.org/packages/Ramstack.FileProviders.Extensions) -in your project, run the following command: -```console -dotnet add package Ramstack.FileProviders.Extensions -``` - -## Overview - -The library provides useful extensions for `IFileProvider`, bringing its capabilities and experience -closer to what's provided by the `DirectoryInfo` and `FileInfo` classes. - -Simply stated, a `FileNode` knows which directory it is located in, and a directory represented -by the `DirectoryNode` class can access its parent directory and list all files within it, recursively. - -```csharp -FileNode file = provider.GetFile("/docs/README"); - -// Prints the full path of the given file -Console.WriteLine($"Reading: {file.FullName}"); - -using StreamReader reader = file.OpenText(); -Console.WriteLine(reader.ReadToEnd()); -``` - -```csharp -DirectoryNode directory = provider.GetDirectory("/docs"); - -foreach (FileNode file in directory.EnumerateFiles()) - Console.WriteLine(file.FullName); -``` - -Furthermore, the methods for enumerating files (`EnumerateFiles`/`EnumerateDirectories`/`EnumerateFileNodes`) -allow specifying glob patterns to search for the desired files, as well as patterns to exclude files from the resulting list. - -It relies on the [Ramstack.Globbing](https://www.nuget.org/packages/Ramstack.Globbing) package for its globbing capabilities. -```csharp -DirectoryNode directory = provider.GetDirectory("/project"); - -// Finds all *.md files and converts them to HTML -foreach (FileNode file in directory.EnumerateFiles(pattern: "**/*.md")) - RenderMarkdown(file); - -// Excludes files in a specific folder -foreach (FileNode file in directory.EnumerateFiles(pattern: "**/*.md", exclude: "vendors/**")) - RenderMarkdown(file); -``` -For convenience, many methods specific to `DirectoryNode` or `FileNode` are also available for `IFileProvider`. - -Thus, if we know the directory in which to look for files or the file to read, there is no need to obtain the -`DirectoryNode` or `FileNode` object. - -```csharp -using StreamReader reader = provider.OpenText("/docs/README", Encoding.UTF8); -Console.WriteLine(reader.ReadToEnd()); - -// Finds all *.md files and converts them to HTML -foreach (FileNode file in provider.EnumerateFiles("/project", pattern: "**/*.md")) - RenderMarkdown(file); -``` - -## Related Packages -- [Ramstack.FileProviders](https://www.nuget.org/packages/Ramstack.FileProviders) — Additional file providers, including `PrefixedFileProvider` and `SubFileProvider`. -- [Ramstack.FileProviders.Globbing](https://www.nuget.org/packages/Ramstack.FileProviders.Globbing) — A file provider that filters files using include and/or exclude glob patterns. Include patterns make only matching files visible, while exclude patterns hide specific files. Both include and exclude patterns can be combined for flexible file visibility control. -- [Ramstack.FileProviders.Composition](https://www.nuget.org/packages/Ramstack.FileProviders.Composition) — Provides a helper class for flattening and composing `IFileProvider`. - - -## Supported versions - -| | Version | -|------|----------------| -| .NET | 6, 7, 8, 9, 10 | - -## Contributions - -Bug reports and contributions are welcome. - -## License -This package is released as open source under the **MIT License**. -See the [LICENSE](https://github.com/rameel/ramstack.fileproviders/blob/main/LICENSE) file for more details. +# Ramstack.FileProviders.Extensions +[![NuGet](https://img.shields.io/nuget/v/Ramstack.FileProviders.Extensions.svg)](https://nuget.org/packages/Ramstack.FileProviders.Extensions) +[![MIT](https://img.shields.io/github/license/rameel/ramstack.fileproviders)](https://github.com/rameel/ramstack.fileproviders/blob/main/LICENSE) + +Represents a lightweight .NET library of useful and convenient extensions for `Microsoft.Extensions.FileProviders` +that enhances file handling capabilities in .NET applications. + +## Getting Started + +To install the `Ramstack.FileProviders.Extensions` [NuGet package](https://www.nuget.org/packages/Ramstack.FileProviders.Extensions) +in your project, run the following command: +```console +dotnet add package Ramstack.FileProviders.Extensions +``` + +## Overview + +The library provides useful extensions for `IFileProvider`, bringing its capabilities and experience +closer to what's provided by the `DirectoryInfo` and `FileInfo` classes. + +Simply stated, a `FileNode` knows which directory it is located in, and a directory represented +by the `DirectoryNode` class can access its parent directory and list all files within it, recursively. + +```csharp +FileNode file = provider.GetFile("/docs/README"); + +// Prints the full path of the given file +Console.WriteLine($"Reading: {file.FullName}"); + +using StreamReader reader = file.OpenText(); +Console.WriteLine(reader.ReadToEnd()); +``` + +```csharp +DirectoryNode directory = provider.GetDirectory("/docs"); + +foreach (FileNode file in directory.EnumerateFiles()) + Console.WriteLine(file.FullName); +``` + +Furthermore, the methods for enumerating files (`EnumerateFiles`/`EnumerateDirectories`/`EnumerateFileNodes`) +allow specifying glob patterns to search for the desired files, as well as patterns to exclude files from the resulting list. + +It relies on the [Ramstack.Globbing](https://www.nuget.org/packages/Ramstack.Globbing) package for its globbing capabilities. +```csharp +DirectoryNode directory = provider.GetDirectory("/project"); + +// Finds all *.md files and converts them to HTML +foreach (FileNode file in directory.EnumerateFiles(pattern: "**/*.md")) + RenderMarkdown(file); + +// Excludes files in a specific folder +foreach (FileNode file in directory.EnumerateFiles(pattern: "**/*.md", exclude: "vendors/**")) + RenderMarkdown(file); +``` +For convenience, many methods specific to `DirectoryNode` or `FileNode` are also available for `IFileProvider`. + +Thus, if we know the directory in which to look for files or the file to read, there is no need to obtain the +`DirectoryNode` or `FileNode` object. + +```csharp +using StreamReader reader = provider.OpenText("/docs/README", Encoding.UTF8); +Console.WriteLine(reader.ReadToEnd()); + +// Finds all *.md files and converts them to HTML +foreach (FileNode file in provider.EnumerateFiles("/project", pattern: "**/*.md")) + RenderMarkdown(file); +``` + +## Related Packages +- [Ramstack.FileProviders](https://www.nuget.org/packages/Ramstack.FileProviders) — Additional file providers, including `PrefixedFileProvider` and `SubFileProvider`. +- [Ramstack.FileProviders.Globbing](https://www.nuget.org/packages/Ramstack.FileProviders.Globbing) — A file provider that filters files using include and/or exclude glob patterns. Include patterns make only matching files visible, while exclude patterns hide specific files. Both include and exclude patterns can be combined for flexible file visibility control. +- [Ramstack.FileProviders.Composition](https://www.nuget.org/packages/Ramstack.FileProviders.Composition) — Provides a helper class for flattening and composing `IFileProvider`. + + +## Supported versions + +| | Version | +|------|----------------| +| .NET | 6, 7, 8, 9, 10 | + +## Contributions + +Bug reports and contributions are welcome. + +## License +This package is released as open source under the **MIT License**. +See the [LICENSE](https://github.com/rameel/ramstack.fileproviders/blob/main/LICENSE) file for more details. diff --git a/src/Ramstack.FileProviders.Extensions/Ramstack.FileProviders.Extensions.csproj b/src/Ramstack.FileProviders.Extensions/Ramstack.FileProviders.Extensions.csproj index 76886f7..40a740a 100644 --- a/src/Ramstack.FileProviders.Extensions/Ramstack.FileProviders.Extensions.csproj +++ b/src/Ramstack.FileProviders.Extensions/Ramstack.FileProviders.Extensions.csproj @@ -1,80 +1,80 @@ - - - - net6.0;net8.0 - A lightweight .NET library of useful and convenient extensions for IFileProvider. - enable - enable - preview - true - true - Ramstack.FileProviders - EXPOSE_FILEPATH - - - - 649;1591 - CS8600;CS8601;CS8602;CS8603;CS8604;CS8610;CS8613;CS8614;CS8618;CS8619;CS8620;CS8625;CS8643;CS8653;CS8714 - - - - https://github.com/rameel/ramstack.fileproviders - Rameel - fileprovider;files - https://github.com/rameel/ramstack.fileproviders#readme - README.md - MIT - true - snupkg - true - true - true - true - - - - true - - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - Properties\AssemblyInfo.cs - - - - - - - - - - - - - Properties\README.md - True - \ - - - - + + + + net6.0;net8.0 + A lightweight .NET library of useful and convenient extensions for IFileProvider. + enable + enable + preview + true + true + Ramstack.FileProviders + EXPOSE_FILEPATH + + + + 649;1591 + CS8600;CS8601;CS8602;CS8603;CS8604;CS8610;CS8613;CS8614;CS8618;CS8619;CS8620;CS8625;CS8643;CS8653;CS8714 + + + + https://github.com/rameel/ramstack.fileproviders + Rameel + fileprovider;files + https://github.com/rameel/ramstack.fileproviders#readme + README.md + MIT + true + snupkg + true + true + true + true + + + + true + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + Properties\AssemblyInfo.cs + + + + + + + + + + + + + Properties\README.md + True + \ + + + + diff --git a/src/Ramstack.FileProviders.Globbing/GlobbingFileProvider.cs b/src/Ramstack.FileProviders.Globbing/GlobbingFileProvider.cs index 74a4b89..1597926 100644 --- a/src/Ramstack.FileProviders.Globbing/GlobbingFileProvider.cs +++ b/src/Ramstack.FileProviders.Globbing/GlobbingFileProvider.cs @@ -1,152 +1,152 @@ -using Ramstack.FileProviders.Internal; - -namespace Ramstack.FileProviders; - -/// -/// Provides a file provider implementation that filters files based on specified glob patterns. -/// -/// -/// The class wraps around another and applies glob-based -/// filtering rules to determine which files to include or exclude. This allows for flexible and powerful file selection -/// using standard glob patterns. -/// -/// -/// -/// var underlyingProvider = new PhysicalFileProvider("C:\\MyDirectory"); -/// var provider = new GlobbingFileProvider(underlyingProvider, patterns: ["**/*.txt", "docs/*.md"], excludes: ["**/README.md"]); -/// foreach (var file in provider.GetDirectoryContents("/")) -/// Console.WriteLine(file.Name); -/// -/// -public sealed class GlobbingFileProvider : IFileProvider -{ - private readonly IFileProvider _provider; - private readonly string[] _patterns; - private readonly string[] _excludes; - - /// - /// Initializes a new instance of the class. - /// - /// The underlying file provider. - /// The pattern to include in the enumeration. - /// The optional pattern to exclude from the enumeration. - public GlobbingFileProvider(IFileProvider provider, string pattern, string? exclude = null) - { - ArgumentNullException.ThrowIfNull(provider); - ArgumentNullException.ThrowIfNull(pattern); - - _provider = provider; - _patterns = [pattern]; - _excludes = exclude is not null ? [exclude] : []; - } - - /// - /// Initializes a new instance of the class. - /// - /// The underlying file provider. - /// The patterns to include in the enumeration. - /// The optional patterns to exclude from the enumeration. - public GlobbingFileProvider(IFileProvider provider, string[] patterns, string[]? excludes = null) - { - ArgumentNullException.ThrowIfNull(provider); - ArgumentNullException.ThrowIfNull(patterns); - - _provider = provider; - _patterns = [..patterns]; - _excludes = [..excludes ?? []]; - } - - /// - public IFileInfo GetFileInfo(string subpath) - { - subpath = FilePath.Normalize(subpath); - return IsFileIncluded(subpath) - ? _provider.GetFileInfo(subpath) - : new NotFoundFileInfo(subpath); - } - - /// - public IDirectoryContents GetDirectoryContents(string subpath) - { - subpath = FilePath.Normalize(subpath); - - if (IsDirectoryIncluded(subpath)) - { - var directory = _provider.GetDirectoryContents(subpath); - if (directory is not NotFoundDirectoryContents) - return new GlobbingDirectoryContents(this, subpath, directory); - } - - return NotFoundDirectoryContents.Singleton; - } - - /// - public IChangeToken Watch(string filter) => - _provider.Watch(filter); - - /// - /// Determines if a file is included based on the specified patterns and exclusions. - /// - /// The path of the file. - /// - /// if the file is included; - /// otherwise, . - /// - private bool IsFileIncluded(string path) => - !PathHelper.IsMatch(path, _excludes) && PathHelper.IsMatch(path, _patterns); - - /// - /// Determines if a directory is included based on the specified exclusions. - /// - /// The path of the directory. - /// - /// if the directory is included; otherwise, . - /// - private bool IsDirectoryIncluded(string path) => - path == "/" || !PathHelper.IsMatch(path, _excludes) && PathHelper.IsPartialMatch(path, _patterns); - - #region Inner type: GlobbingDirectoryContents - - /// - /// Represents the contents of a directory with globbing support for matching file patterns. - /// - private sealed class GlobbingDirectoryContents : IDirectoryContents - { - private readonly GlobbingFileProvider _provider; - private readonly string _directoryPath; - private readonly IDirectoryContents _directory; - - /// - public bool Exists => _directory.Exists; - - /// - /// Initializes a new instance of the class. - /// - /// The providing the files. - /// The path to enumerate. - /// The underlying directory contents. - public GlobbingDirectoryContents(GlobbingFileProvider provider, string directoryPath, IDirectoryContents directory) - { - _provider = provider; - _directoryPath = directoryPath; - _directory = directory; - } - - /// - public IEnumerator GetEnumerator() - { - foreach (var file in _directory) - { - var path = FilePath.Join(_directoryPath, file.Name); - if (file.IsDirectory ? _provider.IsDirectoryIncluded(path) : _provider.IsFileIncluded(path)) - yield return file; - } - } - - /// - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => - GetEnumerator(); - } - - #endregion -} +using Ramstack.FileProviders.Internal; + +namespace Ramstack.FileProviders; + +/// +/// Provides a file provider implementation that filters files based on specified glob patterns. +/// +/// +/// The class wraps around another and applies glob-based +/// filtering rules to determine which files to include or exclude. This allows for flexible and powerful file selection +/// using standard glob patterns. +/// +/// +/// +/// var underlyingProvider = new PhysicalFileProvider("C:\\MyDirectory"); +/// var provider = new GlobbingFileProvider(underlyingProvider, patterns: ["**/*.txt", "docs/*.md"], excludes: ["**/README.md"]); +/// foreach (var file in provider.GetDirectoryContents("/")) +/// Console.WriteLine(file.Name); +/// +/// +public sealed class GlobbingFileProvider : IFileProvider +{ + private readonly IFileProvider _provider; + private readonly string[] _patterns; + private readonly string[] _excludes; + + /// + /// Initializes a new instance of the class. + /// + /// The underlying file provider. + /// The pattern to include in the enumeration. + /// The optional pattern to exclude from the enumeration. + public GlobbingFileProvider(IFileProvider provider, string pattern, string? exclude = null) + { + ArgumentNullException.ThrowIfNull(provider); + ArgumentNullException.ThrowIfNull(pattern); + + _provider = provider; + _patterns = [pattern]; + _excludes = exclude is not null ? [exclude] : []; + } + + /// + /// Initializes a new instance of the class. + /// + /// The underlying file provider. + /// The patterns to include in the enumeration. + /// The optional patterns to exclude from the enumeration. + public GlobbingFileProvider(IFileProvider provider, string[] patterns, string[]? excludes = null) + { + ArgumentNullException.ThrowIfNull(provider); + ArgumentNullException.ThrowIfNull(patterns); + + _provider = provider; + _patterns = [..patterns]; + _excludes = [..excludes ?? []]; + } + + /// + public IFileInfo GetFileInfo(string subpath) + { + subpath = FilePath.Normalize(subpath); + return IsFileIncluded(subpath) + ? _provider.GetFileInfo(subpath) + : new NotFoundFileInfo(subpath); + } + + /// + public IDirectoryContents GetDirectoryContents(string subpath) + { + subpath = FilePath.Normalize(subpath); + + if (IsDirectoryIncluded(subpath)) + { + var directory = _provider.GetDirectoryContents(subpath); + if (directory is not NotFoundDirectoryContents) + return new GlobbingDirectoryContents(this, subpath, directory); + } + + return NotFoundDirectoryContents.Singleton; + } + + /// + public IChangeToken Watch(string filter) => + _provider.Watch(filter); + + /// + /// Determines if a file is included based on the specified patterns and exclusions. + /// + /// The path of the file. + /// + /// if the file is included; + /// otherwise, . + /// + private bool IsFileIncluded(string path) => + !PathHelper.IsMatch(path, _excludes) && PathHelper.IsMatch(path, _patterns); + + /// + /// Determines if a directory is included based on the specified exclusions. + /// + /// The path of the directory. + /// + /// if the directory is included; otherwise, . + /// + private bool IsDirectoryIncluded(string path) => + path == "/" || !PathHelper.IsMatch(path, _excludes) && PathHelper.IsPartialMatch(path, _patterns); + + #region Inner type: GlobbingDirectoryContents + + /// + /// Represents the contents of a directory with globbing support for matching file patterns. + /// + private sealed class GlobbingDirectoryContents : IDirectoryContents + { + private readonly GlobbingFileProvider _provider; + private readonly string _directoryPath; + private readonly IDirectoryContents _directory; + + /// + public bool Exists => _directory.Exists; + + /// + /// Initializes a new instance of the class. + /// + /// The providing the files. + /// The path to enumerate. + /// The underlying directory contents. + public GlobbingDirectoryContents(GlobbingFileProvider provider, string directoryPath, IDirectoryContents directory) + { + _provider = provider; + _directoryPath = directoryPath; + _directory = directory; + } + + /// + public IEnumerator GetEnumerator() + { + foreach (var file in _directory) + { + var path = FilePath.Join(_directoryPath, file.Name); + if (file.IsDirectory ? _provider.IsDirectoryIncluded(path) : _provider.IsFileIncluded(path)) + yield return file; + } + } + + /// + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => + GetEnumerator(); + } + + #endregion +} diff --git a/src/Ramstack.FileProviders.Globbing/Internal/PathHelper.cs b/src/Ramstack.FileProviders.Globbing/Internal/PathHelper.cs index 80b1650..a9c4400 100644 --- a/src/Ramstack.FileProviders.Globbing/Internal/PathHelper.cs +++ b/src/Ramstack.FileProviders.Globbing/Internal/PathHelper.cs @@ -1,342 +1,342 @@ -using System.Numerics; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Runtime.Intrinsics; -using System.Runtime.Intrinsics.Arm; -using System.Runtime.Intrinsics.X86; - -using Ramstack.Globbing; - -namespace Ramstack.FileProviders.Internal; - -/// -/// Provides helper methods for path manipulations. -/// -internal static class PathHelper -{ - /// - /// Determines whether the specified path matches any of the specified patterns. - /// - /// The path to check for a match. - /// An array of patterns to match against the path. - /// - /// if the path matches any of the patterns; - /// otherwise, . - /// - public static bool IsMatch(scoped ReadOnlySpan path, string[] patterns) - { - foreach (var pattern in patterns) - if (Matcher.IsMatch(path, pattern, MatchFlags.Unix)) - return true; - - return false; - } - - /// - /// Determines whether the specified path partially matches any of the specified patterns. - /// - /// The path to be partially matched. - /// An array of patterns to match against the path. - /// - /// if the path partially matches any of the patterns; - /// otherwise, . - /// - public static bool IsPartialMatch(scoped ReadOnlySpan path, string[] patterns) - { - Debug.Assert(path is not "/"); - - var count = CountPathSegments(path); - - foreach (var pattern in patterns) - if (Matcher.IsMatch(path, GetPartialPattern(pattern, count), MatchFlags.Unix)) - return true; - - return false; - } - - /// - /// Counts the number of segments in the specified path. - /// - /// The path to count segments for. - /// - /// The number of segments in the path. - /// - private static int CountPathSegments(scoped ReadOnlySpan path) - { - var count = 0; - var iterator = new PathSegmentIterator(); - ref var s = ref Unsafe.AsRef(in MemoryMarshal.GetReference(path)); - var length = path.Length; - - while (true) - { - var r = iterator.GetNext(ref s, length); - - if (r.start != r.final) - count++; - - if (r.final == length) - break; - } - - if (count == 0) - count = 1; - - return count; - } - - /// - /// Returns a partial pattern from the specified pattern string based on the specified depth. - /// - /// The pattern string to extract from. - /// The depth level to extract the partial pattern up to. - /// - /// A representing the partial pattern. - /// - private static ReadOnlySpan GetPartialPattern(string pattern, int depth) - { - Debug.Assert(depth >= 1); - - var iterator = new PathSegmentIterator(); - ref var s = ref Unsafe.AsRef(in pattern.GetPinnableReference()); - var length = pattern.Length; - - while (true) - { - var r = iterator.GetNext(ref s, length); - if (r.start != r.final) - depth--; - - if (depth < 1 - || r.final == length - || IsGlobStar(ref s, r.start, r.final)) - { - Debug.Assert(r.final >= 0 && r.final <= pattern.Length); - return MemoryMarshal.CreateReadOnlySpan(ref s, r.final); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - static bool IsGlobStar(ref char s, int index, int final) => - index + 2 == final && Unsafe.ReadUnaligned( - ref Unsafe.As( - ref Unsafe.Add(ref s, (nint)(uint)index))) == ('*' << 16 | '*'); - } - - #region Vector helper methods - - /// - /// Loads a 256-bit vector from the specified source. - /// - /// The source from which the vector will be loaded. - /// The offset from the from which the vector will be loaded. - /// - /// The loaded 256-bit vector. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector256 LoadVector256(ref char source, nint offset) => - Unsafe.ReadUnaligned>( - ref Unsafe.As(ref Unsafe.Add(ref source, offset))); - - /// - /// Loads a 128-bit vector from the specified source. - /// - /// The source from which the vector will be loaded. - /// The offset from from which the vector will be loaded. - /// - /// The loaded 128-bit vector. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector128 LoadVector128(ref char source, nint offset) => - Unsafe.ReadUnaligned>( - ref Unsafe.As( - ref Unsafe.Add(ref source, offset))); - - #endregion - - #region Inner type: PathSegmentIterator - - /// - /// Provides functionality to iterate over segments of a path. - /// - private struct PathSegmentIterator - { - private int _last; - private nint _position; - private uint _mask; - - /// - /// Initializes a new instance of the structure. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public PathSegmentIterator() => - _last = -1; - - /// - /// Retrieves the next segment of the path. - /// - /// A reference to the starting character of the path. - /// The total number of characters in the input path starting from . - /// - /// A tuple containing the start and end indices of the next path segment. - /// start indicates the beginning of the segment, and final satisfies - /// the condition that final - start equals the length of the segment. - /// The end of the iteration is indicated by final being equal to the length of the path. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public (int start, int final) GetNext(ref char source, int length) - { - var start = _last + 1; - - while ((int)_position < length) - { - if ((Avx2.IsSupported || Sse2.IsSupported || AdvSimd.Arm64.IsSupported) && _mask != 0) - { - var offset = BitOperations.TrailingZeroCount(_mask); - if (AdvSimd.IsSupported) - { - // - // On ARM, ExtractMostSignificantBits returns a mask where each bit - // represents one vector element (1 bit per ushort), so offset - // directly corresponds to the element index - // - _last = (int)(_position + (nint)(uint)offset); - - // - // Clear the bits for the current separator - // - _mask &= ~(1u << offset); - } - else - { - // - // On x86, MoveMask (and ExtractMostSignificantBits on byte-based vectors) - // returns a mask where each bit represents one byte (2 bits per ushort), - // so we need to divide offset by 2 to get the actual element index - // - _last = (int)(_position + (nint)((uint)offset >> 1)); - - // - // Clear the bits for the current separator - // - _mask &= ~(0b_11u << offset); - } - - // - // Advance position to the next chunk when no separators remain in the mask - // - if (_mask == 0) - { - // - // https://github.com/dotnet/runtime/issues/117416 - // - // Precompute the stride size instead of calculating it inline - // to avoid stack spilling. For some unknown reason, the JIT - // fails to optimize properly when this is written inline, like so: - // _position += Avx2.IsSupported - // ? Vector256.Count - // : Vector128.Count; - // - - var stride = Avx2.IsSupported - ? Vector256.Count - : Vector128.Count; - - _position += stride; - } - - return (start, _last); - } - - if (Avx2.IsSupported && (int)_position <= length - Vector256.Count) - { - var chunk = LoadVector256(ref source, _position); - var slash = Vector256.Create('/'); - var comparison = Avx2.CompareEqual(chunk, slash); - - // - // Store the comparison bitmask and reuse it across iterations - // as long as it contains non-zero bits. - // This avoids reloading SIMD registers and repeating comparisons - // on the same chunk of data. - // - _mask = (uint)Avx2.MoveMask(comparison.AsByte()); - - // - // Advance position to the next chunk when no separators found - // - if (_mask == 0) - _position += Vector256.Count; - } - else if (Sse2.IsSupported && !Avx2.IsSupported && (int)_position <= length - Vector128.Count) - { - var chunk = LoadVector128(ref source, _position); - var slash = Vector128.Create('/'); - var comparison = Sse2.CompareEqual(chunk, slash); - - // - // Store the comparison bitmask and reuse it across iterations - // as long as it contains non-zero bits. - // This avoids reloading SIMD registers and repeating comparisons - // on the same chunk of data. - // - _mask = (uint)Sse2.MoveMask(comparison.AsByte()); - - // - // Advance position to the next chunk when no separators found - // - if (_mask == 0) - _position += Vector128.Count; - } - else if (AdvSimd.Arm64.IsSupported && (int)_position <= length - Vector128.Count) - { - var chunk = LoadVector128(ref source, _position); - var slash = Vector128.Create('/'); - var comparison = AdvSimd.CompareEqual(chunk, slash); - - // - // Store the comparison bitmask and reuse it across iterations - // as long as it contains non-zero bits. - // This avoids reloading SIMD registers and repeating comparisons - // on the same chunk of data. - // - _mask = ExtractMostSignificantBits(comparison); - - // - // Advance position to the next chunk when no separators found - // - if (_mask == 0) - _position += Vector128.Count; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - static uint ExtractMostSignificantBits(Vector128 v) - { - var sum = AdvSimd.Arm64.AddAcross( - AdvSimd.ShiftLogical( - AdvSimd.And(v, Vector128.Create((ushort)0x8000)), - Vector128.Create(-15, -14, -13, -12, -11, -10, -9, -8))); - return sum.ToScalar(); - } - } - else - { - for (; (int)_position < length; _position++) - { - var ch = Unsafe.Add(ref source, _position); - if (ch == '/') - { - _last = (int)_position; - _position++; - - return (start, _last); - } - } - } - } - - return (start, length); - } - } - - #endregion -} +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.Arm; +using System.Runtime.Intrinsics.X86; + +using Ramstack.Globbing; + +namespace Ramstack.FileProviders.Internal; + +/// +/// Provides helper methods for path manipulations. +/// +internal static class PathHelper +{ + /// + /// Determines whether the specified path matches any of the specified patterns. + /// + /// The path to check for a match. + /// An array of patterns to match against the path. + /// + /// if the path matches any of the patterns; + /// otherwise, . + /// + public static bool IsMatch(scoped ReadOnlySpan path, string[] patterns) + { + foreach (var pattern in patterns) + if (Matcher.IsMatch(path, pattern, MatchFlags.Unix)) + return true; + + return false; + } + + /// + /// Determines whether the specified path partially matches any of the specified patterns. + /// + /// The path to be partially matched. + /// An array of patterns to match against the path. + /// + /// if the path partially matches any of the patterns; + /// otherwise, . + /// + public static bool IsPartialMatch(scoped ReadOnlySpan path, string[] patterns) + { + Debug.Assert(path is not "/"); + + var count = CountPathSegments(path); + + foreach (var pattern in patterns) + if (Matcher.IsMatch(path, GetPartialPattern(pattern, count), MatchFlags.Unix)) + return true; + + return false; + } + + /// + /// Counts the number of segments in the specified path. + /// + /// The path to count segments for. + /// + /// The number of segments in the path. + /// + private static int CountPathSegments(scoped ReadOnlySpan path) + { + var count = 0; + var iterator = new PathSegmentIterator(); + ref var s = ref Unsafe.AsRef(in MemoryMarshal.GetReference(path)); + var length = path.Length; + + while (true) + { + var r = iterator.GetNext(ref s, length); + + if (r.start != r.final) + count++; + + if (r.final == length) + break; + } + + if (count == 0) + count = 1; + + return count; + } + + /// + /// Returns a partial pattern from the specified pattern string based on the specified depth. + /// + /// The pattern string to extract from. + /// The depth level to extract the partial pattern up to. + /// + /// A representing the partial pattern. + /// + private static ReadOnlySpan GetPartialPattern(string pattern, int depth) + { + Debug.Assert(depth >= 1); + + var iterator = new PathSegmentIterator(); + ref var s = ref Unsafe.AsRef(in pattern.GetPinnableReference()); + var length = pattern.Length; + + while (true) + { + var r = iterator.GetNext(ref s, length); + if (r.start != r.final) + depth--; + + if (depth < 1 + || r.final == length + || IsGlobStar(ref s, r.start, r.final)) + { + Debug.Assert(r.final >= 0 && r.final <= pattern.Length); + return MemoryMarshal.CreateReadOnlySpan(ref s, r.final); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static bool IsGlobStar(ref char s, int index, int final) => + index + 2 == final && Unsafe.ReadUnaligned( + ref Unsafe.As( + ref Unsafe.Add(ref s, (nint)(uint)index))) == ('*' << 16 | '*'); + } + + #region Vector helper methods + + /// + /// Loads a 256-bit vector from the specified source. + /// + /// The source from which the vector will be loaded. + /// The offset from the from which the vector will be loaded. + /// + /// The loaded 256-bit vector. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 LoadVector256(ref char source, nint offset) => + Unsafe.ReadUnaligned>( + ref Unsafe.As(ref Unsafe.Add(ref source, offset))); + + /// + /// Loads a 128-bit vector from the specified source. + /// + /// The source from which the vector will be loaded. + /// The offset from from which the vector will be loaded. + /// + /// The loaded 128-bit vector. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 LoadVector128(ref char source, nint offset) => + Unsafe.ReadUnaligned>( + ref Unsafe.As( + ref Unsafe.Add(ref source, offset))); + + #endregion + + #region Inner type: PathSegmentIterator + + /// + /// Provides functionality to iterate over segments of a path. + /// + private struct PathSegmentIterator + { + private int _last; + private nint _position; + private uint _mask; + + /// + /// Initializes a new instance of the structure. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public PathSegmentIterator() => + _last = -1; + + /// + /// Retrieves the next segment of the path. + /// + /// A reference to the starting character of the path. + /// The total number of characters in the input path starting from . + /// + /// A tuple containing the start and end indices of the next path segment. + /// start indicates the beginning of the segment, and final satisfies + /// the condition that final - start equals the length of the segment. + /// The end of the iteration is indicated by final being equal to the length of the path. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public (int start, int final) GetNext(ref char source, int length) + { + var start = _last + 1; + + while ((int)_position < length) + { + if ((Avx2.IsSupported || Sse2.IsSupported || AdvSimd.Arm64.IsSupported) && _mask != 0) + { + var offset = BitOperations.TrailingZeroCount(_mask); + if (AdvSimd.IsSupported) + { + // + // On ARM, ExtractMostSignificantBits returns a mask where each bit + // represents one vector element (1 bit per ushort), so offset + // directly corresponds to the element index + // + _last = (int)(_position + (nint)(uint)offset); + + // + // Clear the bits for the current separator + // + _mask &= ~(1u << offset); + } + else + { + // + // On x86, MoveMask (and ExtractMostSignificantBits on byte-based vectors) + // returns a mask where each bit represents one byte (2 bits per ushort), + // so we need to divide offset by 2 to get the actual element index + // + _last = (int)(_position + (nint)((uint)offset >> 1)); + + // + // Clear the bits for the current separator + // + _mask &= ~(0b_11u << offset); + } + + // + // Advance position to the next chunk when no separators remain in the mask + // + if (_mask == 0) + { + // + // https://github.com/dotnet/runtime/issues/117416 + // + // Precompute the stride size instead of calculating it inline + // to avoid stack spilling. For some unknown reason, the JIT + // fails to optimize properly when this is written inline, like so: + // _position += Avx2.IsSupported + // ? Vector256.Count + // : Vector128.Count; + // + + var stride = Avx2.IsSupported + ? Vector256.Count + : Vector128.Count; + + _position += stride; + } + + return (start, _last); + } + + if (Avx2.IsSupported && (int)_position <= length - Vector256.Count) + { + var chunk = LoadVector256(ref source, _position); + var slash = Vector256.Create('/'); + var comparison = Avx2.CompareEqual(chunk, slash); + + // + // Store the comparison bitmask and reuse it across iterations + // as long as it contains non-zero bits. + // This avoids reloading SIMD registers and repeating comparisons + // on the same chunk of data. + // + _mask = (uint)Avx2.MoveMask(comparison.AsByte()); + + // + // Advance position to the next chunk when no separators found + // + if (_mask == 0) + _position += Vector256.Count; + } + else if (Sse2.IsSupported && !Avx2.IsSupported && (int)_position <= length - Vector128.Count) + { + var chunk = LoadVector128(ref source, _position); + var slash = Vector128.Create('/'); + var comparison = Sse2.CompareEqual(chunk, slash); + + // + // Store the comparison bitmask and reuse it across iterations + // as long as it contains non-zero bits. + // This avoids reloading SIMD registers and repeating comparisons + // on the same chunk of data. + // + _mask = (uint)Sse2.MoveMask(comparison.AsByte()); + + // + // Advance position to the next chunk when no separators found + // + if (_mask == 0) + _position += Vector128.Count; + } + else if (AdvSimd.Arm64.IsSupported && (int)_position <= length - Vector128.Count) + { + var chunk = LoadVector128(ref source, _position); + var slash = Vector128.Create('/'); + var comparison = AdvSimd.CompareEqual(chunk, slash); + + // + // Store the comparison bitmask and reuse it across iterations + // as long as it contains non-zero bits. + // This avoids reloading SIMD registers and repeating comparisons + // on the same chunk of data. + // + _mask = ExtractMostSignificantBits(comparison); + + // + // Advance position to the next chunk when no separators found + // + if (_mask == 0) + _position += Vector128.Count; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static uint ExtractMostSignificantBits(Vector128 v) + { + var sum = AdvSimd.Arm64.AddAcross( + AdvSimd.ShiftLogical( + AdvSimd.And(v, Vector128.Create((ushort)0x8000)), + Vector128.Create(-15, -14, -13, -12, -11, -10, -9, -8))); + return sum.ToScalar(); + } + } + else + { + for (; (int)_position < length; _position++) + { + var ch = Unsafe.Add(ref source, _position); + if (ch == '/') + { + _last = (int)_position; + _position++; + + return (start, _last); + } + } + } + } + + return (start, length); + } + } + + #endregion +} diff --git a/src/Ramstack.FileProviders.Globbing/README.md b/src/Ramstack.FileProviders.Globbing/README.md index f0c91f5..a3e4998 100644 --- a/src/Ramstack.FileProviders.Globbing/README.md +++ b/src/Ramstack.FileProviders.Globbing/README.md @@ -1,47 +1,47 @@ -# Ramstack.FileProviders.Globbing -[![NuGet](https://img.shields.io/nuget/v/Ramstack.FileProviders.Globbing.svg)](https://nuget.org/packages/Ramstack.FileProviders.Globbing) -[![MIT](https://img.shields.io/github/license/rameel/ramstack.fileproviders)](https://github.com/rameel/ramstack.fileproviders/blob/main/LICENSE) - -Represents a .NET library implementing an `IFileProvider` that filters files using include and/or exclude glob patterns -for flexible file visibility control. - -## Getting Started - -To install the `Ramstack.FileProviders.Globbing` [NuGet package](https://www.nuget.org/packages/Ramstack.FileProviders.Globbing) -in your project, run the following command: -```console -dotnet add package Ramstack.FileProviders.Globbing -``` - -## GlobbingFileProvider -The `GlobbingFileProvider` class filters files using include and/or exclude glob patterns. Include patterns make only matching files visible, -while exclude patterns hide specific files. Both include and exclude patterns can be combined for flexible file visibility control. - -It relies on the [Ramstack.Globbing](https://www.nuget.org/packages/Ramstack.Globbing) package for its globbing capabilities. - -Example: -```csharp -IFileProvider provider = new GlobbingFileProvider(innerProvider, patterns: ["**/*.txt", "docs/*.md"], excludes: ["**/README.md"]); -foreach (IFileInfo file in provider.GetDirectoryContents("/")) - Console.WriteLine(file.Name); -``` - -## Related Packages -- [Ramstack.FileProviders.Extensions](https://www.nuget.org/packages/Ramstack.FileProviders.Extensions) — Useful and convenient extensions for `IFileProvider`, bringing its capabilities and experience closer to what's provided by the `DirectoryInfo` and `FileInfo` classes. -- [Ramstack.FileProviders](https://www.nuget.org/packages/Ramstack.FileProviders) — Additional file providers, including `PrefixedFileProvider` and `SubFileProvider`. -- [Ramstack.FileProviders.Composition](https://www.nuget.org/packages/Ramstack.FileProviders.Composition) — Provides a helper class for flattening and composing `IFileProvider`. - - -## Supported versions - -| | Version | -|------|----------------| -| .NET | 6, 7, 8, 9, 10 | - -## Contributions - -Bug reports and contributions are welcome. - -## License -This package is released as open source under the **MIT License**. -See the [LICENSE](https://github.com/rameel/ramstack.fileproviders/blob/main/LICENSE) file for more details. +# Ramstack.FileProviders.Globbing +[![NuGet](https://img.shields.io/nuget/v/Ramstack.FileProviders.Globbing.svg)](https://nuget.org/packages/Ramstack.FileProviders.Globbing) +[![MIT](https://img.shields.io/github/license/rameel/ramstack.fileproviders)](https://github.com/rameel/ramstack.fileproviders/blob/main/LICENSE) + +Represents a .NET library implementing an `IFileProvider` that filters files using include and/or exclude glob patterns +for flexible file visibility control. + +## Getting Started + +To install the `Ramstack.FileProviders.Globbing` [NuGet package](https://www.nuget.org/packages/Ramstack.FileProviders.Globbing) +in your project, run the following command: +```console +dotnet add package Ramstack.FileProviders.Globbing +``` + +## GlobbingFileProvider +The `GlobbingFileProvider` class filters files using include and/or exclude glob patterns. Include patterns make only matching files visible, +while exclude patterns hide specific files. Both include and exclude patterns can be combined for flexible file visibility control. + +It relies on the [Ramstack.Globbing](https://www.nuget.org/packages/Ramstack.Globbing) package for its globbing capabilities. + +Example: +```csharp +IFileProvider provider = new GlobbingFileProvider(innerProvider, patterns: ["**/*.txt", "docs/*.md"], excludes: ["**/README.md"]); +foreach (IFileInfo file in provider.GetDirectoryContents("/")) + Console.WriteLine(file.Name); +``` + +## Related Packages +- [Ramstack.FileProviders.Extensions](https://www.nuget.org/packages/Ramstack.FileProviders.Extensions) — Useful and convenient extensions for `IFileProvider`, bringing its capabilities and experience closer to what's provided by the `DirectoryInfo` and `FileInfo` classes. +- [Ramstack.FileProviders](https://www.nuget.org/packages/Ramstack.FileProviders) — Additional file providers, including `PrefixedFileProvider` and `SubFileProvider`. +- [Ramstack.FileProviders.Composition](https://www.nuget.org/packages/Ramstack.FileProviders.Composition) — Provides a helper class for flattening and composing `IFileProvider`. + + +## Supported versions + +| | Version | +|------|----------------| +| .NET | 6, 7, 8, 9, 10 | + +## Contributions + +Bug reports and contributions are welcome. + +## License +This package is released as open source under the **MIT License**. +See the [LICENSE](https://github.com/rameel/ramstack.fileproviders/blob/main/LICENSE) file for more details. diff --git a/src/Ramstack.FileProviders.Globbing/Ramstack.FileProviders.Globbing.csproj b/src/Ramstack.FileProviders.Globbing/Ramstack.FileProviders.Globbing.csproj index 708d948..08e760f 100644 --- a/src/Ramstack.FileProviders.Globbing/Ramstack.FileProviders.Globbing.csproj +++ b/src/Ramstack.FileProviders.Globbing/Ramstack.FileProviders.Globbing.csproj @@ -1,79 +1,79 @@ - - - - net6.0 - A .NET library implementing an IFileProvider that filters files using include and/or exclude glob patterns. - enable - enable - preview - true - true - Ramstack.FileProviders - - - - 649;1591 - CS8600;CS8601;CS8602;CS8603;CS8604;CS8610;CS8613;CS8614;CS8618;CS8619;CS8620;CS8625;CS8643;CS8653;CS8714 - - - - https://github.com/rameel/ramstack.fileproviders - Rameel - fileprovider;files;glob;globbing - https://github.com/rameel/ramstack.fileproviders#readme - README.md - MIT - true - snupkg - true - true - true - true - - - - true - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - Internal\FilePath.cs - - - Internal\PathTokenizer.cs - - - Properties\AssemblyInfo.cs - - - - - - - - - - - - - - Properties\README.md - True - \ - - - - + + + + net6.0 + A .NET library implementing an IFileProvider that filters files using include and/or exclude glob patterns. + enable + enable + preview + true + true + Ramstack.FileProviders + + + + 649;1591 + CS8600;CS8601;CS8602;CS8603;CS8604;CS8610;CS8613;CS8614;CS8618;CS8619;CS8620;CS8625;CS8643;CS8653;CS8714 + + + + https://github.com/rameel/ramstack.fileproviders + Rameel + fileprovider;files;glob;globbing + https://github.com/rameel/ramstack.fileproviders#readme + README.md + MIT + true + snupkg + true + true + true + true + + + + true + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + Internal\FilePath.cs + + + Internal\PathTokenizer.cs + + + Properties\AssemblyInfo.cs + + + + + + + + + + + + + + Properties\README.md + True + \ + + + + diff --git a/src/Ramstack.FileProviders/PrefixedFileProvider.cs b/src/Ramstack.FileProviders/PrefixedFileProvider.cs index ebbb926..84c649c 100644 --- a/src/Ramstack.FileProviders/PrefixedFileProvider.cs +++ b/src/Ramstack.FileProviders/PrefixedFileProvider.cs @@ -1,301 +1,301 @@ -using Ramstack.Globbing; - -namespace Ramstack.FileProviders; - -/// -/// Applies a specified prefix to the paths of files managed by an underlying . -/// -/// -/// This class wraps another file provider, prepending a prefix to the file paths. -/// -[DebuggerDisplay("{_prefix,nq}")] -public sealed class PrefixedFileProvider : IFileProvider, IDisposable -{ - private readonly string _prefix; - private readonly IFileProvider _provider; - private readonly (string Path, string DirectoryName)[] _directories; - - /// - /// Initializes a new instance of the class. - /// - /// The prefix to be applied to the file paths - /// managed by this instance. - /// The underlying file provider that manages the files - /// to which the prefix will be applied. - public PrefixedFileProvider(string prefix, IFileProvider provider) - { - ArgumentNullException.ThrowIfNull(prefix); - ArgumentNullException.ThrowIfNull(provider); - - prefix = FilePath.Normalize(prefix); - (_prefix, _provider, _directories) = (prefix, provider, CreateArtificialDirectories(prefix)); - - static (string Path, string DirectoryName)[] CreateArtificialDirectories(string path) - { - var directories = new List<(string, string)>(); - - while (path != "/") - { - var directoryName = FilePath.GetFileName(path); - path = FilePath.GetDirectoryName(path); - directories.Add((path, directoryName)); - } - - return directories.ToArray(); - } - } - - /// - public IFileInfo GetFileInfo(string subpath) - { - subpath = FilePath.Normalize(subpath); - - var path = ResolvePath(_prefix, subpath); - if (path is not null) - return _provider.GetFileInfo(path); - - return new NotFoundFileInfo(subpath); - } - - /// - public IDirectoryContents GetDirectoryContents(string subpath) - { - subpath = FilePath.Normalize(subpath); - - if (subpath.Length < _prefix.Length) - foreach (ref var entry in _directories.AsSpan()) - if (entry.Path == subpath) - return new ArtificialDirectoryContents(entry.DirectoryName); - - var path = ResolvePath(_prefix, subpath); - if (path is not null) - return _provider.GetDirectoryContents(path); - - return NotFoundDirectoryContents.Singleton; - } - - /// - public IChangeToken Watch(string filter) - { - filter = FilePath.Normalize(filter); - - var path = ResolvePath(_prefix, filter); - if (path is not null) - return _provider.Watch(path); - - var pattern = ResolveGlobFilter(_prefix, filter); - if (pattern is not null) - return _provider.Watch(pattern); - - return NullChangeToken.Singleton; - } - - /// - public void Dispose() => - (_provider as IDisposable)?.Dispose(); - - private static string? ResolvePath(string prefix, string path) - { - Debug.Assert(path == FilePath.Normalize(path)); - - if (prefix == "/") - return path; - - if (path == prefix) - return "/"; - - if ((uint)prefix.Length < (uint)path.Length) - if (path.StartsWith(prefix, StringComparison.Ordinal) && path[prefix.Length] == '/') - return new string(path.AsSpan(prefix.Length)); - - return null; - } - - /// - /// Attempts to resolve a glob filter relative to a virtual path prefix, - /// removing any prefix segments that match corresponding parts of the filter. - /// - /// The virtual path prefix representing the base of the current provider. - /// The incoming glob filter that may include glob patterns. - /// - /// A normalized filter value that can be safely passed to the wrapped file provider - /// or if the filter cannot be applied. - /// - /// - /// The goal is to determine whether a specified glob filter - /// (e.g., "/modules/*/{assets,css,js}/**/*.{css,js}") applies to this provider, which is - /// virtually mounted at a specific prefix path (e.g., "/modules/profile/assets"). - /// - private static string? ResolveGlobFilter(string prefix, string filter) - { - Debug.Assert(prefix == FilePath.Normalize(prefix)); - Debug.Assert(filter == FilePath.Normalize(filter)); - - var prefixSegments = new PathTokenizer(prefix).GetEnumerator(); - var filterSegments = new PathTokenizer(filter).GetEnumerator(); - - while (prefixSegments.MoveNext() && filterSegments.MoveNext()) - { - var fs = filterSegments.Current; - - if (fs is "**") - { - // The globstar '**' matches zero or more path segments. - // Once we encounter '**', we lose the ability to deterministically align - // the remaining filter segments with the remaining prefix segments. - // - // Why this matters: - // We are transforming a filter defined over the 'outer' virtual path - // into a filter for the 'inner' provider (mounted at 'prefix'). - // To do that precisely, we would need to know how many segments '**' consumes. - // - // However, this is fundamentally ambiguous: - // - '**' may consume 0 segments - // - '**' may consume N segments (including prefix tail segments) - // - or it may match entirely within the underlying provider - // - // Example (false negative if we over-reduce): - // prefix: /modules/profile/assets - // filter: /modules/**/assets/*.js - // - // Underlying provider may contain: - // /src/_build/assets/main.js - // - // Which corresponds to: - // /modules/profile/assets/src/_build/assets/main.js - // - // In this case: - // '**/assets/*.js' --> MUST match - // '*.js' --> would NOT match - // - // Counter-example (false negative if we try to keep prefix tail): - // prefix: /modules/profile/assets - // filter: /modules/**/assets/*.js - // - // Suppose 'assets' in the filter refers to the *prefix itself*, - // and the underlying provider contains only flat files: - // /main.js - // - // (i.e. no nested 'assets/' directory inside the provider) - // - // Then: - // '*.js' --> MUST match - // '**/assets/*.js' --> would NOT match - // - // Conclusion: - // After '**', we cannot know whether subsequent segments belong - // to the prefix or to the underlying provider. - // - // Therefore, any attempt to: - // - consume prefix segments (--> '*.js') - // - or preserve intermediate literals (--> '**/assets/*.js') - // will break valid scenarios. - // - // Strategy: - // - Preserve '**' to allow arbitrary depth - // - Drop ambiguous intermediate segments - // - Keep only the final segment after '**', if any (e.g. '*.js') - // - // This guarantees: - // - No false negatives caused by prefix misalignment - // - Possible false positives, which are acceptable for Watch() - while (filterSegments.MoveNext()) - fs = filterSegments.Current; - - return fs is not "**" - ? "**/" + fs.ToString() - : "**"; - } - - if (fs is "*") - { - // '*' matches any prefix segment, continue matching. - continue; - } - - if (Matcher.IsMatch(prefixSegments.Current, fs, MatchFlags.Unix)) - { - // Segment matches the prefix segment, continue matching. - continue; - } - - // Segment doesn't match the prefix at all. - // This means the filter cannot be applied to the underlying provider. - return null; - } - - if (!prefixSegments.MoveNext()) - { - var list = new List(); - - // All prefix segments have been matched and consumed successfully. - // Append all remaining filter segments. - while (filterSegments.MoveNext()) - list.Add(filterSegments.Current.ToString()); - - return string.Join("/", list); - } - - // Not all prefix segments were matched. - // This means the filter cannot be applied to the underlying provider. - return null; - } - - #region Inner type: ArtificialDirectoryContents - - /// - /// Represents an implementation of the . - /// - /// The name of the child directory. - private sealed class ArtificialDirectoryContents(string name) : IDirectoryContents - { - /// - public bool Exists => true; - - /// - public IEnumerator GetEnumerator() - { - var result = new IFileInfo[] { new ArtificialDirectoryInfo(name) }; - return result.AsEnumerable().GetEnumerator(); - } - - /// - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => - GetEnumerator(); - } - - #endregion - - #region Inner type: ArtificialDirectoryInfo - - /// - /// Represents an implementation of the for the artificial directory. - /// - /// The name of the directory. - private sealed class ArtificialDirectoryInfo(string name) : IFileInfo - { - /// - public bool Exists => true; - - /// - public long Length => -1; - - /// - public string? PhysicalPath => null; - - /// - public string Name => name; - - /// - public DateTimeOffset LastModified => default; - - /// - public bool IsDirectory => true; - - /// - public Stream CreateReadStream() => - throw new NotSupportedException("Cannot create a read stream for a directory."); - } - - #endregion -} +using Ramstack.Globbing; + +namespace Ramstack.FileProviders; + +/// +/// Applies a specified prefix to the paths of files managed by an underlying . +/// +/// +/// This class wraps another file provider, prepending a prefix to the file paths. +/// +[DebuggerDisplay("{_prefix,nq}")] +public sealed class PrefixedFileProvider : IFileProvider, IDisposable +{ + private readonly string _prefix; + private readonly IFileProvider _provider; + private readonly (string Path, string DirectoryName)[] _directories; + + /// + /// Initializes a new instance of the class. + /// + /// The prefix to be applied to the file paths + /// managed by this instance. + /// The underlying file provider that manages the files + /// to which the prefix will be applied. + public PrefixedFileProvider(string prefix, IFileProvider provider) + { + ArgumentNullException.ThrowIfNull(prefix); + ArgumentNullException.ThrowIfNull(provider); + + prefix = FilePath.Normalize(prefix); + (_prefix, _provider, _directories) = (prefix, provider, CreateArtificialDirectories(prefix)); + + static (string Path, string DirectoryName)[] CreateArtificialDirectories(string path) + { + var directories = new List<(string, string)>(); + + while (path != "/") + { + var directoryName = FilePath.GetFileName(path); + path = FilePath.GetDirectoryName(path); + directories.Add((path, directoryName)); + } + + return directories.ToArray(); + } + } + + /// + public IFileInfo GetFileInfo(string subpath) + { + subpath = FilePath.Normalize(subpath); + + var path = ResolvePath(_prefix, subpath); + if (path is not null) + return _provider.GetFileInfo(path); + + return new NotFoundFileInfo(subpath); + } + + /// + public IDirectoryContents GetDirectoryContents(string subpath) + { + subpath = FilePath.Normalize(subpath); + + if (subpath.Length < _prefix.Length) + foreach (ref var entry in _directories.AsSpan()) + if (entry.Path == subpath) + return new ArtificialDirectoryContents(entry.DirectoryName); + + var path = ResolvePath(_prefix, subpath); + if (path is not null) + return _provider.GetDirectoryContents(path); + + return NotFoundDirectoryContents.Singleton; + } + + /// + public IChangeToken Watch(string filter) + { + filter = FilePath.Normalize(filter); + + var path = ResolvePath(_prefix, filter); + if (path is not null) + return _provider.Watch(path); + + var pattern = ResolveGlobFilter(_prefix, filter); + if (pattern is not null) + return _provider.Watch(pattern); + + return NullChangeToken.Singleton; + } + + /// + public void Dispose() => + (_provider as IDisposable)?.Dispose(); + + private static string? ResolvePath(string prefix, string path) + { + Debug.Assert(path == FilePath.Normalize(path)); + + if (prefix == "/") + return path; + + if (path == prefix) + return "/"; + + if ((uint)prefix.Length < (uint)path.Length) + if (path.StartsWith(prefix, StringComparison.Ordinal) && path[prefix.Length] == '/') + return new string(path.AsSpan(prefix.Length)); + + return null; + } + + /// + /// Attempts to resolve a glob filter relative to a virtual path prefix, + /// removing any prefix segments that match corresponding parts of the filter. + /// + /// The virtual path prefix representing the base of the current provider. + /// The incoming glob filter that may include glob patterns. + /// + /// A normalized filter value that can be safely passed to the wrapped file provider + /// or if the filter cannot be applied. + /// + /// + /// The goal is to determine whether a specified glob filter + /// (e.g., "/modules/*/{assets,css,js}/**/*.{css,js}") applies to this provider, which is + /// virtually mounted at a specific prefix path (e.g., "/modules/profile/assets"). + /// + private static string? ResolveGlobFilter(string prefix, string filter) + { + Debug.Assert(prefix == FilePath.Normalize(prefix)); + Debug.Assert(filter == FilePath.Normalize(filter)); + + var prefixSegments = new PathTokenizer(prefix).GetEnumerator(); + var filterSegments = new PathTokenizer(filter).GetEnumerator(); + + while (prefixSegments.MoveNext() && filterSegments.MoveNext()) + { + var fs = filterSegments.Current; + + if (fs is "**") + { + // The globstar '**' matches zero or more path segments. + // Once we encounter '**', we lose the ability to deterministically align + // the remaining filter segments with the remaining prefix segments. + // + // Why this matters: + // We are transforming a filter defined over the 'outer' virtual path + // into a filter for the 'inner' provider (mounted at 'prefix'). + // To do that precisely, we would need to know how many segments '**' consumes. + // + // However, this is fundamentally ambiguous: + // - '**' may consume 0 segments + // - '**' may consume N segments (including prefix tail segments) + // - or it may match entirely within the underlying provider + // + // Example (false negative if we over-reduce): + // prefix: /modules/profile/assets + // filter: /modules/**/assets/*.js + // + // Underlying provider may contain: + // /src/_build/assets/main.js + // + // Which corresponds to: + // /modules/profile/assets/src/_build/assets/main.js + // + // In this case: + // '**/assets/*.js' --> MUST match + // '*.js' --> would NOT match + // + // Counter-example (false negative if we try to keep prefix tail): + // prefix: /modules/profile/assets + // filter: /modules/**/assets/*.js + // + // Suppose 'assets' in the filter refers to the *prefix itself*, + // and the underlying provider contains only flat files: + // /main.js + // + // (i.e. no nested 'assets/' directory inside the provider) + // + // Then: + // '*.js' --> MUST match + // '**/assets/*.js' --> would NOT match + // + // Conclusion: + // After '**', we cannot know whether subsequent segments belong + // to the prefix or to the underlying provider. + // + // Therefore, any attempt to: + // - consume prefix segments (--> '*.js') + // - or preserve intermediate literals (--> '**/assets/*.js') + // will break valid scenarios. + // + // Strategy: + // - Preserve '**' to allow arbitrary depth + // - Drop ambiguous intermediate segments + // - Keep only the final segment after '**', if any (e.g. '*.js') + // + // This guarantees: + // - No false negatives caused by prefix misalignment + // - Possible false positives, which are acceptable for Watch() + while (filterSegments.MoveNext()) + fs = filterSegments.Current; + + return fs is not "**" + ? "**/" + fs.ToString() + : "**"; + } + + if (fs is "*") + { + // '*' matches any prefix segment, continue matching. + continue; + } + + if (Matcher.IsMatch(prefixSegments.Current, fs, MatchFlags.Unix)) + { + // Segment matches the prefix segment, continue matching. + continue; + } + + // Segment doesn't match the prefix at all. + // This means the filter cannot be applied to the underlying provider. + return null; + } + + if (!prefixSegments.MoveNext()) + { + var list = new List(); + + // All prefix segments have been matched and consumed successfully. + // Append all remaining filter segments. + while (filterSegments.MoveNext()) + list.Add(filterSegments.Current.ToString()); + + return string.Join("/", list); + } + + // Not all prefix segments were matched. + // This means the filter cannot be applied to the underlying provider. + return null; + } + + #region Inner type: ArtificialDirectoryContents + + /// + /// Represents an implementation of the . + /// + /// The name of the child directory. + private sealed class ArtificialDirectoryContents(string name) : IDirectoryContents + { + /// + public bool Exists => true; + + /// + public IEnumerator GetEnumerator() + { + var result = new IFileInfo[] { new ArtificialDirectoryInfo(name) }; + return result.AsEnumerable().GetEnumerator(); + } + + /// + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => + GetEnumerator(); + } + + #endregion + + #region Inner type: ArtificialDirectoryInfo + + /// + /// Represents an implementation of the for the artificial directory. + /// + /// The name of the directory. + private sealed class ArtificialDirectoryInfo(string name) : IFileInfo + { + /// + public bool Exists => true; + + /// + public long Length => -1; + + /// + public string? PhysicalPath => null; + + /// + public string Name => name; + + /// + public DateTimeOffset LastModified => default; + + /// + public bool IsDirectory => true; + + /// + public Stream CreateReadStream() => + throw new NotSupportedException("Cannot create a read stream for a directory."); + } + + #endregion +} diff --git a/src/Ramstack.FileProviders/Properties/AssemblyInfo.cs b/src/Ramstack.FileProviders/Properties/AssemblyInfo.cs index b5274aa..c65b5dc 100644 --- a/src/Ramstack.FileProviders/Properties/AssemblyInfo.cs +++ b/src/Ramstack.FileProviders/Properties/AssemblyInfo.cs @@ -1 +1 @@ -[module: System.Runtime.CompilerServices.SkipLocalsInit] +[module: System.Runtime.CompilerServices.SkipLocalsInit] diff --git a/src/Ramstack.FileProviders/README.md b/src/Ramstack.FileProviders/README.md index f80248d..431d108 100644 --- a/src/Ramstack.FileProviders/README.md +++ b/src/Ramstack.FileProviders/README.md @@ -1,102 +1,102 @@ -# Ramstack.FileProviders -[![NuGet](https://img.shields.io/nuget/v/Ramstack.FileProviders.svg)](https://nuget.org/packages/Ramstack.FileProviders) -[![MIT](https://img.shields.io/github/license/rameel/ramstack.fileproviders)](https://github.com/rameel/ramstack.fileproviders/blob/main/LICENSE) - -Represents a .NET library that provides additional implementations of `IFileProvider` including: -- `PrefixedFileProvider` -- `SubFileProvider` - -## Getting Started - -To install the `Ramstack.FileProviders` [NuGet package](https://www.nuget.org/packages/Ramstack.FileProviders) -in your project, run the following command: - -```console -dotnet add package Ramstack.FileProviders -``` - -## PrefixedFileProvider - -`PrefixedFileProvider` allows you to apply a prefix to the paths of files and directories. -This is useful when you need to organize files in a virtual hierarchy. - -Example: -```csharp -IFileProvider provider = new PrefixedFileProvider("/project/app", innerProvider); -IFileInfo file = provider.GetFileInfo("/project/app/docs/README"); -Console.WriteLine(file.Exists); -``` - -This is how you can add virtual directories to your project that are outside the project root: -```csharp -string packagesPath = Path.Combine(environment.ContentRootPath, "../Packages"); -string themesPath = Path.Combine(environment.ContentRootPath, "../Themes"); - -environment.ContentRootFileProvider = new CompositeFileProvider( - new PrefixedFileProvider("/Packages", new PhysicalFileProvider(packagesPath)), - new PrefixedFileProvider("/Themes", new PhysicalFileProvider(themesPath)), - environment.ContentRootFileProvider); -``` -The `Packages` and `Themes` directories are now available to the ASP.NET infrastructure under their respective names, -as if they were originally defined within your project. - -**Before:** -``` -/App -├── Controllers -├── Models -├── Views -└── wwwroot - -/Packages -├── package-1 -└── package-2 - -/Themes -├── theme-1 -└── theme-2 -``` - -**After:** -``` -/App -├── Controllers -├── Models -├── Views -├── Packages <-- (virtual) -│ ├── package-1 -│ └── package-2 -├── Themes <-- (virtual) -│ ├── theme-1 -│ └── theme-2 -└── wwwroot -``` - -## SubFileProvider -`SubFileProvider` lets you limit the view of the file system to a specific subdirectory, effectively creating a sandbox. - -Example: -```csharp -IFileProvider provider = new SubFileProvider("/docs", innerProvider); -IFileInfo file = provider.GetFileInfo("/README"); -Console.WriteLine(file.Exists); -``` - -## Related Packages -- [Ramstack.FileProviders.Extensions](https://www.nuget.org/packages/Ramstack.FileProviders.Extensions) — Useful and convenient extensions for `IFileProvider`, bringing its capabilities and experience closer to what's provided by the `DirectoryInfo` and `FileInfo` classes. -- [Ramstack.FileProviders.Globbing](https://www.nuget.org/packages/Ramstack.FileProviders.Globbing) — A file provider that filters files using include and/or exclude glob patterns. Include patterns make only matching files visible, while exclude patterns hide specific files. Both include and exclude patterns can be combined for flexible file visibility control. -- [Ramstack.FileProviders.Composition](https://www.nuget.org/packages/Ramstack.FileProviders.Composition) — Provides a helper class for flattening and composing `IFileProvider`. - -## Supported versions - -| | Version | -|------|----------------| -| .NET | 6, 7, 8, 9, 10 | - -## Contributions - -Bug reports and contributions are welcome. - -## License -This package is released as open source under the **MIT License**. -See the [LICENSE](https://github.com/rameel/ramstack.fileproviders/blob/main/LICENSE) file for more details. +# Ramstack.FileProviders +[![NuGet](https://img.shields.io/nuget/v/Ramstack.FileProviders.svg)](https://nuget.org/packages/Ramstack.FileProviders) +[![MIT](https://img.shields.io/github/license/rameel/ramstack.fileproviders)](https://github.com/rameel/ramstack.fileproviders/blob/main/LICENSE) + +Represents a .NET library that provides additional implementations of `IFileProvider` including: +- `PrefixedFileProvider` +- `SubFileProvider` + +## Getting Started + +To install the `Ramstack.FileProviders` [NuGet package](https://www.nuget.org/packages/Ramstack.FileProviders) +in your project, run the following command: + +```console +dotnet add package Ramstack.FileProviders +``` + +## PrefixedFileProvider + +`PrefixedFileProvider` allows you to apply a prefix to the paths of files and directories. +This is useful when you need to organize files in a virtual hierarchy. + +Example: +```csharp +IFileProvider provider = new PrefixedFileProvider("/project/app", innerProvider); +IFileInfo file = provider.GetFileInfo("/project/app/docs/README"); +Console.WriteLine(file.Exists); +``` + +This is how you can add virtual directories to your project that are outside the project root: +```csharp +string packagesPath = Path.Combine(environment.ContentRootPath, "../Packages"); +string themesPath = Path.Combine(environment.ContentRootPath, "../Themes"); + +environment.ContentRootFileProvider = new CompositeFileProvider( + new PrefixedFileProvider("/Packages", new PhysicalFileProvider(packagesPath)), + new PrefixedFileProvider("/Themes", new PhysicalFileProvider(themesPath)), + environment.ContentRootFileProvider); +``` +The `Packages` and `Themes` directories are now available to the ASP.NET infrastructure under their respective names, +as if they were originally defined within your project. + +**Before:** +``` +/App +├── Controllers +├── Models +├── Views +└── wwwroot + +/Packages +├── package-1 +└── package-2 + +/Themes +├── theme-1 +└── theme-2 +``` + +**After:** +``` +/App +├── Controllers +├── Models +├── Views +├── Packages <-- (virtual) +│ ├── package-1 +│ └── package-2 +├── Themes <-- (virtual) +│ ├── theme-1 +│ └── theme-2 +└── wwwroot +``` + +## SubFileProvider +`SubFileProvider` lets you limit the view of the file system to a specific subdirectory, effectively creating a sandbox. + +Example: +```csharp +IFileProvider provider = new SubFileProvider("/docs", innerProvider); +IFileInfo file = provider.GetFileInfo("/README"); +Console.WriteLine(file.Exists); +``` + +## Related Packages +- [Ramstack.FileProviders.Extensions](https://www.nuget.org/packages/Ramstack.FileProviders.Extensions) — Useful and convenient extensions for `IFileProvider`, bringing its capabilities and experience closer to what's provided by the `DirectoryInfo` and `FileInfo` classes. +- [Ramstack.FileProviders.Globbing](https://www.nuget.org/packages/Ramstack.FileProviders.Globbing) — A file provider that filters files using include and/or exclude glob patterns. Include patterns make only matching files visible, while exclude patterns hide specific files. Both include and exclude patterns can be combined for flexible file visibility control. +- [Ramstack.FileProviders.Composition](https://www.nuget.org/packages/Ramstack.FileProviders.Composition) — Provides a helper class for flattening and composing `IFileProvider`. + +## Supported versions + +| | Version | +|------|----------------| +| .NET | 6, 7, 8, 9, 10 | + +## Contributions + +Bug reports and contributions are welcome. + +## License +This package is released as open source under the **MIT License**. +See the [LICENSE](https://github.com/rameel/ramstack.fileproviders/blob/main/LICENSE) file for more details. diff --git a/src/Ramstack.FileProviders/Ramstack.FileProviders.csproj b/src/Ramstack.FileProviders/Ramstack.FileProviders.csproj index 6baa5b0..860f8a4 100644 --- a/src/Ramstack.FileProviders/Ramstack.FileProviders.csproj +++ b/src/Ramstack.FileProviders/Ramstack.FileProviders.csproj @@ -1,75 +1,75 @@ - - - - net6.0 - A .NET library providing additional implementations of IFileProvider. - enable - enable - preview - true - true - - - - 649;1591 - CS8600;CS8601;CS8602;CS8603;CS8604;CS8610;CS8613;CS8614;CS8618;CS8619;CS8620;CS8625;CS8643;CS8653;CS8714 - - - - https://github.com/rameel/ramstack.fileproviders - Rameel - fileprovider;files - https://github.com/rameel/ramstack.fileproviders#readme - README.md - MIT - true - snupkg - true - true - true - true - - - - true - - - - - - - - - - - - - Internal\FilePath.cs - - - Internal\PathTokenizer.cs - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - Properties\README.md - True - \ - - - - + + + + net6.0 + A .NET library providing additional implementations of IFileProvider. + enable + enable + preview + true + true + + + + 649;1591 + CS8600;CS8601;CS8602;CS8603;CS8604;CS8610;CS8613;CS8614;CS8618;CS8619;CS8620;CS8625;CS8643;CS8653;CS8714 + + + + https://github.com/rameel/ramstack.fileproviders + Rameel + fileprovider;files + https://github.com/rameel/ramstack.fileproviders#readme + README.md + MIT + true + snupkg + true + true + true + true + + + + true + + + + + + + + + + + + + Internal\FilePath.cs + + + Internal\PathTokenizer.cs + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + Properties\README.md + True + \ + + + + diff --git a/src/Ramstack.FileProviders/SubFileProvider.cs b/src/Ramstack.FileProviders/SubFileProvider.cs index cd9bf95..3d59f8c 100644 --- a/src/Ramstack.FileProviders/SubFileProvider.cs +++ b/src/Ramstack.FileProviders/SubFileProvider.cs @@ -1,57 +1,57 @@ -namespace Ramstack.FileProviders; - -/// -/// Represents a file provider that manages a subset of files -/// under a specified path in an underlying . -/// -/// -/// This class provides functionality to handle files and directories that are located under -/// a specific path within the root directory of the underlying file provider. -/// -[DebuggerDisplay("{_path,nq}")] -public sealed class SubFileProvider : IFileProvider, IDisposable -{ - private readonly IFileProvider _provider; - private readonly string _path; - - /// - /// Initializes a new instance of the class. - /// - /// The path under the root directory of the . - /// The underlying file provider. - public SubFileProvider(string path, IFileProvider provider) - { - ArgumentNullException.ThrowIfNull(provider); - (_path, _provider) = (FilePath.Normalize(path), provider); - } - - /// - public IFileInfo GetFileInfo(string subpath) => - _provider.GetFileInfo(ResolvePath(subpath)); - - /// - public IDirectoryContents GetDirectoryContents(string subpath) => - _provider.GetDirectoryContents(ResolvePath(subpath)); - - /// - public IChangeToken Watch(string filter) - { - if (!string.IsNullOrEmpty(filter)) - return _provider.Watch(ResolvePath(filter)); - - return NullChangeToken.Singleton; - } - - /// - public void Dispose() => - (_provider as IDisposable)?.Dispose(); - - private string ResolvePath(string subpath) - { - subpath = FilePath.Normalize(subpath); - if (subpath == "/") - return _path; - - return FilePath.Join(_path, subpath); - } -} +namespace Ramstack.FileProviders; + +/// +/// Represents a file provider that manages a subset of files +/// under a specified path in an underlying . +/// +/// +/// This class provides functionality to handle files and directories that are located under +/// a specific path within the root directory of the underlying file provider. +/// +[DebuggerDisplay("{_path,nq}")] +public sealed class SubFileProvider : IFileProvider, IDisposable +{ + private readonly IFileProvider _provider; + private readonly string _path; + + /// + /// Initializes a new instance of the class. + /// + /// The path under the root directory of the . + /// The underlying file provider. + public SubFileProvider(string path, IFileProvider provider) + { + ArgumentNullException.ThrowIfNull(provider); + (_path, _provider) = (FilePath.Normalize(path), provider); + } + + /// + public IFileInfo GetFileInfo(string subpath) => + _provider.GetFileInfo(ResolvePath(subpath)); + + /// + public IDirectoryContents GetDirectoryContents(string subpath) => + _provider.GetDirectoryContents(ResolvePath(subpath)); + + /// + public IChangeToken Watch(string filter) + { + if (!string.IsNullOrEmpty(filter)) + return _provider.Watch(ResolvePath(filter)); + + return NullChangeToken.Singleton; + } + + /// + public void Dispose() => + (_provider as IDisposable)?.Dispose(); + + private string ResolvePath(string subpath) + { + subpath = FilePath.Normalize(subpath); + if (subpath == "/") + return _path; + + return FilePath.Join(_path, subpath); + } +} diff --git a/tests/Ramstack.FileProviders.Composition.Tests/ChangeTokenComposerTests.cs b/tests/Ramstack.FileProviders.Composition.Tests/ChangeTokenComposerTests.cs index 8ed6583..246f85c 100644 --- a/tests/Ramstack.FileProviders.Composition.Tests/ChangeTokenComposerTests.cs +++ b/tests/Ramstack.FileProviders.Composition.Tests/ChangeTokenComposerTests.cs @@ -1,246 +1,246 @@ -namespace Ramstack.FileProviders.Composition; - -[TestFixture] -public sealed class ChangeTokenComposerTests -{ - [Test] - public void Flatten_ReturnsAsIs_WhenNoComposite() - { - var changeToken = new TestChangeToken(); - var result = ChangeTokenComposer.FlattenChangeToken(changeToken); - - Assert.That(result, Is.SameAs(changeToken)); - } - - [Test] - public void Flatten_ReturnsCompositeProvider_WhenNeedComposite() - { - var changeToken = CreateCompositeChangeToken(new TestChangeToken(), new TestChangeToken()); - - var result = ChangeTokenComposer.FlattenChangeToken(changeToken); - Assert.That(result, Is.InstanceOf()); - } - - [Test] - public void Flatten_ReturnsAsIs_WhenAlreadyFlat() - { - var changeToken = CreateCompositeChangeToken(new TestChangeToken(), new TestChangeToken()); - - var result = ChangeTokenComposer.FlattenChangeToken(changeToken); - Assert.That(result, Is.SameAs(changeToken)); - } - - [Test] - public void Flatten_ReturnsCompositeChangeToken_Flattened() - { - var changeToken = CreateCompositeChangeToken( - new TestChangeToken(), - CreateCompositeChangeToken( - new TestChangeToken(), - new TestChangeToken(), - CreateCompositeChangeToken( - new TestChangeToken()))); - - var result = ChangeTokenComposer.FlattenChangeToken(changeToken); - - Assert.That(result, Is.InstanceOf()); - Assert.That(((CompositeChangeToken)result).ChangeTokens.Count, Is.EqualTo(4)); - Assert.That(((CompositeChangeToken)result).ChangeTokens, Is.All.InstanceOf()); - } - - [Test] - public void Flatten_RemovesNullChangeToken() - { - var changeToken = CreateCompositeChangeToken( - new TestChangeToken(), - CreateCompositeChangeToken( - new TestChangeToken(), - NullChangeToken.Singleton, - new TestChangeToken()), - NullChangeToken.Singleton); - - var result = ChangeTokenComposer.FlattenChangeToken(changeToken); - - Assert.That(result, Is.InstanceOf()); - Assert.That(((CompositeChangeToken)result).ChangeTokens.Count, Is.EqualTo(3)); - Assert.That(((CompositeChangeToken)result).ChangeTokens, Is.All.InstanceOf()); - } - - [Test] - public void Flatten_ReturnsNullChangeToken_WhenNothingReturn() - { - var changeToken = CreateCompositeChangeToken( - CreateCompositeChangeToken( - NullChangeToken.Singleton, - NullChangeToken.Singleton, - CreateCompositeChangeToken( - CreateCompositeChangeToken( - NullChangeToken.Singleton, - NullChangeToken.Singleton), - NullChangeToken.Singleton), - CreateCompositeChangeToken( - NullChangeToken.Singleton, - NullChangeToken.Singleton, - CreateCompositeChangeToken( - CreateCompositeChangeToken( - NullChangeToken.Singleton, - NullChangeToken.Singleton), - NullChangeToken.Singleton))), - NullChangeToken.Singleton, - NullChangeToken.Singleton, - CreateCompositeChangeToken( - NullChangeToken.Singleton, - NullChangeToken.Singleton, - CreateCompositeChangeToken( - CreateCompositeChangeToken( - NullChangeToken.Singleton, - NullChangeToken.Singleton), - NullChangeToken.Singleton)), - NullChangeToken.Singleton, - CreateCompositeChangeToken( - CreateCompositeChangeToken( - NullChangeToken.Singleton, - NullChangeToken.Singleton), - NullChangeToken.Singleton)); - - var result = ChangeTokenComposer.FlattenChangeToken(changeToken); - Assert.That(result, Is.InstanceOf()); - Assert.That(result, Is.SameAs(NullChangeToken.Singleton)); - } - - [Test] - public void Flatten_ReturnsSingleToken_WhenRemainOneToken() - { - var changeToken = CreateCompositeChangeToken( - CreateCompositeChangeToken( - NullChangeToken.Singleton, - NullChangeToken.Singleton, - CreateCompositeChangeToken( - CreateCompositeChangeToken( - NullChangeToken.Singleton, - NullChangeToken.Singleton), - NullChangeToken.Singleton), - CreateCompositeChangeToken( - NullChangeToken.Singleton, - NullChangeToken.Singleton, - CreateCompositeChangeToken( - CreateCompositeChangeToken( - NullChangeToken.Singleton, - new TestChangeToken()), - NullChangeToken.Singleton))), - NullChangeToken.Singleton, - NullChangeToken.Singleton, - CreateCompositeChangeToken( - NullChangeToken.Singleton, - NullChangeToken.Singleton, - CreateCompositeChangeToken( - CreateCompositeChangeToken( - NullChangeToken.Singleton, - NullChangeToken.Singleton), - NullChangeToken.Singleton)), - NullChangeToken.Singleton, - CreateCompositeChangeToken( - CreateCompositeChangeToken( - NullChangeToken.Singleton, - NullChangeToken.Singleton), - NullChangeToken.Singleton)); - - var result = ChangeTokenComposer.FlattenChangeToken(changeToken); - Assert.That(result, Is.InstanceOf()); - } - - [Test] - public void Flatten_MaintainOrder_WhenComposite() - { - var t1 = new TestChangeToken(); - var t2 = new TestChangeToken(); - var t3 = new TestChangeToken(); - var t4 = new TestChangeToken(); - var t5 = new TestChangeToken(); - var t6 = new TestChangeToken(); - var t7 = new TestChangeToken(); - var t8 = new TestChangeToken(); - var t9 = new TestChangeToken(); - - var changeToken = ChangeTokenComposer.ComposeChangeTokens( - CreateCompositeChangeToken( - NullChangeToken.Singleton, - NullChangeToken.Singleton, - CreateCompositeChangeToken( - t1, - CreateCompositeChangeToken( - t2, - NullChangeToken.Singleton, - t3), - t4, - NullChangeToken.Singleton), - t5, - CreateCompositeChangeToken( - NullChangeToken.Singleton, - NullChangeToken.Singleton, - CreateCompositeChangeToken( - CreateCompositeChangeToken( - NullChangeToken.Singleton, - t6), - NullChangeToken.Singleton)), - t7), - NullChangeToken.Singleton, - NullChangeToken.Singleton, - CreateCompositeChangeToken( - NullChangeToken.Singleton, - NullChangeToken.Singleton, - CreateCompositeChangeToken( - CreateCompositeChangeToken( - NullChangeToken.Singleton, - NullChangeToken.Singleton), - NullChangeToken.Singleton, - t8)), - NullChangeToken.Singleton, - CreateCompositeChangeToken( - CreateCompositeChangeToken( - NullChangeToken.Singleton, - NullChangeToken.Singleton), - NullChangeToken.Singleton, - t9)); - - var composite = (CompositeChangeToken)changeToken; - var changeTokens = new IChangeToken[] {t1, t2, t3, t4, t5, t6, t7, t8, t9}; - - Assert.That(changeToken, Is.InstanceOf()); - Assert.That(composite.ChangeTokens, Is.EquivalentTo(changeTokens)); - } - - [Test] - public void Flatten_ReturnsSingleToken_WhenCompositeContainsOnlyOne() - { - var changeToken = - CreateCompositeChangeToken( - CreateCompositeChangeToken( - CreateCompositeChangeToken( - CreateCompositeChangeToken( - new TestChangeToken())))); - - Assert.That(changeToken.Flatten(), Is.InstanceOf()); - } - - [Test] - public void Flatten_EmptyComposite_ReturnsNullChangeToken() - { - var changeToken = CreateCompositeChangeToken().Flatten(); - Assert.That(changeToken, Is.InstanceOf()); - Assert.That(changeToken, Is.SameAs(NullChangeToken.Singleton)); - } - - private static CompositeChangeToken CreateCompositeChangeToken(params IChangeToken[] changeTokens) => - new(changeTokens); - - private sealed class TestChangeToken : IChangeToken - { - public bool HasChanged => false; - - public bool ActiveChangeCallbacks => false; - - public IDisposable RegisterChangeCallback(Action callback, object state) => - NullChangeToken.Singleton.RegisterChangeCallback(callback, state); - } -} +namespace Ramstack.FileProviders.Composition; + +[TestFixture] +public sealed class ChangeTokenComposerTests +{ + [Test] + public void Flatten_ReturnsAsIs_WhenNoComposite() + { + var changeToken = new TestChangeToken(); + var result = ChangeTokenComposer.FlattenChangeToken(changeToken); + + Assert.That(result, Is.SameAs(changeToken)); + } + + [Test] + public void Flatten_ReturnsCompositeProvider_WhenNeedComposite() + { + var changeToken = CreateCompositeChangeToken(new TestChangeToken(), new TestChangeToken()); + + var result = ChangeTokenComposer.FlattenChangeToken(changeToken); + Assert.That(result, Is.InstanceOf()); + } + + [Test] + public void Flatten_ReturnsAsIs_WhenAlreadyFlat() + { + var changeToken = CreateCompositeChangeToken(new TestChangeToken(), new TestChangeToken()); + + var result = ChangeTokenComposer.FlattenChangeToken(changeToken); + Assert.That(result, Is.SameAs(changeToken)); + } + + [Test] + public void Flatten_ReturnsCompositeChangeToken_Flattened() + { + var changeToken = CreateCompositeChangeToken( + new TestChangeToken(), + CreateCompositeChangeToken( + new TestChangeToken(), + new TestChangeToken(), + CreateCompositeChangeToken( + new TestChangeToken()))); + + var result = ChangeTokenComposer.FlattenChangeToken(changeToken); + + Assert.That(result, Is.InstanceOf()); + Assert.That(((CompositeChangeToken)result).ChangeTokens.Count, Is.EqualTo(4)); + Assert.That(((CompositeChangeToken)result).ChangeTokens, Is.All.InstanceOf()); + } + + [Test] + public void Flatten_RemovesNullChangeToken() + { + var changeToken = CreateCompositeChangeToken( + new TestChangeToken(), + CreateCompositeChangeToken( + new TestChangeToken(), + NullChangeToken.Singleton, + new TestChangeToken()), + NullChangeToken.Singleton); + + var result = ChangeTokenComposer.FlattenChangeToken(changeToken); + + Assert.That(result, Is.InstanceOf()); + Assert.That(((CompositeChangeToken)result).ChangeTokens.Count, Is.EqualTo(3)); + Assert.That(((CompositeChangeToken)result).ChangeTokens, Is.All.InstanceOf()); + } + + [Test] + public void Flatten_ReturnsNullChangeToken_WhenNothingReturn() + { + var changeToken = CreateCompositeChangeToken( + CreateCompositeChangeToken( + NullChangeToken.Singleton, + NullChangeToken.Singleton, + CreateCompositeChangeToken( + CreateCompositeChangeToken( + NullChangeToken.Singleton, + NullChangeToken.Singleton), + NullChangeToken.Singleton), + CreateCompositeChangeToken( + NullChangeToken.Singleton, + NullChangeToken.Singleton, + CreateCompositeChangeToken( + CreateCompositeChangeToken( + NullChangeToken.Singleton, + NullChangeToken.Singleton), + NullChangeToken.Singleton))), + NullChangeToken.Singleton, + NullChangeToken.Singleton, + CreateCompositeChangeToken( + NullChangeToken.Singleton, + NullChangeToken.Singleton, + CreateCompositeChangeToken( + CreateCompositeChangeToken( + NullChangeToken.Singleton, + NullChangeToken.Singleton), + NullChangeToken.Singleton)), + NullChangeToken.Singleton, + CreateCompositeChangeToken( + CreateCompositeChangeToken( + NullChangeToken.Singleton, + NullChangeToken.Singleton), + NullChangeToken.Singleton)); + + var result = ChangeTokenComposer.FlattenChangeToken(changeToken); + Assert.That(result, Is.InstanceOf()); + Assert.That(result, Is.SameAs(NullChangeToken.Singleton)); + } + + [Test] + public void Flatten_ReturnsSingleToken_WhenRemainOneToken() + { + var changeToken = CreateCompositeChangeToken( + CreateCompositeChangeToken( + NullChangeToken.Singleton, + NullChangeToken.Singleton, + CreateCompositeChangeToken( + CreateCompositeChangeToken( + NullChangeToken.Singleton, + NullChangeToken.Singleton), + NullChangeToken.Singleton), + CreateCompositeChangeToken( + NullChangeToken.Singleton, + NullChangeToken.Singleton, + CreateCompositeChangeToken( + CreateCompositeChangeToken( + NullChangeToken.Singleton, + new TestChangeToken()), + NullChangeToken.Singleton))), + NullChangeToken.Singleton, + NullChangeToken.Singleton, + CreateCompositeChangeToken( + NullChangeToken.Singleton, + NullChangeToken.Singleton, + CreateCompositeChangeToken( + CreateCompositeChangeToken( + NullChangeToken.Singleton, + NullChangeToken.Singleton), + NullChangeToken.Singleton)), + NullChangeToken.Singleton, + CreateCompositeChangeToken( + CreateCompositeChangeToken( + NullChangeToken.Singleton, + NullChangeToken.Singleton), + NullChangeToken.Singleton)); + + var result = ChangeTokenComposer.FlattenChangeToken(changeToken); + Assert.That(result, Is.InstanceOf()); + } + + [Test] + public void Flatten_MaintainOrder_WhenComposite() + { + var t1 = new TestChangeToken(); + var t2 = new TestChangeToken(); + var t3 = new TestChangeToken(); + var t4 = new TestChangeToken(); + var t5 = new TestChangeToken(); + var t6 = new TestChangeToken(); + var t7 = new TestChangeToken(); + var t8 = new TestChangeToken(); + var t9 = new TestChangeToken(); + + var changeToken = ChangeTokenComposer.ComposeChangeTokens( + CreateCompositeChangeToken( + NullChangeToken.Singleton, + NullChangeToken.Singleton, + CreateCompositeChangeToken( + t1, + CreateCompositeChangeToken( + t2, + NullChangeToken.Singleton, + t3), + t4, + NullChangeToken.Singleton), + t5, + CreateCompositeChangeToken( + NullChangeToken.Singleton, + NullChangeToken.Singleton, + CreateCompositeChangeToken( + CreateCompositeChangeToken( + NullChangeToken.Singleton, + t6), + NullChangeToken.Singleton)), + t7), + NullChangeToken.Singleton, + NullChangeToken.Singleton, + CreateCompositeChangeToken( + NullChangeToken.Singleton, + NullChangeToken.Singleton, + CreateCompositeChangeToken( + CreateCompositeChangeToken( + NullChangeToken.Singleton, + NullChangeToken.Singleton), + NullChangeToken.Singleton, + t8)), + NullChangeToken.Singleton, + CreateCompositeChangeToken( + CreateCompositeChangeToken( + NullChangeToken.Singleton, + NullChangeToken.Singleton), + NullChangeToken.Singleton, + t9)); + + var composite = (CompositeChangeToken)changeToken; + var changeTokens = new IChangeToken[] {t1, t2, t3, t4, t5, t6, t7, t8, t9}; + + Assert.That(changeToken, Is.InstanceOf()); + Assert.That(composite.ChangeTokens, Is.EquivalentTo(changeTokens)); + } + + [Test] + public void Flatten_ReturnsSingleToken_WhenCompositeContainsOnlyOne() + { + var changeToken = + CreateCompositeChangeToken( + CreateCompositeChangeToken( + CreateCompositeChangeToken( + CreateCompositeChangeToken( + new TestChangeToken())))); + + Assert.That(changeToken.Flatten(), Is.InstanceOf()); + } + + [Test] + public void Flatten_EmptyComposite_ReturnsNullChangeToken() + { + var changeToken = CreateCompositeChangeToken().Flatten(); + Assert.That(changeToken, Is.InstanceOf()); + Assert.That(changeToken, Is.SameAs(NullChangeToken.Singleton)); + } + + private static CompositeChangeToken CreateCompositeChangeToken(params IChangeToken[] changeTokens) => + new(changeTokens); + + private sealed class TestChangeToken : IChangeToken + { + public bool HasChanged => false; + + public bool ActiveChangeCallbacks => false; + + public IDisposable RegisterChangeCallback(Action callback, object state) => + NullChangeToken.Singleton.RegisterChangeCallback(callback, state); + } +} diff --git a/tests/Ramstack.FileProviders.Composition.Tests/FileProviderComposerTests.cs b/tests/Ramstack.FileProviders.Composition.Tests/FileProviderComposerTests.cs index 758f947..12b0a60 100644 --- a/tests/Ramstack.FileProviders.Composition.Tests/FileProviderComposerTests.cs +++ b/tests/Ramstack.FileProviders.Composition.Tests/FileProviderComposerTests.cs @@ -1,241 +1,241 @@ -namespace Ramstack.FileProviders.Composition; - -[TestFixture] -public sealed class FileProviderComposerTests -{ - [Test] - public void Flatten_ReturnsAsIs_WhenNoComposite() - { - var provider = new TestFileProvider(); - var result = FileProviderComposer.FlattenProvider(provider); - Assert.That(result, Is.SameAs(provider)); - } - - [Test] - public void Flatten_ReturnsCompositeProvider_WhenNeedComposite() - { - var provider = new CompositeFileProvider(new TestFileProvider(), new TestFileProvider()); - - var result = FileProviderComposer.FlattenProvider(provider); - Assert.That(result, Is.InstanceOf()); - } - - [Test] - public void Flatten_ReturnsAsIs_WhenAlreadyFlat() - { - var provider = new CompositeFileProvider(new TestFileProvider(), new TestFileProvider()); - - var result = FileProviderComposer.FlattenProvider(provider); - Assert.That(result, Is.SameAs(provider)); - } - - [Test] - public void Flatten_ReturnsCompositeProvider_Flattened() - { - var provider = new CompositeFileProvider( - new TestFileProvider(), - new CompositeFileProvider( - new TestFileProvider(), - new TestFileProvider(), - new CompositeFileProvider( - new TestFileProvider()))); - - var result = FileProviderComposer.FlattenProvider(provider); - - Assert.That(result, Is.InstanceOf()); - Assert.That(((CompositeFileProvider)result).FileProviders.Count(), Is.EqualTo(4)); - Assert.That(((CompositeFileProvider)result).FileProviders, Is.All.InstanceOf()); - } - - [Test] - public void Flatten_RemovesNullFileProvider() - { - var provider = new CompositeFileProvider( - new TestFileProvider(), - new CompositeFileProvider( - new TestFileProvider(), - new NullFileProvider(), - new TestFileProvider()), - new NullFileProvider()); - - var result = FileProviderComposer.FlattenProvider(provider); - - Assert.That(result, Is.InstanceOf()); - Assert.That(((CompositeFileProvider)result).FileProviders.Count(), Is.EqualTo(3)); - Assert.That(((CompositeFileProvider)result).FileProviders, Is.All.InstanceOf()); - } - - [Test] - public void Flatten_ReturnsNullFileProvider_WhenNothingReturn() - { - var provider = new CompositeFileProvider( - new CompositeFileProvider( - new NullFileProvider(), - new NullFileProvider(), - new CompositeFileProvider( - new CompositeFileProvider( - new NullFileProvider(), - new NullFileProvider()), - new NullFileProvider()), - new CompositeFileProvider( - new NullFileProvider(), - new NullFileProvider(), - new CompositeFileProvider( - new CompositeFileProvider( - new NullFileProvider(), - new NullFileProvider()), - new NullFileProvider()))), - new NullFileProvider(), - new NullFileProvider(), - new CompositeFileProvider( - new NullFileProvider(), - new NullFileProvider(), - new CompositeFileProvider( - new CompositeFileProvider( - new NullFileProvider(), - new NullFileProvider()), - new NullFileProvider())), - new NullFileProvider(), - new CompositeFileProvider( - new CompositeFileProvider( - new NullFileProvider(), - new NullFileProvider()), - new NullFileProvider())); - - var result = FileProviderComposer.FlattenProvider(provider); - Assert.That(result, Is.InstanceOf()); - } - - [Test] - public void Flatten_ReturnsSingleProvider_WhenRemainOneProvider() - { - var provider = new CompositeFileProvider( - new CompositeFileProvider( - new NullFileProvider(), - new NullFileProvider(), - new CompositeFileProvider( - new CompositeFileProvider( - new NullFileProvider(), - new NullFileProvider()), - new NullFileProvider()), - new CompositeFileProvider( - new NullFileProvider(), - new NullFileProvider(), - new CompositeFileProvider( - new CompositeFileProvider( - new NullFileProvider(), - new TestFileProvider()), - new NullFileProvider()))), - new NullFileProvider(), - new NullFileProvider(), - new CompositeFileProvider( - new NullFileProvider(), - new NullFileProvider(), - new CompositeFileProvider( - new CompositeFileProvider( - new NullFileProvider(), - new NullFileProvider()), - new NullFileProvider())), - new NullFileProvider(), - new CompositeFileProvider( - new CompositeFileProvider( - new NullFileProvider(), - new NullFileProvider()), - new NullFileProvider())); - - var result = FileProviderComposer.FlattenProvider(provider); - Assert.That(result, Is.InstanceOf()); - } - - [Test] - public void Flatten_MaintainOrder_WhenComposite() - { - var p1 = new TestFileProvider(); - var p2 = new TestFileProvider(); - var p3 = new TestFileProvider(); - var p4 = new TestFileProvider(); - var p5 = new TestFileProvider(); - var p6 = new TestFileProvider(); - var p7 = new TestFileProvider(); - var p8 = new TestFileProvider(); - var p9 = new TestFileProvider(); - - var provider = FileProviderComposer.ComposeProviders( - new CompositeFileProvider( - new NullFileProvider(), - new NullFileProvider(), - new CompositeFileProvider( - p1, - new CompositeFileProvider( - p2, - new NullFileProvider(), - p3), - p4, - new NullFileProvider()), - p5, - new CompositeFileProvider( - new NullFileProvider(), - new NullFileProvider(), - new CompositeFileProvider( - new CompositeFileProvider( - new NullFileProvider(), - p6), - new NullFileProvider())), - p7), - new NullFileProvider(), - new NullFileProvider(), - new CompositeFileProvider( - new NullFileProvider(), - new NullFileProvider(), - new CompositeFileProvider( - new CompositeFileProvider( - new NullFileProvider(), - new NullFileProvider()), - new NullFileProvider(), - p8)), - new NullFileProvider(), - new CompositeFileProvider( - new CompositeFileProvider( - new NullFileProvider(), - new NullFileProvider()), - new NullFileProvider(), - p9)); - - var composite = (CompositeFileProvider)provider; - var providers = new IFileProvider[] {p1, p2, p3, p4, p5, p6, p7, p8, p9}; - - Assert.That(provider, Is.InstanceOf()); - Assert.That(composite.FileProviders, Is.EquivalentTo(providers)); - } - - [Test] - public void Flatten_ReturnsSingleProvider_WhenCompositeContainsOnlyOne() - { - var provider = - new CompositeFileProvider( - new CompositeFileProvider( - new CompositeFileProvider( - new TestFileProvider()))); - - Assert.That(provider.Flatten(), Is.InstanceOf()); - } - - [Test] - public void Flatten_EmptyComposite_ReturnsNullProvider() - { - var provider = new CompositeFileProvider().Flatten(); - Assert.That(provider, Is.InstanceOf()); - } - - private sealed class TestFileProvider : IFileProvider - { - public IFileInfo GetFileInfo(string subpath) => - new NotFoundFileInfo(subpath); - - public IDirectoryContents GetDirectoryContents(string subpath) => - NotFoundDirectoryContents.Singleton; - - public IChangeToken Watch(string? filter) => - NullChangeToken.Singleton; - } -} +namespace Ramstack.FileProviders.Composition; + +[TestFixture] +public sealed class FileProviderComposerTests +{ + [Test] + public void Flatten_ReturnsAsIs_WhenNoComposite() + { + var provider = new TestFileProvider(); + var result = FileProviderComposer.FlattenProvider(provider); + Assert.That(result, Is.SameAs(provider)); + } + + [Test] + public void Flatten_ReturnsCompositeProvider_WhenNeedComposite() + { + var provider = new CompositeFileProvider(new TestFileProvider(), new TestFileProvider()); + + var result = FileProviderComposer.FlattenProvider(provider); + Assert.That(result, Is.InstanceOf()); + } + + [Test] + public void Flatten_ReturnsAsIs_WhenAlreadyFlat() + { + var provider = new CompositeFileProvider(new TestFileProvider(), new TestFileProvider()); + + var result = FileProviderComposer.FlattenProvider(provider); + Assert.That(result, Is.SameAs(provider)); + } + + [Test] + public void Flatten_ReturnsCompositeProvider_Flattened() + { + var provider = new CompositeFileProvider( + new TestFileProvider(), + new CompositeFileProvider( + new TestFileProvider(), + new TestFileProvider(), + new CompositeFileProvider( + new TestFileProvider()))); + + var result = FileProviderComposer.FlattenProvider(provider); + + Assert.That(result, Is.InstanceOf()); + Assert.That(((CompositeFileProvider)result).FileProviders.Count(), Is.EqualTo(4)); + Assert.That(((CompositeFileProvider)result).FileProviders, Is.All.InstanceOf()); + } + + [Test] + public void Flatten_RemovesNullFileProvider() + { + var provider = new CompositeFileProvider( + new TestFileProvider(), + new CompositeFileProvider( + new TestFileProvider(), + new NullFileProvider(), + new TestFileProvider()), + new NullFileProvider()); + + var result = FileProviderComposer.FlattenProvider(provider); + + Assert.That(result, Is.InstanceOf()); + Assert.That(((CompositeFileProvider)result).FileProviders.Count(), Is.EqualTo(3)); + Assert.That(((CompositeFileProvider)result).FileProviders, Is.All.InstanceOf()); + } + + [Test] + public void Flatten_ReturnsNullFileProvider_WhenNothingReturn() + { + var provider = new CompositeFileProvider( + new CompositeFileProvider( + new NullFileProvider(), + new NullFileProvider(), + new CompositeFileProvider( + new CompositeFileProvider( + new NullFileProvider(), + new NullFileProvider()), + new NullFileProvider()), + new CompositeFileProvider( + new NullFileProvider(), + new NullFileProvider(), + new CompositeFileProvider( + new CompositeFileProvider( + new NullFileProvider(), + new NullFileProvider()), + new NullFileProvider()))), + new NullFileProvider(), + new NullFileProvider(), + new CompositeFileProvider( + new NullFileProvider(), + new NullFileProvider(), + new CompositeFileProvider( + new CompositeFileProvider( + new NullFileProvider(), + new NullFileProvider()), + new NullFileProvider())), + new NullFileProvider(), + new CompositeFileProvider( + new CompositeFileProvider( + new NullFileProvider(), + new NullFileProvider()), + new NullFileProvider())); + + var result = FileProviderComposer.FlattenProvider(provider); + Assert.That(result, Is.InstanceOf()); + } + + [Test] + public void Flatten_ReturnsSingleProvider_WhenRemainOneProvider() + { + var provider = new CompositeFileProvider( + new CompositeFileProvider( + new NullFileProvider(), + new NullFileProvider(), + new CompositeFileProvider( + new CompositeFileProvider( + new NullFileProvider(), + new NullFileProvider()), + new NullFileProvider()), + new CompositeFileProvider( + new NullFileProvider(), + new NullFileProvider(), + new CompositeFileProvider( + new CompositeFileProvider( + new NullFileProvider(), + new TestFileProvider()), + new NullFileProvider()))), + new NullFileProvider(), + new NullFileProvider(), + new CompositeFileProvider( + new NullFileProvider(), + new NullFileProvider(), + new CompositeFileProvider( + new CompositeFileProvider( + new NullFileProvider(), + new NullFileProvider()), + new NullFileProvider())), + new NullFileProvider(), + new CompositeFileProvider( + new CompositeFileProvider( + new NullFileProvider(), + new NullFileProvider()), + new NullFileProvider())); + + var result = FileProviderComposer.FlattenProvider(provider); + Assert.That(result, Is.InstanceOf()); + } + + [Test] + public void Flatten_MaintainOrder_WhenComposite() + { + var p1 = new TestFileProvider(); + var p2 = new TestFileProvider(); + var p3 = new TestFileProvider(); + var p4 = new TestFileProvider(); + var p5 = new TestFileProvider(); + var p6 = new TestFileProvider(); + var p7 = new TestFileProvider(); + var p8 = new TestFileProvider(); + var p9 = new TestFileProvider(); + + var provider = FileProviderComposer.ComposeProviders( + new CompositeFileProvider( + new NullFileProvider(), + new NullFileProvider(), + new CompositeFileProvider( + p1, + new CompositeFileProvider( + p2, + new NullFileProvider(), + p3), + p4, + new NullFileProvider()), + p5, + new CompositeFileProvider( + new NullFileProvider(), + new NullFileProvider(), + new CompositeFileProvider( + new CompositeFileProvider( + new NullFileProvider(), + p6), + new NullFileProvider())), + p7), + new NullFileProvider(), + new NullFileProvider(), + new CompositeFileProvider( + new NullFileProvider(), + new NullFileProvider(), + new CompositeFileProvider( + new CompositeFileProvider( + new NullFileProvider(), + new NullFileProvider()), + new NullFileProvider(), + p8)), + new NullFileProvider(), + new CompositeFileProvider( + new CompositeFileProvider( + new NullFileProvider(), + new NullFileProvider()), + new NullFileProvider(), + p9)); + + var composite = (CompositeFileProvider)provider; + var providers = new IFileProvider[] {p1, p2, p3, p4, p5, p6, p7, p8, p9}; + + Assert.That(provider, Is.InstanceOf()); + Assert.That(composite.FileProviders, Is.EquivalentTo(providers)); + } + + [Test] + public void Flatten_ReturnsSingleProvider_WhenCompositeContainsOnlyOne() + { + var provider = + new CompositeFileProvider( + new CompositeFileProvider( + new CompositeFileProvider( + new TestFileProvider()))); + + Assert.That(provider.Flatten(), Is.InstanceOf()); + } + + [Test] + public void Flatten_EmptyComposite_ReturnsNullProvider() + { + var provider = new CompositeFileProvider().Flatten(); + Assert.That(provider, Is.InstanceOf()); + } + + private sealed class TestFileProvider : IFileProvider + { + public IFileInfo GetFileInfo(string subpath) => + new NotFoundFileInfo(subpath); + + public IDirectoryContents GetDirectoryContents(string subpath) => + NotFoundDirectoryContents.Singleton; + + public IChangeToken Watch(string? filter) => + NullChangeToken.Singleton; + } +} diff --git a/tests/Ramstack.FileProviders.Composition.Tests/Ramstack.FileProviders.Composition.Tests.csproj b/tests/Ramstack.FileProviders.Composition.Tests/Ramstack.FileProviders.Composition.Tests.csproj index 29da14d..0556bca 100644 --- a/tests/Ramstack.FileProviders.Composition.Tests/Ramstack.FileProviders.Composition.Tests.csproj +++ b/tests/Ramstack.FileProviders.Composition.Tests/Ramstack.FileProviders.Composition.Tests.csproj @@ -1,32 +1,32 @@ - - - - net6.0 - enable - enable - preview - Ramstack.FileProviders.Composition - - - - - - - - - - - - - - - - - - - - - - - - + + + + net6.0 + enable + enable + preview + Ramstack.FileProviders.Composition + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/Ramstack.FileProviders.Extensions.Tests/ExtensionTests.cs b/tests/Ramstack.FileProviders.Extensions.Tests/ExtensionTests.cs index 8c402f7..44584b1 100644 --- a/tests/Ramstack.FileProviders.Extensions.Tests/ExtensionTests.cs +++ b/tests/Ramstack.FileProviders.Extensions.Tests/ExtensionTests.cs @@ -1,125 +1,125 @@ -using Ramstack.FileProviders.Utilities; - -namespace Ramstack.FileProviders; - -[TestFixture] -public class ExtensionTests -{ - private readonly TempFileStorage _storage = new TempFileStorage(); - private readonly PhysicalFileProvider _provider; - - public ExtensionTests() => - _provider = new PhysicalFileProvider(root: _storage.Root, ExclusionFilters.None); - - [OneTimeTearDown] - public void Cleanup() => - _storage.Dispose(); - - [Test] - public void Structures_ShouldMatch() - { - var list = _provider - .EnumerateFileNodes("/", "**") - .Select(f => f.FullName) - .OrderBy(f => f) - .ToArray(); - - var expected = Directory - .GetFileSystemEntries(_storage.Root, "*", SearchOption.AllDirectories) - .Select(p => - FilePath.Normalize( - Path.GetRelativePath(_storage.Root, p))) - .OrderBy(f => f) - .ToArray(); - - Assert.That(list, Is.EquivalentTo(expected)); - } - - [Test] - public void FileNodeBase_Returns_Valid_FileInfo() - { - foreach (var node in _provider.EnumerateFileNodes("/", "**")) - { - var info = node.ToFileInfo(); - - Assert.That(info, Is.Not.Null); - Assert.That(info.IsDirectory, Is.EqualTo(node is DirectoryNode)); - Assert.That(info.Name, Is.EqualTo(node.Name)); - Assert.That(info.Exists, Is.True); - Assert.That(node.Exists, Is.True); - - if (info.IsDirectory) - { - Assert.That(info.Length, Is.EqualTo(-1)); - } - else - { - var file = (FileNode)node; - - Assert.That(info.Length, Is.GreaterThanOrEqualTo(0)); - Assert.That(file.Length, Is.EqualTo(info.Length)); - } - } - } - - [Test] - public void File_Returns_Valid_FileInfo() - { - foreach (var file in _provider.EnumerateFiles("/", "**")) - { - var info = file.ToFileInfo(); - - Assert.That(info, Is.Not.Null); - Assert.That(info.IsDirectory, Is.False); - Assert.That(info.Name, Is.EqualTo(file.Name)); - Assert.That(info.Length, Is.GreaterThanOrEqualTo(0)); - Assert.That(info.Exists, Is.True); - - Assert.That(file.Length, Is.EqualTo(info.Length)); - Assert.That(file.Exists, Is.True); - } - } - - [Test] - public void Directory_Returns_Valid_FileInfo() - { - foreach (var directory in _provider.EnumerateDirectories("/", "**")) - { - var info = directory.ToFileInfo(); - - Assert.That(info, Is.Not.Null); - Assert.That(info.IsDirectory, Is.True); - Assert.That(info.Name, Is.EqualTo(directory.Name)); - Assert.That(info.Exists, Is.True); - Assert.That(info.Length, Is.EqualTo(-1)); - - Assert.That(directory.Exists, Is.True); - } - } - - [Test] - public void Directory_Returns_Valid_DirectoryContents() - { - foreach (var dir in _provider.EnumerateDirectories("/", "**")) - { - var directory = dir.ToDirectoryContents(); - - Assert.That(directory, Is.Not.Null); - Assert.That(directory.Exists, Is.True); - Assert.That(dir.Exists, Is.True); - - var infos = directory.OrderBy(f => f.Name).ToArray(); - var nodes = dir.EnumerateFileNodes().OrderBy(f => f.Name).ToArray(); - - Assert.That( - infos.Select(f => f.Name), - Is.EquivalentTo(nodes.Select(f => f.Name))); - - foreach (var (info, node) in infos.Zip(nodes)) - { - Assert.That(node is DirectoryNode, Is.EqualTo(info.IsDirectory)); - Assert.That(node is FileNode, Is.Not.EqualTo(info.IsDirectory)); - } - } - } -} +using Ramstack.FileProviders.Utilities; + +namespace Ramstack.FileProviders; + +[TestFixture] +public class ExtensionTests +{ + private readonly TempFileStorage _storage = new TempFileStorage(); + private readonly PhysicalFileProvider _provider; + + public ExtensionTests() => + _provider = new PhysicalFileProvider(root: _storage.Root, ExclusionFilters.None); + + [OneTimeTearDown] + public void Cleanup() => + _storage.Dispose(); + + [Test] + public void Structures_ShouldMatch() + { + var list = _provider + .EnumerateFileNodes("/", "**") + .Select(f => f.FullName) + .OrderBy(f => f) + .ToArray(); + + var expected = Directory + .GetFileSystemEntries(_storage.Root, "*", SearchOption.AllDirectories) + .Select(p => + FilePath.Normalize( + Path.GetRelativePath(_storage.Root, p))) + .OrderBy(f => f) + .ToArray(); + + Assert.That(list, Is.EquivalentTo(expected)); + } + + [Test] + public void FileNodeBase_Returns_Valid_FileInfo() + { + foreach (var node in _provider.EnumerateFileNodes("/", "**")) + { + var info = node.ToFileInfo(); + + Assert.That(info, Is.Not.Null); + Assert.That(info.IsDirectory, Is.EqualTo(node is DirectoryNode)); + Assert.That(info.Name, Is.EqualTo(node.Name)); + Assert.That(info.Exists, Is.True); + Assert.That(node.Exists, Is.True); + + if (info.IsDirectory) + { + Assert.That(info.Length, Is.EqualTo(-1)); + } + else + { + var file = (FileNode)node; + + Assert.That(info.Length, Is.GreaterThanOrEqualTo(0)); + Assert.That(file.Length, Is.EqualTo(info.Length)); + } + } + } + + [Test] + public void File_Returns_Valid_FileInfo() + { + foreach (var file in _provider.EnumerateFiles("/", "**")) + { + var info = file.ToFileInfo(); + + Assert.That(info, Is.Not.Null); + Assert.That(info.IsDirectory, Is.False); + Assert.That(info.Name, Is.EqualTo(file.Name)); + Assert.That(info.Length, Is.GreaterThanOrEqualTo(0)); + Assert.That(info.Exists, Is.True); + + Assert.That(file.Length, Is.EqualTo(info.Length)); + Assert.That(file.Exists, Is.True); + } + } + + [Test] + public void Directory_Returns_Valid_FileInfo() + { + foreach (var directory in _provider.EnumerateDirectories("/", "**")) + { + var info = directory.ToFileInfo(); + + Assert.That(info, Is.Not.Null); + Assert.That(info.IsDirectory, Is.True); + Assert.That(info.Name, Is.EqualTo(directory.Name)); + Assert.That(info.Exists, Is.True); + Assert.That(info.Length, Is.EqualTo(-1)); + + Assert.That(directory.Exists, Is.True); + } + } + + [Test] + public void Directory_Returns_Valid_DirectoryContents() + { + foreach (var dir in _provider.EnumerateDirectories("/", "**")) + { + var directory = dir.ToDirectoryContents(); + + Assert.That(directory, Is.Not.Null); + Assert.That(directory.Exists, Is.True); + Assert.That(dir.Exists, Is.True); + + var infos = directory.OrderBy(f => f.Name).ToArray(); + var nodes = dir.EnumerateFileNodes().OrderBy(f => f.Name).ToArray(); + + Assert.That( + infos.Select(f => f.Name), + Is.EquivalentTo(nodes.Select(f => f.Name))); + + foreach (var (info, node) in infos.Zip(nodes)) + { + Assert.That(node is DirectoryNode, Is.EqualTo(info.IsDirectory)); + Assert.That(node is FileNode, Is.Not.EqualTo(info.IsDirectory)); + } + } + } +} diff --git a/tests/Ramstack.FileProviders.Extensions.Tests/Ramstack.FileProviders.Extensions.Tests.csproj b/tests/Ramstack.FileProviders.Extensions.Tests/Ramstack.FileProviders.Extensions.Tests.csproj index 6957d1f..e113b67 100644 --- a/tests/Ramstack.FileProviders.Extensions.Tests/Ramstack.FileProviders.Extensions.Tests.csproj +++ b/tests/Ramstack.FileProviders.Extensions.Tests/Ramstack.FileProviders.Extensions.Tests.csproj @@ -1,34 +1,34 @@ - - - - net6.0;net8.0 - enable - enable - preview - Ramstack.FileProviders - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + net6.0;net8.0 + enable + enable + preview + Ramstack.FileProviders + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/Ramstack.FileProviders.Tests/AbstractFileProviderTests.cs b/tests/Ramstack.FileProviders.Tests/AbstractFileProviderTests.cs index 12a4d0a..3e3a749 100644 --- a/tests/Ramstack.FileProviders.Tests/AbstractFileProviderTests.cs +++ b/tests/Ramstack.FileProviders.Tests/AbstractFileProviderTests.cs @@ -1,105 +1,105 @@ -using Ramstack.FileProviders.Utilities; - -namespace Ramstack.FileProviders; - -/// -/// Represents a base class for the tests. -/// -public abstract class AbstractFileProviderTests -{ - [Test] - public void Structures_ShouldMatch() - { - using var pa = CreateFileProvider(); - using var pb = new PhysicalFileProvider(GetDirectoryInfo().FullName, ExclusionFilters.None); - - CompareDirectories("/"); - - void CompareDirectories(string path) - { - var a = pa.GetDirectoryContents(path); - var b = pb.GetDirectoryContents(path); - - Assert.That(a.Exists, Is.EqualTo(b.Exists), $"Directory existence mismatch for '{path}'"); - - var listA = a.OrderBy(f => f.Name).ToArray(); - var listB = b.OrderBy(f => f.Name).ToArray(); - - Assert.That( - listA.Select(f => f.Name), - Is.EquivalentTo(listB.Select(f => f.Name)), - $"Directory contents mismatch for '{path}'"); - - for (var i = 0; i < listA.Length; i++) - CompareFiles(FilePath.Join(path, listA[i].Name), listA[i], listB[i]); - } - - void CompareFiles(string path, IFileInfo a, IFileInfo b) - { - Assert.That(a.Name, Is.EqualTo(b.Name), $"File name mismatch for '{path}'"); - Assert.That(a.Exists, Is.EqualTo(b.Exists), $"File existence mismatch for '{path}'"); - Assert.That(a.Length, Is.EqualTo(b.Length), $"File length mismatch for '{path}'"); - Assert.That(a.IsDirectory, Is.EqualTo(b.IsDirectory), $"Type mismatch: '{path}' is a directory in one provider but not in the other."); - - if (a.IsDirectory) - { - Assert.That(a.Length, Is.EqualTo(-1), $"Directory '{path}' should return -1"); - CompareDirectories(FilePath.Join(path, a.Name)); - } - else - { - using var ra = new StreamReader(a.CreateReadStream()); - using var rb = new StreamReader(b.CreateReadStream()); - - Assert.That(ra.ReadToEnd(), Is.EqualTo(rb.ReadToEnd()), $"File content mismatch for '{path}'"); - } - } - } - - [Test] - public void GetFileInfo_NonExistingFile() - { - using var provider = CreateFileProvider(); - - var info = provider.GetFileInfo("/project/8f723faf0ee0.txt"); - - Assert.That(info.Name, Is.EqualTo("8f723faf0ee0.txt").Or.EqualTo("/project/8f723faf0ee0.txt")); - Assert.That(info.IsDirectory, Is.False); - Assert.That(info.Exists, Is.False); - } - - [Test] - public void GetDirectoryContents_NonExistingDirectory() - { - using var provider = CreateFileProvider(); - - var info = provider.GetDirectoryContents("/project/c800f57d/7c71"); - - Assert.That(info.Exists, Is.False); - } - - /// - /// Returns an instance of the file provider. - /// - /// - /// An instance of . - /// - protected abstract IFileProvider GetFileProvider(); - - /// - /// Returns the object representing the root of the test directory. - /// - /// - /// A object that points to the root of the test directory. - /// - protected abstract DirectoryInfo GetDirectoryInfo(); - - /// - /// Returns a disposable instance of the file provider. - /// - /// - /// An instance of . - /// - protected DisposableFileProvider CreateFileProvider() => - new DisposableFileProvider(GetFileProvider()); -} +using Ramstack.FileProviders.Utilities; + +namespace Ramstack.FileProviders; + +/// +/// Represents a base class for the tests. +/// +public abstract class AbstractFileProviderTests +{ + [Test] + public void Structures_ShouldMatch() + { + using var pa = CreateFileProvider(); + using var pb = new PhysicalFileProvider(GetDirectoryInfo().FullName, ExclusionFilters.None); + + CompareDirectories("/"); + + void CompareDirectories(string path) + { + var a = pa.GetDirectoryContents(path); + var b = pb.GetDirectoryContents(path); + + Assert.That(a.Exists, Is.EqualTo(b.Exists), $"Directory existence mismatch for '{path}'"); + + var listA = a.OrderBy(f => f.Name).ToArray(); + var listB = b.OrderBy(f => f.Name).ToArray(); + + Assert.That( + listA.Select(f => f.Name), + Is.EquivalentTo(listB.Select(f => f.Name)), + $"Directory contents mismatch for '{path}'"); + + for (var i = 0; i < listA.Length; i++) + CompareFiles(FilePath.Join(path, listA[i].Name), listA[i], listB[i]); + } + + void CompareFiles(string path, IFileInfo a, IFileInfo b) + { + Assert.That(a.Name, Is.EqualTo(b.Name), $"File name mismatch for '{path}'"); + Assert.That(a.Exists, Is.EqualTo(b.Exists), $"File existence mismatch for '{path}'"); + Assert.That(a.Length, Is.EqualTo(b.Length), $"File length mismatch for '{path}'"); + Assert.That(a.IsDirectory, Is.EqualTo(b.IsDirectory), $"Type mismatch: '{path}' is a directory in one provider but not in the other."); + + if (a.IsDirectory) + { + Assert.That(a.Length, Is.EqualTo(-1), $"Directory '{path}' should return -1"); + CompareDirectories(FilePath.Join(path, a.Name)); + } + else + { + using var ra = new StreamReader(a.CreateReadStream()); + using var rb = new StreamReader(b.CreateReadStream()); + + Assert.That(ra.ReadToEnd(), Is.EqualTo(rb.ReadToEnd()), $"File content mismatch for '{path}'"); + } + } + } + + [Test] + public void GetFileInfo_NonExistingFile() + { + using var provider = CreateFileProvider(); + + var info = provider.GetFileInfo("/project/8f723faf0ee0.txt"); + + Assert.That(info.Name, Is.EqualTo("8f723faf0ee0.txt").Or.EqualTo("/project/8f723faf0ee0.txt")); + Assert.That(info.IsDirectory, Is.False); + Assert.That(info.Exists, Is.False); + } + + [Test] + public void GetDirectoryContents_NonExistingDirectory() + { + using var provider = CreateFileProvider(); + + var info = provider.GetDirectoryContents("/project/c800f57d/7c71"); + + Assert.That(info.Exists, Is.False); + } + + /// + /// Returns an instance of the file provider. + /// + /// + /// An instance of . + /// + protected abstract IFileProvider GetFileProvider(); + + /// + /// Returns the object representing the root of the test directory. + /// + /// + /// A object that points to the root of the test directory. + /// + protected abstract DirectoryInfo GetDirectoryInfo(); + + /// + /// Returns a disposable instance of the file provider. + /// + /// + /// An instance of . + /// + protected DisposableFileProvider CreateFileProvider() => + new DisposableFileProvider(GetFileProvider()); +} diff --git a/tests/Ramstack.FileProviders.Tests/FileInfoExtensionsTests.cs b/tests/Ramstack.FileProviders.Tests/FileInfoExtensionsTests.cs index 0d5f911..647c17c 100644 --- a/tests/Ramstack.FileProviders.Tests/FileInfoExtensionsTests.cs +++ b/tests/Ramstack.FileProviders.Tests/FileInfoExtensionsTests.cs @@ -1,65 +1,65 @@ -namespace Ramstack.FileProviders; - -[TestFixture] -public class FileInfoExtensionsTests -{ - private const string FileName = "file.txt"; - - private readonly string _path = Path.Join( - Path.GetTempPath(), Path.GetRandomFileName()); - - [OneTimeSetUp] - public void Setup() - { - var list = new List(); - for (var i = 0; i < 10240; i++) - list.Add($"Hello, 世界! Unicode test: café, weiß, Привет, ёжик! こんにちは! {Guid.NewGuid()}"); - - Directory.CreateDirectory(_path); - File.WriteAllLines(Path.Join(_path, FileName), list); - } - - [OneTimeTearDown] - public void Cleanup() => - Directory.Delete(_path, recursive: true); - - [Test] - public async Task ReadAllText() - { - var provider = new PhysicalFileProvider(_path); - - var text1 = await provider.ReadAllTextAsync(FileName); - var text2 = provider.ReadAllText(FileName); - var expected = await File.ReadAllTextAsync(Path.Join(_path, FileName)); - - Assert.That(text1, Is.EqualTo(expected)); - Assert.That(text2, Is.EqualTo(expected)); - } - - [Test] - public async Task ReadAllLines() - { - var provider = new PhysicalFileProvider(_path); - - var list1 = await provider.ReadAllLinesAsync(FileName); - var list2 = provider.ReadAllLines(FileName); - var expected = await File.ReadAllLinesAsync(Path.Join(_path, FileName)); - - Assert.That(list1, Is.EquivalentTo(expected)); - Assert.That(list2, Is.EquivalentTo(expected)); - } - - [Test] - [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] - public async Task ReadAllBytes() - { - var provider = new PhysicalFileProvider(_path); - - var data1 = await provider.ReadAllBytesAsync(FileName); - var data2 = provider.ReadAllBytes(FileName); - var expected = await File.ReadAllBytesAsync(Path.Join(_path, FileName)); - - Assert.That(data1.SequenceEqual(expected), Is.True); - Assert.That(data2.SequenceEqual(expected), Is.True); - } -} +namespace Ramstack.FileProviders; + +[TestFixture] +public class FileInfoExtensionsTests +{ + private const string FileName = "file.txt"; + + private readonly string _path = Path.Join( + Path.GetTempPath(), Path.GetRandomFileName()); + + [OneTimeSetUp] + public void Setup() + { + var list = new List(); + for (var i = 0; i < 10240; i++) + list.Add($"Hello, 世界! Unicode test: café, weiß, Привет, ёжик! こんにちは! {Guid.NewGuid()}"); + + Directory.CreateDirectory(_path); + File.WriteAllLines(Path.Join(_path, FileName), list); + } + + [OneTimeTearDown] + public void Cleanup() => + Directory.Delete(_path, recursive: true); + + [Test] + public async Task ReadAllText() + { + var provider = new PhysicalFileProvider(_path); + + var text1 = await provider.ReadAllTextAsync(FileName); + var text2 = provider.ReadAllText(FileName); + var expected = await File.ReadAllTextAsync(Path.Join(_path, FileName)); + + Assert.That(text1, Is.EqualTo(expected)); + Assert.That(text2, Is.EqualTo(expected)); + } + + [Test] + public async Task ReadAllLines() + { + var provider = new PhysicalFileProvider(_path); + + var list1 = await provider.ReadAllLinesAsync(FileName); + var list2 = provider.ReadAllLines(FileName); + var expected = await File.ReadAllLinesAsync(Path.Join(_path, FileName)); + + Assert.That(list1, Is.EquivalentTo(expected)); + Assert.That(list2, Is.EquivalentTo(expected)); + } + + [Test] + [SuppressMessage("ReSharper", "MethodHasAsyncOverload")] + public async Task ReadAllBytes() + { + var provider = new PhysicalFileProvider(_path); + + var data1 = await provider.ReadAllBytesAsync(FileName); + var data2 = provider.ReadAllBytes(FileName); + var expected = await File.ReadAllBytesAsync(Path.Join(_path, FileName)); + + Assert.That(data1.SequenceEqual(expected), Is.True); + Assert.That(data2.SequenceEqual(expected), Is.True); + } +} diff --git a/tests/Ramstack.FileProviders.Tests/FilePathTests.cs b/tests/Ramstack.FileProviders.Tests/FilePathTests.cs index 25febd8..ba662f2 100644 --- a/tests/Ramstack.FileProviders.Tests/FilePathTests.cs +++ b/tests/Ramstack.FileProviders.Tests/FilePathTests.cs @@ -1,117 +1,117 @@ -namespace Ramstack.FileProviders; - -[TestFixture] -public class FilePathTests -{ - [TestCase("", "")] - [TestCase(".", "")] - [TestCase("/", "")] - [TestCase("/.", "")] - [TestCase("file.txt", ".txt")] - [TestCase("/path/to/file.txt", ".txt")] - [TestCase("/path/to/.hidden", ".hidden")] - [TestCase("/path/to/file", "")] - [TestCase("/path.with.dots/to/file.txt", ".txt")] - [TestCase("/path/with.dots/file.", "")] - [TestCase("/path.with.dots/to/.hidden.ext", ".ext")] - [TestCase("file.with.multiple.dots.ext", ".ext")] - [TestCase("/path/to/file.with.multiple.dots.ext", ".ext")] - [TestCase("/.hidden", ".hidden")] - public void GetExtension(string path, string expected) - { - foreach (var p in GetPathVariations(path)) - Assert.That(FilePath.GetExtension(p), Is.EqualTo(expected)); - } - - [TestCase("", "")] - [TestCase(".", ".")] - [TestCase(".hidden", ".hidden")] - [TestCase("file.txt", "file.txt")] - [TestCase("/path/to/file.txt", "file.txt")] - [TestCase("/path/to/.hidden", ".hidden")] - [TestCase("/path/to/file", "file")] - [TestCase("/path/with.dots/file.txt", "file.txt")] - [TestCase("/path/with.dots/file.", "file.")] - [TestCase("/path/to/file.with.multiple.dots.ext", "file.with.multiple.dots.ext")] - [TestCase("/path/to/.hidden.ext", ".hidden.ext")] - [TestCase("/.hidden", ".hidden")] - [TestCase("/path/to/", "")] - [TestCase("/path/to/directory/", "")] - public void GetFileName(string path, string expected) - { - foreach (var p in GetPathVariations(path)) - Assert.That(FilePath.GetFileName(p), Is.EqualTo(expected)); - } - - [TestCase("", "")] - [TestCase("/", "")] - [TestCase("dir", "")] - [TestCase("/dir", "/")] - [TestCase("/dir/file", "/dir")] - [TestCase("/dir/dir/", "/dir/dir")] - [TestCase("dir/dir", "dir")] - [TestCase("dir/dir/", "dir/dir")] - [TestCase("//", "")] - [TestCase("///", "")] - [TestCase("//dir", "/")] - [TestCase("///dir", "/")] - [TestCase("////dir", "/")] - [TestCase("/dir///dir", "/dir")] - [TestCase("/dir///dir///", "/dir///dir")] - [TestCase("//dir///dir///", "//dir///dir")] - [TestCase("dir///dir", "dir")] - public void GetDirectoryName(string path, string expected) - { - foreach (var p in GetPathVariations(path)) - { - if (p.Contains('\\') && expected != "/") - expected = expected.Replace("/", "\\"); - - Assert.That(FilePath.GetDirectoryName(p), Is.EqualTo(expected)); - } - } - - [TestCase("/", ExpectedResult = true)] - [TestCase("/a/b/c", ExpectedResult = true)] - [TestCase("/a/ /c", ExpectedResult = true)] - [TestCase("/a/ ", ExpectedResult = true)] - [TestCase("/a/ /c", ExpectedResult = true)] - [TestCase("/a/./b/c", ExpectedResult = false)] - [TestCase("/a/../b/c", ExpectedResult = false)] - [TestCase("/a/./../b/c", ExpectedResult = false)] - [TestCase("/a/", ExpectedResult = false)] - [TestCase("/a//b", ExpectedResult = false)] - [TestCase("a/b", ExpectedResult = false)] - [TestCase("a/b/", ExpectedResult = false)] - [TestCase("/a\\b", ExpectedResult = false)] - [TestCase("", ExpectedResult = false)] - [TestCase(" ", ExpectedResult = false)] - [TestCase(" /", ExpectedResult = false)] - public bool IsNormalized(string path) => - FilePath.IsNormalized(path); - - [TestCase("", "/")] - [TestCase(".", "/")] - [TestCase(".", "/")] - [TestCase("/home/", "/home")] - [TestCase("/home/..folder1/.folder2/file", "/home/..folder1/.folder2/file")] - [TestCase("/home/././", "/home")] - [TestCase("/././././/home/user/documents", "/home/user/documents")] - [TestCase("/home/./user/./././/documents", "/home/user/documents")] - [TestCase("/home/../home/user//documents", "/home/user/documents")] - [TestCase("/home/../home/user/../../home/config/documents", "/home/config/documents")] - [TestCase("/home/../home/user/./.././.././home/config/documents", "/home/config/documents")] - [TestCase("..", "/")] - [TestCase("../..", "/")] - [TestCase("../../../", "/")] - [TestCase("/home/../..", "/")] - [TestCase("/home/../../..", "/")] - public void Normalize(string path, string expected) - { - foreach (var p in GetPathVariations(path)) - Assert.That(FilePath.Normalize(p),Is.EqualTo(expected)); - } - - private static string[] GetPathVariations(string path) => - [path, path.Replace('/', '\\')]; -} +namespace Ramstack.FileProviders; + +[TestFixture] +public class FilePathTests +{ + [TestCase("", "")] + [TestCase(".", "")] + [TestCase("/", "")] + [TestCase("/.", "")] + [TestCase("file.txt", ".txt")] + [TestCase("/path/to/file.txt", ".txt")] + [TestCase("/path/to/.hidden", ".hidden")] + [TestCase("/path/to/file", "")] + [TestCase("/path.with.dots/to/file.txt", ".txt")] + [TestCase("/path/with.dots/file.", "")] + [TestCase("/path.with.dots/to/.hidden.ext", ".ext")] + [TestCase("file.with.multiple.dots.ext", ".ext")] + [TestCase("/path/to/file.with.multiple.dots.ext", ".ext")] + [TestCase("/.hidden", ".hidden")] + public void GetExtension(string path, string expected) + { + foreach (var p in GetPathVariations(path)) + Assert.That(FilePath.GetExtension(p), Is.EqualTo(expected)); + } + + [TestCase("", "")] + [TestCase(".", ".")] + [TestCase(".hidden", ".hidden")] + [TestCase("file.txt", "file.txt")] + [TestCase("/path/to/file.txt", "file.txt")] + [TestCase("/path/to/.hidden", ".hidden")] + [TestCase("/path/to/file", "file")] + [TestCase("/path/with.dots/file.txt", "file.txt")] + [TestCase("/path/with.dots/file.", "file.")] + [TestCase("/path/to/file.with.multiple.dots.ext", "file.with.multiple.dots.ext")] + [TestCase("/path/to/.hidden.ext", ".hidden.ext")] + [TestCase("/.hidden", ".hidden")] + [TestCase("/path/to/", "")] + [TestCase("/path/to/directory/", "")] + public void GetFileName(string path, string expected) + { + foreach (var p in GetPathVariations(path)) + Assert.That(FilePath.GetFileName(p), Is.EqualTo(expected)); + } + + [TestCase("", "")] + [TestCase("/", "")] + [TestCase("dir", "")] + [TestCase("/dir", "/")] + [TestCase("/dir/file", "/dir")] + [TestCase("/dir/dir/", "/dir/dir")] + [TestCase("dir/dir", "dir")] + [TestCase("dir/dir/", "dir/dir")] + [TestCase("//", "")] + [TestCase("///", "")] + [TestCase("//dir", "/")] + [TestCase("///dir", "/")] + [TestCase("////dir", "/")] + [TestCase("/dir///dir", "/dir")] + [TestCase("/dir///dir///", "/dir///dir")] + [TestCase("//dir///dir///", "//dir///dir")] + [TestCase("dir///dir", "dir")] + public void GetDirectoryName(string path, string expected) + { + foreach (var p in GetPathVariations(path)) + { + if (p.Contains('\\') && expected != "/") + expected = expected.Replace("/", "\\"); + + Assert.That(FilePath.GetDirectoryName(p), Is.EqualTo(expected)); + } + } + + [TestCase("/", ExpectedResult = true)] + [TestCase("/a/b/c", ExpectedResult = true)] + [TestCase("/a/ /c", ExpectedResult = true)] + [TestCase("/a/ ", ExpectedResult = true)] + [TestCase("/a/ /c", ExpectedResult = true)] + [TestCase("/a/./b/c", ExpectedResult = false)] + [TestCase("/a/../b/c", ExpectedResult = false)] + [TestCase("/a/./../b/c", ExpectedResult = false)] + [TestCase("/a/", ExpectedResult = false)] + [TestCase("/a//b", ExpectedResult = false)] + [TestCase("a/b", ExpectedResult = false)] + [TestCase("a/b/", ExpectedResult = false)] + [TestCase("/a\\b", ExpectedResult = false)] + [TestCase("", ExpectedResult = false)] + [TestCase(" ", ExpectedResult = false)] + [TestCase(" /", ExpectedResult = false)] + public bool IsNormalized(string path) => + FilePath.IsNormalized(path); + + [TestCase("", "/")] + [TestCase(".", "/")] + [TestCase(".", "/")] + [TestCase("/home/", "/home")] + [TestCase("/home/..folder1/.folder2/file", "/home/..folder1/.folder2/file")] + [TestCase("/home/././", "/home")] + [TestCase("/././././/home/user/documents", "/home/user/documents")] + [TestCase("/home/./user/./././/documents", "/home/user/documents")] + [TestCase("/home/../home/user//documents", "/home/user/documents")] + [TestCase("/home/../home/user/../../home/config/documents", "/home/config/documents")] + [TestCase("/home/../home/user/./.././.././home/config/documents", "/home/config/documents")] + [TestCase("..", "/")] + [TestCase("../..", "/")] + [TestCase("../../../", "/")] + [TestCase("/home/../..", "/")] + [TestCase("/home/../../..", "/")] + public void Normalize(string path, string expected) + { + foreach (var p in GetPathVariations(path)) + Assert.That(FilePath.Normalize(p),Is.EqualTo(expected)); + } + + private static string[] GetPathVariations(string path) => + [path, path.Replace('/', '\\')]; +} diff --git a/tests/Ramstack.FileProviders.Tests/GlobbingFileProviderTests.cs b/tests/Ramstack.FileProviders.Tests/GlobbingFileProviderTests.cs index b605cde..8e1c18f 100644 --- a/tests/Ramstack.FileProviders.Tests/GlobbingFileProviderTests.cs +++ b/tests/Ramstack.FileProviders.Tests/GlobbingFileProviderTests.cs @@ -1,143 +1,143 @@ -using Ramstack.FileProviders.Utilities; - -namespace Ramstack.FileProviders; - -[TestFixture] -public class GlobbingFileProviderTests : AbstractFileProviderTests -{ - private readonly TempFileStorage _storage1; - private readonly TempFileStorage _storage2; - - public GlobbingFileProviderTests() - { - _storage1 = new TempFileStorage(); - _storage2 = new TempFileStorage(_storage1); - } - - [OneTimeSetUp] - public void Setup() - { - var directory = new DirectoryInfo( - Path.Join(_storage1.Root, "project")); - - foreach (var di in directory.GetDirectories("*", SearchOption.TopDirectoryOnly)) - if (di.Name != "assets") - di.Delete(recursive: true); - - foreach (var fi in directory.GetFiles("*", SearchOption.TopDirectoryOnly)) - if (fi.Name != "README.md") - fi.Delete(); - } - - [OneTimeTearDown] - public void Cleanup() - { - _storage1.Dispose(); - _storage2.Dispose(); - } - - [Test] - public void Glob_MatchStructures() - { - using var provider = CreateFileProvider(); - - Assert.That( - provider - .EnumerateDirectories("/", "**") - .OrderBy(f => f.FullName) - .Select(f => f.FullName) - .ToArray(), - Is.EquivalentTo( - [ - "/project", - "/project/assets", - "/project/assets/fonts", - "/project/assets/images", - "/project/assets/images/backgrounds", - "/project/assets/styles" - ])); - - Assert.That( - provider - .EnumerateFiles("/", "**") - .OrderBy(f => f.FullName) - .Select(f => f.FullName) - .ToArray(), - Is.EquivalentTo( - [ - "/project/assets/fonts/Arial.ttf", - "/project/assets/fonts/Roboto.ttf", - "/project/assets/images/backgrounds/dark.jpeg", - "/project/assets/images/backgrounds/light.jpg", - "/project/assets/images/icon.svg", - "/project/assets/images/logo.png", - "/project/assets/styles/main.css", - "/project/assets/styles/print.css", - "/project/README.md" - ])); - } - - [Test] - public void ExcludedDirectory_HasNoFileNodes() - { - var provider = new GlobbingFileProvider( - new PhysicalFileProvider(_storage2.Root), - pattern: "**", - exclude: "/project/src/**"); - - var directories = new[] - { - "/project/src", - "/project/src/App", - "/project/src/Modules", - "/project/src/Modules/Module1", - "/project/src/Modules/Module1/Submodule", - "/project/src/Modules/Module2" - }; - - foreach (var path in directories) - { - var directory = provider.GetDirectoryContents(path); - - Assert.That( - directory.Exists, - Is.False); - - Assert.That( - directory.Any(), - Is.False); - } - - var files = new[] - { - "/project/src/App/App.csproj", - "/project/src/App/Program.cs", - "/project/src/App/Utils.cs", - "/project/src/Modules/Module1/Module1.cs", - "/project/src/Modules/Module1/Module1.csproj", - "/project/src/Modules/Module1/Submodule/Submodule1.cs", - "/project/src/Modules/Module1/Submodule/Submodule2.cs", - "/project/src/Modules/Module1/Submodule/Submodule.csproj", - "/project/src/Modules/Module2/Module2.cs", - "/project/src/Modules/Module2/Module2.csproj", - "/project/src/App.sln" - }; - - foreach (var path in files) - { - var file = provider.GetFileInfo(path); - Assert.That(file.Exists, Is.False); - } - } - - protected override IFileProvider GetFileProvider() - { - var provider = new PhysicalFileProvider(_storage2.Root); - return new GlobbingFileProvider(provider, - patterns: ["project/assets/**", "project/README.md", "project/global.json"], - excludes: ["project/*.json"]); - } - - protected override DirectoryInfo GetDirectoryInfo() => - new DirectoryInfo(_storage1.Root); -} +using Ramstack.FileProviders.Utilities; + +namespace Ramstack.FileProviders; + +[TestFixture] +public class GlobbingFileProviderTests : AbstractFileProviderTests +{ + private readonly TempFileStorage _storage1; + private readonly TempFileStorage _storage2; + + public GlobbingFileProviderTests() + { + _storage1 = new TempFileStorage(); + _storage2 = new TempFileStorage(_storage1); + } + + [OneTimeSetUp] + public void Setup() + { + var directory = new DirectoryInfo( + Path.Join(_storage1.Root, "project")); + + foreach (var di in directory.GetDirectories("*", SearchOption.TopDirectoryOnly)) + if (di.Name != "assets") + di.Delete(recursive: true); + + foreach (var fi in directory.GetFiles("*", SearchOption.TopDirectoryOnly)) + if (fi.Name != "README.md") + fi.Delete(); + } + + [OneTimeTearDown] + public void Cleanup() + { + _storage1.Dispose(); + _storage2.Dispose(); + } + + [Test] + public void Glob_MatchStructures() + { + using var provider = CreateFileProvider(); + + Assert.That( + provider + .EnumerateDirectories("/", "**") + .OrderBy(f => f.FullName) + .Select(f => f.FullName) + .ToArray(), + Is.EquivalentTo( + [ + "/project", + "/project/assets", + "/project/assets/fonts", + "/project/assets/images", + "/project/assets/images/backgrounds", + "/project/assets/styles" + ])); + + Assert.That( + provider + .EnumerateFiles("/", "**") + .OrderBy(f => f.FullName) + .Select(f => f.FullName) + .ToArray(), + Is.EquivalentTo( + [ + "/project/assets/fonts/Arial.ttf", + "/project/assets/fonts/Roboto.ttf", + "/project/assets/images/backgrounds/dark.jpeg", + "/project/assets/images/backgrounds/light.jpg", + "/project/assets/images/icon.svg", + "/project/assets/images/logo.png", + "/project/assets/styles/main.css", + "/project/assets/styles/print.css", + "/project/README.md" + ])); + } + + [Test] + public void ExcludedDirectory_HasNoFileNodes() + { + var provider = new GlobbingFileProvider( + new PhysicalFileProvider(_storage2.Root), + pattern: "**", + exclude: "/project/src/**"); + + var directories = new[] + { + "/project/src", + "/project/src/App", + "/project/src/Modules", + "/project/src/Modules/Module1", + "/project/src/Modules/Module1/Submodule", + "/project/src/Modules/Module2" + }; + + foreach (var path in directories) + { + var directory = provider.GetDirectoryContents(path); + + Assert.That( + directory.Exists, + Is.False); + + Assert.That( + directory.Any(), + Is.False); + } + + var files = new[] + { + "/project/src/App/App.csproj", + "/project/src/App/Program.cs", + "/project/src/App/Utils.cs", + "/project/src/Modules/Module1/Module1.cs", + "/project/src/Modules/Module1/Module1.csproj", + "/project/src/Modules/Module1/Submodule/Submodule1.cs", + "/project/src/Modules/Module1/Submodule/Submodule2.cs", + "/project/src/Modules/Module1/Submodule/Submodule.csproj", + "/project/src/Modules/Module2/Module2.cs", + "/project/src/Modules/Module2/Module2.csproj", + "/project/src/App.sln" + }; + + foreach (var path in files) + { + var file = provider.GetFileInfo(path); + Assert.That(file.Exists, Is.False); + } + } + + protected override IFileProvider GetFileProvider() + { + var provider = new PhysicalFileProvider(_storage2.Root); + return new GlobbingFileProvider(provider, + patterns: ["project/assets/**", "project/README.md", "project/global.json"], + excludes: ["project/*.json"]); + } + + protected override DirectoryInfo GetDirectoryInfo() => + new DirectoryInfo(_storage1.Root); +} diff --git a/tests/Ramstack.FileProviders.Tests/PrefixedFileProviderTests.cs b/tests/Ramstack.FileProviders.Tests/PrefixedFileProviderTests.cs index da0591b..adc9fdb 100644 --- a/tests/Ramstack.FileProviders.Tests/PrefixedFileProviderTests.cs +++ b/tests/Ramstack.FileProviders.Tests/PrefixedFileProviderTests.cs @@ -1,99 +1,99 @@ -using System.Reflection; - -using Ramstack.FileProviders.Utilities; - -namespace Ramstack.FileProviders; - -[TestFixture] -public sealed class PrefixedFileProviderTests : AbstractFileProviderTests -{ - private static readonly Func s_resolveGlobFilter = - typeof(PrefixedFileProvider) - .GetMethod("ResolveGlobFilter", BindingFlags.Static | BindingFlags.NonPublic)! - .CreateDelegate>(); - - private static readonly Func s_resolvePath = - typeof(PrefixedFileProvider) - .GetMethod("ResolvePath", BindingFlags.Static | BindingFlags.NonPublic)! - .CreateDelegate>(); - - private readonly TempFileStorage _storage = new TempFileStorage(); - - protected override IFileProvider GetFileProvider() => - new PrefixedFileProvider("/project", - new PhysicalFileProvider( - Path.Join(_storage.Root, "project"))); - - protected override DirectoryInfo GetDirectoryInfo() => - new DirectoryInfo(_storage.Root); - - [TestCase("/modules/profile/assets", "/modules/**", ExpectedResult = "**")] - [TestCase("/modules/profile/assets", "/modules/**/*.js", ExpectedResult = "**/*.js")] - [TestCase("/modules/profile/assets", "/modules/**/assets/*.js", ExpectedResult = "**/*.js")] - [TestCase("/modules/profile/assets", "/modules/**/profile/assets/*.{js,css}", ExpectedResult = "**/*.{js,css}")] - [TestCase("/modules/profile/assets", "/modules/**/profile/assets/**", ExpectedResult = "**")] - [TestCase("/modules/profile/assets", "/modules/**/profile/assets/**/*", ExpectedResult = "**/*")] - [TestCase("/modules/profile/assets", "/**/*.js", ExpectedResult = "**/*.js")] - [TestCase("/modules/profile/assets", "/**", ExpectedResult = "**")] - [TestCase("/modules/profile/assets", "/modules/profile/*/*.js", ExpectedResult = "*.js")] - [TestCase("/modules/profile/assets", "/modules/profile/{js,css,assets}/*.js", ExpectedResult = "*.js")] - [TestCase("/modules/profile/assets", "/modules/{settings,profile}/{js,css,assets}/*.js", ExpectedResult = "*.js")] - [TestCase("/modules/profile/assets", "/modules/profile/assets/*.js", ExpectedResult = "*.js")] - [TestCase("/modules/profile/assets", "/modules/profile/assets/js/jquery/*.js", ExpectedResult = "js/jquery/*.js")] - [TestCase("/modules/profile/assets", "/modules/profiles/assets/*.js", ExpectedResult = null)] - [TestCase("/modules/profile/assets", "/modules/profile/js/*.js", ExpectedResult = null)] - [TestCase("/modules/profile/assets", "/module/profile/assets/*.js", ExpectedResult = null)] - [TestCase("/modules/profile/assets", "/modules/profile/*.js", ExpectedResult = null)] - [TestCase("/modules/profile/assets", "/modules/*.js", ExpectedResult = null)] - [TestCase("/modules/profile/assets", "/*.js", ExpectedResult = null)] - public string? ResolveGlobFilter(string prefix, string filter) => - s_resolveGlobFilter(prefix, filter); - - [TestCase("/", "/", ExpectedResult = "/")] - [TestCase("/", "/foo", ExpectedResult = "/foo")] - [TestCase("/", "/a/b/c", ExpectedResult = "/a/b/c")] - - [TestCase("/a/b", "/a/b", ExpectedResult = "/")] - - [TestCase("/a/b", "/a/b/c", ExpectedResult = "/c")] - [TestCase("/a/b", "/a/b/c/d", ExpectedResult = "/c/d")] - - [TestCase("/a/b", "/a/bc", ExpectedResult = null)] - - [TestCase("/a/b", "/a/c", ExpectedResult = null)] - [TestCase("/a/b", "/a", ExpectedResult = null)] - public string? ResolvePath(string prefix, string path) => - s_resolvePath(prefix, path); - - [Test] - public void GetFileInfo_RootPrefix_DelegatesToInnerProvider() - { - using var provider = new PrefixedFileProvider("/", - new PhysicalFileProvider(_storage.Root, ExclusionFilters.None)); - - var file = provider.GetFileInfo("/project/README.md"); - Assert.That(file.Exists, Is.True); - Assert.That(file.IsDirectory, Is.False); - } - - [Test] - public void GetDirectoryContents_RootPrefix_DelegatesToInnerProvider() - { - using var provider = new PrefixedFileProvider("/", - new PhysicalFileProvider(_storage.Root, ExclusionFilters.None)); - - var contents = provider.GetDirectoryContents("/project/docs"); - Assert.That(contents.Exists, Is.True); - Assert.That(contents.Any(), Is.True); - } - - [Test] - public void GetFileInfo_RootPrefix_MissingFile_ReturnsNotFound() - { - using var provider = new PrefixedFileProvider("/", - new PhysicalFileProvider(_storage.Root, ExclusionFilters.None)); - - var file = provider.GetFileInfo("/project/nonexistent.txt"); - Assert.That(file.Exists, Is.False); - } -} +using System.Reflection; + +using Ramstack.FileProviders.Utilities; + +namespace Ramstack.FileProviders; + +[TestFixture] +public sealed class PrefixedFileProviderTests : AbstractFileProviderTests +{ + private static readonly Func s_resolveGlobFilter = + typeof(PrefixedFileProvider) + .GetMethod("ResolveGlobFilter", BindingFlags.Static | BindingFlags.NonPublic)! + .CreateDelegate>(); + + private static readonly Func s_resolvePath = + typeof(PrefixedFileProvider) + .GetMethod("ResolvePath", BindingFlags.Static | BindingFlags.NonPublic)! + .CreateDelegate>(); + + private readonly TempFileStorage _storage = new TempFileStorage(); + + protected override IFileProvider GetFileProvider() => + new PrefixedFileProvider("/project", + new PhysicalFileProvider( + Path.Join(_storage.Root, "project"))); + + protected override DirectoryInfo GetDirectoryInfo() => + new DirectoryInfo(_storage.Root); + + [TestCase("/modules/profile/assets", "/modules/**", ExpectedResult = "**")] + [TestCase("/modules/profile/assets", "/modules/**/*.js", ExpectedResult = "**/*.js")] + [TestCase("/modules/profile/assets", "/modules/**/assets/*.js", ExpectedResult = "**/*.js")] + [TestCase("/modules/profile/assets", "/modules/**/profile/assets/*.{js,css}", ExpectedResult = "**/*.{js,css}")] + [TestCase("/modules/profile/assets", "/modules/**/profile/assets/**", ExpectedResult = "**")] + [TestCase("/modules/profile/assets", "/modules/**/profile/assets/**/*", ExpectedResult = "**/*")] + [TestCase("/modules/profile/assets", "/**/*.js", ExpectedResult = "**/*.js")] + [TestCase("/modules/profile/assets", "/**", ExpectedResult = "**")] + [TestCase("/modules/profile/assets", "/modules/profile/*/*.js", ExpectedResult = "*.js")] + [TestCase("/modules/profile/assets", "/modules/profile/{js,css,assets}/*.js", ExpectedResult = "*.js")] + [TestCase("/modules/profile/assets", "/modules/{settings,profile}/{js,css,assets}/*.js", ExpectedResult = "*.js")] + [TestCase("/modules/profile/assets", "/modules/profile/assets/*.js", ExpectedResult = "*.js")] + [TestCase("/modules/profile/assets", "/modules/profile/assets/js/jquery/*.js", ExpectedResult = "js/jquery/*.js")] + [TestCase("/modules/profile/assets", "/modules/profiles/assets/*.js", ExpectedResult = null)] + [TestCase("/modules/profile/assets", "/modules/profile/js/*.js", ExpectedResult = null)] + [TestCase("/modules/profile/assets", "/module/profile/assets/*.js", ExpectedResult = null)] + [TestCase("/modules/profile/assets", "/modules/profile/*.js", ExpectedResult = null)] + [TestCase("/modules/profile/assets", "/modules/*.js", ExpectedResult = null)] + [TestCase("/modules/profile/assets", "/*.js", ExpectedResult = null)] + public string? ResolveGlobFilter(string prefix, string filter) => + s_resolveGlobFilter(prefix, filter); + + [TestCase("/", "/", ExpectedResult = "/")] + [TestCase("/", "/foo", ExpectedResult = "/foo")] + [TestCase("/", "/a/b/c", ExpectedResult = "/a/b/c")] + + [TestCase("/a/b", "/a/b", ExpectedResult = "/")] + + [TestCase("/a/b", "/a/b/c", ExpectedResult = "/c")] + [TestCase("/a/b", "/a/b/c/d", ExpectedResult = "/c/d")] + + [TestCase("/a/b", "/a/bc", ExpectedResult = null)] + + [TestCase("/a/b", "/a/c", ExpectedResult = null)] + [TestCase("/a/b", "/a", ExpectedResult = null)] + public string? ResolvePath(string prefix, string path) => + s_resolvePath(prefix, path); + + [Test] + public void GetFileInfo_RootPrefix_DelegatesToInnerProvider() + { + using var provider = new PrefixedFileProvider("/", + new PhysicalFileProvider(_storage.Root, ExclusionFilters.None)); + + var file = provider.GetFileInfo("/project/README.md"); + Assert.That(file.Exists, Is.True); + Assert.That(file.IsDirectory, Is.False); + } + + [Test] + public void GetDirectoryContents_RootPrefix_DelegatesToInnerProvider() + { + using var provider = new PrefixedFileProvider("/", + new PhysicalFileProvider(_storage.Root, ExclusionFilters.None)); + + var contents = provider.GetDirectoryContents("/project/docs"); + Assert.That(contents.Exists, Is.True); + Assert.That(contents.Any(), Is.True); + } + + [Test] + public void GetFileInfo_RootPrefix_MissingFile_ReturnsNotFound() + { + using var provider = new PrefixedFileProvider("/", + new PhysicalFileProvider(_storage.Root, ExclusionFilters.None)); + + var file = provider.GetFileInfo("/project/nonexistent.txt"); + Assert.That(file.Exists, Is.False); + } +} diff --git a/tests/Ramstack.FileProviders.Tests/Ramstack.FileProviders.Tests.csproj b/tests/Ramstack.FileProviders.Tests/Ramstack.FileProviders.Tests.csproj index ea933c5..ab88ea6 100644 --- a/tests/Ramstack.FileProviders.Tests/Ramstack.FileProviders.Tests.csproj +++ b/tests/Ramstack.FileProviders.Tests/Ramstack.FileProviders.Tests.csproj @@ -1,36 +1,36 @@ - - - - net6.0 - enable - enable - preview - Ramstack.FileProviders - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + net6.0 + enable + enable + preview + Ramstack.FileProviders + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/Ramstack.FileProviders.Tests/SubFileProviderTests.cs b/tests/Ramstack.FileProviders.Tests/SubFileProviderTests.cs index 270f0a0..3d3daa5 100644 --- a/tests/Ramstack.FileProviders.Tests/SubFileProviderTests.cs +++ b/tests/Ramstack.FileProviders.Tests/SubFileProviderTests.cs @@ -1,21 +1,21 @@ -using Ramstack.FileProviders.Utilities; - -namespace Ramstack.FileProviders; - -[TestFixture] -public sealed class SubFileProviderTests : AbstractFileProviderTests -{ - private readonly TempFileStorage _storage = new TempFileStorage(); - - [OneTimeTearDown] - public void Cleanup() => - _storage.Dispose(); - - protected override IFileProvider GetFileProvider() => - new SubFileProvider("/bin/app", - new PrefixedFileProvider("/bin/app", - new PhysicalFileProvider(_storage.Root))); - - protected override DirectoryInfo GetDirectoryInfo() => - new DirectoryInfo(Path.Join(_storage.Root)); -} +using Ramstack.FileProviders.Utilities; + +namespace Ramstack.FileProviders; + +[TestFixture] +public sealed class SubFileProviderTests : AbstractFileProviderTests +{ + private readonly TempFileStorage _storage = new TempFileStorage(); + + [OneTimeTearDown] + public void Cleanup() => + _storage.Dispose(); + + protected override IFileProvider GetFileProvider() => + new SubFileProvider("/bin/app", + new PrefixedFileProvider("/bin/app", + new PhysicalFileProvider(_storage.Root))); + + protected override DirectoryInfo GetDirectoryInfo() => + new DirectoryInfo(Path.Join(_storage.Root)); +} diff --git a/tests/Ramstack.FileProviders.Tests/Utilities/DisposableFileProvider.cs b/tests/Ramstack.FileProviders.Tests/Utilities/DisposableFileProvider.cs index cb79600..eaa1a3e 100644 --- a/tests/Ramstack.FileProviders.Tests/Utilities/DisposableFileProvider.cs +++ b/tests/Ramstack.FileProviders.Tests/Utilities/DisposableFileProvider.cs @@ -1,16 +1,16 @@ -namespace Ramstack.FileProviders.Utilities; - -public sealed class DisposableFileProvider(IFileProvider provider) : IFileProvider, IDisposable -{ - public IFileInfo GetFileInfo(string subpath) => - provider.GetFileInfo(subpath); - - public IDirectoryContents GetDirectoryContents(string subpath) => - provider.GetDirectoryContents(subpath); - - public IChangeToken Watch(string filter) => - provider.Watch(filter); - - public void Dispose() => - (provider as IDisposable)?.Dispose(); -} +namespace Ramstack.FileProviders.Utilities; + +public sealed class DisposableFileProvider(IFileProvider provider) : IFileProvider, IDisposable +{ + public IFileInfo GetFileInfo(string subpath) => + provider.GetFileInfo(subpath); + + public IDirectoryContents GetDirectoryContents(string subpath) => + provider.GetDirectoryContents(subpath); + + public IChangeToken Watch(string filter) => + provider.Watch(filter); + + public void Dispose() => + (provider as IDisposable)?.Dispose(); +} diff --git a/tests/Ramstack.FileProviders.Tests/Utilities/TempFileStorage.cs b/tests/Ramstack.FileProviders.Tests/Utilities/TempFileStorage.cs index 574736c..268b413 100644 --- a/tests/Ramstack.FileProviders.Tests/Utilities/TempFileStorage.cs +++ b/tests/Ramstack.FileProviders.Tests/Utilities/TempFileStorage.cs @@ -1,124 +1,124 @@ -namespace Ramstack.FileProviders.Utilities; - -public sealed class TempFileStorage : IDisposable -{ - public string Root { get; } = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); - - public TempFileStorage() - { - var path = Root; - var list = new[] - { - "project/docs/user_manual.pdf", - "project/docs/api_reference.md", - "project/docs/troubleshooting/common_issues.txt", - "project/docs/troubleshooting/faq.docx", - - "project/docs_generated/html/index.html", - "project/docs_generated/html/api.html", - "project/docs_generated/pdf/documentation.pdf", - - "project/src/App/App.csproj", - "project/src/App/Program.cs", - "project/src/App/Utils.cs", - "project/src/Modules/Module1/Module1.cs", - "project/src/Modules/Module1/Module1.csproj", - "project/src/Modules/Module1/Submodule/Submodule1.cs", - "project/src/Modules/Module1/Submodule/Submodule2.cs", - "project/src/Modules/Module1/Submodule/Submodule.csproj", - "project/src/Modules/Module2/Module2.cs", - "project/src/Modules/Module2/Module2.csproj", - "project/src/App.sln", - - "project/tests/TestMain.cs", - "project/tests/TestUtils.cs", - "project/tests/Tests.csproj", - "project/tests/Fixtures/SampleData.json", - "project/tests/Fixtures/MockResponses.xml", - - "project/data/raw/dataset1.csv", - "project/data/raw/dataset2.csv", - "project/data/raw/dataset-[data]-{1}.csv", - "project/data/processed/cleaned_data.csv", - "project/data/processed/aggregated_results.json", - - "project/data/temp/temp_file1.tmp", - "project/data/temp/temp_file2.tmp", - "project/data/temp/ac/b2/34/2d/7e/temp_file2.tmp", - - "project/scripts/setup.p1", - "project/scripts/deploy.ps1", - "project/scripts/build.bat", - "project/scripts/build.sh", - - "project/logs/app.log", - "project/logs/error.log", - "project/logs/archive/2019/01/app_2019-01.log", - "project/logs/archive/2019/02/app_2019-02.log", - "project/logs/archive/2019/03/app_2019-03.log", - - "project/config/production.json", - "project/config/development.json", - "project/config/test.json", - - "project/assets/images/logo.png", - "project/assets/images/icon.svg", - "project/assets/images/backgrounds/light.jpg", - "project/assets/images/backgrounds/dark.jpeg", - - "project/assets/fonts/Arial.ttf", - "project/assets/fonts/Roboto.ttf", - "project/assets/styles/main.css", - "project/assets/styles/print.css", - - "project/packages/Ramstack.Globbing.2.1.0/lib/net60/Ramstack.Globbing.dll", - "project/packages/Ramstack.Globbing.2.1.0/Ramstack.Globbing.2.1.0.zip", - - "project/.gitignore", - "project/.editorconfig", - "project/README.md", - "project/global.json", - "project/nuget.config", - } - .Select(p => Path.Combine(path, p)) - .ToArray(); - - var directories = list - .Select(p => - Path.GetDirectoryName(p)!) - .Distinct() - .ToArray(); - - foreach (var d in directories) - Directory.CreateDirectory(d); - - foreach (var f in list) - File.WriteAllText(f, $"Automatically generated on {DateTime.Now:s}\n\nId:{Guid.NewGuid()}"); - } - - public TempFileStorage(TempFileStorage storage) - { - CopyDirectory(storage.Root, Root); - - static void CopyDirectory(string sourcePath, string destinationPath) - { - Directory.CreateDirectory(destinationPath); - - foreach (var sourceFileName in Directory.GetFiles(sourcePath)) - { - var name = Path.GetFileName(sourceFileName); - var destFileName = Path.Join(destinationPath, name); - File.Copy(sourceFileName, destFileName); - } - - foreach (var directoryPath in Directory.GetDirectories(sourcePath)) - { - var destination = Path.Join(destinationPath, Path.GetFileName(directoryPath)); - CopyDirectory(directoryPath, destination); - } - } - } - - public void Dispose() => - Directory.Delete(Root, true); -} +namespace Ramstack.FileProviders.Utilities; + +public sealed class TempFileStorage : IDisposable +{ + public string Root { get; } = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + + public TempFileStorage() + { + var path = Root; + var list = new[] + { + "project/docs/user_manual.pdf", + "project/docs/api_reference.md", + "project/docs/troubleshooting/common_issues.txt", + "project/docs/troubleshooting/faq.docx", + + "project/docs_generated/html/index.html", + "project/docs_generated/html/api.html", + "project/docs_generated/pdf/documentation.pdf", + + "project/src/App/App.csproj", + "project/src/App/Program.cs", + "project/src/App/Utils.cs", + "project/src/Modules/Module1/Module1.cs", + "project/src/Modules/Module1/Module1.csproj", + "project/src/Modules/Module1/Submodule/Submodule1.cs", + "project/src/Modules/Module1/Submodule/Submodule2.cs", + "project/src/Modules/Module1/Submodule/Submodule.csproj", + "project/src/Modules/Module2/Module2.cs", + "project/src/Modules/Module2/Module2.csproj", + "project/src/App.sln", + + "project/tests/TestMain.cs", + "project/tests/TestUtils.cs", + "project/tests/Tests.csproj", + "project/tests/Fixtures/SampleData.json", + "project/tests/Fixtures/MockResponses.xml", + + "project/data/raw/dataset1.csv", + "project/data/raw/dataset2.csv", + "project/data/raw/dataset-[data]-{1}.csv", + "project/data/processed/cleaned_data.csv", + "project/data/processed/aggregated_results.json", + + "project/data/temp/temp_file1.tmp", + "project/data/temp/temp_file2.tmp", + "project/data/temp/ac/b2/34/2d/7e/temp_file2.tmp", + + "project/scripts/setup.p1", + "project/scripts/deploy.ps1", + "project/scripts/build.bat", + "project/scripts/build.sh", + + "project/logs/app.log", + "project/logs/error.log", + "project/logs/archive/2019/01/app_2019-01.log", + "project/logs/archive/2019/02/app_2019-02.log", + "project/logs/archive/2019/03/app_2019-03.log", + + "project/config/production.json", + "project/config/development.json", + "project/config/test.json", + + "project/assets/images/logo.png", + "project/assets/images/icon.svg", + "project/assets/images/backgrounds/light.jpg", + "project/assets/images/backgrounds/dark.jpeg", + + "project/assets/fonts/Arial.ttf", + "project/assets/fonts/Roboto.ttf", + "project/assets/styles/main.css", + "project/assets/styles/print.css", + + "project/packages/Ramstack.Globbing.2.1.0/lib/net60/Ramstack.Globbing.dll", + "project/packages/Ramstack.Globbing.2.1.0/Ramstack.Globbing.2.1.0.zip", + + "project/.gitignore", + "project/.editorconfig", + "project/README.md", + "project/global.json", + "project/nuget.config", + } + .Select(p => Path.Combine(path, p)) + .ToArray(); + + var directories = list + .Select(p => + Path.GetDirectoryName(p)!) + .Distinct() + .ToArray(); + + foreach (var d in directories) + Directory.CreateDirectory(d); + + foreach (var f in list) + File.WriteAllText(f, $"Automatically generated on {DateTime.Now:s}\n\nId:{Guid.NewGuid()}"); + } + + public TempFileStorage(TempFileStorage storage) + { + CopyDirectory(storage.Root, Root); + + static void CopyDirectory(string sourcePath, string destinationPath) + { + Directory.CreateDirectory(destinationPath); + + foreach (var sourceFileName in Directory.GetFiles(sourcePath)) + { + var name = Path.GetFileName(sourceFileName); + var destFileName = Path.Join(destinationPath, name); + File.Copy(sourceFileName, destFileName); + } + + foreach (var directoryPath in Directory.GetDirectories(sourcePath)) + { + var destination = Path.Join(destinationPath, Path.GetFileName(directoryPath)); + CopyDirectory(directoryPath, destination); + } + } + } + + public void Dispose() => + Directory.Delete(Root, true); +}