From 7e3fe5c0511f1e43f9768208908ec991773f3781 Mon Sep 17 00:00:00 2001 From: rameel Date: Thu, 22 Oct 2026 03:36:30 +0500 Subject: [PATCH] Normalize line endings --- .editorconfig | 160 +- .gitattributes | 1 + .github/workflows/publish.yml | 76 +- .github/workflows/tests.yml | 190 +- .gitignore | 74 +- Directory.Build.props | 10 +- Directory.Packages.props | 42 +- LICENSE | 18 +- README.md | 332 +-- Ramstack.FileSystem.slnx | 1 + src/.editorconfig | 4 +- .../IVirtualFileSystem.cs | 60 +- .../Null/NotFoundDirectory.cs | 94 +- .../Null/NotFoundFile.cs | 118 +- .../Null/NullFileSystem.cs | 48 +- .../Properties/AssemblyInfo.cs | 2 +- .../README.md | 344 +-- .../Ramstack.FileSystem.Abstractions.csproj | 128 +- .../Utilities/EnumerableExtensions.cs | 128 +- .../Utilities/PathTokenizer.cs | 188 +- .../VirtualDirectory.cs | 578 ++--- .../VirtualDirectoryExtensions.cs | 276 +- .../VirtualFile.cs | 534 ++-- .../VirtualFileExtensions.cs | 714 +++--- ...VirtualFileSystemExtensions.Enumeration.cs | 338 +-- .../VirtualFileSystemExtensions.cs | 810 +++--- .../VirtualNode.cs | 308 +-- .../VirtualNodeExtensions.cs | 38 +- .../VirtualNodeProperties.cs | 184 +- .../VirtualPath.cs | 802 +++--- src/Ramstack.FileSystem.Adapters/README.md | 122 +- .../Ramstack.FileSystem.Adapters.csproj | 154 +- .../VirtualDirectoryAdapter.cs | 236 +- .../VirtualFileAdapter.cs | 122 +- .../VirtualFileSystemAdapter.cs | 102 +- .../AccessControl.cs | 132 +- .../AmazonS3FileSystem.cs | 316 +-- src/Ramstack.FileSystem.Amazon/README.md | 144 +- .../Ramstack.FileSystem.Amazon.csproj | 136 +- src/Ramstack.FileSystem.Amazon/S3Directory.cs | 774 +++--- src/Ramstack.FileSystem.Amazon/S3File.cs | 352 +-- .../S3UploadStream.cs | 636 ++--- .../Utilities/NullSynchronizationContext.cs | 66 +- .../AzureDirectory.cs | 718 +++--- src/Ramstack.FileSystem.Azure/AzureFile.cs | 330 +-- .../AzureFileSystem.cs | 328 +-- src/Ramstack.FileSystem.Azure/README.md | 128 +- .../Ramstack.FileSystem.Azure.csproj | 152 +- .../CompositeDirectory.cs | 184 +- .../CompositeFile.cs | 164 +- .../CompositeFileSystem.Helpers.cs | 166 +- .../CompositeFileSystem.cs | 118 +- src/Ramstack.FileSystem.Composite/README.md | 110 +- .../Ramstack.FileSystem.Composite.csproj | 132 +- .../GlobbingDirectory.cs | 282 +-- .../GlobbingFile.cs | 144 +- .../GlobbingFileSystem.cs | 214 +- .../Internal/PathHelper.cs | 686 ++--- src/Ramstack.FileSystem.Globbing/README.md | 120 +- .../Ramstack.FileSystem.Globbing.csproj | 134 +- .../GcsDirectory.cs | 630 ++--- src/Ramstack.FileSystem.Google/GcsFile.cs | 324 +-- .../GcsWriteStream.cs | 380 +-- .../GoogleFileSystem.cs | 170 +- src/Ramstack.FileSystem.Google/README.md | 134 +- .../Ramstack.FileSystem.Google.csproj | 136 +- .../PhysicalDirectory.cs | 310 +-- .../PhysicalFile.cs | 282 +-- .../PhysicalFileSystem.cs | 144 +- src/Ramstack.FileSystem.Physical/README.md | 124 +- .../Ramstack.FileSystem.Physical.csproj | 138 +- .../PrefixedDirectory.cs | 228 +- .../PrefixedFile.cs | 102 +- .../PrefixedFileSystem.cs | 338 +-- src/Ramstack.FileSystem.Prefixed/README.md | 118 +- .../Ramstack.FileSystem.Prefixed.csproj | 138 +- src/Ramstack.FileSystem.Readonly/README.md | 112 +- .../Ramstack.FileSystem.Readonly.csproj | 132 +- .../ReadonlyDirectory.cs | 194 +- .../ReadonlyFile.cs | 98 +- .../ReadonlyFileSystem.cs | 64 +- src/Ramstack.FileSystem.Sub/README.md | 112 +- .../Ramstack.FileSystem.Sub.csproj | 132 +- src/Ramstack.FileSystem.Sub/SubDirectory.cs | 228 +- src/Ramstack.FileSystem.Sub/SubFile.cs | 100 +- src/Ramstack.FileSystem.Sub/SubFileSystem.cs | 200 +- ...stack.FileSystem.Abstractions.Tests.csproj | 52 +- .../VirtualPathTests.cs | 296 +-- .../Ramstack.FileSystem.Amazon.Tests.csproj | 56 +- .../ReadonlyAmazonFileSystemTests.cs | 120 +- .../WritableAmazonFileSystemTests.cs | 660 ++--- .../Ramstack.FileSystem.Azure.Tests.csproj | 56 +- .../ReadonlyAzureFileSystemTests.cs | 102 +- .../WritableAzureFileSystemTests.cs | 288 +-- .../CompositeFileSystemTests.cs | 118 +- .../CompositionHelperTests.cs | 460 ++-- ...Ramstack.FileSystem.Composite.Tests.csproj | 58 +- .../TestFileSystem.cs | 18 +- ...stack.FileSystem.FileProvider.Tests.csproj | 56 +- .../VirtualFileSystemAdapterTests.cs | 50 +- .../GlobingFileSystemTests.cs | 278 +-- .../Ramstack.FileSystem.Globbing.Tests.csproj | 56 +- .../Ramstack.FileSystem.Google.Tests.csproj | 56 +- .../ReadonlyGoogleFileSystemTests.cs | 112 +- .../WritableGoogleFileSystemTests.cs | 428 ++-- .../Ramstack.FileSystem.Physical.Tests.csproj | 54 +- .../ReadonlyPhysicalFileSystemTests.cs | 54 +- .../WriteablePhysicalFileSystemTests.cs | 116 +- .../PrefixedFileSystemTests.cs | 244 +- .../Ramstack.FileSystem.Prefixed.Tests.csproj | 56 +- .../Ramstack.FileSystem.Readonly.Tests.csproj | 56 +- .../ReadOnlyFileSystemTests.cs | 46 +- .../README.md | 118 +- ...tack.FileSystem.Specification.Tests.csproj | 118 +- .../Utilities/TempFileStorage.cs | 254 +- .../VirtualFileSystemSpecificationTests.cs | 2222 ++++++++--------- .../Ramstack.FileSystem.Sub.Tests.csproj | 58 +- .../SubFileSystemTests.cs | 48 +- 118 files changed, 12927 insertions(+), 12907 deletions(-) create mode 100644 .gitattributes diff --git a/.editorconfig b/.editorconfig index 03ce37f..426c83c 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,71 +1,89 @@ -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 - -# IDE0059: Unnecessary assigment of a value -dotnet_diagnostic.IDE0059.severity = none - -# IDE0060: Remove unused parameter -dotnet_diagnostic.IDE0060.severity = none - -# IDE0079: Remove unnecessary suppression -# dotnet_diagnostic.IDE0079.severity = none - -# IDE0079: Remove unnecessary suppression -dotnet_remove_unnecessary_suppression_exclusions = category:ReSharper - -# 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 +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 + +# IDE0059: Unnecessary assigment of a value +dotnet_diagnostic.IDE0059.severity = none + +# IDE0060: Remove unused parameter +dotnet_diagnostic.IDE0060.severity = none + +# IDE0079: Remove unnecessary suppression +# dotnet_diagnostic.IDE0079.severity = none + +# IDE0079: Remove unnecessary suppression +dotnet_remove_unnecessary_suppression_exclusions = category:ReSharper + +# 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 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 17d779c..b4b2a07 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,38 +1,38 @@ -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: 10.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: 10.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/tests.yml b/.github/workflows/tests.yml index 37da93f..f562c3a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,95 +1,95 @@ -name: Build & Test - -on: - pull_request: - branches: [main] - -jobs: - - main: - name: "Test: Main projects, ${{ matrix.os.name }}" - runs-on: ${{ matrix.os.id }} - strategy: - matrix: - os: - - id: windows-latest - name: Windows - - id: ubuntu-latest - name: Ubuntu - steps: - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: | - 6.x - 7.x - 8.x - 9.x - 10.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 --filter "TestCategory!~Cloud" - - name: Test (Release) - run: dotnet test -c Release --no-build --filter "TestCategory!~Cloud" - - azure: - name: "Test: AzureFileSystem" - runs-on: ubuntu-latest - steps: - - name: Start Azurite - run: | - docker run -d \ - -p 10000:10000 \ - -p 10001:10001 \ - -p 10002:10002 \ - mcr.microsoft.com/azure-storage/azurite \ - azurite --skipApiVersionCheck \ - --blobHost 0.0.0.0 \ - --queueHost 0.0.0.0 \ - --tableHost 0.0.0.0 - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: | - 6.x - 7.x - 8.x - 9.x - 10.x - - name: Checkout - uses: actions/checkout@v4 - - name: Build - run: dotnet build -c Debug - - name: Test - run: dotnet test --no-build --filter TestCategory=Cloud:Azure -maxcpucount:1 - - s3: - name: "Test: S3FileSystem" - runs-on: ubuntu-latest - services: - rustfs: - image: docker.io/rustfs/rustfs:latest - ports: - - 9000:9000 - - 9001:9001 - steps: - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: | - 6.x - 7.x - 8.x - 9.x - 10.x - - name: Checkout - uses: actions/checkout@v4 - - name: Build - run: dotnet build -c Debug - - name: Test - run: dotnet test --no-build --filter TestCategory=Cloud:Amazon -maxcpucount:1 +name: Build & Test + +on: + pull_request: + branches: [main] + +jobs: + + main: + name: "Test: Main projects, ${{ matrix.os.name }}" + runs-on: ${{ matrix.os.id }} + strategy: + matrix: + os: + - id: windows-latest + name: Windows + - id: ubuntu-latest + name: Ubuntu + steps: + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 6.x + 7.x + 8.x + 9.x + 10.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 --filter "TestCategory!~Cloud" + - name: Test (Release) + run: dotnet test -c Release --no-build --filter "TestCategory!~Cloud" + + azure: + name: "Test: AzureFileSystem" + runs-on: ubuntu-latest + steps: + - name: Start Azurite + run: | + docker run -d \ + -p 10000:10000 \ + -p 10001:10001 \ + -p 10002:10002 \ + mcr.microsoft.com/azure-storage/azurite \ + azurite --skipApiVersionCheck \ + --blobHost 0.0.0.0 \ + --queueHost 0.0.0.0 \ + --tableHost 0.0.0.0 + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 6.x + 7.x + 8.x + 9.x + 10.x + - name: Checkout + uses: actions/checkout@v4 + - name: Build + run: dotnet build -c Debug + - name: Test + run: dotnet test --no-build --filter TestCategory=Cloud:Azure -maxcpucount:1 + + s3: + name: "Test: S3FileSystem" + runs-on: ubuntu-latest + services: + rustfs: + image: docker.io/rustfs/rustfs:latest + ports: + - 9000:9000 + - 9001:9001 + steps: + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 6.x + 7.x + 8.x + 9.x + 10.x + - name: Checkout + uses: actions/checkout@v4 + - name: Build + run: dotnet build -c Debug + - name: Test + run: dotnet test --no-build --filter TestCategory=Cloud:Amazon -maxcpucount:1 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 aec1e52..cda6b3d 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,21 +1,21 @@ - - - - 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 cedb310..9e01180 100644 --- a/README.md +++ b/README.md @@ -1,166 +1,166 @@ -# Ramstack.FileSystem -[![NuGet](https://img.shields.io/nuget/v/Ramstack.FileSystem.Abstractions.svg)](https://nuget.org/packages/Ramstack.FileSystem.Abstractions) -[![MIT](https://img.shields.io/github/license/rameel/ramstack.filesystem)](https://github.com/rameel/ramstack.filesystem/blob/main/LICENSE) - -A .NET library providing a virtual file system abstraction. - -**For current implementations, see [Related Projects](#related-projects)** - -## Overview - -The primary interface is `IVirtualFileSystem`, which exposes methods to: -- Access files (`VirtualFile`). -- Access directories (`VirtualDirectory`). - -### VirtualFile - -The `VirtualFile` class provides properties and methods for creating, deleting, copying, and opening files within the virtual file system. - -```csharp -using Ramstack.FileSystem; - -public class Program -{ - public static async Task Sample(IVirtualFileSystem fs) - { - var file = fs.GetFile("/sample/hello.txt"); - - Console.WriteLine($"Writing 'Hello, World!' to '{file.FullName}'"); - - await using (Stream stream = await file.OpenWriteAsync()) - await using (StreamWriter writer = new StreamWriter(stream)) - { - await writer.WriteLineAsync("Hello, World!"); - } - - Console.WriteLine($"Reading contents of '{file.FullName}'"); - using (StreamReader reader = await file.OpenTextAsync()) - { - Console.WriteLine(await reader.ReadToEndAsync()); - } - - // Replace content of the file with data from an existing file - await using FileStream input = File.OpenRead(@"D:\sample\hello-world.txt"); - await file.WriteAsync(input, overwrite: true); - - Console.WriteLine($"The file '{file.Name}' has a length of {await file.GetLengthAsync()} bytes"); - - Console.WriteLine($"Deleting file '{file.FullName}'"); - await file.DeleteAsync(); - } -} -``` - -The `VirtualFile` class provides the following properties to retrieve information about a file: -- `FileSystem`: Gets the `IVirtualFileSystem` associated with the file. -- `Directory`: Gets the `VirtualDirectory` representing the parent directory. -- `DirectoryName`: Gets the full path of the parent directory. -- `IsReadOnly`: Indicates whether the file is read-only. -- `Name`: Gets the name of the file. -- `Extension`: Gets the extension of the file. - -The `VirtualFile` class also exposes a `GetPropertiesAsync` method to retrieve file properties: -- `Length`: Gets the size of the file in bytes. -- `Exists`: Indicates whether the file exists. -- `CreationTime`: Gets the creation time of the file. -- `LastAccessTime`: Gets the last access time of the file. -- `LastWriteTime`: Gets the last modification time of the file. - -For convenience, many methods specific to `VirtualFile` are also available as extension methods on `IVirtualFileSystem`. - -For example, if we know the path of the file we want to read, we don't need to obtain a `VirtualFile` object explicitly: -```csharp -using StreamReader reader = await fs.OpenTextAsync("/docs/README", Encoding.UTF8); -Console.WriteLine(await reader.ReadToEndAsync()); -``` - -### VirtualDirectory - -The `VirtualDirectory` class provides properties and methods for creating, deleting, and enumerating directories and subdirectories. - -```csharp -public static async Task PrintFilesAsync(VirtualDirectory directory, string padding = "", CancellationToken cancellationToken = default) -{ - await foreach (VirtualNode node in directory.GetFileNodesAsync(cancellationToken)) - { - Console.WriteLine($"{padding}{node.Name}"); - - if (node is VirtualDirectory dir) - { - await PrintFilesAsync(dir, padding + " ", cancellationToken); - } - } -} - -// Print the file tree -await PrintFilesAsync(fs.GetDirectory("/sample")); -``` - -The `VirtualDirectory` class provides the following properties to retrieve information about a directory: -- `FileSystem`: Gets the `IVirtualFileSystem` associated with the directory. -- `IsRoot`: Indicates whether the directory is the root directory. -- `Parent`: Gets the `VirtualDirectory` representing the parent directory. -- `DirectoryName`: Gets the full path of the parent directory. -- `IsReadOnly`: Indicates whether the directory is read-only. -- `Name`: Gets the name of the directory. - -The `VirtualDirectory` class also exposes a `GetPropertiesAsync` method to retrieve directory properties, if available: -- `Exists`: Indicates whether the directory exists. -- `CreationTime`: Gets the creation time of the directory. -- `LastAccessTime`: Gets the last access time of the directory. -- `LastWriteTime`: Gets the last modification time of the directory. -- `Length`: Always returns `0` for directories. - -For convenience, many methods specific to `VirtualDirectory` are also available as extension methods on `IVirtualFileSystem`. - -For example, if we know the path of the desired directory, we don't need to obtain a `VirtualDirectory` object explicitly: -```csharp -await foreach (VirtualFile file in fs.GetFilesAsync("/sample/directory")) -{ - Console.WriteLine($"Processing: {file.FullName}"); -} - -// Delete the directory recursively -await fs.DeleteDirectoryAsync("/sample/directory"); -``` - -## Remarks - -The file system in use may be read-only, and as a result, any modifying operations on files and directories will throw an exception. -To check if the file system is read-only, the `IVirtualFileSystem`, `VirtualFile`, and `VirtualDirectory` classes provide the `IsReadOnly` property. - -```csharp -if (!fs.IsReadOnly) -{ - await fs.DeleteFileAsync("/hello.txt"); -} -``` - -## Related Projects -- [Ramstack.FileSystem.Abstractions](https://www.nuget.org/packages/Ramstack.FileSystem.Abstractions) - Provides a virtual file system abstraction. -- [Ramstack.FileSystem.Physical](https://www.nuget.org/packages/Ramstack.FileSystem.Physical) - Provides an implementation based on the local file system. -- [Ramstack.FileSystem.Azure](https://www.nuget.org/packages/Ramstack.FileSystem.Azure) - Provides an implementation using Azure Blob Storage. -- [Ramstack.FileSystem.Amazon](https://www.nuget.org/packages/Ramstack.FileSystem.Amazon) - Provides an implementation using Amazon S3 storage. -- [Ramstack.FileSystem.Google](https://www.nuget.org/packages/Ramstack.FileSystem.Google) - Provides an implementation using Google Cloud Storage. -- [Ramstack.FileSystem.Readonly](https://www.nuget.org/packages/Ramstack.FileSystem.Readonly) - Provides a read-only wrapper for the underlying file system. -- [Ramstack.FileSystem.Globbing](https://www.nuget.org/packages/Ramstack.FileSystem.Globbing) - Wraps the file system, filtering files and directories using glob patterns. -- [Ramstack.FileSystem.Prefixed](https://www.nuget.org/packages/Ramstack.FileSystem.Prefixed) - Adds a prefix to file paths within the underlying file system. -- [Ramstack.FileSystem.Sub](https://www.nuget.org/packages/Ramstack.FileSystem.Sub) - Wraps the underlying file system, restricting access to a specific subpath. -- [Ramstack.FileSystem.Adapters](https://www.nuget.org/packages/Ramstack.FileSystem.Adapters) - Provides integration with `Microsoft.Extensions.FileProviders`. -- [Ramstack.FileSystem.Composite](https://www.nuget.org/packages/Ramstack.FileSystem.Composite) - Provides an implementation that combines multiple file systems into a single composite file system. - -## 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.filesystem/blob/main/LICENSE) file for more details. - +# Ramstack.FileSystem +[![NuGet](https://img.shields.io/nuget/v/Ramstack.FileSystem.Abstractions.svg)](https://nuget.org/packages/Ramstack.FileSystem.Abstractions) +[![MIT](https://img.shields.io/github/license/rameel/ramstack.filesystem)](https://github.com/rameel/ramstack.filesystem/blob/main/LICENSE) + +A .NET library providing a virtual file system abstraction. + +**For current implementations, see [Related Projects](#related-projects)** + +## Overview + +The primary interface is `IVirtualFileSystem`, which exposes methods to: +- Access files (`VirtualFile`). +- Access directories (`VirtualDirectory`). + +### VirtualFile + +The `VirtualFile` class provides properties and methods for creating, deleting, copying, and opening files within the virtual file system. + +```csharp +using Ramstack.FileSystem; + +public class Program +{ + public static async Task Sample(IVirtualFileSystem fs) + { + var file = fs.GetFile("/sample/hello.txt"); + + Console.WriteLine($"Writing 'Hello, World!' to '{file.FullName}'"); + + await using (Stream stream = await file.OpenWriteAsync()) + await using (StreamWriter writer = new StreamWriter(stream)) + { + await writer.WriteLineAsync("Hello, World!"); + } + + Console.WriteLine($"Reading contents of '{file.FullName}'"); + using (StreamReader reader = await file.OpenTextAsync()) + { + Console.WriteLine(await reader.ReadToEndAsync()); + } + + // Replace content of the file with data from an existing file + await using FileStream input = File.OpenRead(@"D:\sample\hello-world.txt"); + await file.WriteAsync(input, overwrite: true); + + Console.WriteLine($"The file '{file.Name}' has a length of {await file.GetLengthAsync()} bytes"); + + Console.WriteLine($"Deleting file '{file.FullName}'"); + await file.DeleteAsync(); + } +} +``` + +The `VirtualFile` class provides the following properties to retrieve information about a file: +- `FileSystem`: Gets the `IVirtualFileSystem` associated with the file. +- `Directory`: Gets the `VirtualDirectory` representing the parent directory. +- `DirectoryName`: Gets the full path of the parent directory. +- `IsReadOnly`: Indicates whether the file is read-only. +- `Name`: Gets the name of the file. +- `Extension`: Gets the extension of the file. + +The `VirtualFile` class also exposes a `GetPropertiesAsync` method to retrieve file properties: +- `Length`: Gets the size of the file in bytes. +- `Exists`: Indicates whether the file exists. +- `CreationTime`: Gets the creation time of the file. +- `LastAccessTime`: Gets the last access time of the file. +- `LastWriteTime`: Gets the last modification time of the file. + +For convenience, many methods specific to `VirtualFile` are also available as extension methods on `IVirtualFileSystem`. + +For example, if we know the path of the file we want to read, we don't need to obtain a `VirtualFile` object explicitly: +```csharp +using StreamReader reader = await fs.OpenTextAsync("/docs/README", Encoding.UTF8); +Console.WriteLine(await reader.ReadToEndAsync()); +``` + +### VirtualDirectory + +The `VirtualDirectory` class provides properties and methods for creating, deleting, and enumerating directories and subdirectories. + +```csharp +public static async Task PrintFilesAsync(VirtualDirectory directory, string padding = "", CancellationToken cancellationToken = default) +{ + await foreach (VirtualNode node in directory.GetFileNodesAsync(cancellationToken)) + { + Console.WriteLine($"{padding}{node.Name}"); + + if (node is VirtualDirectory dir) + { + await PrintFilesAsync(dir, padding + " ", cancellationToken); + } + } +} + +// Print the file tree +await PrintFilesAsync(fs.GetDirectory("/sample")); +``` + +The `VirtualDirectory` class provides the following properties to retrieve information about a directory: +- `FileSystem`: Gets the `IVirtualFileSystem` associated with the directory. +- `IsRoot`: Indicates whether the directory is the root directory. +- `Parent`: Gets the `VirtualDirectory` representing the parent directory. +- `DirectoryName`: Gets the full path of the parent directory. +- `IsReadOnly`: Indicates whether the directory is read-only. +- `Name`: Gets the name of the directory. + +The `VirtualDirectory` class also exposes a `GetPropertiesAsync` method to retrieve directory properties, if available: +- `Exists`: Indicates whether the directory exists. +- `CreationTime`: Gets the creation time of the directory. +- `LastAccessTime`: Gets the last access time of the directory. +- `LastWriteTime`: Gets the last modification time of the directory. +- `Length`: Always returns `0` for directories. + +For convenience, many methods specific to `VirtualDirectory` are also available as extension methods on `IVirtualFileSystem`. + +For example, if we know the path of the desired directory, we don't need to obtain a `VirtualDirectory` object explicitly: +```csharp +await foreach (VirtualFile file in fs.GetFilesAsync("/sample/directory")) +{ + Console.WriteLine($"Processing: {file.FullName}"); +} + +// Delete the directory recursively +await fs.DeleteDirectoryAsync("/sample/directory"); +``` + +## Remarks + +The file system in use may be read-only, and as a result, any modifying operations on files and directories will throw an exception. +To check if the file system is read-only, the `IVirtualFileSystem`, `VirtualFile`, and `VirtualDirectory` classes provide the `IsReadOnly` property. + +```csharp +if (!fs.IsReadOnly) +{ + await fs.DeleteFileAsync("/hello.txt"); +} +``` + +## Related Projects +- [Ramstack.FileSystem.Abstractions](https://www.nuget.org/packages/Ramstack.FileSystem.Abstractions) - Provides a virtual file system abstraction. +- [Ramstack.FileSystem.Physical](https://www.nuget.org/packages/Ramstack.FileSystem.Physical) - Provides an implementation based on the local file system. +- [Ramstack.FileSystem.Azure](https://www.nuget.org/packages/Ramstack.FileSystem.Azure) - Provides an implementation using Azure Blob Storage. +- [Ramstack.FileSystem.Amazon](https://www.nuget.org/packages/Ramstack.FileSystem.Amazon) - Provides an implementation using Amazon S3 storage. +- [Ramstack.FileSystem.Google](https://www.nuget.org/packages/Ramstack.FileSystem.Google) - Provides an implementation using Google Cloud Storage. +- [Ramstack.FileSystem.Readonly](https://www.nuget.org/packages/Ramstack.FileSystem.Readonly) - Provides a read-only wrapper for the underlying file system. +- [Ramstack.FileSystem.Globbing](https://www.nuget.org/packages/Ramstack.FileSystem.Globbing) - Wraps the file system, filtering files and directories using glob patterns. +- [Ramstack.FileSystem.Prefixed](https://www.nuget.org/packages/Ramstack.FileSystem.Prefixed) - Adds a prefix to file paths within the underlying file system. +- [Ramstack.FileSystem.Sub](https://www.nuget.org/packages/Ramstack.FileSystem.Sub) - Wraps the underlying file system, restricting access to a specific subpath. +- [Ramstack.FileSystem.Adapters](https://www.nuget.org/packages/Ramstack.FileSystem.Adapters) - Provides integration with `Microsoft.Extensions.FileProviders`. +- [Ramstack.FileSystem.Composite](https://www.nuget.org/packages/Ramstack.FileSystem.Composite) - Provides an implementation that combines multiple file systems into a single composite file system. + +## 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.filesystem/blob/main/LICENSE) file for more details. + diff --git a/Ramstack.FileSystem.slnx b/Ramstack.FileSystem.slnx index e14578d..b9f2965 100644 --- a/Ramstack.FileSystem.slnx +++ b/Ramstack.FileSystem.slnx @@ -1,6 +1,7 @@ + diff --git a/src/.editorconfig b/src/.editorconfig index 9b27adf..c83167b 100644 --- a/src/.editorconfig +++ b/src/.editorconfig @@ -1,2 +1,2 @@ -[*.cs] -configure_await_analysis_mode = library +[*.cs] +configure_await_analysis_mode = library diff --git a/src/Ramstack.FileSystem.Abstractions/IVirtualFileSystem.cs b/src/Ramstack.FileSystem.Abstractions/IVirtualFileSystem.cs index c2d071b..400066d 100644 --- a/src/Ramstack.FileSystem.Abstractions/IVirtualFileSystem.cs +++ b/src/Ramstack.FileSystem.Abstractions/IVirtualFileSystem.cs @@ -1,30 +1,30 @@ -namespace Ramstack.FileSystem; - -/// -/// Provides an abstraction for accessing files and directories within a virtual file system. -/// -public interface IVirtualFileSystem : IDisposable -{ - /// - /// Gets a value indicating whether the file system is read-only. - /// - bool IsReadOnly { get; } - - /// - /// Retrieves a file located at the specified path. - /// - /// The path of the file to locate. - /// - /// A instance representing the specified file. - /// - VirtualFile GetFile(string path); - - /// - /// Retrieves a directory located at the specified path. - /// - /// The path of the directory to locate. - /// - /// A instance representing the specified directory. - /// - VirtualDirectory GetDirectory(string path); -} +namespace Ramstack.FileSystem; + +/// +/// Provides an abstraction for accessing files and directories within a virtual file system. +/// +public interface IVirtualFileSystem : IDisposable +{ + /// + /// Gets a value indicating whether the file system is read-only. + /// + bool IsReadOnly { get; } + + /// + /// Retrieves a file located at the specified path. + /// + /// The path of the file to locate. + /// + /// A instance representing the specified file. + /// + VirtualFile GetFile(string path); + + /// + /// Retrieves a directory located at the specified path. + /// + /// The path of the directory to locate. + /// + /// A instance representing the specified directory. + /// + VirtualDirectory GetDirectory(string path); +} diff --git a/src/Ramstack.FileSystem.Abstractions/Null/NotFoundDirectory.cs b/src/Ramstack.FileSystem.Abstractions/Null/NotFoundDirectory.cs index 5d05bab..cb6c9a4 100644 --- a/src/Ramstack.FileSystem.Abstractions/Null/NotFoundDirectory.cs +++ b/src/Ramstack.FileSystem.Abstractions/Null/NotFoundDirectory.cs @@ -1,47 +1,47 @@ -using Ramstack.FileSystem.Utilities; - -namespace Ramstack.FileSystem.Null; - -/// -/// Represents a non-existing directory. -/// -public sealed class NotFoundDirectory : VirtualDirectory -{ - /// - public override IVirtualFileSystem FileSystem { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The file system associated with this directory. - /// The path of the directory. - public NotFoundDirectory(IVirtualFileSystem fileSystem, string path) : base(VirtualPath.Normalize(path)) => - FileSystem = fileSystem; - - /// - protected override ValueTask GetPropertiesCoreAsync(CancellationToken cancellationToken) => - default; - - /// - protected override ValueTask ExistsCoreAsync(CancellationToken cancellationToken) => - default; - - /// - protected override ValueTask CreateCoreAsync(CancellationToken cancellationToken) - { - Error_UnauthorizedAccess(FullName); - return default; - } - - /// - protected override ValueTask DeleteCoreAsync(CancellationToken cancellationToken) => - default; - - /// - protected override IAsyncEnumerable GetFileNodesCoreAsync(CancellationToken cancellationToken) => - Array.Empty().ToAsyncEnumerable(); - - [DoesNotReturn] - private static void Error_UnauthorizedAccess(string path) => - throw new UnauthorizedAccessException($"Access to the path '{path}' is denied."); -} +using Ramstack.FileSystem.Utilities; + +namespace Ramstack.FileSystem.Null; + +/// +/// Represents a non-existing directory. +/// +public sealed class NotFoundDirectory : VirtualDirectory +{ + /// + public override IVirtualFileSystem FileSystem { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The file system associated with this directory. + /// The path of the directory. + public NotFoundDirectory(IVirtualFileSystem fileSystem, string path) : base(VirtualPath.Normalize(path)) => + FileSystem = fileSystem; + + /// + protected override ValueTask GetPropertiesCoreAsync(CancellationToken cancellationToken) => + default; + + /// + protected override ValueTask ExistsCoreAsync(CancellationToken cancellationToken) => + default; + + /// + protected override ValueTask CreateCoreAsync(CancellationToken cancellationToken) + { + Error_UnauthorizedAccess(FullName); + return default; + } + + /// + protected override ValueTask DeleteCoreAsync(CancellationToken cancellationToken) => + default; + + /// + protected override IAsyncEnumerable GetFileNodesCoreAsync(CancellationToken cancellationToken) => + Array.Empty().ToAsyncEnumerable(); + + [DoesNotReturn] + private static void Error_UnauthorizedAccess(string path) => + throw new UnauthorizedAccessException($"Access to the path '{path}' is denied."); +} diff --git a/src/Ramstack.FileSystem.Abstractions/Null/NotFoundFile.cs b/src/Ramstack.FileSystem.Abstractions/Null/NotFoundFile.cs index 241a8f4..c7cff63 100644 --- a/src/Ramstack.FileSystem.Abstractions/Null/NotFoundFile.cs +++ b/src/Ramstack.FileSystem.Abstractions/Null/NotFoundFile.cs @@ -1,59 +1,59 @@ -namespace Ramstack.FileSystem.Null; - -/// -/// Represents a non-existing file. -/// -public sealed class NotFoundFile : VirtualFile -{ - /// - public override IVirtualFileSystem FileSystem { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The file system associated with this file. - /// The path of the file. - public NotFoundFile(IVirtualFileSystem fileSystem, string path) : base(VirtualPath.Normalize(path)) => - FileSystem = fileSystem; - - /// - protected override ValueTask GetPropertiesCoreAsync(CancellationToken cancellationToken) => - default; - - /// - protected override ValueTask ExistsCoreAsync(CancellationToken cancellationToken) => - default; - - /// - protected override ValueTask OpenReadCoreAsync(CancellationToken cancellationToken) - { - Error_FileNotFound(FullName); - return default; - } - - /// - protected override ValueTask OpenWriteCoreAsync(CancellationToken cancellationToken) - { - Error_FileNotFound(FullName); - return default; - } - - /// - protected override ValueTask WriteCoreAsync(Stream stream, bool overwrite, CancellationToken cancellationToken) - { - Error_UnauthorizedAccess(FullName); - return default; - } - - /// - protected override ValueTask DeleteCoreAsync(CancellationToken cancellationToken) => - default; - - [DoesNotReturn] - private static void Error_FileNotFound(string path) => - throw new FileNotFoundException($"Unable to find file '{path}'.", path); - - [DoesNotReturn] - private static void Error_UnauthorizedAccess(string path) => - throw new UnauthorizedAccessException($"Access to the path '{path}' is denied."); -} +namespace Ramstack.FileSystem.Null; + +/// +/// Represents a non-existing file. +/// +public sealed class NotFoundFile : VirtualFile +{ + /// + public override IVirtualFileSystem FileSystem { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The file system associated with this file. + /// The path of the file. + public NotFoundFile(IVirtualFileSystem fileSystem, string path) : base(VirtualPath.Normalize(path)) => + FileSystem = fileSystem; + + /// + protected override ValueTask GetPropertiesCoreAsync(CancellationToken cancellationToken) => + default; + + /// + protected override ValueTask ExistsCoreAsync(CancellationToken cancellationToken) => + default; + + /// + protected override ValueTask OpenReadCoreAsync(CancellationToken cancellationToken) + { + Error_FileNotFound(FullName); + return default; + } + + /// + protected override ValueTask OpenWriteCoreAsync(CancellationToken cancellationToken) + { + Error_FileNotFound(FullName); + return default; + } + + /// + protected override ValueTask WriteCoreAsync(Stream stream, bool overwrite, CancellationToken cancellationToken) + { + Error_UnauthorizedAccess(FullName); + return default; + } + + /// + protected override ValueTask DeleteCoreAsync(CancellationToken cancellationToken) => + default; + + [DoesNotReturn] + private static void Error_FileNotFound(string path) => + throw new FileNotFoundException($"Unable to find file '{path}'.", path); + + [DoesNotReturn] + private static void Error_UnauthorizedAccess(string path) => + throw new UnauthorizedAccessException($"Access to the path '{path}' is denied."); +} diff --git a/src/Ramstack.FileSystem.Abstractions/Null/NullFileSystem.cs b/src/Ramstack.FileSystem.Abstractions/Null/NullFileSystem.cs index f3a2d00..fa46224 100644 --- a/src/Ramstack.FileSystem.Abstractions/Null/NullFileSystem.cs +++ b/src/Ramstack.FileSystem.Abstractions/Null/NullFileSystem.cs @@ -1,24 +1,24 @@ -namespace Ramstack.FileSystem.Null; - -/// -/// Represents a virtual file system that does not provide any actual file or directory functionality. -/// All operations on this file system result in a or . -/// -public sealed class NullFileSystem : IVirtualFileSystem -{ - /// - public bool IsReadOnly => true; - - /// - public VirtualFile GetFile(string path) => - new NotFoundFile(this, path); - - /// - public VirtualDirectory GetDirectory(string path) => - new NotFoundDirectory(this, path); - - /// - void IDisposable.Dispose() - { - } -} +namespace Ramstack.FileSystem.Null; + +/// +/// Represents a virtual file system that does not provide any actual file or directory functionality. +/// All operations on this file system result in a or . +/// +public sealed class NullFileSystem : IVirtualFileSystem +{ + /// + public bool IsReadOnly => true; + + /// + public VirtualFile GetFile(string path) => + new NotFoundFile(this, path); + + /// + public VirtualDirectory GetDirectory(string path) => + new NotFoundDirectory(this, path); + + /// + void IDisposable.Dispose() + { + } +} diff --git a/src/Ramstack.FileSystem.Abstractions/Properties/AssemblyInfo.cs b/src/Ramstack.FileSystem.Abstractions/Properties/AssemblyInfo.cs index b5274aa..c65b5dc 100644 --- a/src/Ramstack.FileSystem.Abstractions/Properties/AssemblyInfo.cs +++ b/src/Ramstack.FileSystem.Abstractions/Properties/AssemblyInfo.cs @@ -1 +1 @@ -[module: System.Runtime.CompilerServices.SkipLocalsInit] +[module: System.Runtime.CompilerServices.SkipLocalsInit] diff --git a/src/Ramstack.FileSystem.Abstractions/README.md b/src/Ramstack.FileSystem.Abstractions/README.md index d4de1a7..a3edd65 100644 --- a/src/Ramstack.FileSystem.Abstractions/README.md +++ b/src/Ramstack.FileSystem.Abstractions/README.md @@ -1,172 +1,172 @@ -# Ramstack.FileSystem.Abstractions -[![NuGet](https://img.shields.io/nuget/v/Ramstack.FileSystem.Abstractions.svg)](https://nuget.org/packages/Ramstack.FileSystem.Abstractions) -[![MIT](https://img.shields.io/github/license/rameel/ramstack.filesystem)](https://github.com/rameel/ramstack.filesystem/blob/main/LICENSE) - -Provides a virtual file system abstraction. - -**For current implementations, see [Related Projects](#related-projects)** - -## Getting Started - -To install the `Ramstack.FileSystem.Abstractions` [NuGet package](https://www.nuget.org/packages/Ramstack.FileSystem.Abstractions) -in your project, run the following command: - -```bash -dotnet add package Ramstack.FileSystem.Abstractions -``` - -## Overview - -The primary interface is `IVirtualFileSystem`, which exposes methods to: -- Access files (`VirtualFile`). -- Access directories (`VirtualDirectory`). - -### VirtualFile - -The `VirtualFile` class provides properties and methods for creating, deleting, copying, and opening files within the virtual file system. - -```csharp -using Ramstack.FileSystem; - -public class Program -{ - public static async Task Sample(IVirtualFileSystem fs) - { - var file = fs.GetFile("/sample/hello.txt"); - - Console.WriteLine($"Writing 'Hello, World!' to '{file.FullName}'"); - - await using (Stream stream = await file.OpenWriteAsync()) - await using (StreamWriter writer = new StreamWriter(stream)) - { - await writer.WriteLineAsync("Hello, World!"); - } - - Console.WriteLine($"Reading contents of '{file.FullName}'"); - using (StreamReader reader = await file.OpenTextAsync()) - { - Console.WriteLine(await reader.ReadToEndAsync()); - } - - // Replace content of the file with data from an existing file - await using FileStream input = File.OpenRead(@"D:\sample\hello-world.txt"); - await file.WriteAsync(input, overwrite: true); - - Console.WriteLine($"The file '{file.Name}' has a length of {await file.GetLengthAsync()} bytes"); - - Console.WriteLine($"Deleting file '{file.FullName}'"); - await file.DeleteAsync(); - } -} -``` - -The `VirtualFile` class provides the following properties to retrieve information about a file: -- `FileSystem`: Gets the `IVirtualFileSystem` associated with the file. -- `Directory`: Gets the `VirtualDirectory` representing the parent directory. -- `DirectoryName`: Gets the full path of the parent directory. -- `IsReadOnly`: Indicates whether the file is read-only. -- `Name`: Gets the name of the file. -- `Extension`: Gets the extension of the file. - -The `VirtualFile` class also exposes a `GetPropertiesAsync` method to retrieve file properties: -- `Length`: Gets the size of the file in bytes. -- `Exists`: Indicates whether the file exists. -- `CreationTime`: Gets the creation time of the file. -- `LastAccessTime`: Gets the last access time of the file. -- `LastWriteTime`: Gets the last modification time of the file. - -For convenience, many methods specific to `VirtualFile` are also available as extension methods on `IVirtualFileSystem`. - -For example, if we know the path of the file we want to read, we don't need to obtain a `VirtualFile` object explicitly: -```csharp -using StreamReader reader = await fs.OpenTextAsync("/docs/README", Encoding.UTF8); -Console.WriteLine(await reader.ReadToEndAsync()); -``` - -### VirtualDirectory - -The `VirtualDirectory` class provides properties and methods for creating, deleting, and enumerating directories and subdirectories. - -```csharp -public static async Task PrintFilesAsync(VirtualDirectory directory, string padding = "", CancellationToken cancellationToken = default) -{ - await foreach (VirtualNode node in directory.GetFileNodesAsync(cancellationToken)) - { - Console.WriteLine($"{padding}{node.Name}"); - - if (node is VirtualDirectory dir) - { - await PrintFilesAsync(dir, padding + " ", cancellationToken); - } - } -} - -// Print the file tree -await PrintFilesAsync(fs.GetDirectory("/sample")); -``` - -The `VirtualDirectory` class provides the following properties to retrieve information about a directory: -- `FileSystem`: Gets the `IVirtualFileSystem` associated with the directory. -- `IsRoot`: Indicates whether the directory is the root directory. -- `Parent`: Gets the `VirtualDirectory` representing the parent directory. -- `DirectoryName`: Gets the full path of the parent directory. -- `IsReadOnly`: Indicates whether the directory is read-only. -- `Name`: Gets the name of the directory. - -The `VirtualDirectory` class also exposes a `GetPropertiesAsync` method to retrieve directory properties, if available: -- `Exists`: Indicates whether the directory exists. -- `CreationTime`: Gets the creation time of the directory. -- `LastAccessTime`: Gets the last access time of the directory. -- `LastWriteTime`: Gets the last modification time of the directory. -- `Length`: Always returns `0` for directories. - -For convenience, many methods specific to `VirtualDirectory` are also available as extension methods on `IVirtualFileSystem`. - -For example, if we know the path of the desired directory, we don't need to obtain a `VirtualDirectory` object explicitly: -```csharp -await foreach (VirtualFile file in fs.GetFilesAsync("/sample/directory")) -{ - Console.WriteLine($"Processing: {file.FullName}"); -} - -// Delete the directory recursively -await fs.DeleteDirectoryAsync("/sample/directory"); -``` - -## Remarks - -The file system in use may be read-only, and as a result, any modifying operations on files and directories will throw an exception. -To check if the file system is read-only, the `IVirtualFileSystem`, `VirtualFile`, and `VirtualDirectory` classes provide the `IsReadOnly` property. - -```csharp -if (!fs.IsReadOnly) -{ - await fs.DeleteFileAsync("/hello.txt"); -} -``` - -## Related Projects -- [Ramstack.FileSystem.Physical](https://www.nuget.org/packages/Ramstack.FileSystem.Physical) - Provides an implementation based on the local file system. -- [Ramstack.FileSystem.Azure](https://www.nuget.org/packages/Ramstack.FileSystem.Azure) - Provides an implementation using Azure Blob Storage. -- [Ramstack.FileSystem.Amazon](https://www.nuget.org/packages/Ramstack.FileSystem.Amazon) - Provides an implementation using Amazon S3 storage. -- [Ramstack.FileSystem.Google](https://www.nuget.org/packages/Ramstack.FileSystem.Google) - Provides an implementation using Google Cloud Storage. -- [Ramstack.FileSystem.Readonly](https://www.nuget.org/packages/Ramstack.FileSystem.Readonly) - Provides a read-only wrapper for the underlying file system. -- [Ramstack.FileSystem.Globbing](https://www.nuget.org/packages/Ramstack.FileSystem.Globbing) - Wraps the file system, filtering files and directories using glob patterns. -- [Ramstack.FileSystem.Prefixed](https://www.nuget.org/packages/Ramstack.FileSystem.Prefixed) - Adds a prefix to file paths within the underlying file system. -- [Ramstack.FileSystem.Sub](https://www.nuget.org/packages/Ramstack.FileSystem.Sub) - Wraps the underlying file system, restricting access to a specific subpath. -- [Ramstack.FileSystem.Adapters](https://www.nuget.org/packages/Ramstack.FileSystem.Adapters) - Provides integration with `Microsoft.Extensions.FileProviders`. -- [Ramstack.FileSystem.Composite](https://www.nuget.org/packages/Ramstack.FileSystem.Composite) - Provides an implementation that combines multiple file systems into a single composite file system. - -## 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](LICENSE) file for more details. +# Ramstack.FileSystem.Abstractions +[![NuGet](https://img.shields.io/nuget/v/Ramstack.FileSystem.Abstractions.svg)](https://nuget.org/packages/Ramstack.FileSystem.Abstractions) +[![MIT](https://img.shields.io/github/license/rameel/ramstack.filesystem)](https://github.com/rameel/ramstack.filesystem/blob/main/LICENSE) + +Provides a virtual file system abstraction. + +**For current implementations, see [Related Projects](#related-projects)** + +## Getting Started + +To install the `Ramstack.FileSystem.Abstractions` [NuGet package](https://www.nuget.org/packages/Ramstack.FileSystem.Abstractions) +in your project, run the following command: + +```bash +dotnet add package Ramstack.FileSystem.Abstractions +``` + +## Overview + +The primary interface is `IVirtualFileSystem`, which exposes methods to: +- Access files (`VirtualFile`). +- Access directories (`VirtualDirectory`). + +### VirtualFile + +The `VirtualFile` class provides properties and methods for creating, deleting, copying, and opening files within the virtual file system. + +```csharp +using Ramstack.FileSystem; + +public class Program +{ + public static async Task Sample(IVirtualFileSystem fs) + { + var file = fs.GetFile("/sample/hello.txt"); + + Console.WriteLine($"Writing 'Hello, World!' to '{file.FullName}'"); + + await using (Stream stream = await file.OpenWriteAsync()) + await using (StreamWriter writer = new StreamWriter(stream)) + { + await writer.WriteLineAsync("Hello, World!"); + } + + Console.WriteLine($"Reading contents of '{file.FullName}'"); + using (StreamReader reader = await file.OpenTextAsync()) + { + Console.WriteLine(await reader.ReadToEndAsync()); + } + + // Replace content of the file with data from an existing file + await using FileStream input = File.OpenRead(@"D:\sample\hello-world.txt"); + await file.WriteAsync(input, overwrite: true); + + Console.WriteLine($"The file '{file.Name}' has a length of {await file.GetLengthAsync()} bytes"); + + Console.WriteLine($"Deleting file '{file.FullName}'"); + await file.DeleteAsync(); + } +} +``` + +The `VirtualFile` class provides the following properties to retrieve information about a file: +- `FileSystem`: Gets the `IVirtualFileSystem` associated with the file. +- `Directory`: Gets the `VirtualDirectory` representing the parent directory. +- `DirectoryName`: Gets the full path of the parent directory. +- `IsReadOnly`: Indicates whether the file is read-only. +- `Name`: Gets the name of the file. +- `Extension`: Gets the extension of the file. + +The `VirtualFile` class also exposes a `GetPropertiesAsync` method to retrieve file properties: +- `Length`: Gets the size of the file in bytes. +- `Exists`: Indicates whether the file exists. +- `CreationTime`: Gets the creation time of the file. +- `LastAccessTime`: Gets the last access time of the file. +- `LastWriteTime`: Gets the last modification time of the file. + +For convenience, many methods specific to `VirtualFile` are also available as extension methods on `IVirtualFileSystem`. + +For example, if we know the path of the file we want to read, we don't need to obtain a `VirtualFile` object explicitly: +```csharp +using StreamReader reader = await fs.OpenTextAsync("/docs/README", Encoding.UTF8); +Console.WriteLine(await reader.ReadToEndAsync()); +``` + +### VirtualDirectory + +The `VirtualDirectory` class provides properties and methods for creating, deleting, and enumerating directories and subdirectories. + +```csharp +public static async Task PrintFilesAsync(VirtualDirectory directory, string padding = "", CancellationToken cancellationToken = default) +{ + await foreach (VirtualNode node in directory.GetFileNodesAsync(cancellationToken)) + { + Console.WriteLine($"{padding}{node.Name}"); + + if (node is VirtualDirectory dir) + { + await PrintFilesAsync(dir, padding + " ", cancellationToken); + } + } +} + +// Print the file tree +await PrintFilesAsync(fs.GetDirectory("/sample")); +``` + +The `VirtualDirectory` class provides the following properties to retrieve information about a directory: +- `FileSystem`: Gets the `IVirtualFileSystem` associated with the directory. +- `IsRoot`: Indicates whether the directory is the root directory. +- `Parent`: Gets the `VirtualDirectory` representing the parent directory. +- `DirectoryName`: Gets the full path of the parent directory. +- `IsReadOnly`: Indicates whether the directory is read-only. +- `Name`: Gets the name of the directory. + +The `VirtualDirectory` class also exposes a `GetPropertiesAsync` method to retrieve directory properties, if available: +- `Exists`: Indicates whether the directory exists. +- `CreationTime`: Gets the creation time of the directory. +- `LastAccessTime`: Gets the last access time of the directory. +- `LastWriteTime`: Gets the last modification time of the directory. +- `Length`: Always returns `0` for directories. + +For convenience, many methods specific to `VirtualDirectory` are also available as extension methods on `IVirtualFileSystem`. + +For example, if we know the path of the desired directory, we don't need to obtain a `VirtualDirectory` object explicitly: +```csharp +await foreach (VirtualFile file in fs.GetFilesAsync("/sample/directory")) +{ + Console.WriteLine($"Processing: {file.FullName}"); +} + +// Delete the directory recursively +await fs.DeleteDirectoryAsync("/sample/directory"); +``` + +## Remarks + +The file system in use may be read-only, and as a result, any modifying operations on files and directories will throw an exception. +To check if the file system is read-only, the `IVirtualFileSystem`, `VirtualFile`, and `VirtualDirectory` classes provide the `IsReadOnly` property. + +```csharp +if (!fs.IsReadOnly) +{ + await fs.DeleteFileAsync("/hello.txt"); +} +``` + +## Related Projects +- [Ramstack.FileSystem.Physical](https://www.nuget.org/packages/Ramstack.FileSystem.Physical) - Provides an implementation based on the local file system. +- [Ramstack.FileSystem.Azure](https://www.nuget.org/packages/Ramstack.FileSystem.Azure) - Provides an implementation using Azure Blob Storage. +- [Ramstack.FileSystem.Amazon](https://www.nuget.org/packages/Ramstack.FileSystem.Amazon) - Provides an implementation using Amazon S3 storage. +- [Ramstack.FileSystem.Google](https://www.nuget.org/packages/Ramstack.FileSystem.Google) - Provides an implementation using Google Cloud Storage. +- [Ramstack.FileSystem.Readonly](https://www.nuget.org/packages/Ramstack.FileSystem.Readonly) - Provides a read-only wrapper for the underlying file system. +- [Ramstack.FileSystem.Globbing](https://www.nuget.org/packages/Ramstack.FileSystem.Globbing) - Wraps the file system, filtering files and directories using glob patterns. +- [Ramstack.FileSystem.Prefixed](https://www.nuget.org/packages/Ramstack.FileSystem.Prefixed) - Adds a prefix to file paths within the underlying file system. +- [Ramstack.FileSystem.Sub](https://www.nuget.org/packages/Ramstack.FileSystem.Sub) - Wraps the underlying file system, restricting access to a specific subpath. +- [Ramstack.FileSystem.Adapters](https://www.nuget.org/packages/Ramstack.FileSystem.Adapters) - Provides integration with `Microsoft.Extensions.FileProviders`. +- [Ramstack.FileSystem.Composite](https://www.nuget.org/packages/Ramstack.FileSystem.Composite) - Provides an implementation that combines multiple file systems into a single composite file system. + +## 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](LICENSE) file for more details. diff --git a/src/Ramstack.FileSystem.Abstractions/Ramstack.FileSystem.Abstractions.csproj b/src/Ramstack.FileSystem.Abstractions/Ramstack.FileSystem.Abstractions.csproj index 51eb9b4..6f50f5e 100644 --- a/src/Ramstack.FileSystem.Abstractions/Ramstack.FileSystem.Abstractions.csproj +++ b/src/Ramstack.FileSystem.Abstractions/Ramstack.FileSystem.Abstractions.csproj @@ -1,64 +1,64 @@ - - - - net6.0;net8.0 - A .NET library that provides a virtual file system abstraction. - enable - enable - preview - true - true - Ramstack.FileSystem - - - - 649;1591 - CS8600;CS8601;CS8602;CS8603;CS8604;CS8610;CS8613;CS8614;CS8618;CS8619;CS8620;CS8625;CS8643;CS8653;CS8714 - - - - https://github.com/rameel/ramstack.filesystem - Rameel - filesystem;fileprovider - https://github.com/rameel/ramstack.filesystem#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\README.md - True - \ - - - - + + + + net6.0;net8.0 + A .NET library that provides a virtual file system abstraction. + enable + enable + preview + true + true + Ramstack.FileSystem + + + + 649;1591 + CS8600;CS8601;CS8602;CS8603;CS8604;CS8610;CS8613;CS8614;CS8618;CS8619;CS8620;CS8625;CS8643;CS8653;CS8714 + + + + https://github.com/rameel/ramstack.filesystem + Rameel + filesystem;fileprovider + https://github.com/rameel/ramstack.filesystem#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\README.md + True + \ + + + + diff --git a/src/Ramstack.FileSystem.Abstractions/Utilities/EnumerableExtensions.cs b/src/Ramstack.FileSystem.Abstractions/Utilities/EnumerableExtensions.cs index 613dce0..5cbe63d 100644 --- a/src/Ramstack.FileSystem.Abstractions/Utilities/EnumerableExtensions.cs +++ b/src/Ramstack.FileSystem.Abstractions/Utilities/EnumerableExtensions.cs @@ -1,64 +1,64 @@ -namespace Ramstack.FileSystem.Utilities; - -/// -/// Provides extension methods for the . -/// -internal static class EnumerableExtensions -{ - /// - /// Converts an enumerable sequence to an async-enumerable sequence. - /// - /// The type of the elements in the source sequence. - /// The enumerable sequence to convert to an async-enumerable sequence. - /// - /// The async-enumerable sequence whose elements are pulled from the given enumerable sequence. - /// - public static IAsyncEnumerable ToAsyncEnumerable(this IEnumerable source) => - new AsyncEnumerableAdapter(source); - - #region Inner type: AsyncEnumerableAdapter - - /// - /// Wraps the to the . - /// - /// The type of the elements in the collection. - /// The to wrap. - private sealed class AsyncEnumerableAdapter(IEnumerable source) : IAsyncEnumerable - { - /// - public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken) => - new AsyncEnumeratorAdapter(source.GetEnumerator(), cancellationToken); - } - - #endregion - - #region Inner type: AsyncEnumeratorAdapter - - /// - /// Wraps the to the . - /// - /// The type of the elements in the collection. - /// The to wrap. - /// An optional cancellation token to cancel the operation. - private sealed class AsyncEnumeratorAdapter(IEnumerator enumerator, CancellationToken cancellationToken) : IAsyncEnumerator - { - /// - public T Current => enumerator.Current; - - /// - public ValueTask MoveNextAsync() - { - cancellationToken.ThrowIfCancellationRequested(); - return new ValueTask(enumerator.MoveNext()); - } - - /// - public ValueTask DisposeAsync() - { - enumerator.Dispose(); - return default; - } - } - - #endregion -} +namespace Ramstack.FileSystem.Utilities; + +/// +/// Provides extension methods for the . +/// +internal static class EnumerableExtensions +{ + /// + /// Converts an enumerable sequence to an async-enumerable sequence. + /// + /// The type of the elements in the source sequence. + /// The enumerable sequence to convert to an async-enumerable sequence. + /// + /// The async-enumerable sequence whose elements are pulled from the given enumerable sequence. + /// + public static IAsyncEnumerable ToAsyncEnumerable(this IEnumerable source) => + new AsyncEnumerableAdapter(source); + + #region Inner type: AsyncEnumerableAdapter + + /// + /// Wraps the to the . + /// + /// The type of the elements in the collection. + /// The to wrap. + private sealed class AsyncEnumerableAdapter(IEnumerable source) : IAsyncEnumerable + { + /// + public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken) => + new AsyncEnumeratorAdapter(source.GetEnumerator(), cancellationToken); + } + + #endregion + + #region Inner type: AsyncEnumeratorAdapter + + /// + /// Wraps the to the . + /// + /// The type of the elements in the collection. + /// The to wrap. + /// An optional cancellation token to cancel the operation. + private sealed class AsyncEnumeratorAdapter(IEnumerator enumerator, CancellationToken cancellationToken) : IAsyncEnumerator + { + /// + public T Current => enumerator.Current; + + /// + public ValueTask MoveNextAsync() + { + cancellationToken.ThrowIfCancellationRequested(); + return new ValueTask(enumerator.MoveNext()); + } + + /// + public ValueTask DisposeAsync() + { + enumerator.Dispose(); + return default; + } + } + + #endregion +} diff --git a/src/Ramstack.FileSystem.Abstractions/Utilities/PathTokenizer.cs b/src/Ramstack.FileSystem.Abstractions/Utilities/PathTokenizer.cs index 879a53d..8bd3ca1 100644 --- a/src/Ramstack.FileSystem.Abstractions/Utilities/PathTokenizer.cs +++ b/src/Ramstack.FileSystem.Abstractions/Utilities/PathTokenizer.cs @@ -1,94 +1,94 @@ -namespace Ramstack.FileSystem.Utilities; - -/// -/// 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 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. - 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 -} +namespace Ramstack.FileSystem.Utilities; + +/// +/// 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 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. + 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.FileSystem.Abstractions/VirtualDirectory.cs b/src/Ramstack.FileSystem.Abstractions/VirtualDirectory.cs index f9f2dfc..55888a7 100644 --- a/src/Ramstack.FileSystem.Abstractions/VirtualDirectory.cs +++ b/src/Ramstack.FileSystem.Abstractions/VirtualDirectory.cs @@ -1,289 +1,289 @@ -using Ramstack.Globbing; -using Ramstack.Globbing.Traversal; - -namespace Ramstack.FileSystem; - -/// -/// Represents a virtual directory within a specified file system. -/// -[DebuggerDisplay("{FullName,nq}")] -public abstract class VirtualDirectory : VirtualNode -{ - /// - /// Gets a value indicating whether the current directory is the root directory. - /// - public bool IsRoot => FullName == "/"; - - /// - /// Gets a instance representing the parent directory. - /// - public VirtualDirectory? Parent - { - get - { - if (IsRoot) - return null; - - var directoryName = VirtualPath.GetDirectoryName(FullName); - return FileSystem.GetDirectory(directoryName); - } - } - - /// - /// Initializes a new instance of the class with the specified path. - /// - /// The full path of the directory. - protected VirtualDirectory(string path) : base(path) - { - } - - /// - /// Initializes a new instance of the class with the specified path and properties. - /// - /// The full path of the directory. - /// The properties of the directory. - protected VirtualDirectory(string path, VirtualNodeProperties? properties) : base(path, properties) - { - } - - /// - /// Asynchronously creates the current directory. - /// - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation. - /// - public ValueTask CreateAsync(CancellationToken cancellationToken = default) - { - EnsureWritable(); - Refresh(); - - return CreateCoreAsync(cancellationToken); - } - - /// - /// Asynchronously deletes the current directory, including all its subdirectories and files. - /// - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation. - /// - public ValueTask DeleteAsync(CancellationToken cancellationToken = default) - { - EnsureWritable(); - Refresh(); - - return DeleteCoreAsync(cancellationToken); - } - - /// - /// Asynchronously returns an async-enumerable collection of file nodes (both directories and files) within the current directory. - /// - /// An optional cancellation token to cancel the operation. - /// - /// An async-enumerable collection of instances. - /// - public IAsyncEnumerable GetFileNodesAsync(CancellationToken cancellationToken = default) => - GetFileNodesCoreAsync(cancellationToken); - - /// - /// Asynchronously returns an async-enumerable collection of files within the current directory. - /// - /// An optional cancellation token to cancel the operation. - /// - /// An async-enumerable collection of instances. - /// - public IAsyncEnumerable GetFilesAsync(CancellationToken cancellationToken = default) => - GetFilesCoreAsync(cancellationToken); - - /// - /// Asynchronously returns an async-enumerable collection of directories within the current directory. - /// - /// An optional cancellation token to cancel the operation. - /// - /// An async-enumerable collection of instances. - /// - public IAsyncEnumerable GetDirectoriesAsync(CancellationToken cancellationToken = default) => - GetDirectoriesCoreAsync(cancellationToken); - - /// - /// Asynchronously returns an async-enumerable collection of file nodes (both directories and files) - /// within the current directory that match any of the specified glob patterns. - /// - /// An array of glob patterns to match against the names of file nodes. - /// An optional array of glob patterns to exclude file nodes. - /// An optional cancellation token to cancel the operation. - /// - /// An async-enumerable collection of instances. - /// - public IAsyncEnumerable GetFileNodesAsync(string[] patterns, string[]? excludes, CancellationToken cancellationToken = default) - { - ArgumentNullException.ThrowIfNull(patterns); - return GetFileNodesCoreAsync(patterns, excludes, cancellationToken); - } - - /// - /// Asynchronously returns an async-enumerable collection of files within the current directory - /// that match any of the specified glob patterns. - /// - /// An array of glob patterns to match against the names of files. - /// An optional array of glob patterns to exclude files. - /// An optional cancellation token to cancel the operation. - /// - /// An async-enumerable collection of instances. - /// - public IAsyncEnumerable GetFilesAsync(string[] patterns, string[]? excludes, CancellationToken cancellationToken = default) - { - ArgumentNullException.ThrowIfNull(patterns); - return GetFilesCoreAsync(patterns, excludes, cancellationToken); - } - - /// - /// Asynchronously returns an async-enumerable collection of directories within the current directory - /// that match any of the specified glob patterns. - /// - /// An array of glob patterns to match against the names of directories. - /// An optional array of glob patterns to exclude directories. - /// An optional cancellation token to cancel the operation. - /// - /// An async-enumerable collection of instances. - /// - public IAsyncEnumerable GetDirectoriesAsync(string[] patterns, string[]? excludes, CancellationToken cancellationToken = default) - { - ArgumentNullException.ThrowIfNull(patterns); - return GetDirectoriesCoreAsync(patterns, excludes, cancellationToken); - } - - /// - /// Core implementation for asynchronously creating the current directory. - /// - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation. - /// - protected abstract ValueTask CreateCoreAsync(CancellationToken cancellationToken); - - /// - /// Core implementation for asynchronously deleting the current directory, including all its subdirectories and files. - /// - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation. - /// - protected abstract ValueTask DeleteCoreAsync(CancellationToken cancellationToken); - - /// - /// Core implementation for asynchronously returning an async-enumerable collection of file nodes (both directories and files) within the current directory. - /// - /// An optional cancellation token to cancel the operation. - /// - /// An async-enumerable collection of instances. - /// - protected abstract IAsyncEnumerable GetFileNodesCoreAsync(CancellationToken cancellationToken); - - /// - /// Core implementation for asynchronously returning an async-enumerable collection of files within the current directory. - /// - /// An optional cancellation token to cancel the operation. - /// - /// An async-enumerable collection of instances. - /// - protected virtual async IAsyncEnumerable GetFilesCoreAsync([EnumeratorCancellation] CancellationToken cancellationToken) - { - await foreach (var node in GetFileNodesCoreAsync(cancellationToken).ConfigureAwait(false)) - { - if (node is VirtualFile file) - yield return file; - } - } - - /// - /// Core implementation for asynchronously returning an async-enumerable collection of directories within the current directory. - /// - /// An optional cancellation token to cancel the operation. - /// - /// An async-enumerable collection of instances. - /// - protected virtual async IAsyncEnumerable GetDirectoriesCoreAsync([EnumeratorCancellation] CancellationToken cancellationToken) - { - await foreach (var node in GetFileNodesCoreAsync(cancellationToken).ConfigureAwait(false)) - { - if (node is VirtualDirectory directory) - yield return directory; - } - } - - /// - /// Core implementation for asynchronously returning an async-enumerable collection of file nodes (both directories and files) - /// within the current directory that match any of the specified glob patterns. - /// - /// An array of glob patterns to match against the names of file nodes. - /// An optional array of glob patterns to exclude file nodes. - /// An optional cancellation token to cancel the operation. - /// - /// An async-enumerable collection of instances. - /// - protected virtual IAsyncEnumerable GetFileNodesCoreAsync(string[] patterns, string[]? excludes, CancellationToken cancellationToken) - { - return new FileTreeAsyncEnumerable(this, cancellationToken) - { - Patterns = patterns, - Excludes = excludes ?? [], - Flags = MatchFlags.Unix, - FileNameSelector = node => node.Name, - ShouldRecursePredicate = node => node is VirtualDirectory, - ChildrenSelector = (node, token) => ((VirtualDirectory)node).GetFileNodesCoreAsync(token), - ResultSelector = node => node - }; - } - - /// - /// Core implementation for asynchronously returning an async-enumerable collection of files within the current directory - /// that match any of the specified glob patterns. - /// - /// An array of glob patterns to match against the names of files. - /// An optional array of glob patterns to exclude files. - /// An optional cancellation token to cancel the operation. - /// - /// An async-enumerable collection of instances. - /// - protected virtual IAsyncEnumerable GetFilesCoreAsync(string[] patterns, string[]? excludes, CancellationToken cancellationToken) - { - return new FileTreeAsyncEnumerable(this, cancellationToken) - { - Patterns = patterns, - Excludes = excludes ?? [], - Flags = MatchFlags.Unix, - FileNameSelector = node => node.Name, - ShouldIncludePredicate = node => node is VirtualFile, - ShouldRecursePredicate = node => node is VirtualDirectory, - ChildrenSelector = (node, token) => ((VirtualDirectory)node).GetFileNodesCoreAsync(token), - ResultSelector = node => (VirtualFile)node - }; - } - - /// - /// Core implementation for asynchronously returning an async-enumerable collection of directories within the current directory - /// that match any of the specified glob patterns. - /// - /// An array of glob patterns to match against the names of directories. - /// An optional array of glob patterns to exclude directories. - /// An optional cancellation token to cancel the operation. - /// - /// An async-enumerable collection of instances. - /// - protected virtual IAsyncEnumerable GetDirectoriesCoreAsync(string[] patterns, string[]? excludes, CancellationToken cancellationToken) - { - return new FileTreeAsyncEnumerable(this, cancellationToken) - { - Patterns = patterns, - Excludes = excludes ?? [], - Flags = MatchFlags.Unix, - FileNameSelector = node => node.Name, - ShouldIncludePredicate = node => node is VirtualDirectory, - ShouldRecursePredicate = node => node is VirtualDirectory, - ChildrenSelector = (node, token) => ((VirtualDirectory)node).GetFileNodesCoreAsync(token), - ResultSelector = node => (VirtualDirectory)node - }; - } -} +using Ramstack.Globbing; +using Ramstack.Globbing.Traversal; + +namespace Ramstack.FileSystem; + +/// +/// Represents a virtual directory within a specified file system. +/// +[DebuggerDisplay("{FullName,nq}")] +public abstract class VirtualDirectory : VirtualNode +{ + /// + /// Gets a value indicating whether the current directory is the root directory. + /// + public bool IsRoot => FullName == "/"; + + /// + /// Gets a instance representing the parent directory. + /// + public VirtualDirectory? Parent + { + get + { + if (IsRoot) + return null; + + var directoryName = VirtualPath.GetDirectoryName(FullName); + return FileSystem.GetDirectory(directoryName); + } + } + + /// + /// Initializes a new instance of the class with the specified path. + /// + /// The full path of the directory. + protected VirtualDirectory(string path) : base(path) + { + } + + /// + /// Initializes a new instance of the class with the specified path and properties. + /// + /// The full path of the directory. + /// The properties of the directory. + protected VirtualDirectory(string path, VirtualNodeProperties? properties) : base(path, properties) + { + } + + /// + /// Asynchronously creates the current directory. + /// + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation. + /// + public ValueTask CreateAsync(CancellationToken cancellationToken = default) + { + EnsureWritable(); + Refresh(); + + return CreateCoreAsync(cancellationToken); + } + + /// + /// Asynchronously deletes the current directory, including all its subdirectories and files. + /// + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation. + /// + public ValueTask DeleteAsync(CancellationToken cancellationToken = default) + { + EnsureWritable(); + Refresh(); + + return DeleteCoreAsync(cancellationToken); + } + + /// + /// Asynchronously returns an async-enumerable collection of file nodes (both directories and files) within the current directory. + /// + /// An optional cancellation token to cancel the operation. + /// + /// An async-enumerable collection of instances. + /// + public IAsyncEnumerable GetFileNodesAsync(CancellationToken cancellationToken = default) => + GetFileNodesCoreAsync(cancellationToken); + + /// + /// Asynchronously returns an async-enumerable collection of files within the current directory. + /// + /// An optional cancellation token to cancel the operation. + /// + /// An async-enumerable collection of instances. + /// + public IAsyncEnumerable GetFilesAsync(CancellationToken cancellationToken = default) => + GetFilesCoreAsync(cancellationToken); + + /// + /// Asynchronously returns an async-enumerable collection of directories within the current directory. + /// + /// An optional cancellation token to cancel the operation. + /// + /// An async-enumerable collection of instances. + /// + public IAsyncEnumerable GetDirectoriesAsync(CancellationToken cancellationToken = default) => + GetDirectoriesCoreAsync(cancellationToken); + + /// + /// Asynchronously returns an async-enumerable collection of file nodes (both directories and files) + /// within the current directory that match any of the specified glob patterns. + /// + /// An array of glob patterns to match against the names of file nodes. + /// An optional array of glob patterns to exclude file nodes. + /// An optional cancellation token to cancel the operation. + /// + /// An async-enumerable collection of instances. + /// + public IAsyncEnumerable GetFileNodesAsync(string[] patterns, string[]? excludes, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(patterns); + return GetFileNodesCoreAsync(patterns, excludes, cancellationToken); + } + + /// + /// Asynchronously returns an async-enumerable collection of files within the current directory + /// that match any of the specified glob patterns. + /// + /// An array of glob patterns to match against the names of files. + /// An optional array of glob patterns to exclude files. + /// An optional cancellation token to cancel the operation. + /// + /// An async-enumerable collection of instances. + /// + public IAsyncEnumerable GetFilesAsync(string[] patterns, string[]? excludes, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(patterns); + return GetFilesCoreAsync(patterns, excludes, cancellationToken); + } + + /// + /// Asynchronously returns an async-enumerable collection of directories within the current directory + /// that match any of the specified glob patterns. + /// + /// An array of glob patterns to match against the names of directories. + /// An optional array of glob patterns to exclude directories. + /// An optional cancellation token to cancel the operation. + /// + /// An async-enumerable collection of instances. + /// + public IAsyncEnumerable GetDirectoriesAsync(string[] patterns, string[]? excludes, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(patterns); + return GetDirectoriesCoreAsync(patterns, excludes, cancellationToken); + } + + /// + /// Core implementation for asynchronously creating the current directory. + /// + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation. + /// + protected abstract ValueTask CreateCoreAsync(CancellationToken cancellationToken); + + /// + /// Core implementation for asynchronously deleting the current directory, including all its subdirectories and files. + /// + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation. + /// + protected abstract ValueTask DeleteCoreAsync(CancellationToken cancellationToken); + + /// + /// Core implementation for asynchronously returning an async-enumerable collection of file nodes (both directories and files) within the current directory. + /// + /// An optional cancellation token to cancel the operation. + /// + /// An async-enumerable collection of instances. + /// + protected abstract IAsyncEnumerable GetFileNodesCoreAsync(CancellationToken cancellationToken); + + /// + /// Core implementation for asynchronously returning an async-enumerable collection of files within the current directory. + /// + /// An optional cancellation token to cancel the operation. + /// + /// An async-enumerable collection of instances. + /// + protected virtual async IAsyncEnumerable GetFilesCoreAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + await foreach (var node in GetFileNodesCoreAsync(cancellationToken).ConfigureAwait(false)) + { + if (node is VirtualFile file) + yield return file; + } + } + + /// + /// Core implementation for asynchronously returning an async-enumerable collection of directories within the current directory. + /// + /// An optional cancellation token to cancel the operation. + /// + /// An async-enumerable collection of instances. + /// + protected virtual async IAsyncEnumerable GetDirectoriesCoreAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + await foreach (var node in GetFileNodesCoreAsync(cancellationToken).ConfigureAwait(false)) + { + if (node is VirtualDirectory directory) + yield return directory; + } + } + + /// + /// Core implementation for asynchronously returning an async-enumerable collection of file nodes (both directories and files) + /// within the current directory that match any of the specified glob patterns. + /// + /// An array of glob patterns to match against the names of file nodes. + /// An optional array of glob patterns to exclude file nodes. + /// An optional cancellation token to cancel the operation. + /// + /// An async-enumerable collection of instances. + /// + protected virtual IAsyncEnumerable GetFileNodesCoreAsync(string[] patterns, string[]? excludes, CancellationToken cancellationToken) + { + return new FileTreeAsyncEnumerable(this, cancellationToken) + { + Patterns = patterns, + Excludes = excludes ?? [], + Flags = MatchFlags.Unix, + FileNameSelector = node => node.Name, + ShouldRecursePredicate = node => node is VirtualDirectory, + ChildrenSelector = (node, token) => ((VirtualDirectory)node).GetFileNodesCoreAsync(token), + ResultSelector = node => node + }; + } + + /// + /// Core implementation for asynchronously returning an async-enumerable collection of files within the current directory + /// that match any of the specified glob patterns. + /// + /// An array of glob patterns to match against the names of files. + /// An optional array of glob patterns to exclude files. + /// An optional cancellation token to cancel the operation. + /// + /// An async-enumerable collection of instances. + /// + protected virtual IAsyncEnumerable GetFilesCoreAsync(string[] patterns, string[]? excludes, CancellationToken cancellationToken) + { + return new FileTreeAsyncEnumerable(this, cancellationToken) + { + Patterns = patterns, + Excludes = excludes ?? [], + Flags = MatchFlags.Unix, + FileNameSelector = node => node.Name, + ShouldIncludePredicate = node => node is VirtualFile, + ShouldRecursePredicate = node => node is VirtualDirectory, + ChildrenSelector = (node, token) => ((VirtualDirectory)node).GetFileNodesCoreAsync(token), + ResultSelector = node => (VirtualFile)node + }; + } + + /// + /// Core implementation for asynchronously returning an async-enumerable collection of directories within the current directory + /// that match any of the specified glob patterns. + /// + /// An array of glob patterns to match against the names of directories. + /// An optional array of glob patterns to exclude directories. + /// An optional cancellation token to cancel the operation. + /// + /// An async-enumerable collection of instances. + /// + protected virtual IAsyncEnumerable GetDirectoriesCoreAsync(string[] patterns, string[]? excludes, CancellationToken cancellationToken) + { + return new FileTreeAsyncEnumerable(this, cancellationToken) + { + Patterns = patterns, + Excludes = excludes ?? [], + Flags = MatchFlags.Unix, + FileNameSelector = node => node.Name, + ShouldIncludePredicate = node => node is VirtualDirectory, + ShouldRecursePredicate = node => node is VirtualDirectory, + ChildrenSelector = (node, token) => ((VirtualDirectory)node).GetFileNodesCoreAsync(token), + ResultSelector = node => (VirtualDirectory)node + }; + } +} diff --git a/src/Ramstack.FileSystem.Abstractions/VirtualDirectoryExtensions.cs b/src/Ramstack.FileSystem.Abstractions/VirtualDirectoryExtensions.cs index 800c0d6..268911b 100644 --- a/src/Ramstack.FileSystem.Abstractions/VirtualDirectoryExtensions.cs +++ b/src/Ramstack.FileSystem.Abstractions/VirtualDirectoryExtensions.cs @@ -1,138 +1,138 @@ -namespace Ramstack.FileSystem; - -/// -/// Provides extension methods for the class. -/// -public static class VirtualDirectoryExtensions -{ - /// - /// Asynchronously returns an async-enumerable collection of file nodes (both directories and files) - /// within the specified directory that match the given glob pattern. - /// - /// The representing the directory to search. - /// The glob pattern to match against the names of file nodes. - /// An optional cancellation token to cancel the operation. - /// - /// An async-enumerable collection of instances. - /// - public static IAsyncEnumerable GetFileNodesAsync(this VirtualDirectory directory, string pattern, CancellationToken cancellationToken = default) => - directory.GetFileNodesAsync(pattern, exclude: null, cancellationToken); - - /// - /// Asynchronously returns an async-enumerable collection of file nodes (both directories and files) - /// within the specified directory that match the given glob pattern. - /// - /// The representing the directory to search. - /// The glob pattern to match against the names of file nodes. - /// An optional glob pattern to exclude certain file nodes. - /// An optional cancellation token to cancel the operation. - /// - /// An async-enumerable collection of instances. - /// - public static IAsyncEnumerable GetFileNodesAsync(this VirtualDirectory directory, string pattern, string? exclude, CancellationToken cancellationToken = default) - { - ArgumentNullException.ThrowIfNull(pattern); - - return directory.GetFileNodesAsync([pattern], exclude is not null ? [exclude] : null, cancellationToken); - } - - /// - /// Asynchronously returns an async-enumerable collection of file nodes (both directories and files) - /// within the specified directory that match any of the given glob patterns. - /// - /// The representing the directory to search. - /// An array of glob patterns to match against the names of file nodes. - /// An optional cancellation token to cancel the operation. - /// - /// An async-enumerable collection of instances. - /// - public static IAsyncEnumerable GetFileNodesAsync(this VirtualDirectory directory, string[] patterns, CancellationToken cancellationToken = default) => - directory.GetFileNodesAsync(patterns, excludes: null, cancellationToken); - - /// - /// Asynchronously returns an async-enumerable collection of files within the specified directory - /// that match the given glob pattern. - /// - /// The representing the directory to search. - /// The glob pattern to match against the names of files. - /// An optional cancellation token to cancel the operation. - /// - /// An async-enumerable collection of instances. - /// - public static IAsyncEnumerable GetFilesAsync(this VirtualDirectory directory, string pattern, CancellationToken cancellationToken = default) => - directory.GetFilesAsync(pattern, exclude: null, cancellationToken); - - /// - /// Asynchronously returns an async-enumerable collection of files within the specified directory - /// that match the given glob pattern. - /// - /// The representing the directory to search. - /// The glob pattern to match against the names of files. - /// An optional glob pattern to exclude certain files. - /// An optional cancellation token to cancel the operation. - /// - /// An async-enumerable collection of instances. - public static IAsyncEnumerable GetFilesAsync(this VirtualDirectory directory, string pattern, string? exclude, CancellationToken cancellationToken = default) - { - ArgumentNullException.ThrowIfNull(pattern); - - return directory.GetFilesAsync([pattern], exclude is not null ? [exclude] : null, cancellationToken); - } - - /// - /// Asynchronously returns an async-enumerable collection of files within the specified directory - /// that match any of the given glob patterns. - /// - /// The representing the directory to search. - /// An array of glob patterns to match against the names of files. - /// An optional cancellation token to cancel the operation. - /// - /// An async-enumerable collection of instances. - /// - public static IAsyncEnumerable GetFilesAsync(this VirtualDirectory directory, string[] patterns, CancellationToken cancellationToken = default) => - directory.GetFilesAsync(patterns, excludes: null, cancellationToken); - - /// - /// Asynchronously returns an async-enumerable collection of directories within the specified directory - /// that match the given glob pattern. - /// - /// The representing the directory to search. - /// The glob pattern to match against the names of directories. - /// An optional cancellation token to cancel the operation. - /// - /// An async-enumerable collection of instances. - /// - public static IAsyncEnumerable GetDirectoriesAsync(this VirtualDirectory directory, string pattern, CancellationToken cancellationToken = default) => - directory.GetDirectoriesAsync(pattern, exclude: null, cancellationToken); - - /// - /// Asynchronously returns an async-enumerable collection of directories within the specified directory - /// that match the given glob pattern. - /// - /// The representing the directory to search. - /// The glob pattern to match against the names of directories. - /// An optional glob pattern to exclude certain directories. - /// An optional cancellation token to cancel the operation. - /// - /// An async-enumerable collection of instances. - /// - public static IAsyncEnumerable GetDirectoriesAsync(this VirtualDirectory directory, string pattern, string? exclude, CancellationToken cancellationToken = default) - { - ArgumentNullException.ThrowIfNull(pattern); - - return directory.GetDirectoriesAsync([pattern], exclude is not null ? [exclude] : null, cancellationToken); - } - - /// - /// Asynchronously returns an async-enumerable collection of directories within the specified directory - /// that match any of the given glob patterns. - /// - /// The representing the directory to search. - /// An array of glob patterns to match against the names of directories. - /// An optional cancellation token to cancel the operation. - /// - /// An async-enumerable collection of instances. - /// - public static IAsyncEnumerable GetDirectoriesAsync(this VirtualDirectory directory, string[] patterns, CancellationToken cancellationToken = default) => - directory.GetDirectoriesAsync(patterns, excludes: null, cancellationToken); -} +namespace Ramstack.FileSystem; + +/// +/// Provides extension methods for the class. +/// +public static class VirtualDirectoryExtensions +{ + /// + /// Asynchronously returns an async-enumerable collection of file nodes (both directories and files) + /// within the specified directory that match the given glob pattern. + /// + /// The representing the directory to search. + /// The glob pattern to match against the names of file nodes. + /// An optional cancellation token to cancel the operation. + /// + /// An async-enumerable collection of instances. + /// + public static IAsyncEnumerable GetFileNodesAsync(this VirtualDirectory directory, string pattern, CancellationToken cancellationToken = default) => + directory.GetFileNodesAsync(pattern, exclude: null, cancellationToken); + + /// + /// Asynchronously returns an async-enumerable collection of file nodes (both directories and files) + /// within the specified directory that match the given glob pattern. + /// + /// The representing the directory to search. + /// The glob pattern to match against the names of file nodes. + /// An optional glob pattern to exclude certain file nodes. + /// An optional cancellation token to cancel the operation. + /// + /// An async-enumerable collection of instances. + /// + public static IAsyncEnumerable GetFileNodesAsync(this VirtualDirectory directory, string pattern, string? exclude, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(pattern); + + return directory.GetFileNodesAsync([pattern], exclude is not null ? [exclude] : null, cancellationToken); + } + + /// + /// Asynchronously returns an async-enumerable collection of file nodes (both directories and files) + /// within the specified directory that match any of the given glob patterns. + /// + /// The representing the directory to search. + /// An array of glob patterns to match against the names of file nodes. + /// An optional cancellation token to cancel the operation. + /// + /// An async-enumerable collection of instances. + /// + public static IAsyncEnumerable GetFileNodesAsync(this VirtualDirectory directory, string[] patterns, CancellationToken cancellationToken = default) => + directory.GetFileNodesAsync(patterns, excludes: null, cancellationToken); + + /// + /// Asynchronously returns an async-enumerable collection of files within the specified directory + /// that match the given glob pattern. + /// + /// The representing the directory to search. + /// The glob pattern to match against the names of files. + /// An optional cancellation token to cancel the operation. + /// + /// An async-enumerable collection of instances. + /// + public static IAsyncEnumerable GetFilesAsync(this VirtualDirectory directory, string pattern, CancellationToken cancellationToken = default) => + directory.GetFilesAsync(pattern, exclude: null, cancellationToken); + + /// + /// Asynchronously returns an async-enumerable collection of files within the specified directory + /// that match the given glob pattern. + /// + /// The representing the directory to search. + /// The glob pattern to match against the names of files. + /// An optional glob pattern to exclude certain files. + /// An optional cancellation token to cancel the operation. + /// + /// An async-enumerable collection of instances. + public static IAsyncEnumerable GetFilesAsync(this VirtualDirectory directory, string pattern, string? exclude, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(pattern); + + return directory.GetFilesAsync([pattern], exclude is not null ? [exclude] : null, cancellationToken); + } + + /// + /// Asynchronously returns an async-enumerable collection of files within the specified directory + /// that match any of the given glob patterns. + /// + /// The representing the directory to search. + /// An array of glob patterns to match against the names of files. + /// An optional cancellation token to cancel the operation. + /// + /// An async-enumerable collection of instances. + /// + public static IAsyncEnumerable GetFilesAsync(this VirtualDirectory directory, string[] patterns, CancellationToken cancellationToken = default) => + directory.GetFilesAsync(patterns, excludes: null, cancellationToken); + + /// + /// Asynchronously returns an async-enumerable collection of directories within the specified directory + /// that match the given glob pattern. + /// + /// The representing the directory to search. + /// The glob pattern to match against the names of directories. + /// An optional cancellation token to cancel the operation. + /// + /// An async-enumerable collection of instances. + /// + public static IAsyncEnumerable GetDirectoriesAsync(this VirtualDirectory directory, string pattern, CancellationToken cancellationToken = default) => + directory.GetDirectoriesAsync(pattern, exclude: null, cancellationToken); + + /// + /// Asynchronously returns an async-enumerable collection of directories within the specified directory + /// that match the given glob pattern. + /// + /// The representing the directory to search. + /// The glob pattern to match against the names of directories. + /// An optional glob pattern to exclude certain directories. + /// An optional cancellation token to cancel the operation. + /// + /// An async-enumerable collection of instances. + /// + public static IAsyncEnumerable GetDirectoriesAsync(this VirtualDirectory directory, string pattern, string? exclude, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(pattern); + + return directory.GetDirectoriesAsync([pattern], exclude is not null ? [exclude] : null, cancellationToken); + } + + /// + /// Asynchronously returns an async-enumerable collection of directories within the specified directory + /// that match any of the given glob patterns. + /// + /// The representing the directory to search. + /// An array of glob patterns to match against the names of directories. + /// An optional cancellation token to cancel the operation. + /// + /// An async-enumerable collection of instances. + /// + public static IAsyncEnumerable GetDirectoriesAsync(this VirtualDirectory directory, string[] patterns, CancellationToken cancellationToken = default) => + directory.GetDirectoriesAsync(patterns, excludes: null, cancellationToken); +} diff --git a/src/Ramstack.FileSystem.Abstractions/VirtualFile.cs b/src/Ramstack.FileSystem.Abstractions/VirtualFile.cs index 5db7414..97bde75 100644 --- a/src/Ramstack.FileSystem.Abstractions/VirtualFile.cs +++ b/src/Ramstack.FileSystem.Abstractions/VirtualFile.cs @@ -1,267 +1,267 @@ -namespace Ramstack.FileSystem; - -/// -/// Represents a virtual file within a specified file system. -/// -[DebuggerDisplay("{FullName,nq}")] -public abstract class VirtualFile : VirtualNode -{ - /// - /// Gets the full path of the directory containing this file. - /// - public string DirectoryName => VirtualPath.GetDirectoryName(FullName); - - /// - /// Gets a instance representing the parent directory of this file. - /// - public VirtualDirectory Directory => FileSystem.GetDirectory(DirectoryName); - - /// - /// Initializes a new instance of the class with the specified path. - /// - /// The full path of the file. - protected VirtualFile(string path) : base(path) - { - } - - /// - /// Initializes a new instance of the class with the specified path and properties. - /// - /// The full path of the file. - /// The properties of the file. - protected VirtualFile(string path, VirtualNodeProperties? properties) : base(path, properties) - { - } - - /// - /// Asynchronously opens the file for reading. - /// - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation, - /// with a result of type for reading the file content. - /// - public ValueTask OpenReadAsync(CancellationToken cancellationToken = default) => - OpenReadCoreAsync(cancellationToken); - - /// - /// Asynchronously opens the file for writing. - /// - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation, - /// with a result of type for writing to the file. - /// - public ValueTask OpenWriteAsync(CancellationToken cancellationToken = default) - { - EnsureWritable(); - Refresh(); - - return OpenWriteCoreAsync(cancellationToken); - } - - /// - /// Asynchronously writes the specified content to the file, creating a new file or overwriting an existing one. - /// - /// A containing the content to write to the file. - /// to overwrite an existing file; to throw an exception if the file already exists. - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation. - /// - /// - /// - /// If the file does not exist, it will be created. - /// If the file exists and is , the existing file will be overwritten. - /// If is and the file exists, an exception will be thrown. - /// - /// - public ValueTask WriteAsync(Stream stream, bool overwrite, CancellationToken cancellationToken = default) - { - EnsureWritable(); - Refresh(); - - return WriteCoreAsync(stream, overwrite, cancellationToken); - } - - /// - /// Asynchronously deletes the current file. - /// - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation. - /// - public ValueTask DeleteAsync(CancellationToken cancellationToken = default) - { - EnsureWritable(); - Refresh(); - - return DeleteCoreAsync(cancellationToken); - } - - /// - /// Asynchronously copies the file to the specified destination path. - /// - /// The path of the destination file. This cannot be a directory. - /// to overwrite an existing file; to throw an exception if the file already exists. - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation. - /// - /// - /// - /// If the file does not exist, it will be created. - /// If the file exists and is , the existing file will be overwritten. - /// If is and the file exists, an exception will be thrown. - /// - /// - public ValueTask CopyToAsync(string destinationPath, bool overwrite, CancellationToken cancellationToken = default) - { - EnsureWritable(); - - destinationPath = VirtualPath.Normalize(destinationPath); - EnsureDistinctTargets(FullName, destinationPath); - - return CopyToCoreAsync(destinationPath, overwrite, cancellationToken); - } - - /// - /// Asynchronously copies the contents of the current to the specified destination . - /// - /// The destination where the contents will be copied to. - /// to overwrite an existing file; to throw an exception if the file already exists. - /// An optional cancellation token to cancel the operation. - /// - /// A that represents the asynchronous copy operation. - /// - /// - /// - /// If the file does not exist, it will be created. - /// If the file exists and is , the existing file will be overwritten. - /// If is and the file exists, an exception will be thrown. - /// - /// - public ValueTask CopyToAsync(VirtualFile destination, bool overwrite, CancellationToken cancellationToken = default) - { - destination.EnsureWritable(); - destination.Refresh(); - - if (destination.FileSystem != FileSystem) - return CopyToCoreAsync(destination, overwrite, cancellationToken); - - EnsureDistinctTargets(FullName, destination.FullName); - return CopyToCoreAsync(destination.FullName, overwrite, cancellationToken); - } - - /// - /// Core implementation for asynchronously opening the file for reading. - /// - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation, - /// with a result of type for reading the file content. - /// - protected abstract ValueTask OpenReadCoreAsync(CancellationToken cancellationToken); - - /// - /// Core implementation for asynchronously opening the file for writing. - /// - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation, - /// with a result of type for writing to the file. - /// - protected abstract ValueTask OpenWriteCoreAsync(CancellationToken cancellationToken); - - /// - /// Core implementation for asynchronously writing the specified content to the file, - /// creating a new file or overwriting an existing one. - /// - /// A containing the content to write to the file. - /// to overwrite an existing file; to throw an exception if the file already exists. - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation. - /// - /// - /// - /// If the file does not exist, it must be created. - /// If the file exists and is , the existing file must be overwritten. - /// If is and the file exists, an exception must be thrown. - /// - /// - protected abstract ValueTask WriteCoreAsync(Stream stream, bool overwrite, CancellationToken cancellationToken); - - /// - /// Core implementation for asynchronously deleting the current file. - /// - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation. - /// - protected abstract ValueTask DeleteCoreAsync(CancellationToken cancellationToken); - - /// - /// Core implementation for asynchronously copying the file to the specified destination path. - /// - /// The path of the destination file. - /// to overwrite an existing file; to throw an exception if the file already exists. - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation. - /// - /// - /// - /// If the file does not exist, it must be created. - /// If the file exists and is , the existing file must be overwritten. - /// If is and the file exists, an exception must be thrown. - /// - /// - protected virtual async ValueTask CopyToCoreAsync(string destinationPath, bool overwrite, CancellationToken cancellationToken) - { - await using var source = await OpenReadAsync(cancellationToken).ConfigureAwait(false); - await FileSystem.WriteAsync(destinationPath, source, overwrite, cancellationToken).ConfigureAwait(false); - } - - /// - /// Core implementation for asynchronously copying the contents of the current to the specified destination . - /// - /// The destination where the contents will be copied to. - /// to overwrite an existing file; to throw an exception if the file already exists. - /// An optional cancellation token to cancel the operation. - /// - /// A that represents the asynchronous copy operation. - /// - /// - /// - /// If the file does not exist, it must be created. - /// If the file exists and is , the existing file must be overwritten. - /// If is and the file exists, an exception must be thrown. - /// - /// - protected virtual async ValueTask CopyToCoreAsync(VirtualFile destination, bool overwrite, CancellationToken cancellationToken) - { - await using var stream = await OpenReadAsync(cancellationToken).ConfigureAwait(false); - await destination.WriteAsync(stream, overwrite, cancellationToken).ConfigureAwait(false); - } - - /// - /// Ensures that the source and destination paths are distinct. - /// - /// The source file path. - /// The destination file path. - /// - /// Distinct target validation, if the underlying file system is case-insensitive, - /// should be handled by the appropriate provider. - /// - private static void EnsureDistinctTargets(string path, string destinationPath) - { - // Distinct target validation, if the underlying file system is case-insensitive, - // should be handled by the appropriate provider. - if (path == destinationPath) - Error(path); - - static void Error(string path) => - throw new IOException($"Cannot copy a file '{path}' to itself."); - } -} +namespace Ramstack.FileSystem; + +/// +/// Represents a virtual file within a specified file system. +/// +[DebuggerDisplay("{FullName,nq}")] +public abstract class VirtualFile : VirtualNode +{ + /// + /// Gets the full path of the directory containing this file. + /// + public string DirectoryName => VirtualPath.GetDirectoryName(FullName); + + /// + /// Gets a instance representing the parent directory of this file. + /// + public VirtualDirectory Directory => FileSystem.GetDirectory(DirectoryName); + + /// + /// Initializes a new instance of the class with the specified path. + /// + /// The full path of the file. + protected VirtualFile(string path) : base(path) + { + } + + /// + /// Initializes a new instance of the class with the specified path and properties. + /// + /// The full path of the file. + /// The properties of the file. + protected VirtualFile(string path, VirtualNodeProperties? properties) : base(path, properties) + { + } + + /// + /// Asynchronously opens the file for reading. + /// + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation, + /// with a result of type for reading the file content. + /// + public ValueTask OpenReadAsync(CancellationToken cancellationToken = default) => + OpenReadCoreAsync(cancellationToken); + + /// + /// Asynchronously opens the file for writing. + /// + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation, + /// with a result of type for writing to the file. + /// + public ValueTask OpenWriteAsync(CancellationToken cancellationToken = default) + { + EnsureWritable(); + Refresh(); + + return OpenWriteCoreAsync(cancellationToken); + } + + /// + /// Asynchronously writes the specified content to the file, creating a new file or overwriting an existing one. + /// + /// A containing the content to write to the file. + /// to overwrite an existing file; to throw an exception if the file already exists. + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation. + /// + /// + /// + /// If the file does not exist, it will be created. + /// If the file exists and is , the existing file will be overwritten. + /// If is and the file exists, an exception will be thrown. + /// + /// + public ValueTask WriteAsync(Stream stream, bool overwrite, CancellationToken cancellationToken = default) + { + EnsureWritable(); + Refresh(); + + return WriteCoreAsync(stream, overwrite, cancellationToken); + } + + /// + /// Asynchronously deletes the current file. + /// + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation. + /// + public ValueTask DeleteAsync(CancellationToken cancellationToken = default) + { + EnsureWritable(); + Refresh(); + + return DeleteCoreAsync(cancellationToken); + } + + /// + /// Asynchronously copies the file to the specified destination path. + /// + /// The path of the destination file. This cannot be a directory. + /// to overwrite an existing file; to throw an exception if the file already exists. + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation. + /// + /// + /// + /// If the file does not exist, it will be created. + /// If the file exists and is , the existing file will be overwritten. + /// If is and the file exists, an exception will be thrown. + /// + /// + public ValueTask CopyToAsync(string destinationPath, bool overwrite, CancellationToken cancellationToken = default) + { + EnsureWritable(); + + destinationPath = VirtualPath.Normalize(destinationPath); + EnsureDistinctTargets(FullName, destinationPath); + + return CopyToCoreAsync(destinationPath, overwrite, cancellationToken); + } + + /// + /// Asynchronously copies the contents of the current to the specified destination . + /// + /// The destination where the contents will be copied to. + /// to overwrite an existing file; to throw an exception if the file already exists. + /// An optional cancellation token to cancel the operation. + /// + /// A that represents the asynchronous copy operation. + /// + /// + /// + /// If the file does not exist, it will be created. + /// If the file exists and is , the existing file will be overwritten. + /// If is and the file exists, an exception will be thrown. + /// + /// + public ValueTask CopyToAsync(VirtualFile destination, bool overwrite, CancellationToken cancellationToken = default) + { + destination.EnsureWritable(); + destination.Refresh(); + + if (destination.FileSystem != FileSystem) + return CopyToCoreAsync(destination, overwrite, cancellationToken); + + EnsureDistinctTargets(FullName, destination.FullName); + return CopyToCoreAsync(destination.FullName, overwrite, cancellationToken); + } + + /// + /// Core implementation for asynchronously opening the file for reading. + /// + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation, + /// with a result of type for reading the file content. + /// + protected abstract ValueTask OpenReadCoreAsync(CancellationToken cancellationToken); + + /// + /// Core implementation for asynchronously opening the file for writing. + /// + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation, + /// with a result of type for writing to the file. + /// + protected abstract ValueTask OpenWriteCoreAsync(CancellationToken cancellationToken); + + /// + /// Core implementation for asynchronously writing the specified content to the file, + /// creating a new file or overwriting an existing one. + /// + /// A containing the content to write to the file. + /// to overwrite an existing file; to throw an exception if the file already exists. + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation. + /// + /// + /// + /// If the file does not exist, it must be created. + /// If the file exists and is , the existing file must be overwritten. + /// If is and the file exists, an exception must be thrown. + /// + /// + protected abstract ValueTask WriteCoreAsync(Stream stream, bool overwrite, CancellationToken cancellationToken); + + /// + /// Core implementation for asynchronously deleting the current file. + /// + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation. + /// + protected abstract ValueTask DeleteCoreAsync(CancellationToken cancellationToken); + + /// + /// Core implementation for asynchronously copying the file to the specified destination path. + /// + /// The path of the destination file. + /// to overwrite an existing file; to throw an exception if the file already exists. + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation. + /// + /// + /// + /// If the file does not exist, it must be created. + /// If the file exists and is , the existing file must be overwritten. + /// If is and the file exists, an exception must be thrown. + /// + /// + protected virtual async ValueTask CopyToCoreAsync(string destinationPath, bool overwrite, CancellationToken cancellationToken) + { + await using var source = await OpenReadAsync(cancellationToken).ConfigureAwait(false); + await FileSystem.WriteAsync(destinationPath, source, overwrite, cancellationToken).ConfigureAwait(false); + } + + /// + /// Core implementation for asynchronously copying the contents of the current to the specified destination . + /// + /// The destination where the contents will be copied to. + /// to overwrite an existing file; to throw an exception if the file already exists. + /// An optional cancellation token to cancel the operation. + /// + /// A that represents the asynchronous copy operation. + /// + /// + /// + /// If the file does not exist, it must be created. + /// If the file exists and is , the existing file must be overwritten. + /// If is and the file exists, an exception must be thrown. + /// + /// + protected virtual async ValueTask CopyToCoreAsync(VirtualFile destination, bool overwrite, CancellationToken cancellationToken) + { + await using var stream = await OpenReadAsync(cancellationToken).ConfigureAwait(false); + await destination.WriteAsync(stream, overwrite, cancellationToken).ConfigureAwait(false); + } + + /// + /// Ensures that the source and destination paths are distinct. + /// + /// The source file path. + /// The destination file path. + /// + /// Distinct target validation, if the underlying file system is case-insensitive, + /// should be handled by the appropriate provider. + /// + private static void EnsureDistinctTargets(string path, string destinationPath) + { + // Distinct target validation, if the underlying file system is case-insensitive, + // should be handled by the appropriate provider. + if (path == destinationPath) + Error(path); + + static void Error(string path) => + throw new IOException($"Cannot copy a file '{path}' to itself."); + } +} diff --git a/src/Ramstack.FileSystem.Abstractions/VirtualFileExtensions.cs b/src/Ramstack.FileSystem.Abstractions/VirtualFileExtensions.cs index f82d159..425eb32 100644 --- a/src/Ramstack.FileSystem.Abstractions/VirtualFileExtensions.cs +++ b/src/Ramstack.FileSystem.Abstractions/VirtualFileExtensions.cs @@ -1,357 +1,357 @@ -using System.Buffers; -using System.Text; - -namespace Ramstack.FileSystem; - -/// -/// Provides extension methods for the class. -/// -public static class VirtualFileExtensions -{ - /// - /// Asynchronously returns a with - /// character encoding that reads from the specified text file. - /// - /// The file to get the for. - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation. - /// The result is a that reads from the text file. - /// - public static ValueTask OpenTextAsync(this VirtualFile file, CancellationToken cancellationToken = default) => - file.OpenTextAsync(encoding: null, cancellationToken); - - /// - /// Asynchronously returns a with the specified character encoding - /// that reads from the specified text file. - /// - /// The file to get the for. - /// The encoding applied to the contents. - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation. - /// The result is a that reads from the text file. - /// - public static async ValueTask OpenTextAsync(this VirtualFile file, Encoding? encoding, CancellationToken cancellationToken = default) - { - var stream = await file.OpenReadAsync(cancellationToken).ConfigureAwait(false); - return new StreamReader(stream, encoding!); - } - - /// - /// Asynchronously writes the specified content to a file. If the file exists, an exception will be thrown. - /// - /// The file to write to. - /// A containing the content to write to the file. - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation. - /// - public static ValueTask WriteAsync(this VirtualFile file, Stream stream, CancellationToken cancellationToken = default) => - file.WriteAsync(stream, overwrite: false, cancellationToken); - - /// - /// 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 VirtualFile 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 VirtualFile file, Encoding? encoding, CancellationToken cancellationToken = default) - { - // ReSharper disable once UseAwaitUsing - using var stream = await file.OpenReadAsync(cancellationToken).ConfigureAwait(false); - 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 VirtualFile 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 VirtualFile file, Encoding? encoding, CancellationToken cancellationToken = default) - { - var stream = await file.OpenReadAsync(cancellationToken).ConfigureAwait(false); - 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 VirtualFile file, CancellationToken cancellationToken = default) - { - // ReSharper disable once UseAwaitUsing - using var stream = await file.OpenReadAsync(cancellationToken).ConfigureAwait(false); - - 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; - - 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; - } - } - - static void Error_EndOfStream() => - throw new EndOfStreamException(); - } - - /// - /// Asynchronously writes the specified string to the current file. If the file already exists, it is truncated and overwritten. - /// - /// The file to write to. - /// The contents to write to the file. - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation. - /// - public static ValueTask WriteAllTextAsync(this VirtualFile file, string contents, CancellationToken cancellationToken = default) => - WriteAllTextAsync(file, contents.AsMemory(), encoding: null, cancellationToken); - - /// - /// Asynchronously writes the specified string to the current file. If the file already exists, it is truncated and overwritten. - /// - /// The file to write to. - /// The contents to write to the file. - /// The encoding to apply to the string. - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation. - /// - public static ValueTask WriteAllTextAsync(this VirtualFile file, string contents, Encoding? encoding, CancellationToken cancellationToken = default) => - WriteAllTextAsync(file, contents.AsMemory(), encoding, cancellationToken); - - /// - /// Asynchronously writes the specified string to the current file. If the file already exists, it is truncated and overwritten. - /// - /// The file to write to. - /// The contents to write to the file. - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation. - /// - public static ValueTask WriteAllTextAsync(this VirtualFile file, ReadOnlyMemory contents, CancellationToken cancellationToken = default) => - WriteAllTextAsync(file, contents, encoding: null, cancellationToken); - - /// - /// Asynchronously writes the specified string to the current file. If the file already exists, it is truncated and overwritten. - /// - /// The file to write to. - /// The contents to write to the file. - /// The encoding to apply to the string. - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation. - /// - public static async ValueTask WriteAllTextAsync(this VirtualFile file, ReadOnlyMemory contents, Encoding? encoding, CancellationToken cancellationToken = default) - { - await using var stream = await file.OpenWriteAsync(cancellationToken).ConfigureAwait(false); - await using var writer = new StreamWriter(stream, encoding!); - await writer.WriteAsync(contents, cancellationToken).ConfigureAwait(false); - } - - /// - /// Asynchronously writes the specified lines to the current file. If the file already exists, it is truncated and overwritten. - /// - /// The file to write to. - /// The contents to write to the file. - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation. - /// - public static ValueTask WriteAllLinesAsync(this VirtualFile file, IEnumerable contents, CancellationToken cancellationToken = default) => - WriteAllLinesAsync(file, contents, encoding: null, cancellationToken); - - /// - /// Asynchronously writes the specified lines to the current file. If the file already exists, it is truncated and overwritten. - /// - /// The file to write to. - /// The contents to write to the file. - /// The encoding to apply to the string. - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation. - /// - public static async ValueTask WriteAllLinesAsync(this VirtualFile file, IEnumerable contents, Encoding? encoding, CancellationToken cancellationToken = default) - { - await using var stream = await file.OpenWriteAsync(cancellationToken).ConfigureAwait(false); - await using var writer = new StreamWriter(stream, encoding, bufferSize: -1, leaveOpen: false); - - foreach (var line in contents) - await writer.WriteLineAsync(line.AsMemory(), cancellationToken).ConfigureAwait(false); - } - - /// - /// Asynchronously writes the specified byte array to the current file. If the file already exists, it is truncated and overwritten. - /// - /// The file to write to. - /// The bytes to write to the file. - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation. - /// - public static ValueTask WriteAllBytesAsync(this VirtualFile file, byte[] bytes, CancellationToken cancellationToken = default) => - WriteAllBytesAsync(file, bytes.AsMemory(), cancellationToken); - - /// - /// Asynchronously writes the specified byte array to the current file. If the file already exists, it is truncated and overwritten. - /// - /// The file to write to. - /// The bytes to write to the file. - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation. - /// - public static async ValueTask WriteAllBytesAsync(this VirtualFile file, ReadOnlyMemory bytes, CancellationToken cancellationToken = default) - { - await using var stream = await file.OpenWriteAsync(cancellationToken).ConfigureAwait(false); - await stream.WriteAsync(bytes, cancellationToken).ConfigureAwait(false); - } - - /// - /// Asynchronously copies the file to the specified destination path. - /// - /// The source to copy from. - /// The path of the destination file. This cannot be a directory. - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation. - /// - public static ValueTask CopyToAsync(this VirtualFile file, string destinationPath, CancellationToken cancellationToken = default) => - file.CopyToAsync(destinationPath, overwrite: false, cancellationToken); - - /// - /// Asynchronously copies the contents of the current to the specified destination . - /// - /// The source to copy from. - /// The destination where the contents will be copied to. - /// An optional cancellation token to cancel the operation. Defaults to . - /// - /// A that represents the asynchronous copy operation. - /// - public static ValueTask CopyToAsync(this VirtualFile file, VirtualFile destination, CancellationToken cancellationToken = default) => - file.CopyToAsync(destination, overwrite: false, cancellationToken); - - /// - /// Asynchronously returns the size of the specified file in bytes. - /// - /// The file to get the size of. - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation. - /// The result is the size of the specified file in bytes, or -1 if the file does not exist. - /// - public static async ValueTask GetLengthAsync(this VirtualFile file, CancellationToken cancellationToken = default) - { - var properties = await file.GetPropertiesAsync(cancellationToken).ConfigureAwait(false); - return properties.Length; - } -} +using System.Buffers; +using System.Text; + +namespace Ramstack.FileSystem; + +/// +/// Provides extension methods for the class. +/// +public static class VirtualFileExtensions +{ + /// + /// Asynchronously returns a with + /// character encoding that reads from the specified text file. + /// + /// The file to get the for. + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation. + /// The result is a that reads from the text file. + /// + public static ValueTask OpenTextAsync(this VirtualFile file, CancellationToken cancellationToken = default) => + file.OpenTextAsync(encoding: null, cancellationToken); + + /// + /// Asynchronously returns a with the specified character encoding + /// that reads from the specified text file. + /// + /// The file to get the for. + /// The encoding applied to the contents. + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation. + /// The result is a that reads from the text file. + /// + public static async ValueTask OpenTextAsync(this VirtualFile file, Encoding? encoding, CancellationToken cancellationToken = default) + { + var stream = await file.OpenReadAsync(cancellationToken).ConfigureAwait(false); + return new StreamReader(stream, encoding!); + } + + /// + /// Asynchronously writes the specified content to a file. If the file exists, an exception will be thrown. + /// + /// The file to write to. + /// A containing the content to write to the file. + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation. + /// + public static ValueTask WriteAsync(this VirtualFile file, Stream stream, CancellationToken cancellationToken = default) => + file.WriteAsync(stream, overwrite: false, cancellationToken); + + /// + /// 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 VirtualFile 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 VirtualFile file, Encoding? encoding, CancellationToken cancellationToken = default) + { + // ReSharper disable once UseAwaitUsing + using var stream = await file.OpenReadAsync(cancellationToken).ConfigureAwait(false); + 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 VirtualFile 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 VirtualFile file, Encoding? encoding, CancellationToken cancellationToken = default) + { + var stream = await file.OpenReadAsync(cancellationToken).ConfigureAwait(false); + 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 VirtualFile file, CancellationToken cancellationToken = default) + { + // ReSharper disable once UseAwaitUsing + using var stream = await file.OpenReadAsync(cancellationToken).ConfigureAwait(false); + + 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; + + 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; + } + } + + static void Error_EndOfStream() => + throw new EndOfStreamException(); + } + + /// + /// Asynchronously writes the specified string to the current file. If the file already exists, it is truncated and overwritten. + /// + /// The file to write to. + /// The contents to write to the file. + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation. + /// + public static ValueTask WriteAllTextAsync(this VirtualFile file, string contents, CancellationToken cancellationToken = default) => + WriteAllTextAsync(file, contents.AsMemory(), encoding: null, cancellationToken); + + /// + /// Asynchronously writes the specified string to the current file. If the file already exists, it is truncated and overwritten. + /// + /// The file to write to. + /// The contents to write to the file. + /// The encoding to apply to the string. + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation. + /// + public static ValueTask WriteAllTextAsync(this VirtualFile file, string contents, Encoding? encoding, CancellationToken cancellationToken = default) => + WriteAllTextAsync(file, contents.AsMemory(), encoding, cancellationToken); + + /// + /// Asynchronously writes the specified string to the current file. If the file already exists, it is truncated and overwritten. + /// + /// The file to write to. + /// The contents to write to the file. + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation. + /// + public static ValueTask WriteAllTextAsync(this VirtualFile file, ReadOnlyMemory contents, CancellationToken cancellationToken = default) => + WriteAllTextAsync(file, contents, encoding: null, cancellationToken); + + /// + /// Asynchronously writes the specified string to the current file. If the file already exists, it is truncated and overwritten. + /// + /// The file to write to. + /// The contents to write to the file. + /// The encoding to apply to the string. + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation. + /// + public static async ValueTask WriteAllTextAsync(this VirtualFile file, ReadOnlyMemory contents, Encoding? encoding, CancellationToken cancellationToken = default) + { + await using var stream = await file.OpenWriteAsync(cancellationToken).ConfigureAwait(false); + await using var writer = new StreamWriter(stream, encoding!); + await writer.WriteAsync(contents, cancellationToken).ConfigureAwait(false); + } + + /// + /// Asynchronously writes the specified lines to the current file. If the file already exists, it is truncated and overwritten. + /// + /// The file to write to. + /// The contents to write to the file. + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation. + /// + public static ValueTask WriteAllLinesAsync(this VirtualFile file, IEnumerable contents, CancellationToken cancellationToken = default) => + WriteAllLinesAsync(file, contents, encoding: null, cancellationToken); + + /// + /// Asynchronously writes the specified lines to the current file. If the file already exists, it is truncated and overwritten. + /// + /// The file to write to. + /// The contents to write to the file. + /// The encoding to apply to the string. + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation. + /// + public static async ValueTask WriteAllLinesAsync(this VirtualFile file, IEnumerable contents, Encoding? encoding, CancellationToken cancellationToken = default) + { + await using var stream = await file.OpenWriteAsync(cancellationToken).ConfigureAwait(false); + await using var writer = new StreamWriter(stream, encoding, bufferSize: -1, leaveOpen: false); + + foreach (var line in contents) + await writer.WriteLineAsync(line.AsMemory(), cancellationToken).ConfigureAwait(false); + } + + /// + /// Asynchronously writes the specified byte array to the current file. If the file already exists, it is truncated and overwritten. + /// + /// The file to write to. + /// The bytes to write to the file. + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation. + /// + public static ValueTask WriteAllBytesAsync(this VirtualFile file, byte[] bytes, CancellationToken cancellationToken = default) => + WriteAllBytesAsync(file, bytes.AsMemory(), cancellationToken); + + /// + /// Asynchronously writes the specified byte array to the current file. If the file already exists, it is truncated and overwritten. + /// + /// The file to write to. + /// The bytes to write to the file. + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation. + /// + public static async ValueTask WriteAllBytesAsync(this VirtualFile file, ReadOnlyMemory bytes, CancellationToken cancellationToken = default) + { + await using var stream = await file.OpenWriteAsync(cancellationToken).ConfigureAwait(false); + await stream.WriteAsync(bytes, cancellationToken).ConfigureAwait(false); + } + + /// + /// Asynchronously copies the file to the specified destination path. + /// + /// The source to copy from. + /// The path of the destination file. This cannot be a directory. + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation. + /// + public static ValueTask CopyToAsync(this VirtualFile file, string destinationPath, CancellationToken cancellationToken = default) => + file.CopyToAsync(destinationPath, overwrite: false, cancellationToken); + + /// + /// Asynchronously copies the contents of the current to the specified destination . + /// + /// The source to copy from. + /// The destination where the contents will be copied to. + /// An optional cancellation token to cancel the operation. Defaults to . + /// + /// A that represents the asynchronous copy operation. + /// + public static ValueTask CopyToAsync(this VirtualFile file, VirtualFile destination, CancellationToken cancellationToken = default) => + file.CopyToAsync(destination, overwrite: false, cancellationToken); + + /// + /// Asynchronously returns the size of the specified file in bytes. + /// + /// The file to get the size of. + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation. + /// The result is the size of the specified file in bytes, or -1 if the file does not exist. + /// + public static async ValueTask GetLengthAsync(this VirtualFile file, CancellationToken cancellationToken = default) + { + var properties = await file.GetPropertiesAsync(cancellationToken).ConfigureAwait(false); + return properties.Length; + } +} diff --git a/src/Ramstack.FileSystem.Abstractions/VirtualFileSystemExtensions.Enumeration.cs b/src/Ramstack.FileSystem.Abstractions/VirtualFileSystemExtensions.Enumeration.cs index 6e226e1..7661802 100644 --- a/src/Ramstack.FileSystem.Abstractions/VirtualFileSystemExtensions.Enumeration.cs +++ b/src/Ramstack.FileSystem.Abstractions/VirtualFileSystemExtensions.Enumeration.cs @@ -1,169 +1,169 @@ -namespace Ramstack.FileSystem; - -partial class VirtualFileSystemExtensions -{ - /// - /// Asynchronously returns an async-enumerable collection of file nodes (both directories and files) - /// in the specified path that match the given glob pattern. - /// - /// The file system to use. - /// The path to the directory to search. - /// The glob pattern to match against the names of file nodes. - /// An optional cancellation token to cancel the operation. - /// - /// An async-enumerable collection of instances. - /// - public static IAsyncEnumerable GetFileNodesAsync(this IVirtualFileSystem fs, string path, string pattern, CancellationToken cancellationToken = default) => - fs.GetDirectory(path).GetFileNodesAsync(pattern, exclude: null, cancellationToken); - - /// - /// Asynchronously returns an async-enumerable collection of file nodes (both directories and files) - /// in the specified path that match the given glob pattern. - /// - /// The file system to use. - /// The path to the directory to search. - /// The glob pattern to match against the names of file nodes. - /// An optional glob pattern to exclude certain file nodes. - /// An optional cancellation token to cancel the operation. - /// - /// An async-enumerable collection of instances. - /// - public static IAsyncEnumerable GetFileNodesAsync(this IVirtualFileSystem fs, string path, string pattern, string? exclude, CancellationToken cancellationToken = default) => - fs.GetDirectory(path).GetFileNodesAsync([pattern], exclude is not null ? [exclude] : null, cancellationToken); - - /// - /// Asynchronously returns an async-enumerable collection of file nodes (both directories and files) - /// in the specified path that match any of the given glob patterns. - /// - /// The file system to use. - /// The path to the directory to search. - /// An array of glob patterns to match against the names of file nodes. - /// An optional cancellation token to cancel the operation. - /// - /// An async-enumerable collection of instances. - /// - public static IAsyncEnumerable GetFileNodesAsync(this IVirtualFileSystem fs, string path, string[] patterns, CancellationToken cancellationToken = default) => - fs.GetDirectory(path).GetFileNodesAsync(patterns, excludes: null, cancellationToken); - - /// - /// Asynchronously returns an async-enumerable collection of file nodes (both directories and files) - /// in the specified path that match any of the given glob patterns. - /// - /// The file system to use. - /// The path to 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 optional cancellation token to cancel the operation. - /// - /// An async-enumerable collection of instances. - /// - public static IAsyncEnumerable GetFileNodesAsync(this IVirtualFileSystem fs, string path, string[] patterns, string[]? excludes, CancellationToken cancellationToken = default) => - fs.GetDirectory(path).GetFileNodesAsync(patterns, excludes, cancellationToken); - - /// - /// Asynchronously returns an async-enumerable collection of files in the specified path that match the given glob pattern. - /// - /// The file system to use. - /// The path to the directory to search. - /// The glob pattern to match against the names of files. - /// An optional cancellation token to cancel the operation. - /// - /// An async-enumerable collection of instances. - /// - public static IAsyncEnumerable GetFilesAsync(this IVirtualFileSystem fs, string path, string pattern, CancellationToken cancellationToken = default) => - fs.GetDirectory(path).GetFilesAsync(pattern, exclude: null, cancellationToken); - - /// - /// Asynchronously returns an async-enumerable collection of files in the specified path that match the given glob pattern. - /// - /// The file system to use. - /// The path to the directory to search. - /// The glob pattern to match against the names of files. - /// An optional glob pattern to exclude certain files. - /// An optional cancellation token to cancel the operation. - /// - /// An async-enumerable collection of instances. - public static IAsyncEnumerable GetFilesAsync(this IVirtualFileSystem fs, string path, string pattern, string? exclude, CancellationToken cancellationToken = default) => - fs.GetDirectory(path).GetFilesAsync([pattern], exclude is not null ? [exclude] : null, cancellationToken); - - /// - /// Asynchronously returns an async-enumerable collection of files in the specified path that match any of the given glob patterns. - /// - /// The file system to use. - /// The path to the directory to search. - /// An array of glob patterns to match against the names of files. - /// An optional cancellation token to cancel the operation. - /// - /// An async-enumerable collection of instances. - /// - public static IAsyncEnumerable GetFilesAsync(this IVirtualFileSystem fs, string path, string[] patterns, CancellationToken cancellationToken = default) => - fs.GetDirectory(path).GetFilesAsync(patterns, excludes: null, cancellationToken); - - /// - /// Asynchronously returns an async-enumerable collection of files in the specified path that match any of the given glob patterns. - /// - /// The file system 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 optional cancellation token to cancel the operation. - /// - /// An async-enumerable collection of instances. - /// - public static IAsyncEnumerable GetFilesAsync(this IVirtualFileSystem fs, string path, string[] patterns, string[]? excludes, CancellationToken cancellationToken = default) => - fs.GetDirectory(path).GetFilesAsync(patterns, excludes, cancellationToken); - - /// - /// Asynchronously returns an async-enumerable collection of directories in the specified path that match the given glob pattern. - /// - /// The file system to use. - /// The path to the directory to search. - /// The glob pattern to match against the names of directories. - /// An optional cancellation token to cancel the operation. - /// - /// An async-enumerable collection of instances. - /// - public static IAsyncEnumerable GetDirectoriesAsync(this IVirtualFileSystem fs, string path, string pattern, CancellationToken cancellationToken = default) => - fs.GetDirectory(path).GetDirectoriesAsync(pattern, exclude: null, cancellationToken); - - /// - /// Asynchronously returns an async-enumerable collection of directories in the specified path that match the given glob pattern. - /// - /// The file system to use. - /// The path to the directory to search. - /// The glob pattern to match against the names of directories. - /// An optional glob pattern to exclude certain directories. - /// An optional cancellation token to cancel the operation. - /// - /// An async-enumerable collection of instances. - /// - public static IAsyncEnumerable GetDirectoriesAsync(this IVirtualFileSystem fs, string path, string pattern, string? exclude, CancellationToken cancellationToken = default) => - fs.GetDirectory(path).GetDirectoriesAsync([pattern], exclude is not null ? [exclude] : null, cancellationToken); - - /// - /// Asynchronously returns an async-enumerable collection of directories in the specified path that match any of the given glob patterns. - /// - /// The file system to use. - /// The path to the directory to search. - /// An array of glob patterns to match against the names of directories. - /// An optional cancellation token to cancel the operation. - /// - /// An async-enumerable collection of instances. - /// - public static IAsyncEnumerable GetDirectoriesAsync(this IVirtualFileSystem fs, string path, string[] patterns, CancellationToken cancellationToken = default) => - fs.GetDirectory(path).GetDirectoriesAsync(patterns, excludes: null, cancellationToken); - - /// - /// Asynchronously returns an async-enumerable collection of directories in the specified path that match any of the given glob patterns. - /// - /// The file system 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 optional cancellation token to cancel the operation. - /// - /// An async-enumerable collection of instances. - /// - public static IAsyncEnumerable GetDirectoriesAsync(this IVirtualFileSystem fs, string path, string[] patterns, string[]? excludes, CancellationToken cancellationToken = default) => - fs.GetDirectory(path).GetDirectoriesAsync(patterns, excludes, cancellationToken); -} +namespace Ramstack.FileSystem; + +partial class VirtualFileSystemExtensions +{ + /// + /// Asynchronously returns an async-enumerable collection of file nodes (both directories and files) + /// in the specified path that match the given glob pattern. + /// + /// The file system to use. + /// The path to the directory to search. + /// The glob pattern to match against the names of file nodes. + /// An optional cancellation token to cancel the operation. + /// + /// An async-enumerable collection of instances. + /// + public static IAsyncEnumerable GetFileNodesAsync(this IVirtualFileSystem fs, string path, string pattern, CancellationToken cancellationToken = default) => + fs.GetDirectory(path).GetFileNodesAsync(pattern, exclude: null, cancellationToken); + + /// + /// Asynchronously returns an async-enumerable collection of file nodes (both directories and files) + /// in the specified path that match the given glob pattern. + /// + /// The file system to use. + /// The path to the directory to search. + /// The glob pattern to match against the names of file nodes. + /// An optional glob pattern to exclude certain file nodes. + /// An optional cancellation token to cancel the operation. + /// + /// An async-enumerable collection of instances. + /// + public static IAsyncEnumerable GetFileNodesAsync(this IVirtualFileSystem fs, string path, string pattern, string? exclude, CancellationToken cancellationToken = default) => + fs.GetDirectory(path).GetFileNodesAsync([pattern], exclude is not null ? [exclude] : null, cancellationToken); + + /// + /// Asynchronously returns an async-enumerable collection of file nodes (both directories and files) + /// in the specified path that match any of the given glob patterns. + /// + /// The file system to use. + /// The path to the directory to search. + /// An array of glob patterns to match against the names of file nodes. + /// An optional cancellation token to cancel the operation. + /// + /// An async-enumerable collection of instances. + /// + public static IAsyncEnumerable GetFileNodesAsync(this IVirtualFileSystem fs, string path, string[] patterns, CancellationToken cancellationToken = default) => + fs.GetDirectory(path).GetFileNodesAsync(patterns, excludes: null, cancellationToken); + + /// + /// Asynchronously returns an async-enumerable collection of file nodes (both directories and files) + /// in the specified path that match any of the given glob patterns. + /// + /// The file system to use. + /// The path to 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 optional cancellation token to cancel the operation. + /// + /// An async-enumerable collection of instances. + /// + public static IAsyncEnumerable GetFileNodesAsync(this IVirtualFileSystem fs, string path, string[] patterns, string[]? excludes, CancellationToken cancellationToken = default) => + fs.GetDirectory(path).GetFileNodesAsync(patterns, excludes, cancellationToken); + + /// + /// Asynchronously returns an async-enumerable collection of files in the specified path that match the given glob pattern. + /// + /// The file system to use. + /// The path to the directory to search. + /// The glob pattern to match against the names of files. + /// An optional cancellation token to cancel the operation. + /// + /// An async-enumerable collection of instances. + /// + public static IAsyncEnumerable GetFilesAsync(this IVirtualFileSystem fs, string path, string pattern, CancellationToken cancellationToken = default) => + fs.GetDirectory(path).GetFilesAsync(pattern, exclude: null, cancellationToken); + + /// + /// Asynchronously returns an async-enumerable collection of files in the specified path that match the given glob pattern. + /// + /// The file system to use. + /// The path to the directory to search. + /// The glob pattern to match against the names of files. + /// An optional glob pattern to exclude certain files. + /// An optional cancellation token to cancel the operation. + /// + /// An async-enumerable collection of instances. + public static IAsyncEnumerable GetFilesAsync(this IVirtualFileSystem fs, string path, string pattern, string? exclude, CancellationToken cancellationToken = default) => + fs.GetDirectory(path).GetFilesAsync([pattern], exclude is not null ? [exclude] : null, cancellationToken); + + /// + /// Asynchronously returns an async-enumerable collection of files in the specified path that match any of the given glob patterns. + /// + /// The file system to use. + /// The path to the directory to search. + /// An array of glob patterns to match against the names of files. + /// An optional cancellation token to cancel the operation. + /// + /// An async-enumerable collection of instances. + /// + public static IAsyncEnumerable GetFilesAsync(this IVirtualFileSystem fs, string path, string[] patterns, CancellationToken cancellationToken = default) => + fs.GetDirectory(path).GetFilesAsync(patterns, excludes: null, cancellationToken); + + /// + /// Asynchronously returns an async-enumerable collection of files in the specified path that match any of the given glob patterns. + /// + /// The file system 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 optional cancellation token to cancel the operation. + /// + /// An async-enumerable collection of instances. + /// + public static IAsyncEnumerable GetFilesAsync(this IVirtualFileSystem fs, string path, string[] patterns, string[]? excludes, CancellationToken cancellationToken = default) => + fs.GetDirectory(path).GetFilesAsync(patterns, excludes, cancellationToken); + + /// + /// Asynchronously returns an async-enumerable collection of directories in the specified path that match the given glob pattern. + /// + /// The file system to use. + /// The path to the directory to search. + /// The glob pattern to match against the names of directories. + /// An optional cancellation token to cancel the operation. + /// + /// An async-enumerable collection of instances. + /// + public static IAsyncEnumerable GetDirectoriesAsync(this IVirtualFileSystem fs, string path, string pattern, CancellationToken cancellationToken = default) => + fs.GetDirectory(path).GetDirectoriesAsync(pattern, exclude: null, cancellationToken); + + /// + /// Asynchronously returns an async-enumerable collection of directories in the specified path that match the given glob pattern. + /// + /// The file system to use. + /// The path to the directory to search. + /// The glob pattern to match against the names of directories. + /// An optional glob pattern to exclude certain directories. + /// An optional cancellation token to cancel the operation. + /// + /// An async-enumerable collection of instances. + /// + public static IAsyncEnumerable GetDirectoriesAsync(this IVirtualFileSystem fs, string path, string pattern, string? exclude, CancellationToken cancellationToken = default) => + fs.GetDirectory(path).GetDirectoriesAsync([pattern], exclude is not null ? [exclude] : null, cancellationToken); + + /// + /// Asynchronously returns an async-enumerable collection of directories in the specified path that match any of the given glob patterns. + /// + /// The file system to use. + /// The path to the directory to search. + /// An array of glob patterns to match against the names of directories. + /// An optional cancellation token to cancel the operation. + /// + /// An async-enumerable collection of instances. + /// + public static IAsyncEnumerable GetDirectoriesAsync(this IVirtualFileSystem fs, string path, string[] patterns, CancellationToken cancellationToken = default) => + fs.GetDirectory(path).GetDirectoriesAsync(patterns, excludes: null, cancellationToken); + + /// + /// Asynchronously returns an async-enumerable collection of directories in the specified path that match any of the given glob patterns. + /// + /// The file system 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 optional cancellation token to cancel the operation. + /// + /// An async-enumerable collection of instances. + /// + public static IAsyncEnumerable GetDirectoriesAsync(this IVirtualFileSystem fs, string path, string[] patterns, string[]? excludes, CancellationToken cancellationToken = default) => + fs.GetDirectory(path).GetDirectoriesAsync(patterns, excludes, cancellationToken); +} diff --git a/src/Ramstack.FileSystem.Abstractions/VirtualFileSystemExtensions.cs b/src/Ramstack.FileSystem.Abstractions/VirtualFileSystemExtensions.cs index 6239b78..59e42c2 100644 --- a/src/Ramstack.FileSystem.Abstractions/VirtualFileSystemExtensions.cs +++ b/src/Ramstack.FileSystem.Abstractions/VirtualFileSystemExtensions.cs @@ -1,405 +1,405 @@ -using System.Text; - -namespace Ramstack.FileSystem; - -/// -/// Provides extension methods for the interface. -/// -public static partial class VirtualFileSystemExtensions -{ - /// - /// Asynchronously opens the file at the specified path for reading. - /// - /// The file system to use. - /// The path of the file to open. - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation, containing a stream for reading the file content. - /// - public static ValueTask OpenReadAsync(this IVirtualFileSystem fs, string path, CancellationToken cancellationToken = default) => - fs.GetFile(path).OpenReadAsync(cancellationToken); - - /// - /// Asynchronously returns a with encoding for a file at the specified path. - /// - /// The file system to use. - /// The path of the file to open. - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation, - /// containing a that reads from the text file. - /// - public static Task OpenTextAsync(this IVirtualFileSystem fs, string path, CancellationToken cancellationToken = default) => - fs.OpenTextAsync(path, Encoding.UTF8, cancellationToken); - - /// - /// Asynchronously returns a with the specified character encoding for a file at the specified path. - /// - /// The file system to use. - /// The path of the file to open. - /// The encoding applied to the contents. - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation, - /// containing a that reads from the text file. - /// - public static async Task OpenTextAsync(this IVirtualFileSystem fs, string path, Encoding encoding, CancellationToken cancellationToken = default) - { - var stream = await fs.OpenReadAsync(path, cancellationToken).ConfigureAwait(false); - return new StreamReader(stream, encoding, detectEncodingFromByteOrderMarks: true, bufferSize: -1, leaveOpen: false); - } - - /// - /// Asynchronously opens the file at the specified path for writing. - /// - /// The file system to use. - /// The path of the file to open or create. - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation, containing a stream for writing to the file content. - /// - public static ValueTask OpenWriteAsync(this IVirtualFileSystem fs, string path, CancellationToken cancellationToken = default) => - fs.GetFile(path).OpenWriteAsync(cancellationToken); - - /// - /// Asynchronously writes the specified content to a file at the specified path. If the file exists, an exception will be thrown. - /// - /// The file system to use. - /// The path of the file. - /// A containing the content to write to the file. - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation. - /// - public static ValueTask WriteAsync(this IVirtualFileSystem fs, string path, Stream stream, CancellationToken cancellationToken = default) => - fs.GetFile(path).WriteAsync(stream, overwrite: false, cancellationToken); - - /// - /// Asynchronously writes the specified content to a file at the specified path, creating a new file or overwriting an existing one. - /// - /// The file system to use. - /// The path of the file. - /// A containing the content to write to the file. - /// to overwrite an existing file; - /// to throw an exception if the file already exists. - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation. - /// - /// - /// - /// If the file does not exist, it will be created. - /// If it exists and is , the existing file will be overwritten. - /// If is and the file exists, an exception will be thrown. - /// - /// - public static ValueTask WriteAsync(this IVirtualFileSystem fs, string path, Stream stream, bool overwrite, CancellationToken cancellationToken = default) => - fs.GetFile(path).WriteAsync(stream, overwrite, cancellationToken); - - /// - /// Asynchronously reads all the text in the file. - /// - /// The file system to use. - /// 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 IVirtualFileSystem fs, string path, CancellationToken cancellationToken = default) => - fs.GetFile(path).ReadAllTextAsync(cancellationToken); - - /// - /// Asynchronously reads all the text in the file with the specified encoding. - /// - /// The file system to use. - /// 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 IVirtualFileSystem fs, string path, Encoding? encoding, CancellationToken cancellationToken = default) => - fs.GetFile(path).ReadAllTextAsync(encoding, cancellationToken); - - /// - /// Asynchronously reads all lines of the file. - /// - /// The file system to use. - /// 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 IVirtualFileSystem fs, string path, CancellationToken cancellationToken = default) => - fs.GetFile(path).ReadAllLinesAsync(cancellationToken); - - /// - /// Asynchronously reads all lines of the file with the specified encoding. - /// - /// The file system to use. - /// 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 IVirtualFileSystem fs, string path, Encoding? encoding, CancellationToken cancellationToken = default) => - fs.GetFile(path).ReadAllLinesAsync(encoding, cancellationToken); - - /// - /// Asynchronously reads the entire contents of the specified file into a byte array. - /// - /// The file system to use. - /// 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 IVirtualFileSystem fs, string path, CancellationToken cancellationToken = default) => - fs.GetFile(path).ReadAllBytesAsync(cancellationToken); - - /// - /// Asynchronously writes the specified string to the specified file. If the file already exists, it is truncated and overwritten. - /// - /// The file system to use. - /// The file to write to. - /// The contents to write to the file. - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation. - /// - public static ValueTask WriteAllTextAsync(this IVirtualFileSystem fs, string path, string contents, CancellationToken cancellationToken = default) => - fs.GetFile(path).WriteAllTextAsync(contents, cancellationToken); - - /// - /// Asynchronously writes the specified string to the specified file. If the file already exists, it is truncated and overwritten. - /// - /// The file system to use. - /// The file to write to. - /// The contents to write to the file. - /// The encoding to apply to the string. - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation. - /// - public static ValueTask WriteAllTextAsync(this IVirtualFileSystem fs, string path, string contents, Encoding? encoding, CancellationToken cancellationToken = default) => - fs.GetFile(path).WriteAllTextAsync(contents, encoding, cancellationToken); - - /// - /// Asynchronously writes the specified string to the specified file. If the file already exists, it is truncated and overwritten. - /// - /// The file system to use. - /// The file to write to. - /// The contents to write to the file. - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation. - /// - public static ValueTask WriteAllTextAsync(this IVirtualFileSystem fs, string path, ReadOnlyMemory contents, CancellationToken cancellationToken = default) => - fs.GetFile(path).WriteAllTextAsync(contents, cancellationToken); - - /// - /// Asynchronously writes the specified string to the specified file. If the file already exists, it is truncated and overwritten. - /// - /// The file system to use. - /// The file to write to. - /// The contents to write to the file. - /// The encoding to apply to the string. - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation. - /// - public static ValueTask WriteAllTextAsync(this IVirtualFileSystem fs, string path, ReadOnlyMemory contents, Encoding? encoding, CancellationToken cancellationToken = default) => - fs.GetFile(path).WriteAllTextAsync(contents, encoding, cancellationToken); - - /// - /// Asynchronously writes the specified lines to the specified file. If the file already exists, it is truncated and overwritten. - /// - /// The file system to use. - /// The file to write to. - /// The contents to write to the file. - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation. - /// - public static ValueTask WriteAllLinesAsync(this IVirtualFileSystem fs, string path, IEnumerable contents, CancellationToken cancellationToken = default) => - fs.GetFile(path).WriteAllLinesAsync(contents, cancellationToken); - - /// - /// Asynchronously writes the specified lines to the specified file. If the file already exists, it is truncated and overwritten. - /// - /// The file system to use. - /// The file to write to. - /// The contents to write to the file. - /// The encoding to apply to the string. - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation. - /// - public static ValueTask WriteAllLinesAsync(this IVirtualFileSystem fs, string path, IEnumerable contents, Encoding? encoding, CancellationToken cancellationToken = default) => - fs.GetFile(path).WriteAllLinesAsync(contents, encoding, cancellationToken); - - /// - /// Asynchronously writes the specified byte array to the specified file. If the file already exists, it is truncated and overwritten. - /// - /// The file system to use. - /// The file to write to. - /// The bytes to write to the file. - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation. - /// - public static ValueTask WriteAllBytesAsync(this IVirtualFileSystem fs, string path, byte[] bytes, CancellationToken cancellationToken = default) => - fs.GetFile(path).WriteAllBytesAsync(bytes, cancellationToken); - - /// - /// Asynchronously writes the specified byte array to the specified file. If the file already exists, it is truncated and overwritten. - /// - /// The file system to use. - /// The file to write to. - /// The bytes to write to the file. - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation. - /// - public static ValueTask WriteAllBytesAsync(this IVirtualFileSystem fs, string path, ReadOnlyMemory bytes, CancellationToken cancellationToken = default) => - fs.GetFile(path).WriteAllBytesAsync(bytes, cancellationToken); - - /// - /// Asynchronously determines whether the file exists. - /// - /// The file system to use. - /// The path of the file. - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation. - /// The task result is if the file exists; otherwise, . - /// - public static ValueTask FileExistsAsync(this IVirtualFileSystem fs, string path, CancellationToken cancellationToken = default) => - fs.GetFile(path).ExistsAsync(cancellationToken); - - /// - /// Asynchronously deletes the file at the specified path. No exception is thrown if the file does not exist. - /// - /// The file system to use. - /// The path of the file to delete. - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation. - /// - public static ValueTask DeleteFileAsync(this IVirtualFileSystem fs, string path, CancellationToken cancellationToken = default) => - fs.GetFile(path).DeleteAsync(cancellationToken); - - /// - /// Asynchronously copies a file within the file system to the specified destination path. - /// - /// The file system to use. - /// The path of the file to copy. - /// The path where the file will be copied to. - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation. - /// - public static ValueTask CopyFileAsync(this IVirtualFileSystem fs, string path, string destinationPath, CancellationToken cancellationToken = default) => - fs.GetFile(path).CopyToAsync(destinationPath, overwrite: false, cancellationToken); - - /// - /// Asynchronously copies a file within the file system to the specified destination path. - /// - /// The file system to use. - /// The path of the file to copy. - /// The path where the file will be copied to. - /// to overwrite an existing file; to throw an exception if the file already exists. - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation. - /// - /// - /// - /// If the file does not exist, it will be created. - /// If it exists and is , the existing file will be overwritten. - /// If is and the file exists, an exception will be thrown. - /// - /// - public static ValueTask CopyFileAsync(this IVirtualFileSystem fs, string path, string destinationPath, bool overwrite, CancellationToken cancellationToken = default) => - fs.GetFile(path).CopyToAsync(destinationPath, overwrite, cancellationToken); - - /// - /// Asynchronously determines whether the directory exists. - /// - /// The file system to use. - /// The path of the directory. - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation. - /// The task result is if the directory exists; otherwise, . - /// - public static ValueTask DirectoryExistsAsync(this IVirtualFileSystem fs, string path, CancellationToken cancellationToken = default) => - fs.GetDirectory(path).ExistsAsync(cancellationToken); - - /// - /// Asynchronously creates a directory at the specified path. - /// - /// The file system to use. - /// The path of the directory to create. - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation. - /// - public static ValueTask CreateDirectoryAsync(this IVirtualFileSystem fs, string path, CancellationToken cancellationToken = default) => - fs.GetDirectory(path).CreateAsync(cancellationToken); - - /// - /// Asynchronously deletes a directory at the specified path, including its subdirectories and all files. - /// - /// The file system to use. - /// The path of the directory to delete. - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation. - /// - public static ValueTask DeleteDirectoryAsync(this IVirtualFileSystem fs, string path, CancellationToken cancellationToken = default) => - fs.GetDirectory(path).DeleteAsync(cancellationToken); - - /// - /// Asynchronously returns an async-enumerable collection of file nodes (both directories and files) - /// within the directory at the specified path. - /// - /// The file system to use. - /// The path of the directory. - /// An optional cancellation token to cancel the operation. - /// - /// An async-enumerable collection of instances. - /// - public static IAsyncEnumerable GetFileNodesAsync(this IVirtualFileSystem fs, string path, CancellationToken cancellationToken = default) => - fs.GetDirectory(path).GetFileNodesAsync(cancellationToken); - - /// - /// Asynchronously returns an async-enumerable collection of files within the directory at the specified path. - /// - /// The file system to use. - /// The path of the directory. - /// An optional cancellation token to cancel the operation. - /// - /// An async-enumerable collection of instances. - /// - public static IAsyncEnumerable GetFilesAsync(this IVirtualFileSystem fs, string path, CancellationToken cancellationToken = default) => - fs.GetDirectory(path).GetFilesAsync(cancellationToken); - - /// - /// Asynchronously returns an async-enumerable collection of directories within the directory at the specified path. - /// - /// The file system to use. - /// The path of the directory. - /// An optional cancellation token to cancel the operation. - /// - /// An async-enumerable collection of instances. - /// - public static IAsyncEnumerable GetDirectoriesAsync(this IVirtualFileSystem fs, string path, CancellationToken cancellationToken = default) => - fs.GetDirectory(path).GetDirectoriesAsync(cancellationToken); -} +using System.Text; + +namespace Ramstack.FileSystem; + +/// +/// Provides extension methods for the interface. +/// +public static partial class VirtualFileSystemExtensions +{ + /// + /// Asynchronously opens the file at the specified path for reading. + /// + /// The file system to use. + /// The path of the file to open. + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation, containing a stream for reading the file content. + /// + public static ValueTask OpenReadAsync(this IVirtualFileSystem fs, string path, CancellationToken cancellationToken = default) => + fs.GetFile(path).OpenReadAsync(cancellationToken); + + /// + /// Asynchronously returns a with encoding for a file at the specified path. + /// + /// The file system to use. + /// The path of the file to open. + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation, + /// containing a that reads from the text file. + /// + public static Task OpenTextAsync(this IVirtualFileSystem fs, string path, CancellationToken cancellationToken = default) => + fs.OpenTextAsync(path, Encoding.UTF8, cancellationToken); + + /// + /// Asynchronously returns a with the specified character encoding for a file at the specified path. + /// + /// The file system to use. + /// The path of the file to open. + /// The encoding applied to the contents. + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation, + /// containing a that reads from the text file. + /// + public static async Task OpenTextAsync(this IVirtualFileSystem fs, string path, Encoding encoding, CancellationToken cancellationToken = default) + { + var stream = await fs.OpenReadAsync(path, cancellationToken).ConfigureAwait(false); + return new StreamReader(stream, encoding, detectEncodingFromByteOrderMarks: true, bufferSize: -1, leaveOpen: false); + } + + /// + /// Asynchronously opens the file at the specified path for writing. + /// + /// The file system to use. + /// The path of the file to open or create. + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation, containing a stream for writing to the file content. + /// + public static ValueTask OpenWriteAsync(this IVirtualFileSystem fs, string path, CancellationToken cancellationToken = default) => + fs.GetFile(path).OpenWriteAsync(cancellationToken); + + /// + /// Asynchronously writes the specified content to a file at the specified path. If the file exists, an exception will be thrown. + /// + /// The file system to use. + /// The path of the file. + /// A containing the content to write to the file. + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation. + /// + public static ValueTask WriteAsync(this IVirtualFileSystem fs, string path, Stream stream, CancellationToken cancellationToken = default) => + fs.GetFile(path).WriteAsync(stream, overwrite: false, cancellationToken); + + /// + /// Asynchronously writes the specified content to a file at the specified path, creating a new file or overwriting an existing one. + /// + /// The file system to use. + /// The path of the file. + /// A containing the content to write to the file. + /// to overwrite an existing file; + /// to throw an exception if the file already exists. + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation. + /// + /// + /// + /// If the file does not exist, it will be created. + /// If it exists and is , the existing file will be overwritten. + /// If is and the file exists, an exception will be thrown. + /// + /// + public static ValueTask WriteAsync(this IVirtualFileSystem fs, string path, Stream stream, bool overwrite, CancellationToken cancellationToken = default) => + fs.GetFile(path).WriteAsync(stream, overwrite, cancellationToken); + + /// + /// Asynchronously reads all the text in the file. + /// + /// The file system to use. + /// 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 IVirtualFileSystem fs, string path, CancellationToken cancellationToken = default) => + fs.GetFile(path).ReadAllTextAsync(cancellationToken); + + /// + /// Asynchronously reads all the text in the file with the specified encoding. + /// + /// The file system to use. + /// 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 IVirtualFileSystem fs, string path, Encoding? encoding, CancellationToken cancellationToken = default) => + fs.GetFile(path).ReadAllTextAsync(encoding, cancellationToken); + + /// + /// Asynchronously reads all lines of the file. + /// + /// The file system to use. + /// 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 IVirtualFileSystem fs, string path, CancellationToken cancellationToken = default) => + fs.GetFile(path).ReadAllLinesAsync(cancellationToken); + + /// + /// Asynchronously reads all lines of the file with the specified encoding. + /// + /// The file system to use. + /// 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 IVirtualFileSystem fs, string path, Encoding? encoding, CancellationToken cancellationToken = default) => + fs.GetFile(path).ReadAllLinesAsync(encoding, cancellationToken); + + /// + /// Asynchronously reads the entire contents of the specified file into a byte array. + /// + /// The file system to use. + /// 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 IVirtualFileSystem fs, string path, CancellationToken cancellationToken = default) => + fs.GetFile(path).ReadAllBytesAsync(cancellationToken); + + /// + /// Asynchronously writes the specified string to the specified file. If the file already exists, it is truncated and overwritten. + /// + /// The file system to use. + /// The file to write to. + /// The contents to write to the file. + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation. + /// + public static ValueTask WriteAllTextAsync(this IVirtualFileSystem fs, string path, string contents, CancellationToken cancellationToken = default) => + fs.GetFile(path).WriteAllTextAsync(contents, cancellationToken); + + /// + /// Asynchronously writes the specified string to the specified file. If the file already exists, it is truncated and overwritten. + /// + /// The file system to use. + /// The file to write to. + /// The contents to write to the file. + /// The encoding to apply to the string. + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation. + /// + public static ValueTask WriteAllTextAsync(this IVirtualFileSystem fs, string path, string contents, Encoding? encoding, CancellationToken cancellationToken = default) => + fs.GetFile(path).WriteAllTextAsync(contents, encoding, cancellationToken); + + /// + /// Asynchronously writes the specified string to the specified file. If the file already exists, it is truncated and overwritten. + /// + /// The file system to use. + /// The file to write to. + /// The contents to write to the file. + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation. + /// + public static ValueTask WriteAllTextAsync(this IVirtualFileSystem fs, string path, ReadOnlyMemory contents, CancellationToken cancellationToken = default) => + fs.GetFile(path).WriteAllTextAsync(contents, cancellationToken); + + /// + /// Asynchronously writes the specified string to the specified file. If the file already exists, it is truncated and overwritten. + /// + /// The file system to use. + /// The file to write to. + /// The contents to write to the file. + /// The encoding to apply to the string. + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation. + /// + public static ValueTask WriteAllTextAsync(this IVirtualFileSystem fs, string path, ReadOnlyMemory contents, Encoding? encoding, CancellationToken cancellationToken = default) => + fs.GetFile(path).WriteAllTextAsync(contents, encoding, cancellationToken); + + /// + /// Asynchronously writes the specified lines to the specified file. If the file already exists, it is truncated and overwritten. + /// + /// The file system to use. + /// The file to write to. + /// The contents to write to the file. + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation. + /// + public static ValueTask WriteAllLinesAsync(this IVirtualFileSystem fs, string path, IEnumerable contents, CancellationToken cancellationToken = default) => + fs.GetFile(path).WriteAllLinesAsync(contents, cancellationToken); + + /// + /// Asynchronously writes the specified lines to the specified file. If the file already exists, it is truncated and overwritten. + /// + /// The file system to use. + /// The file to write to. + /// The contents to write to the file. + /// The encoding to apply to the string. + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation. + /// + public static ValueTask WriteAllLinesAsync(this IVirtualFileSystem fs, string path, IEnumerable contents, Encoding? encoding, CancellationToken cancellationToken = default) => + fs.GetFile(path).WriteAllLinesAsync(contents, encoding, cancellationToken); + + /// + /// Asynchronously writes the specified byte array to the specified file. If the file already exists, it is truncated and overwritten. + /// + /// The file system to use. + /// The file to write to. + /// The bytes to write to the file. + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation. + /// + public static ValueTask WriteAllBytesAsync(this IVirtualFileSystem fs, string path, byte[] bytes, CancellationToken cancellationToken = default) => + fs.GetFile(path).WriteAllBytesAsync(bytes, cancellationToken); + + /// + /// Asynchronously writes the specified byte array to the specified file. If the file already exists, it is truncated and overwritten. + /// + /// The file system to use. + /// The file to write to. + /// The bytes to write to the file. + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation. + /// + public static ValueTask WriteAllBytesAsync(this IVirtualFileSystem fs, string path, ReadOnlyMemory bytes, CancellationToken cancellationToken = default) => + fs.GetFile(path).WriteAllBytesAsync(bytes, cancellationToken); + + /// + /// Asynchronously determines whether the file exists. + /// + /// The file system to use. + /// The path of the file. + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation. + /// The task result is if the file exists; otherwise, . + /// + public static ValueTask FileExistsAsync(this IVirtualFileSystem fs, string path, CancellationToken cancellationToken = default) => + fs.GetFile(path).ExistsAsync(cancellationToken); + + /// + /// Asynchronously deletes the file at the specified path. No exception is thrown if the file does not exist. + /// + /// The file system to use. + /// The path of the file to delete. + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation. + /// + public static ValueTask DeleteFileAsync(this IVirtualFileSystem fs, string path, CancellationToken cancellationToken = default) => + fs.GetFile(path).DeleteAsync(cancellationToken); + + /// + /// Asynchronously copies a file within the file system to the specified destination path. + /// + /// The file system to use. + /// The path of the file to copy. + /// The path where the file will be copied to. + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation. + /// + public static ValueTask CopyFileAsync(this IVirtualFileSystem fs, string path, string destinationPath, CancellationToken cancellationToken = default) => + fs.GetFile(path).CopyToAsync(destinationPath, overwrite: false, cancellationToken); + + /// + /// Asynchronously copies a file within the file system to the specified destination path. + /// + /// The file system to use. + /// The path of the file to copy. + /// The path where the file will be copied to. + /// to overwrite an existing file; to throw an exception if the file already exists. + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation. + /// + /// + /// + /// If the file does not exist, it will be created. + /// If it exists and is , the existing file will be overwritten. + /// If is and the file exists, an exception will be thrown. + /// + /// + public static ValueTask CopyFileAsync(this IVirtualFileSystem fs, string path, string destinationPath, bool overwrite, CancellationToken cancellationToken = default) => + fs.GetFile(path).CopyToAsync(destinationPath, overwrite, cancellationToken); + + /// + /// Asynchronously determines whether the directory exists. + /// + /// The file system to use. + /// The path of the directory. + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation. + /// The task result is if the directory exists; otherwise, . + /// + public static ValueTask DirectoryExistsAsync(this IVirtualFileSystem fs, string path, CancellationToken cancellationToken = default) => + fs.GetDirectory(path).ExistsAsync(cancellationToken); + + /// + /// Asynchronously creates a directory at the specified path. + /// + /// The file system to use. + /// The path of the directory to create. + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation. + /// + public static ValueTask CreateDirectoryAsync(this IVirtualFileSystem fs, string path, CancellationToken cancellationToken = default) => + fs.GetDirectory(path).CreateAsync(cancellationToken); + + /// + /// Asynchronously deletes a directory at the specified path, including its subdirectories and all files. + /// + /// The file system to use. + /// The path of the directory to delete. + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation. + /// + public static ValueTask DeleteDirectoryAsync(this IVirtualFileSystem fs, string path, CancellationToken cancellationToken = default) => + fs.GetDirectory(path).DeleteAsync(cancellationToken); + + /// + /// Asynchronously returns an async-enumerable collection of file nodes (both directories and files) + /// within the directory at the specified path. + /// + /// The file system to use. + /// The path of the directory. + /// An optional cancellation token to cancel the operation. + /// + /// An async-enumerable collection of instances. + /// + public static IAsyncEnumerable GetFileNodesAsync(this IVirtualFileSystem fs, string path, CancellationToken cancellationToken = default) => + fs.GetDirectory(path).GetFileNodesAsync(cancellationToken); + + /// + /// Asynchronously returns an async-enumerable collection of files within the directory at the specified path. + /// + /// The file system to use. + /// The path of the directory. + /// An optional cancellation token to cancel the operation. + /// + /// An async-enumerable collection of instances. + /// + public static IAsyncEnumerable GetFilesAsync(this IVirtualFileSystem fs, string path, CancellationToken cancellationToken = default) => + fs.GetDirectory(path).GetFilesAsync(cancellationToken); + + /// + /// Asynchronously returns an async-enumerable collection of directories within the directory at the specified path. + /// + /// The file system to use. + /// The path of the directory. + /// An optional cancellation token to cancel the operation. + /// + /// An async-enumerable collection of instances. + /// + public static IAsyncEnumerable GetDirectoriesAsync(this IVirtualFileSystem fs, string path, CancellationToken cancellationToken = default) => + fs.GetDirectory(path).GetDirectoriesAsync(cancellationToken); +} diff --git a/src/Ramstack.FileSystem.Abstractions/VirtualNode.cs b/src/Ramstack.FileSystem.Abstractions/VirtualNode.cs index f83ca8b..a51a590 100644 --- a/src/Ramstack.FileSystem.Abstractions/VirtualNode.cs +++ b/src/Ramstack.FileSystem.Abstractions/VirtualNode.cs @@ -1,154 +1,154 @@ -namespace Ramstack.FileSystem; - -/// -/// Represents a node in a virtual file system, which could be either a file or a directory. -/// -public abstract class VirtualNode -{ - private VirtualNodeProperties? _properties; - - /// - /// Gets the instance associated with this node. - /// - public abstract IVirtualFileSystem FileSystem { get; } - - /// - /// Gets a value indicating whether the file or directory is read-only. - /// - public bool IsReadOnly => FileSystem.IsReadOnly; - - /// - /// Gets the full path of the file or directory. - /// - public string FullName { get; } - - /// - /// Gets the name of the file or directory, excluding the path. - /// - public string Name => VirtualPath.GetFileName(FullName); - - /// - /// Gets the file extension, including the leading dot ., or an empty string if there is no extension. - /// - public string Extension => VirtualPath.GetExtension(FullName); - - /// - /// Initializes a new instance of the class. - /// - /// The full path of the file or directory. - protected VirtualNode(string path) - { - Debug.Assert(VirtualPath.IsNormalized(path)); - FullName = path; - } - - /// - /// Initializes a new instance of the class with specified properties. - /// - /// The full path of the file or directory. - /// The properties of the file or directory. - protected VirtualNode(string path, VirtualNodeProperties? properties) : this(path) => - _properties = properties; - - /// - /// Refreshes the state of the file or directory node, clearing any cached or stored values. - /// - public void Refresh() - { - _properties = null; - RefreshCore(); - } - - /// - /// Asynchronously determines whether the file or directory exists. - /// - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation. - /// The task result is if the file or directory exists; otherwise, . - /// - public ValueTask ExistsAsync(CancellationToken cancellationToken = default) - { - if (TryGetProperties(out var properties)) - return new ValueTask(properties.Exists); - - return ExistsCoreAsync(cancellationToken); - } - - /// - /// Asynchronously retrieves the properties of the file or directory. - /// - /// A boolean indicating whether to refresh the state before retrieving the properties. - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation, - /// with a result of type containing the file or directory properties. - /// - public ValueTask GetPropertiesAsync(bool refresh, CancellationToken cancellationToken = default) - { - var properties = _properties; - return refresh || properties is null - ? GetPropertiesImpl(cancellationToken) - : new ValueTask(properties); - - async ValueTask GetPropertiesImpl(CancellationToken token) => - _properties = await GetPropertiesCoreAsync(token).ConfigureAwait(false) ?? VirtualNodeProperties.Unavailable; - } - - /// - /// Core implementation for refreshing the state of the file or directory node, - /// clearing any cached or stored values. - /// - protected virtual void RefreshCore() - { - } - - /// - /// Core implementation for asynchronously retrieving the current properties of the file or directory. - /// - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation, - /// with a result of type containing the updated properties, - /// or if the properties cannot be retrieved. - /// - protected abstract ValueTask GetPropertiesCoreAsync(CancellationToken cancellationToken); - - /// - /// Core implementation for asynchronously determining whether the file or directory exists. - /// - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation. - /// The task result is if the file or directory exists; otherwise, . - /// - protected virtual async ValueTask ExistsCoreAsync(CancellationToken cancellationToken) => - (await GetPropertiesAsync(refresh: true, cancellationToken).ConfigureAwait(false)).Exists; - - /// - /// Attempts to retrieve the current properties of the file or directory. - /// - /// When this method returns, contains the current if available; - /// otherwise, . - /// - /// if the properties were successfully retrieved; - /// otherwise, if the properties have not been set or have been invalidated. - /// - protected bool TryGetProperties([NotNullWhen(true)] out VirtualNodeProperties? properties) - { - properties = _properties; - return properties is not null; - } - - /// - /// Ensures that the current file or directory is writable. - /// - /// Thrown when the current file or directory is read-only - /// and modifications are not supported. - /// - protected void EnsureWritable() - { - if (FileSystem.IsReadOnly) - throw new NotSupportedException("Write operations are not supported on a read-only instance."); - } -} +namespace Ramstack.FileSystem; + +/// +/// Represents a node in a virtual file system, which could be either a file or a directory. +/// +public abstract class VirtualNode +{ + private VirtualNodeProperties? _properties; + + /// + /// Gets the instance associated with this node. + /// + public abstract IVirtualFileSystem FileSystem { get; } + + /// + /// Gets a value indicating whether the file or directory is read-only. + /// + public bool IsReadOnly => FileSystem.IsReadOnly; + + /// + /// Gets the full path of the file or directory. + /// + public string FullName { get; } + + /// + /// Gets the name of the file or directory, excluding the path. + /// + public string Name => VirtualPath.GetFileName(FullName); + + /// + /// Gets the file extension, including the leading dot ., or an empty string if there is no extension. + /// + public string Extension => VirtualPath.GetExtension(FullName); + + /// + /// Initializes a new instance of the class. + /// + /// The full path of the file or directory. + protected VirtualNode(string path) + { + Debug.Assert(VirtualPath.IsNormalized(path)); + FullName = path; + } + + /// + /// Initializes a new instance of the class with specified properties. + /// + /// The full path of the file or directory. + /// The properties of the file or directory. + protected VirtualNode(string path, VirtualNodeProperties? properties) : this(path) => + _properties = properties; + + /// + /// Refreshes the state of the file or directory node, clearing any cached or stored values. + /// + public void Refresh() + { + _properties = null; + RefreshCore(); + } + + /// + /// Asynchronously determines whether the file or directory exists. + /// + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation. + /// The task result is if the file or directory exists; otherwise, . + /// + public ValueTask ExistsAsync(CancellationToken cancellationToken = default) + { + if (TryGetProperties(out var properties)) + return new ValueTask(properties.Exists); + + return ExistsCoreAsync(cancellationToken); + } + + /// + /// Asynchronously retrieves the properties of the file or directory. + /// + /// A boolean indicating whether to refresh the state before retrieving the properties. + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation, + /// with a result of type containing the file or directory properties. + /// + public ValueTask GetPropertiesAsync(bool refresh, CancellationToken cancellationToken = default) + { + var properties = _properties; + return refresh || properties is null + ? GetPropertiesImpl(cancellationToken) + : new ValueTask(properties); + + async ValueTask GetPropertiesImpl(CancellationToken token) => + _properties = await GetPropertiesCoreAsync(token).ConfigureAwait(false) ?? VirtualNodeProperties.Unavailable; + } + + /// + /// Core implementation for refreshing the state of the file or directory node, + /// clearing any cached or stored values. + /// + protected virtual void RefreshCore() + { + } + + /// + /// Core implementation for asynchronously retrieving the current properties of the file or directory. + /// + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation, + /// with a result of type containing the updated properties, + /// or if the properties cannot be retrieved. + /// + protected abstract ValueTask GetPropertiesCoreAsync(CancellationToken cancellationToken); + + /// + /// Core implementation for asynchronously determining whether the file or directory exists. + /// + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation. + /// The task result is if the file or directory exists; otherwise, . + /// + protected virtual async ValueTask ExistsCoreAsync(CancellationToken cancellationToken) => + (await GetPropertiesAsync(refresh: true, cancellationToken).ConfigureAwait(false)).Exists; + + /// + /// Attempts to retrieve the current properties of the file or directory. + /// + /// When this method returns, contains the current if available; + /// otherwise, . + /// + /// if the properties were successfully retrieved; + /// otherwise, if the properties have not been set or have been invalidated. + /// + protected bool TryGetProperties([NotNullWhen(true)] out VirtualNodeProperties? properties) + { + properties = _properties; + return properties is not null; + } + + /// + /// Ensures that the current file or directory is writable. + /// + /// Thrown when the current file or directory is read-only + /// and modifications are not supported. + /// + protected void EnsureWritable() + { + if (FileSystem.IsReadOnly) + throw new NotSupportedException("Write operations are not supported on a read-only instance."); + } +} diff --git a/src/Ramstack.FileSystem.Abstractions/VirtualNodeExtensions.cs b/src/Ramstack.FileSystem.Abstractions/VirtualNodeExtensions.cs index cf3cb4e..3fb3f56 100644 --- a/src/Ramstack.FileSystem.Abstractions/VirtualNodeExtensions.cs +++ b/src/Ramstack.FileSystem.Abstractions/VirtualNodeExtensions.cs @@ -1,19 +1,19 @@ -namespace Ramstack.FileSystem; - -/// -/// Provides extension methods for the class. -/// -public static class VirtualNodeExtensions -{ - /// - /// Asynchronously retrieves the properties of the specified file or directory. - /// - /// The source file or directory for which to get the . - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation, - /// with a result of type containing the node properties. - /// - public static ValueTask GetPropertiesAsync(this VirtualNode node, CancellationToken cancellationToken = default) => - node.GetPropertiesAsync(refresh: false, cancellationToken); -} +namespace Ramstack.FileSystem; + +/// +/// Provides extension methods for the class. +/// +public static class VirtualNodeExtensions +{ + /// + /// Asynchronously retrieves the properties of the specified file or directory. + /// + /// The source file or directory for which to get the . + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation, + /// with a result of type containing the node properties. + /// + public static ValueTask GetPropertiesAsync(this VirtualNode node, CancellationToken cancellationToken = default) => + node.GetPropertiesAsync(refresh: false, cancellationToken); +} diff --git a/src/Ramstack.FileSystem.Abstractions/VirtualNodeProperties.cs b/src/Ramstack.FileSystem.Abstractions/VirtualNodeProperties.cs index 32bd808..a2d7903 100644 --- a/src/Ramstack.FileSystem.Abstractions/VirtualNodeProperties.cs +++ b/src/Ramstack.FileSystem.Abstractions/VirtualNodeProperties.cs @@ -1,92 +1,92 @@ -namespace Ramstack.FileSystem; - -/// -/// Represents the properties of a file node, which can be either a file or a directory. -/// -[DebuggerDisplay("{ToStringDebugger(),nq}")] -public sealed class VirtualNodeProperties -{ - /// - /// Gets an instance of that represents a node with an unavailable state. - /// - public static VirtualNodeProperties Unavailable { get; } = - new VirtualNodeProperties(default, default, default, -1); - - /// - /// Gets an instance of that represents a node with no data. - /// - public static VirtualNodeProperties None { get; } = - new VirtualNodeProperties(default, default, default, 0); - - /// - /// Gets the time when the current file or directory was created. - /// - public DateTimeOffset CreationTime { get; } - - /// - /// Gets the time when the current file or directory was last accessed. - /// - public DateTimeOffset LastAccessTime { get; } - - /// - /// Gets the time when the current file or directory was last modified. - /// - public DateTimeOffset LastWriteTime { get; } - - /// - /// Gets the size of the file in bytes. - /// Returns 0 for directories and -1 for non-existent nodes. - /// - public long Length { get; } - - /// - /// Gets a value indicating whether the current file or directory exists. - /// Returns if the node exists; otherwise, . - /// - public bool Exists => Length >= 0; - - /// - /// Initializes a new instance of the class. - /// - /// The time when the node was created. - /// The time when the node was last accessed. - /// The time when the node was last modified. - /// The size of the file in bytes, or 0 for directories, or -1 for non-existent nodes. - private VirtualNodeProperties(DateTimeOffset creationTime, DateTimeOffset lastAccessTime, DateTimeOffset lastWriteTime, long length) => - (CreationTime, LastAccessTime, LastWriteTime, Length) = (creationTime, lastAccessTime, lastWriteTime, length); - - /// - /// Creates a new instance of for a file. - /// - /// The time when the file was created. - /// The time when the file was last accessed. - /// The time when the file was last modified. - /// The size of the file in bytes. - /// - /// A new instance representing a file. - /// - public static VirtualNodeProperties CreateFileProperties(DateTimeOffset creationTime, DateTimeOffset lastAccessTime, DateTimeOffset lastWriteTime, long length) => - new VirtualNodeProperties(creationTime, lastAccessTime, lastWriteTime, length); - - /// - /// Creates a new instance of for a directory. - /// - /// The time when the directory was created. - /// The time when the directory was last accessed. - /// The time when the directory was last modified. - /// - /// A new instance representing a directory. - /// - public static VirtualNodeProperties CreateDirectoryProperties(DateTimeOffset creationTime, DateTimeOffset lastAccessTime, DateTimeOffset lastWriteTime) => - new VirtualNodeProperties(creationTime, lastAccessTime, lastWriteTime, 0); - - /// - /// Returns a string representation of the current instance for debugging purposes. - /// - /// - /// A string that provides information about the length of the current instance. - /// If the length is -1, it returns "Unavailable". - /// - private string ToStringDebugger() => - Length >= 0 ? $"Length = {Length}" : "Unavailable"; -} +namespace Ramstack.FileSystem; + +/// +/// Represents the properties of a file node, which can be either a file or a directory. +/// +[DebuggerDisplay("{ToStringDebugger(),nq}")] +public sealed class VirtualNodeProperties +{ + /// + /// Gets an instance of that represents a node with an unavailable state. + /// + public static VirtualNodeProperties Unavailable { get; } = + new VirtualNodeProperties(default, default, default, -1); + + /// + /// Gets an instance of that represents a node with no data. + /// + public static VirtualNodeProperties None { get; } = + new VirtualNodeProperties(default, default, default, 0); + + /// + /// Gets the time when the current file or directory was created. + /// + public DateTimeOffset CreationTime { get; } + + /// + /// Gets the time when the current file or directory was last accessed. + /// + public DateTimeOffset LastAccessTime { get; } + + /// + /// Gets the time when the current file or directory was last modified. + /// + public DateTimeOffset LastWriteTime { get; } + + /// + /// Gets the size of the file in bytes. + /// Returns 0 for directories and -1 for non-existent nodes. + /// + public long Length { get; } + + /// + /// Gets a value indicating whether the current file or directory exists. + /// Returns if the node exists; otherwise, . + /// + public bool Exists => Length >= 0; + + /// + /// Initializes a new instance of the class. + /// + /// The time when the node was created. + /// The time when the node was last accessed. + /// The time when the node was last modified. + /// The size of the file in bytes, or 0 for directories, or -1 for non-existent nodes. + private VirtualNodeProperties(DateTimeOffset creationTime, DateTimeOffset lastAccessTime, DateTimeOffset lastWriteTime, long length) => + (CreationTime, LastAccessTime, LastWriteTime, Length) = (creationTime, lastAccessTime, lastWriteTime, length); + + /// + /// Creates a new instance of for a file. + /// + /// The time when the file was created. + /// The time when the file was last accessed. + /// The time when the file was last modified. + /// The size of the file in bytes. + /// + /// A new instance representing a file. + /// + public static VirtualNodeProperties CreateFileProperties(DateTimeOffset creationTime, DateTimeOffset lastAccessTime, DateTimeOffset lastWriteTime, long length) => + new VirtualNodeProperties(creationTime, lastAccessTime, lastWriteTime, length); + + /// + /// Creates a new instance of for a directory. + /// + /// The time when the directory was created. + /// The time when the directory was last accessed. + /// The time when the directory was last modified. + /// + /// A new instance representing a directory. + /// + public static VirtualNodeProperties CreateDirectoryProperties(DateTimeOffset creationTime, DateTimeOffset lastAccessTime, DateTimeOffset lastWriteTime) => + new VirtualNodeProperties(creationTime, lastAccessTime, lastWriteTime, 0); + + /// + /// Returns a string representation of the current instance for debugging purposes. + /// + /// + /// A string that provides information about the length of the current instance. + /// If the length is -1, it returns "Unavailable". + /// + private string ToStringDebugger() => + Length >= 0 ? $"Length = {Length}" : "Unavailable"; +} diff --git a/src/Ramstack.FileSystem.Abstractions/VirtualPath.cs b/src/Ramstack.FileSystem.Abstractions/VirtualPath.cs index dd75453..5648368 100644 --- a/src/Ramstack.FileSystem.Abstractions/VirtualPath.cs +++ b/src/Ramstack.FileSystem.Abstractions/VirtualPath.cs @@ -1,401 +1,401 @@ -using System.Buffers; - -using Ramstack.FileSystem.Utilities; - -namespace Ramstack.FileSystem; - -/// -/// 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. -/// -/// -public static class VirtualPath -{ - /// - /// 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. - /// - /// The directory portion of , or an empty span if - /// is empty or 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. - /// - /// The directory portion of , or an empty span if - /// is empty or denotes a root directory. - /// - public static ReadOnlySpan GetDirectoryName(ReadOnlySpan path) - { - var offset = GetDirectoryNameOffset(path); - - if (offset > 0 && (uint)offset < (uint)path.Length) - return path.Slice(0, offset); - - return offset == 0 ? "/" : ""; - } - - /// - /// Determines if 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) - { - // - // Short-circuit optimization: - // Many paths are already normalized except for a missing leading slash. - // It's more efficient to first check/add the leading slash - // before performing full normalization. - // - - if (!path.StartsWith('/')) - path = $"/{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; + +using Ramstack.FileSystem.Utilities; + +namespace Ramstack.FileSystem; + +/// +/// 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. +/// +/// +public static class VirtualPath +{ + /// + /// 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. + /// + /// The directory portion of , or an empty span if + /// is empty or 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. + /// + /// The directory portion of , or an empty span if + /// is empty or denotes a root directory. + /// + public static ReadOnlySpan GetDirectoryName(ReadOnlySpan path) + { + var offset = GetDirectoryNameOffset(path); + + if (offset > 0 && (uint)offset < (uint)path.Length) + return path.Slice(0, offset); + + return offset == 0 ? "/" : ""; + } + + /// + /// Determines if 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) + { + // + // Short-circuit optimization: + // Many paths are already normalized except for a missing leading slash. + // It's more efficient to first check/add the leading slash + // before performing full normalization. + // + + if (!path.StartsWith('/')) + path = $"/{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.FileSystem.Adapters/README.md b/src/Ramstack.FileSystem.Adapters/README.md index 9032aa6..013d4a7 100644 --- a/src/Ramstack.FileSystem.Adapters/README.md +++ b/src/Ramstack.FileSystem.Adapters/README.md @@ -1,61 +1,61 @@ -# Ramstack.FileSystem.Adapters -[![NuGet](https://img.shields.io/nuget/v/Ramstack.FileSystem.Adapters.svg)](https://nuget.org/packages/Ramstack.FileSystem.Adapters) -[![MIT](https://img.shields.io/github/license/rameel/ramstack.filesystem)](https://github.com/rameel/ramstack.filesystem/blob/main/LICENSE) - -Provides an implementation of `Ramstack.FileSystem` that integrates with `Microsoft.Extensions.FileProviders`. - -## Getting Started - -To install the `Ramstack.FileSystem.Adapters` [NuGet package](https://www.nuget.org/packages/Ramstack.FileSystem.Adapters) -in your project, run the following command: -```console -dotnet add package Ramstack.FileSystem.Adapters -``` -## Usage - -```csharp -using Microsoft.Extensions.FileProviders; -using Microsoft.Extensions.FileProviders.Physical; -using Ramstack.FileSystem.Adapters; - -// Create a physical file provider that points to the specified directory -IFileProvider provider = new PhysicalFileProvider(@"C:\path"); - -// Create a virtual file system adapter that wraps the physical file provider. -// -// An added benefit is that now information about the entire file hierarchy is available -// for files and directories provided by the underlying provider. -IVirtualFileSystem fs = new VirtualFileSystemAdapter(provider); - -await foreach (VirtualFile file in fs.GetFilesAsync("/")) -{ - Console.WriteLine(file.FullName); -} -``` - -## Related Projects -- [Ramstack.FileSystem.Abstractions](https://www.nuget.org/packages/Ramstack.FileSystem.Abstractions) - Provides a virtual file system abstraction. -- [Ramstack.FileSystem.Physical](https://www.nuget.org/packages/Ramstack.FileSystem.Physical) - Provides an implementation based on the local file system. -- [Ramstack.FileSystem.Azure](https://www.nuget.org/packages/Ramstack.FileSystem.Azure) - Provides an implementation using Azure Blob Storage. -- [Ramstack.FileSystem.Amazon](https://www.nuget.org/packages/Ramstack.FileSystem.Amazon) - Provides an implementation using Amazon S3 storage. -- [Ramstack.FileSystem.Google](https://www.nuget.org/packages/Ramstack.FileSystem.Google) - Provides an implementation using Google Cloud Storage. -- [Ramstack.FileSystem.Readonly](https://www.nuget.org/packages/Ramstack.FileSystem.Readonly) - Provides a read-only wrapper for the underlying file system. -- [Ramstack.FileSystem.Globbing](https://www.nuget.org/packages/Ramstack.FileSystem.Globbing) - Wraps the file system, filtering files and directories using glob patterns. -- [Ramstack.FileSystem.Prefixed](https://www.nuget.org/packages/Ramstack.FileSystem.Prefixed) - Adds a prefix to file paths within the underlying file system. -- [Ramstack.FileSystem.Sub](https://www.nuget.org/packages/Ramstack.FileSystem.Sub) - Wraps the underlying file system, restricting access to a specific subpath. -- [Ramstack.FileSystem.Composite](https://www.nuget.org/packages/Ramstack.FileSystem.Composite) - Provides an implementation that combines multiple file systems into a single composite file system. - -## 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.virtualfiles/blob/main/LICENSE) file for more details. +# Ramstack.FileSystem.Adapters +[![NuGet](https://img.shields.io/nuget/v/Ramstack.FileSystem.Adapters.svg)](https://nuget.org/packages/Ramstack.FileSystem.Adapters) +[![MIT](https://img.shields.io/github/license/rameel/ramstack.filesystem)](https://github.com/rameel/ramstack.filesystem/blob/main/LICENSE) + +Provides an implementation of `Ramstack.FileSystem` that integrates with `Microsoft.Extensions.FileProviders`. + +## Getting Started + +To install the `Ramstack.FileSystem.Adapters` [NuGet package](https://www.nuget.org/packages/Ramstack.FileSystem.Adapters) +in your project, run the following command: +```console +dotnet add package Ramstack.FileSystem.Adapters +``` +## Usage + +```csharp +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.FileProviders.Physical; +using Ramstack.FileSystem.Adapters; + +// Create a physical file provider that points to the specified directory +IFileProvider provider = new PhysicalFileProvider(@"C:\path"); + +// Create a virtual file system adapter that wraps the physical file provider. +// +// An added benefit is that now information about the entire file hierarchy is available +// for files and directories provided by the underlying provider. +IVirtualFileSystem fs = new VirtualFileSystemAdapter(provider); + +await foreach (VirtualFile file in fs.GetFilesAsync("/")) +{ + Console.WriteLine(file.FullName); +} +``` + +## Related Projects +- [Ramstack.FileSystem.Abstractions](https://www.nuget.org/packages/Ramstack.FileSystem.Abstractions) - Provides a virtual file system abstraction. +- [Ramstack.FileSystem.Physical](https://www.nuget.org/packages/Ramstack.FileSystem.Physical) - Provides an implementation based on the local file system. +- [Ramstack.FileSystem.Azure](https://www.nuget.org/packages/Ramstack.FileSystem.Azure) - Provides an implementation using Azure Blob Storage. +- [Ramstack.FileSystem.Amazon](https://www.nuget.org/packages/Ramstack.FileSystem.Amazon) - Provides an implementation using Amazon S3 storage. +- [Ramstack.FileSystem.Google](https://www.nuget.org/packages/Ramstack.FileSystem.Google) - Provides an implementation using Google Cloud Storage. +- [Ramstack.FileSystem.Readonly](https://www.nuget.org/packages/Ramstack.FileSystem.Readonly) - Provides a read-only wrapper for the underlying file system. +- [Ramstack.FileSystem.Globbing](https://www.nuget.org/packages/Ramstack.FileSystem.Globbing) - Wraps the file system, filtering files and directories using glob patterns. +- [Ramstack.FileSystem.Prefixed](https://www.nuget.org/packages/Ramstack.FileSystem.Prefixed) - Adds a prefix to file paths within the underlying file system. +- [Ramstack.FileSystem.Sub](https://www.nuget.org/packages/Ramstack.FileSystem.Sub) - Wraps the underlying file system, restricting access to a specific subpath. +- [Ramstack.FileSystem.Composite](https://www.nuget.org/packages/Ramstack.FileSystem.Composite) - Provides an implementation that combines multiple file systems into a single composite file system. + +## 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.virtualfiles/blob/main/LICENSE) file for more details. diff --git a/src/Ramstack.FileSystem.Adapters/Ramstack.FileSystem.Adapters.csproj b/src/Ramstack.FileSystem.Adapters/Ramstack.FileSystem.Adapters.csproj index 89d8e6d..0934aae 100644 --- a/src/Ramstack.FileSystem.Adapters/Ramstack.FileSystem.Adapters.csproj +++ b/src/Ramstack.FileSystem.Adapters/Ramstack.FileSystem.Adapters.csproj @@ -1,77 +1,77 @@ - - - - net6.0;net8.0 - Provides adapters for integrating Ramstack.FileSystem with Microsoft.Extensions.FileProviders. - 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.filesystem - Rameel - filesystem;fileprovider - https://github.com/rameel/ramstack.filesystem#readme - README.md - MIT - true - snupkg - true - true - true - true - - - - true - - - - - Properties\AssemblyInfo.cs - - - Utilities\EnumerableExtensions.cs - - - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - Properties\README.md - True - \ - - - - + + + + net6.0;net8.0 + Provides adapters for integrating Ramstack.FileSystem with Microsoft.Extensions.FileProviders. + 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.filesystem + Rameel + filesystem;fileprovider + https://github.com/rameel/ramstack.filesystem#readme + README.md + MIT + true + snupkg + true + true + true + true + + + + true + + + + + Properties\AssemblyInfo.cs + + + Utilities\EnumerableExtensions.cs + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + Properties\README.md + True + \ + + + + diff --git a/src/Ramstack.FileSystem.Adapters/VirtualDirectoryAdapter.cs b/src/Ramstack.FileSystem.Adapters/VirtualDirectoryAdapter.cs index 672610c..daee4e2 100644 --- a/src/Ramstack.FileSystem.Adapters/VirtualDirectoryAdapter.cs +++ b/src/Ramstack.FileSystem.Adapters/VirtualDirectoryAdapter.cs @@ -1,118 +1,118 @@ -using Microsoft.Extensions.FileProviders; - -using Ramstack.FileSystem.Utilities; - -namespace Ramstack.FileSystem.Adapters; - -/// -/// Represents an adapter for the instance. -/// -internal sealed class VirtualDirectoryAdapter : VirtualDirectory -{ - private readonly VirtualFileSystemAdapter _fs; - private IFileInfo? _file; - private IDirectoryContents? _directory; - - /// - public override IVirtualFileSystem FileSystem => _fs; - - /// - /// Initializes a new instance of the class. - /// - /// The file system associated with this directory. - /// The path of the directory. - /// The associated with this directory. - public VirtualDirectoryAdapter(VirtualFileSystemAdapter fileSystem, string path, IFileInfo file) : base(path) => - // ReSharper disable once SuspiciousTypeConversion.Global - (_fs, _file, _directory) = (fileSystem, file, file as IDirectoryContents); - - /// - /// Initializes a new instance of the class. - /// - /// The file system associated with this directory. - /// The path of the directory. - /// The associated with this directory. - public VirtualDirectoryAdapter(VirtualFileSystemAdapter fileSystem, string path, IDirectoryContents directory) : base(path) => - (_fs, _directory) = (fileSystem, directory); - - /// - protected override void RefreshCore() - { - _file = null; - _directory = _fs.FileProvider.GetDirectoryContents(FullName); - } - - /// - protected override ValueTask GetPropertiesCoreAsync(CancellationToken cancellationToken) - { - var properties = _file?.Exists ?? _directory!.Exists - ? VirtualNodeProperties.None - : VirtualNodeProperties.Unavailable; - - return new ValueTask(properties); - } - - /// - protected override ValueTask ExistsCoreAsync(CancellationToken cancellationToken) - { - var exists = _file?.Exists ?? _directory!.Exists; - return new ValueTask(exists); - } - - /// - protected override ValueTask CreateCoreAsync(CancellationToken cancellationToken) => - default; - - /// - protected override ValueTask DeleteCoreAsync(CancellationToken cancellationToken) => - default; - - /// - protected override IAsyncEnumerable GetFileNodesCoreAsync(CancellationToken cancellationToken) - { - return GetFileNodes().ToAsyncEnumerable(); - - IEnumerable GetFileNodes() - { - foreach (var fi in _directory ??= _fs.FileProvider.GetDirectoryContents(FullName)) - { - var path = VirtualPath.Join(FullName, fi.Name); - yield return fi.IsDirectory - ? new VirtualDirectoryAdapter(_fs, path, fi) - : new VirtualFileAdapter(_fs, path, fi); - } - } - } - - /// - protected override IAsyncEnumerable GetFilesCoreAsync(CancellationToken cancellationToken) - { - return GetFiles().ToAsyncEnumerable(); - - IEnumerable GetFiles() - { - foreach (var fi in _directory ??= _fs.FileProvider.GetDirectoryContents(FullName)) - { - var path = VirtualPath.Join(FullName, fi.Name); - if (!fi.IsDirectory) - yield return new VirtualFileAdapter(_fs, path, fi); - } - } - } - - /// - protected override IAsyncEnumerable GetDirectoriesCoreAsync(CancellationToken cancellationToken) - { - return GetDirectories().ToAsyncEnumerable(); - - IEnumerable GetDirectories() - { - foreach (var fi in _directory ??= _fs.FileProvider.GetDirectoryContents(FullName)) - { - var path = VirtualPath.Join(FullName, fi.Name); - if (fi.IsDirectory) - yield return new VirtualDirectoryAdapter(_fs, path, fi); - } - } - } -} +using Microsoft.Extensions.FileProviders; + +using Ramstack.FileSystem.Utilities; + +namespace Ramstack.FileSystem.Adapters; + +/// +/// Represents an adapter for the instance. +/// +internal sealed class VirtualDirectoryAdapter : VirtualDirectory +{ + private readonly VirtualFileSystemAdapter _fs; + private IFileInfo? _file; + private IDirectoryContents? _directory; + + /// + public override IVirtualFileSystem FileSystem => _fs; + + /// + /// Initializes a new instance of the class. + /// + /// The file system associated with this directory. + /// The path of the directory. + /// The associated with this directory. + public VirtualDirectoryAdapter(VirtualFileSystemAdapter fileSystem, string path, IFileInfo file) : base(path) => + // ReSharper disable once SuspiciousTypeConversion.Global + (_fs, _file, _directory) = (fileSystem, file, file as IDirectoryContents); + + /// + /// Initializes a new instance of the class. + /// + /// The file system associated with this directory. + /// The path of the directory. + /// The associated with this directory. + public VirtualDirectoryAdapter(VirtualFileSystemAdapter fileSystem, string path, IDirectoryContents directory) : base(path) => + (_fs, _directory) = (fileSystem, directory); + + /// + protected override void RefreshCore() + { + _file = null; + _directory = _fs.FileProvider.GetDirectoryContents(FullName); + } + + /// + protected override ValueTask GetPropertiesCoreAsync(CancellationToken cancellationToken) + { + var properties = _file?.Exists ?? _directory!.Exists + ? VirtualNodeProperties.None + : VirtualNodeProperties.Unavailable; + + return new ValueTask(properties); + } + + /// + protected override ValueTask ExistsCoreAsync(CancellationToken cancellationToken) + { + var exists = _file?.Exists ?? _directory!.Exists; + return new ValueTask(exists); + } + + /// + protected override ValueTask CreateCoreAsync(CancellationToken cancellationToken) => + default; + + /// + protected override ValueTask DeleteCoreAsync(CancellationToken cancellationToken) => + default; + + /// + protected override IAsyncEnumerable GetFileNodesCoreAsync(CancellationToken cancellationToken) + { + return GetFileNodes().ToAsyncEnumerable(); + + IEnumerable GetFileNodes() + { + foreach (var fi in _directory ??= _fs.FileProvider.GetDirectoryContents(FullName)) + { + var path = VirtualPath.Join(FullName, fi.Name); + yield return fi.IsDirectory + ? new VirtualDirectoryAdapter(_fs, path, fi) + : new VirtualFileAdapter(_fs, path, fi); + } + } + } + + /// + protected override IAsyncEnumerable GetFilesCoreAsync(CancellationToken cancellationToken) + { + return GetFiles().ToAsyncEnumerable(); + + IEnumerable GetFiles() + { + foreach (var fi in _directory ??= _fs.FileProvider.GetDirectoryContents(FullName)) + { + var path = VirtualPath.Join(FullName, fi.Name); + if (!fi.IsDirectory) + yield return new VirtualFileAdapter(_fs, path, fi); + } + } + } + + /// + protected override IAsyncEnumerable GetDirectoriesCoreAsync(CancellationToken cancellationToken) + { + return GetDirectories().ToAsyncEnumerable(); + + IEnumerable GetDirectories() + { + foreach (var fi in _directory ??= _fs.FileProvider.GetDirectoryContents(FullName)) + { + var path = VirtualPath.Join(FullName, fi.Name); + if (fi.IsDirectory) + yield return new VirtualDirectoryAdapter(_fs, path, fi); + } + } + } +} diff --git a/src/Ramstack.FileSystem.Adapters/VirtualFileAdapter.cs b/src/Ramstack.FileSystem.Adapters/VirtualFileAdapter.cs index 98177b6..0a6e6e5 100644 --- a/src/Ramstack.FileSystem.Adapters/VirtualFileAdapter.cs +++ b/src/Ramstack.FileSystem.Adapters/VirtualFileAdapter.cs @@ -1,61 +1,61 @@ -using Microsoft.Extensions.FileProviders; - -namespace Ramstack.FileSystem.Adapters; - -/// -/// Represents an adapter for the instance. -/// -internal sealed class VirtualFileAdapter : VirtualFile -{ - private readonly VirtualFileSystemAdapter _fs; - private IFileInfo _file; - - /// - public override IVirtualFileSystem FileSystem => _fs; - - /// - /// Initializes a new instance of the class. - /// - /// The file system associated with this file. - /// The path of the file. - /// The associated with this file. - public VirtualFileAdapter(VirtualFileSystemAdapter fileSystem, string path, IFileInfo file) : base(path) => - (_fs, _file) = (fileSystem, file); - - /// - protected override void RefreshCore() => - _file = _fs.FileProvider.GetFileInfo(FullName); - - /// - protected override ValueTask GetPropertiesCoreAsync(CancellationToken cancellationToken) - { - var properties = _file.Exists - ? VirtualNodeProperties.CreateFileProperties(default, default, lastWriteTime: _file.LastModified, _file.Length) - : null; - - return new ValueTask(properties); - } - - /// - protected override ValueTask ExistsCoreAsync(CancellationToken cancellationToken) => - new ValueTask(_file.Exists); - - /// - protected override ValueTask OpenReadCoreAsync(CancellationToken cancellationToken) - { - var stream = _file.CreateReadStream(); - return new ValueTask(stream); - } - - /// - protected override ValueTask OpenWriteCoreAsync(CancellationToken cancellationToken) => - default; - - /// - protected override ValueTask WriteCoreAsync(Stream stream, bool overwrite, CancellationToken cancellationToken) => - default; - - /// - protected override ValueTask DeleteCoreAsync(CancellationToken cancellationToken) => - default; -} +using Microsoft.Extensions.FileProviders; + +namespace Ramstack.FileSystem.Adapters; + +/// +/// Represents an adapter for the instance. +/// +internal sealed class VirtualFileAdapter : VirtualFile +{ + private readonly VirtualFileSystemAdapter _fs; + private IFileInfo _file; + + /// + public override IVirtualFileSystem FileSystem => _fs; + + /// + /// Initializes a new instance of the class. + /// + /// The file system associated with this file. + /// The path of the file. + /// The associated with this file. + public VirtualFileAdapter(VirtualFileSystemAdapter fileSystem, string path, IFileInfo file) : base(path) => + (_fs, _file) = (fileSystem, file); + + /// + protected override void RefreshCore() => + _file = _fs.FileProvider.GetFileInfo(FullName); + + /// + protected override ValueTask GetPropertiesCoreAsync(CancellationToken cancellationToken) + { + var properties = _file.Exists + ? VirtualNodeProperties.CreateFileProperties(default, default, lastWriteTime: _file.LastModified, _file.Length) + : null; + + return new ValueTask(properties); + } + + /// + protected override ValueTask ExistsCoreAsync(CancellationToken cancellationToken) => + new ValueTask(_file.Exists); + + /// + protected override ValueTask OpenReadCoreAsync(CancellationToken cancellationToken) + { + var stream = _file.CreateReadStream(); + return new ValueTask(stream); + } + + /// + protected override ValueTask OpenWriteCoreAsync(CancellationToken cancellationToken) => + default; + + /// + protected override ValueTask WriteCoreAsync(Stream stream, bool overwrite, CancellationToken cancellationToken) => + default; + + /// + protected override ValueTask DeleteCoreAsync(CancellationToken cancellationToken) => + default; +} diff --git a/src/Ramstack.FileSystem.Adapters/VirtualFileSystemAdapter.cs b/src/Ramstack.FileSystem.Adapters/VirtualFileSystemAdapter.cs index 2c87d3a..8569890 100644 --- a/src/Ramstack.FileSystem.Adapters/VirtualFileSystemAdapter.cs +++ b/src/Ramstack.FileSystem.Adapters/VirtualFileSystemAdapter.cs @@ -1,51 +1,51 @@ -using Microsoft.Extensions.FileProviders; - -namespace Ramstack.FileSystem.Adapters; - -/// -/// Represents an implementation of that enables working with virtual files -/// using the underlying instance. -/// -public sealed class VirtualFileSystemAdapter : IVirtualFileSystem -{ - /// - public bool IsReadOnly => true; - - /// - /// Gets the underlying instance. - /// - public IFileProvider FileProvider { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The instance to wrap. - public VirtualFileSystemAdapter(IFileProvider provider) - { - ArgumentNullException.ThrowIfNull(provider); - FileProvider = provider; - } - - /// - public VirtualDirectory GetDirectory(string path) - { - path = VirtualPath.Normalize(path); - var directory = FileProvider.GetDirectoryContents(path); - - return new VirtualDirectoryAdapter(this, path, directory); - } - - /// - public VirtualFile GetFile(string path) - { - path = VirtualPath.Normalize(path); - var file = FileProvider.GetFileInfo(path); - - return new VirtualFileAdapter(this, path, file); - } - - /// - public void Dispose() => - // ReSharper disable once SuspiciousTypeConversion.Global - (FileProvider as IDisposable)?.Dispose(); -} +using Microsoft.Extensions.FileProviders; + +namespace Ramstack.FileSystem.Adapters; + +/// +/// Represents an implementation of that enables working with virtual files +/// using the underlying instance. +/// +public sealed class VirtualFileSystemAdapter : IVirtualFileSystem +{ + /// + public bool IsReadOnly => true; + + /// + /// Gets the underlying instance. + /// + public IFileProvider FileProvider { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The instance to wrap. + public VirtualFileSystemAdapter(IFileProvider provider) + { + ArgumentNullException.ThrowIfNull(provider); + FileProvider = provider; + } + + /// + public VirtualDirectory GetDirectory(string path) + { + path = VirtualPath.Normalize(path); + var directory = FileProvider.GetDirectoryContents(path); + + return new VirtualDirectoryAdapter(this, path, directory); + } + + /// + public VirtualFile GetFile(string path) + { + path = VirtualPath.Normalize(path); + var file = FileProvider.GetFileInfo(path); + + return new VirtualFileAdapter(this, path, file); + } + + /// + public void Dispose() => + // ReSharper disable once SuspiciousTypeConversion.Global + (FileProvider as IDisposable)?.Dispose(); +} diff --git a/src/Ramstack.FileSystem.Amazon/AccessControl.cs b/src/Ramstack.FileSystem.Amazon/AccessControl.cs index aaab177..f506919 100644 --- a/src/Ramstack.FileSystem.Amazon/AccessControl.cs +++ b/src/Ramstack.FileSystem.Amazon/AccessControl.cs @@ -1,66 +1,66 @@ -namespace Ramstack.FileSystem.Amazon; - -/// -/// An enumeration of all possible CannedACLs that can be used -/// for S3 Buckets or S3 Objects. For more information about CannedACLs, refer to -/// . -/// -public enum AccessControl -{ - /// - /// Owner gets FULL_CONTROL. - /// No one else has access rights (default). - /// - NoAcl, - - /// - /// Owner gets FULL_CONTROL. - /// No one else has access rights (default). - /// - Private, - - /// - /// Owner gets FULL_CONTROL and the anonymous principal is granted READ access. - /// If this policy is used on an object, it can be read from a browser with no authentication. - /// - PublicRead, - - /// - /// Owner gets FULL_CONTROL, the anonymous principal is granted READ and WRITE access. - /// This can be a useful policy to apply to a bucket, but is generally not recommended. - /// - PublicReadWrite, - - /// - /// Owner gets FULL_CONTROL, and any principal authenticated as a registered Amazon - /// S3 user is granted READ access. - /// - AuthenticatedRead, - - /// - /// Owner gets FULL_CONTROL. Amazon EC2 gets READ access to GET an - /// Amazon Machine Image (AMI) bundle from Amazon S3. - /// - AwsExecRead, - - /// - /// Object Owner gets FULL_CONTROL, Bucket Owner gets READ. - /// This ACL applies only to objects and is equivalent to private when used with PUT Bucket. - /// You use this ACL to let someone other than the bucket owner write content (get full control) - /// in the bucket but still grant the bucket owner read access to the objects. - /// - BucketOwnerRead, - - /// - /// Object Owner gets FULL_CONTROL, Bucket Owner gets FULL_CONTROL. - /// This ACL applies only to objects and is equivalent to private when used with PUT Bucket. - /// You use this ACL to let someone other than the bucket owner write content (get full control) - /// in the bucket but still grant the bucket owner full rights over the objects. - /// - BucketOwnerFullControl, - - /// - /// The LogDelivery group gets WRITE and READ_ACP permissions on the bucket. - /// - LogDeliveryWrite -} +namespace Ramstack.FileSystem.Amazon; + +/// +/// An enumeration of all possible CannedACLs that can be used +/// for S3 Buckets or S3 Objects. For more information about CannedACLs, refer to +/// . +/// +public enum AccessControl +{ + /// + /// Owner gets FULL_CONTROL. + /// No one else has access rights (default). + /// + NoAcl, + + /// + /// Owner gets FULL_CONTROL. + /// No one else has access rights (default). + /// + Private, + + /// + /// Owner gets FULL_CONTROL and the anonymous principal is granted READ access. + /// If this policy is used on an object, it can be read from a browser with no authentication. + /// + PublicRead, + + /// + /// Owner gets FULL_CONTROL, the anonymous principal is granted READ and WRITE access. + /// This can be a useful policy to apply to a bucket, but is generally not recommended. + /// + PublicReadWrite, + + /// + /// Owner gets FULL_CONTROL, and any principal authenticated as a registered Amazon + /// S3 user is granted READ access. + /// + AuthenticatedRead, + + /// + /// Owner gets FULL_CONTROL. Amazon EC2 gets READ access to GET an + /// Amazon Machine Image (AMI) bundle from Amazon S3. + /// + AwsExecRead, + + /// + /// Object Owner gets FULL_CONTROL, Bucket Owner gets READ. + /// This ACL applies only to objects and is equivalent to private when used with PUT Bucket. + /// You use this ACL to let someone other than the bucket owner write content (get full control) + /// in the bucket but still grant the bucket owner read access to the objects. + /// + BucketOwnerRead, + + /// + /// Object Owner gets FULL_CONTROL, Bucket Owner gets FULL_CONTROL. + /// This ACL applies only to objects and is equivalent to private when used with PUT Bucket. + /// You use this ACL to let someone other than the bucket owner write content (get full control) + /// in the bucket but still grant the bucket owner full rights over the objects. + /// + BucketOwnerFullControl, + + /// + /// The LogDelivery group gets WRITE and READ_ACP permissions on the bucket. + /// + LogDeliveryWrite +} diff --git a/src/Ramstack.FileSystem.Amazon/AmazonS3FileSystem.cs b/src/Ramstack.FileSystem.Amazon/AmazonS3FileSystem.cs index 9e32056..41f290b 100644 --- a/src/Ramstack.FileSystem.Amazon/AmazonS3FileSystem.cs +++ b/src/Ramstack.FileSystem.Amazon/AmazonS3FileSystem.cs @@ -1,158 +1,158 @@ -using Amazon; -using Amazon.Runtime; -using Amazon.S3; -using Amazon.S3.Model; -using Amazon.S3.Util; - -namespace Ramstack.FileSystem.Amazon; - -/// -/// Represents a file system implementation using Amazon S3 storage. -/// -public sealed class AmazonS3FileSystem : IVirtualFileSystem -{ - /// - /// Gets the instance. - /// - internal IAmazonS3 AmazonClient { get; } - - /// - /// Gets the name of the S3 bucket used by this file system. - /// - internal string BucketName { get; } - - /// - public bool IsReadOnly { get; init; } - - /// - /// Initializes a new instance of the class using an existing Amazon S3 client. - /// - /// The Amazon S3 client instance. - /// The name of the S3 bucket. - public AmazonS3FileSystem(AmazonS3Client client, string bucketName) - { - ArgumentNullException.ThrowIfNull(client); - ArgumentNullException.ThrowIfNull(bucketName); - - (AmazonClient, BucketName) = (client, bucketName); - } - - /// - /// Initializes a new instance of the class using AWS credentials and configuration. - /// - /// The AWS credentials to use. - /// The Amazon S3 configuration settings. - /// The name of the S3 bucket. - public AmazonS3FileSystem(AWSCredentials credentials, AmazonS3Config config, string bucketName) - { - ArgumentNullException.ThrowIfNull(bucketName); - - AmazonClient = new AmazonS3Client(credentials, config); - BucketName = bucketName; - } - - /// - /// Initializes a new instance of the class using AWS access keys and region name. - /// - /// The AWS access key ID. - /// The AWS secret access key. - /// The name of the AWS region. - /// The name of the S3 bucket. - public AmazonS3FileSystem(string accessKeyId, string secretAccessKey, string regionName, string bucketName) - : this(accessKeyId, secretAccessKey, RegionEndpoint.GetBySystemName(regionName), bucketName) - { - } - - /// - /// Initializes a new instance of the class using AWS access keys and region endpoint. - /// - /// The AWS access key ID. - /// The AWS secret access key. - /// The AWS region endpoint. - /// The name of the S3 bucket. - public AmazonS3FileSystem(string accessKeyId, string secretAccessKey, RegionEndpoint region, string bucketName) - : this(new BasicAWSCredentials(accessKeyId, secretAccessKey), new AmazonS3Config { RegionEndpoint = region }, bucketName) - { - } - - /// - /// Initializes a new instance of the class using AWS access keys and region endpoint. - /// - /// The AWS access key ID. - /// The AWS secret access key. - /// The Amazon S3 configuration settings. - /// The name of the S3 bucket. - public AmazonS3FileSystem(string accessKeyId, string secretAccessKey, AmazonS3Config config, string bucketName) - : this(new BasicAWSCredentials(accessKeyId, secretAccessKey), config, bucketName) - { - } - - /// - public VirtualFile GetFile(string path) - { - path = VirtualPath.Normalize(path); - return new S3File(this, path); - } - - /// - public VirtualDirectory GetDirectory(string path) - { - path = VirtualPath.Normalize(path); - return new S3Directory(this, path); - } - - /// - public void Dispose() => - AmazonClient.Dispose(); - - /// - /// Creates the S3 bucket if it does not already exist. - /// - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation. - /// - public ValueTask CreateBucketAsync(CancellationToken cancellationToken = default) => - CreateBucketAsync(AccessControl.NoAcl, cancellationToken); - - /// - /// Creates the S3 bucket if it does not already exist. - /// - /// The ACL to apply to the bucket. - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation. - /// - public async ValueTask CreateBucketAsync(AccessControl accessControl, CancellationToken cancellationToken = default) - { - var exists = AmazonS3Util - .DoesS3BucketExistV2Async(AmazonClient, BucketName) - .ConfigureAwait(false); - - if (await exists) - return; - - var request = new PutBucketRequest - { - BucketName = BucketName, - UseClientRegion = true, - CannedACL = accessControl switch - { - AccessControl.NoAcl => S3CannedACL.NoACL, - AccessControl.Private => S3CannedACL.Private, - AccessControl.PublicRead => S3CannedACL.PublicRead, - AccessControl.PublicReadWrite => S3CannedACL.PublicReadWrite, - AccessControl.AuthenticatedRead => S3CannedACL.AuthenticatedRead, - AccessControl.AwsExecRead => S3CannedACL.AWSExecRead, - AccessControl.BucketOwnerRead => S3CannedACL.BucketOwnerRead, - AccessControl.BucketOwnerFullControl => S3CannedACL.BucketOwnerFullControl, - AccessControl.LogDeliveryWrite => S3CannedACL.LogDeliveryWrite, - _ => throw new ArgumentOutOfRangeException(nameof(accessControl)) - } - }; - - await AmazonClient - .PutBucketAsync(request, cancellationToken) - .ConfigureAwait(false); - } -} +using Amazon; +using Amazon.Runtime; +using Amazon.S3; +using Amazon.S3.Model; +using Amazon.S3.Util; + +namespace Ramstack.FileSystem.Amazon; + +/// +/// Represents a file system implementation using Amazon S3 storage. +/// +public sealed class AmazonS3FileSystem : IVirtualFileSystem +{ + /// + /// Gets the instance. + /// + internal IAmazonS3 AmazonClient { get; } + + /// + /// Gets the name of the S3 bucket used by this file system. + /// + internal string BucketName { get; } + + /// + public bool IsReadOnly { get; init; } + + /// + /// Initializes a new instance of the class using an existing Amazon S3 client. + /// + /// The Amazon S3 client instance. + /// The name of the S3 bucket. + public AmazonS3FileSystem(AmazonS3Client client, string bucketName) + { + ArgumentNullException.ThrowIfNull(client); + ArgumentNullException.ThrowIfNull(bucketName); + + (AmazonClient, BucketName) = (client, bucketName); + } + + /// + /// Initializes a new instance of the class using AWS credentials and configuration. + /// + /// The AWS credentials to use. + /// The Amazon S3 configuration settings. + /// The name of the S3 bucket. + public AmazonS3FileSystem(AWSCredentials credentials, AmazonS3Config config, string bucketName) + { + ArgumentNullException.ThrowIfNull(bucketName); + + AmazonClient = new AmazonS3Client(credentials, config); + BucketName = bucketName; + } + + /// + /// Initializes a new instance of the class using AWS access keys and region name. + /// + /// The AWS access key ID. + /// The AWS secret access key. + /// The name of the AWS region. + /// The name of the S3 bucket. + public AmazonS3FileSystem(string accessKeyId, string secretAccessKey, string regionName, string bucketName) + : this(accessKeyId, secretAccessKey, RegionEndpoint.GetBySystemName(regionName), bucketName) + { + } + + /// + /// Initializes a new instance of the class using AWS access keys and region endpoint. + /// + /// The AWS access key ID. + /// The AWS secret access key. + /// The AWS region endpoint. + /// The name of the S3 bucket. + public AmazonS3FileSystem(string accessKeyId, string secretAccessKey, RegionEndpoint region, string bucketName) + : this(new BasicAWSCredentials(accessKeyId, secretAccessKey), new AmazonS3Config { RegionEndpoint = region }, bucketName) + { + } + + /// + /// Initializes a new instance of the class using AWS access keys and region endpoint. + /// + /// The AWS access key ID. + /// The AWS secret access key. + /// The Amazon S3 configuration settings. + /// The name of the S3 bucket. + public AmazonS3FileSystem(string accessKeyId, string secretAccessKey, AmazonS3Config config, string bucketName) + : this(new BasicAWSCredentials(accessKeyId, secretAccessKey), config, bucketName) + { + } + + /// + public VirtualFile GetFile(string path) + { + path = VirtualPath.Normalize(path); + return new S3File(this, path); + } + + /// + public VirtualDirectory GetDirectory(string path) + { + path = VirtualPath.Normalize(path); + return new S3Directory(this, path); + } + + /// + public void Dispose() => + AmazonClient.Dispose(); + + /// + /// Creates the S3 bucket if it does not already exist. + /// + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation. + /// + public ValueTask CreateBucketAsync(CancellationToken cancellationToken = default) => + CreateBucketAsync(AccessControl.NoAcl, cancellationToken); + + /// + /// Creates the S3 bucket if it does not already exist. + /// + /// The ACL to apply to the bucket. + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation. + /// + public async ValueTask CreateBucketAsync(AccessControl accessControl, CancellationToken cancellationToken = default) + { + var exists = AmazonS3Util + .DoesS3BucketExistV2Async(AmazonClient, BucketName) + .ConfigureAwait(false); + + if (await exists) + return; + + var request = new PutBucketRequest + { + BucketName = BucketName, + UseClientRegion = true, + CannedACL = accessControl switch + { + AccessControl.NoAcl => S3CannedACL.NoACL, + AccessControl.Private => S3CannedACL.Private, + AccessControl.PublicRead => S3CannedACL.PublicRead, + AccessControl.PublicReadWrite => S3CannedACL.PublicReadWrite, + AccessControl.AuthenticatedRead => S3CannedACL.AuthenticatedRead, + AccessControl.AwsExecRead => S3CannedACL.AWSExecRead, + AccessControl.BucketOwnerRead => S3CannedACL.BucketOwnerRead, + AccessControl.BucketOwnerFullControl => S3CannedACL.BucketOwnerFullControl, + AccessControl.LogDeliveryWrite => S3CannedACL.LogDeliveryWrite, + _ => throw new ArgumentOutOfRangeException(nameof(accessControl)) + } + }; + + await AmazonClient + .PutBucketAsync(request, cancellationToken) + .ConfigureAwait(false); + } +} diff --git a/src/Ramstack.FileSystem.Amazon/README.md b/src/Ramstack.FileSystem.Amazon/README.md index 4128229..ec1e86d 100644 --- a/src/Ramstack.FileSystem.Amazon/README.md +++ b/src/Ramstack.FileSystem.Amazon/README.md @@ -1,72 +1,72 @@ -# Ramstack.FileSystem.Amazon -[![NuGet](https://img.shields.io/nuget/v/Ramstack.FileSystem.Amazon.svg)](https://nuget.org/packages/Ramstack.FileSystem.Amazon) -[![MIT](https://img.shields.io/github/license/rameel/ramstack.filesystem)](https://github.com/rameel/ramstack.filesystem/blob/main/LICENSE) - -Provides an implementation of `Ramstack.FileSystem` using Amazon S3 storage. - -## Getting Started - -To install the `Ramstack.FileSystem.Amazon` [NuGet package](https://www.nuget.org/packages/Ramstack.FileSystem.Amazon) -in your project, run the following command: -```console -dotnet add package Ramstack.FileSystem.Amazon -``` - -## Usage - -```csharp -using Ramstack.FileSystem.Amazon; - -AmazonS3FileSystem fs = new AmazonS3FileSystem( - accessKeyId: "...", - secretAccessKey: "...", - region: RegionEndpoint.USEast1, - bucketName: "my-storage"); - -// Create S3 bucket if it doesn't exist -await fs.CreateBucketAsync(AccessControl.Private); - -await foreach (VirtualFile file in fs.GetFilesAsync("/")) -{ - Console.WriteLine(node.Name); -} -``` - -You can also configure the file system to be read-only: -```csharp -AmazonS3FileSystem fs = new AmazonS3FileSystem( - accessKeyId: "...", - secretAccessKey: "...", - region: RegionEndpoint.USEast1, - bucketName: "my-storage") -{ - IsReadOnly = true -}; -``` - -## Related Projects -- [Ramstack.FileSystem.Abstractions](https://www.nuget.org/packages/Ramstack.FileSystem.Abstractions) - Provides a virtual file system abstraction. -- [Ramstack.FileSystem.Physical](https://www.nuget.org/packages/Ramstack.FileSystem.Physical) - Provides an implementation based on the local file system. -- [Ramstack.FileSystem.Azure](https://www.nuget.org/packages/Ramstack.FileSystem.Azure) - Provides an implementation using Azure Blob Storage. -- [Ramstack.FileSystem.Google](https://www.nuget.org/packages/Ramstack.FileSystem.Google) - Provides an implementation using Google Cloud Storage. -- [Ramstack.FileSystem.Readonly](https://www.nuget.org/packages/Ramstack.FileSystem.Readonly) - Provides a read-only wrapper for the underlying file system. -- [Ramstack.FileSystem.Globbing](https://www.nuget.org/packages/Ramstack.FileSystem.Globbing) - Wraps the file system, filtering files and directories using glob patterns. -- [Ramstack.FileSystem.Prefixed](https://www.nuget.org/packages/Ramstack.FileSystem.Prefixed) - Adds a prefix to file paths within the underlying file system. -- [Ramstack.FileSystem.Sub](https://www.nuget.org/packages/Ramstack.FileSystem.Sub) - Wraps the underlying file system, restricting access to a specific subpath. -- [Ramstack.FileSystem.Adapters](https://www.nuget.org/packages/Ramstack.FileSystem.Adapters) - Provides integration with `Microsoft.Extensions.FileProviders`. -- [Ramstack.FileSystem.Composite](https://www.nuget.org/packages/Ramstack.FileSystem.Composite) - Provides an implementation that combines multiple file systems into a single composite file system. - -## 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.virtualfiles/blob/main/LICENSE) file for more details. +# Ramstack.FileSystem.Amazon +[![NuGet](https://img.shields.io/nuget/v/Ramstack.FileSystem.Amazon.svg)](https://nuget.org/packages/Ramstack.FileSystem.Amazon) +[![MIT](https://img.shields.io/github/license/rameel/ramstack.filesystem)](https://github.com/rameel/ramstack.filesystem/blob/main/LICENSE) + +Provides an implementation of `Ramstack.FileSystem` using Amazon S3 storage. + +## Getting Started + +To install the `Ramstack.FileSystem.Amazon` [NuGet package](https://www.nuget.org/packages/Ramstack.FileSystem.Amazon) +in your project, run the following command: +```console +dotnet add package Ramstack.FileSystem.Amazon +``` + +## Usage + +```csharp +using Ramstack.FileSystem.Amazon; + +AmazonS3FileSystem fs = new AmazonS3FileSystem( + accessKeyId: "...", + secretAccessKey: "...", + region: RegionEndpoint.USEast1, + bucketName: "my-storage"); + +// Create S3 bucket if it doesn't exist +await fs.CreateBucketAsync(AccessControl.Private); + +await foreach (VirtualFile file in fs.GetFilesAsync("/")) +{ + Console.WriteLine(node.Name); +} +``` + +You can also configure the file system to be read-only: +```csharp +AmazonS3FileSystem fs = new AmazonS3FileSystem( + accessKeyId: "...", + secretAccessKey: "...", + region: RegionEndpoint.USEast1, + bucketName: "my-storage") +{ + IsReadOnly = true +}; +``` + +## Related Projects +- [Ramstack.FileSystem.Abstractions](https://www.nuget.org/packages/Ramstack.FileSystem.Abstractions) - Provides a virtual file system abstraction. +- [Ramstack.FileSystem.Physical](https://www.nuget.org/packages/Ramstack.FileSystem.Physical) - Provides an implementation based on the local file system. +- [Ramstack.FileSystem.Azure](https://www.nuget.org/packages/Ramstack.FileSystem.Azure) - Provides an implementation using Azure Blob Storage. +- [Ramstack.FileSystem.Google](https://www.nuget.org/packages/Ramstack.FileSystem.Google) - Provides an implementation using Google Cloud Storage. +- [Ramstack.FileSystem.Readonly](https://www.nuget.org/packages/Ramstack.FileSystem.Readonly) - Provides a read-only wrapper for the underlying file system. +- [Ramstack.FileSystem.Globbing](https://www.nuget.org/packages/Ramstack.FileSystem.Globbing) - Wraps the file system, filtering files and directories using glob patterns. +- [Ramstack.FileSystem.Prefixed](https://www.nuget.org/packages/Ramstack.FileSystem.Prefixed) - Adds a prefix to file paths within the underlying file system. +- [Ramstack.FileSystem.Sub](https://www.nuget.org/packages/Ramstack.FileSystem.Sub) - Wraps the underlying file system, restricting access to a specific subpath. +- [Ramstack.FileSystem.Adapters](https://www.nuget.org/packages/Ramstack.FileSystem.Adapters) - Provides integration with `Microsoft.Extensions.FileProviders`. +- [Ramstack.FileSystem.Composite](https://www.nuget.org/packages/Ramstack.FileSystem.Composite) - Provides an implementation that combines multiple file systems into a single composite file system. + +## 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.virtualfiles/blob/main/LICENSE) file for more details. diff --git a/src/Ramstack.FileSystem.Amazon/Ramstack.FileSystem.Amazon.csproj b/src/Ramstack.FileSystem.Amazon/Ramstack.FileSystem.Amazon.csproj index ac0f556..8341790 100644 --- a/src/Ramstack.FileSystem.Amazon/Ramstack.FileSystem.Amazon.csproj +++ b/src/Ramstack.FileSystem.Amazon/Ramstack.FileSystem.Amazon.csproj @@ -1,68 +1,68 @@ - - - - net6.0;net8.0 - Provides an implementation of Ramstack.FileSystem using Amazon S3 storage. - 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.filesystem - Rameel - filesystem;fileprovider - https://github.com/rameel/ramstack.filesystem#readme - README.md - MIT - true - snupkg - true - true - true - true - - - - true - - - - - Properties\AssemblyInfo.cs - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - Properties\README.md - True - \ - - - - + + + + net6.0;net8.0 + Provides an implementation of Ramstack.FileSystem using Amazon S3 storage. + 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.filesystem + Rameel + filesystem;fileprovider + https://github.com/rameel/ramstack.filesystem#readme + README.md + MIT + true + snupkg + true + true + true + true + + + + true + + + + + Properties\AssemblyInfo.cs + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + Properties\README.md + True + \ + + + + diff --git a/src/Ramstack.FileSystem.Amazon/S3Directory.cs b/src/Ramstack.FileSystem.Amazon/S3Directory.cs index e339e9e..696e052 100644 --- a/src/Ramstack.FileSystem.Amazon/S3Directory.cs +++ b/src/Ramstack.FileSystem.Amazon/S3Directory.cs @@ -1,387 +1,387 @@ -using System.Diagnostics; -using System.Runtime.CompilerServices; - -using Amazon.S3.Model; - -using Ramstack.Globbing; - -namespace Ramstack.FileSystem.Amazon; - -/// -/// Represents an implementation of that maps a directory -/// to a path within a specified Amazon S3 bucket. -/// -internal sealed class S3Directory : VirtualDirectory -{ - private readonly AmazonS3FileSystem _fs; - - /// - public override IVirtualFileSystem FileSystem => _fs; - - /// - /// Initializes a new instance of the class. - /// - /// The file system associated with this directory. - /// The path to the directory within the specified Amazon S3 bucket. - public S3Directory(AmazonS3FileSystem fileSystem, string path) : base(path) => - _fs = fileSystem; - - /// - protected override ValueTask GetPropertiesCoreAsync(CancellationToken cancellationToken) => - new ValueTask(VirtualNodeProperties.None); - - /// - protected override ValueTask CreateCoreAsync(CancellationToken cancellationToken) => - default; - - /// - protected override async ValueTask DeleteCoreAsync(CancellationToken cancellationToken) - { - var lr = new ListObjectsV2Request - { - BucketName = _fs.BucketName, - Prefix = GetPrefix(FullName) - }; - - var dr = new DeleteObjectsRequest - { - BucketName = _fs.BucketName - }; - - dr.Objects ??= []; - - do - { - // The maximum number of objects returned is MaxKeys, which is 1000, - // and the maximum number of objects that can be deleted at once is also 1000. - // Therefore, we can rely on this and avoid splitting - // the retrieved objects into separate batches. - - var response = await _fs.AmazonClient - .ListObjectsV2Async(lr, cancellationToken) - .ConfigureAwait(false); - - if (response.S3Objects is not null) - foreach (var obj in response.S3Objects) - dr.Objects.Add(new KeyVersion { Key = obj.Key }); - - if (dr.Objects.Count != 0) - await _fs.AmazonClient - .DeleteObjectsAsync(dr, cancellationToken) - .ConfigureAwait(false); - - dr.Objects.Clear(); - lr.ContinuationToken = response.NextContinuationToken; - } - while (lr.ContinuationToken is not null && !cancellationToken.IsCancellationRequested); - } - - /// - protected override async IAsyncEnumerable GetFileNodesCoreAsync([EnumeratorCancellation] CancellationToken cancellationToken) - { - var request = new ListObjectsV2Request - { - BucketName = _fs.BucketName, - Prefix = GetPrefix(FullName), - Delimiter = "/" - }; - - do - { - var response = await _fs.AmazonClient - .ListObjectsV2Async(request, cancellationToken) - .ConfigureAwait(false); - - if (response.CommonPrefixes is not null) - foreach (var prefix in response.CommonPrefixes) - yield return new S3Directory(_fs, VirtualPath.Normalize(prefix)); - - if (response.S3Objects is not null) - foreach (var obj in response.S3Objects) - yield return CreateVirtualFile(obj); - - request.ContinuationToken = response.NextContinuationToken; - } - while (request.ContinuationToken is not null && !cancellationToken.IsCancellationRequested); - } - - /// - protected override async IAsyncEnumerable GetFilesCoreAsync([EnumeratorCancellation] CancellationToken cancellationToken) - { - var request = new ListObjectsV2Request - { - BucketName = _fs.BucketName, - Prefix = GetPrefix(FullName), - Delimiter = "/" - }; - - do - { - var response = await _fs.AmazonClient - .ListObjectsV2Async(request, cancellationToken) - .ConfigureAwait(false); - - if (response.S3Objects is not null) - foreach (var obj in response.S3Objects) - yield return CreateVirtualFile(obj); - - request.ContinuationToken = response.NextContinuationToken; - } - while (request.ContinuationToken is not null && !cancellationToken.IsCancellationRequested); - } - - /// - protected override async IAsyncEnumerable GetDirectoriesCoreAsync([EnumeratorCancellation] CancellationToken cancellationToken) - { - var request = new ListObjectsV2Request - { - BucketName = _fs.BucketName, - Prefix = GetPrefix(FullName), - Delimiter = "/" - }; - - do - { - var response = await _fs.AmazonClient - .ListObjectsV2Async(request, cancellationToken) - .ConfigureAwait(false); - - if (response.CommonPrefixes is not null) - foreach (var prefix in response.CommonPrefixes) - yield return new S3Directory(_fs, VirtualPath.Normalize(prefix)); - - request.ContinuationToken = response.NextContinuationToken; - } - while (request.ContinuationToken is not null && !cancellationToken.IsCancellationRequested); - } - - /// - protected override async IAsyncEnumerable GetFileNodesCoreAsync(string[] patterns, string[]? excludes, [EnumeratorCancellation] CancellationToken cancellationToken) - { - // - // Cloud storage optimization strategy: - // 1. List all objects in one batch using prefix filtering (matches our virtual directory). - // 2. Perform pattern matching and exclusion filters locally. - // - // Benefits: - // - Single API call instead of per-directory requests - // - Reduced network latency, especially for deep directory structures - // - More efficient than recursive directory scanning - // - - var request = new ListObjectsV2Request - { - BucketName = _fs.BucketName, - Prefix = GetPrefix(FullName) - }; - - var directories = new HashSet - { - FullName - }; - - do - { - var response = await _fs.AmazonClient - .ListObjectsV2Async(request, cancellationToken) - .ConfigureAwait(false); - - if (response.S3Objects is not null) - { - foreach (var obj in response.S3Objects) - { - var path = VirtualPath.Normalize(obj.Key); - var directoryPath = VirtualPath.GetDirectoryName(path); - - while (directoryPath.Length != 0 && directories.Add(directoryPath)) - { - // - // Directories are yielded in reverse order (deepest first). - // - // Note: We could use a Stack to control the order, - // but since order isn't guaranteed anyway and to avoid - // unnecessary memory allocation, we process them directly. - // - if (IsMatched(directoryPath.AsSpan(FullName.Length), patterns, excludes)) - yield return new S3Directory(_fs, directoryPath); - - directoryPath = VirtualPath.GetDirectoryName(directoryPath); - } - - if (IsMatched(obj.Key.AsSpan(request.Prefix.Length), patterns, excludes)) - yield return CreateVirtualFile(obj, path); - } - } - - request.ContinuationToken = response.NextContinuationToken; - } - while (request.ContinuationToken is not null && !cancellationToken.IsCancellationRequested); - } - - /// - protected override async IAsyncEnumerable GetFilesCoreAsync(string[] patterns, string[]? excludes, [EnumeratorCancellation] CancellationToken cancellationToken) - { - // - // Cloud storage optimization strategy: - // 1. List all objects in one batch using prefix filtering (matches our virtual directory). - // 2. Perform pattern matching and exclusion filters locally. - // - // Benefits: - // - Single API call instead of per-directory requests - // - Reduced network latency, especially for deep directory structures - // - More efficient than recursive directory scanning - // - - var request = new ListObjectsV2Request - { - BucketName = _fs.BucketName, - Prefix = GetPrefix(FullName) - }; - - do - { - var response = await _fs.AmazonClient - .ListObjectsV2Async(request, cancellationToken) - .ConfigureAwait(false); - - if (response.S3Objects is not null) - foreach (var obj in response.S3Objects) - if (IsMatched(obj.Key.AsSpan(request.Prefix.Length), patterns, excludes)) - yield return CreateVirtualFile(obj); - - request.ContinuationToken = response.NextContinuationToken; - } - while (request.ContinuationToken is not null && !cancellationToken.IsCancellationRequested); - } - - /// - protected override async IAsyncEnumerable GetDirectoriesCoreAsync(string[] patterns, string[]? excludes, [EnumeratorCancellation] CancellationToken cancellationToken) - { - // - // Cloud storage optimization strategy: - // 1. List all objects in one batch using prefix filtering (matches our virtual directory). - // 2. Perform pattern matching and exclusion filters locally. - // - // Benefits: - // - Single API call instead of per-directory requests - // - Reduced network latency, especially for deep directory structures - // - More efficient than recursive directory scanning - // - - var request = new ListObjectsV2Request - { - BucketName = _fs.BucketName, - Prefix = GetPrefix(FullName) - }; - - var directories = new HashSet - { - FullName - }; - - do - { - var response = await _fs.AmazonClient - .ListObjectsV2Async(request, cancellationToken) - .ConfigureAwait(false); - - if (response.S3Objects is not null) - { - foreach (var obj in response.S3Objects) - { - var directoryPath = VirtualPath.GetDirectoryName( - VirtualPath.Normalize(obj.Key)); - - while (directoryPath.Length != 0 && directories.Add(directoryPath)) - { - // - // Directories are yielded in reverse order (deepest first). - // - // Note: We could use a Stack to control the order, - // but since order isn't guaranteed anyway and to avoid - // unnecessary memory allocation, we process them directly. - // - if (IsMatched(directoryPath.AsSpan(FullName.Length), patterns, excludes)) - yield return new S3Directory(_fs, directoryPath); - - directoryPath = VirtualPath.GetDirectoryName(directoryPath); - } - } - } - - request.ContinuationToken = response.NextContinuationToken; - } - while (request.ContinuationToken is not null && !cancellationToken.IsCancellationRequested); - } - - /// - /// Creates a instance based on the specified object. - /// - /// The representing the file. - /// The normalized name of the object. - /// - /// A new instance representing the file. - /// - private S3File CreateVirtualFile(S3Object obj, string? normalizedName = null) - { - var properties = VirtualNodeProperties - .CreateFileProperties( - creationTime: default, - lastAccessTime: default, - lastWriteTime: obj.LastModified.GetValueOrDefault(), - length: obj.Size ?? 0); - - var path = normalizedName ?? VirtualPath.Normalize(obj.Key); - return new S3File(_fs, path, properties); - } - - /// - /// Determines whether the specified path matches any of the inclusion patterns and none of the exclusion patterns. - /// - /// The path to match. - /// The inclusion patterns to match against the path. - /// An optional array of exclusion patterns. If the path matches any of these, - /// the method returns . - /// - /// if the path matches at least one inclusion pattern and no exclusion patterns; - /// otherwise, . - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsMatched(scoped ReadOnlySpan path, string[] patterns, string[]? excludes) - { - if (excludes is not null) - foreach (var pattern in excludes) - if (Matcher.IsMatch(path, pattern, MatchFlags.Unix)) - return false; - - foreach (var pattern in patterns) - if (Matcher.IsMatch(path, pattern, MatchFlags.Unix)) - return true; - - return false; - } - - /// - /// Returns a cloud storage compatible prefix for the specified directory path. - /// - /// The directory path. - /// - /// A string formatted for cloud storage prefix filtering: - /// - /// Empty string for the root directory ("/") - /// Path without leading slash but with trailing slash for subdirectories - /// - /// - /// - /// - /// GetPrefix("/") // returns "" - /// GetPrefix("/folder") // returns "folder/" - /// GetPrefix("/sub/folder") // returns "sub/folder/" - /// - /// - private static string GetPrefix(string path) - { - Debug.Assert(VirtualPath.IsNormalized(path)); - return path == "/" ? "" : $"{path[1..]}/"; - } -} +using System.Diagnostics; +using System.Runtime.CompilerServices; + +using Amazon.S3.Model; + +using Ramstack.Globbing; + +namespace Ramstack.FileSystem.Amazon; + +/// +/// Represents an implementation of that maps a directory +/// to a path within a specified Amazon S3 bucket. +/// +internal sealed class S3Directory : VirtualDirectory +{ + private readonly AmazonS3FileSystem _fs; + + /// + public override IVirtualFileSystem FileSystem => _fs; + + /// + /// Initializes a new instance of the class. + /// + /// The file system associated with this directory. + /// The path to the directory within the specified Amazon S3 bucket. + public S3Directory(AmazonS3FileSystem fileSystem, string path) : base(path) => + _fs = fileSystem; + + /// + protected override ValueTask GetPropertiesCoreAsync(CancellationToken cancellationToken) => + new ValueTask(VirtualNodeProperties.None); + + /// + protected override ValueTask CreateCoreAsync(CancellationToken cancellationToken) => + default; + + /// + protected override async ValueTask DeleteCoreAsync(CancellationToken cancellationToken) + { + var lr = new ListObjectsV2Request + { + BucketName = _fs.BucketName, + Prefix = GetPrefix(FullName) + }; + + var dr = new DeleteObjectsRequest + { + BucketName = _fs.BucketName + }; + + dr.Objects ??= []; + + do + { + // The maximum number of objects returned is MaxKeys, which is 1000, + // and the maximum number of objects that can be deleted at once is also 1000. + // Therefore, we can rely on this and avoid splitting + // the retrieved objects into separate batches. + + var response = await _fs.AmazonClient + .ListObjectsV2Async(lr, cancellationToken) + .ConfigureAwait(false); + + if (response.S3Objects is not null) + foreach (var obj in response.S3Objects) + dr.Objects.Add(new KeyVersion { Key = obj.Key }); + + if (dr.Objects.Count != 0) + await _fs.AmazonClient + .DeleteObjectsAsync(dr, cancellationToken) + .ConfigureAwait(false); + + dr.Objects.Clear(); + lr.ContinuationToken = response.NextContinuationToken; + } + while (lr.ContinuationToken is not null && !cancellationToken.IsCancellationRequested); + } + + /// + protected override async IAsyncEnumerable GetFileNodesCoreAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + var request = new ListObjectsV2Request + { + BucketName = _fs.BucketName, + Prefix = GetPrefix(FullName), + Delimiter = "/" + }; + + do + { + var response = await _fs.AmazonClient + .ListObjectsV2Async(request, cancellationToken) + .ConfigureAwait(false); + + if (response.CommonPrefixes is not null) + foreach (var prefix in response.CommonPrefixes) + yield return new S3Directory(_fs, VirtualPath.Normalize(prefix)); + + if (response.S3Objects is not null) + foreach (var obj in response.S3Objects) + yield return CreateVirtualFile(obj); + + request.ContinuationToken = response.NextContinuationToken; + } + while (request.ContinuationToken is not null && !cancellationToken.IsCancellationRequested); + } + + /// + protected override async IAsyncEnumerable GetFilesCoreAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + var request = new ListObjectsV2Request + { + BucketName = _fs.BucketName, + Prefix = GetPrefix(FullName), + Delimiter = "/" + }; + + do + { + var response = await _fs.AmazonClient + .ListObjectsV2Async(request, cancellationToken) + .ConfigureAwait(false); + + if (response.S3Objects is not null) + foreach (var obj in response.S3Objects) + yield return CreateVirtualFile(obj); + + request.ContinuationToken = response.NextContinuationToken; + } + while (request.ContinuationToken is not null && !cancellationToken.IsCancellationRequested); + } + + /// + protected override async IAsyncEnumerable GetDirectoriesCoreAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + var request = new ListObjectsV2Request + { + BucketName = _fs.BucketName, + Prefix = GetPrefix(FullName), + Delimiter = "/" + }; + + do + { + var response = await _fs.AmazonClient + .ListObjectsV2Async(request, cancellationToken) + .ConfigureAwait(false); + + if (response.CommonPrefixes is not null) + foreach (var prefix in response.CommonPrefixes) + yield return new S3Directory(_fs, VirtualPath.Normalize(prefix)); + + request.ContinuationToken = response.NextContinuationToken; + } + while (request.ContinuationToken is not null && !cancellationToken.IsCancellationRequested); + } + + /// + protected override async IAsyncEnumerable GetFileNodesCoreAsync(string[] patterns, string[]? excludes, [EnumeratorCancellation] CancellationToken cancellationToken) + { + // + // Cloud storage optimization strategy: + // 1. List all objects in one batch using prefix filtering (matches our virtual directory). + // 2. Perform pattern matching and exclusion filters locally. + // + // Benefits: + // - Single API call instead of per-directory requests + // - Reduced network latency, especially for deep directory structures + // - More efficient than recursive directory scanning + // + + var request = new ListObjectsV2Request + { + BucketName = _fs.BucketName, + Prefix = GetPrefix(FullName) + }; + + var directories = new HashSet + { + FullName + }; + + do + { + var response = await _fs.AmazonClient + .ListObjectsV2Async(request, cancellationToken) + .ConfigureAwait(false); + + if (response.S3Objects is not null) + { + foreach (var obj in response.S3Objects) + { + var path = VirtualPath.Normalize(obj.Key); + var directoryPath = VirtualPath.GetDirectoryName(path); + + while (directoryPath.Length != 0 && directories.Add(directoryPath)) + { + // + // Directories are yielded in reverse order (deepest first). + // + // Note: We could use a Stack to control the order, + // but since order isn't guaranteed anyway and to avoid + // unnecessary memory allocation, we process them directly. + // + if (IsMatched(directoryPath.AsSpan(FullName.Length), patterns, excludes)) + yield return new S3Directory(_fs, directoryPath); + + directoryPath = VirtualPath.GetDirectoryName(directoryPath); + } + + if (IsMatched(obj.Key.AsSpan(request.Prefix.Length), patterns, excludes)) + yield return CreateVirtualFile(obj, path); + } + } + + request.ContinuationToken = response.NextContinuationToken; + } + while (request.ContinuationToken is not null && !cancellationToken.IsCancellationRequested); + } + + /// + protected override async IAsyncEnumerable GetFilesCoreAsync(string[] patterns, string[]? excludes, [EnumeratorCancellation] CancellationToken cancellationToken) + { + // + // Cloud storage optimization strategy: + // 1. List all objects in one batch using prefix filtering (matches our virtual directory). + // 2. Perform pattern matching and exclusion filters locally. + // + // Benefits: + // - Single API call instead of per-directory requests + // - Reduced network latency, especially for deep directory structures + // - More efficient than recursive directory scanning + // + + var request = new ListObjectsV2Request + { + BucketName = _fs.BucketName, + Prefix = GetPrefix(FullName) + }; + + do + { + var response = await _fs.AmazonClient + .ListObjectsV2Async(request, cancellationToken) + .ConfigureAwait(false); + + if (response.S3Objects is not null) + foreach (var obj in response.S3Objects) + if (IsMatched(obj.Key.AsSpan(request.Prefix.Length), patterns, excludes)) + yield return CreateVirtualFile(obj); + + request.ContinuationToken = response.NextContinuationToken; + } + while (request.ContinuationToken is not null && !cancellationToken.IsCancellationRequested); + } + + /// + protected override async IAsyncEnumerable GetDirectoriesCoreAsync(string[] patterns, string[]? excludes, [EnumeratorCancellation] CancellationToken cancellationToken) + { + // + // Cloud storage optimization strategy: + // 1. List all objects in one batch using prefix filtering (matches our virtual directory). + // 2. Perform pattern matching and exclusion filters locally. + // + // Benefits: + // - Single API call instead of per-directory requests + // - Reduced network latency, especially for deep directory structures + // - More efficient than recursive directory scanning + // + + var request = new ListObjectsV2Request + { + BucketName = _fs.BucketName, + Prefix = GetPrefix(FullName) + }; + + var directories = new HashSet + { + FullName + }; + + do + { + var response = await _fs.AmazonClient + .ListObjectsV2Async(request, cancellationToken) + .ConfigureAwait(false); + + if (response.S3Objects is not null) + { + foreach (var obj in response.S3Objects) + { + var directoryPath = VirtualPath.GetDirectoryName( + VirtualPath.Normalize(obj.Key)); + + while (directoryPath.Length != 0 && directories.Add(directoryPath)) + { + // + // Directories are yielded in reverse order (deepest first). + // + // Note: We could use a Stack to control the order, + // but since order isn't guaranteed anyway and to avoid + // unnecessary memory allocation, we process them directly. + // + if (IsMatched(directoryPath.AsSpan(FullName.Length), patterns, excludes)) + yield return new S3Directory(_fs, directoryPath); + + directoryPath = VirtualPath.GetDirectoryName(directoryPath); + } + } + } + + request.ContinuationToken = response.NextContinuationToken; + } + while (request.ContinuationToken is not null && !cancellationToken.IsCancellationRequested); + } + + /// + /// Creates a instance based on the specified object. + /// + /// The representing the file. + /// The normalized name of the object. + /// + /// A new instance representing the file. + /// + private S3File CreateVirtualFile(S3Object obj, string? normalizedName = null) + { + var properties = VirtualNodeProperties + .CreateFileProperties( + creationTime: default, + lastAccessTime: default, + lastWriteTime: obj.LastModified.GetValueOrDefault(), + length: obj.Size ?? 0); + + var path = normalizedName ?? VirtualPath.Normalize(obj.Key); + return new S3File(_fs, path, properties); + } + + /// + /// Determines whether the specified path matches any of the inclusion patterns and none of the exclusion patterns. + /// + /// The path to match. + /// The inclusion patterns to match against the path. + /// An optional array of exclusion patterns. If the path matches any of these, + /// the method returns . + /// + /// if the path matches at least one inclusion pattern and no exclusion patterns; + /// otherwise, . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsMatched(scoped ReadOnlySpan path, string[] patterns, string[]? excludes) + { + if (excludes is not null) + foreach (var pattern in excludes) + if (Matcher.IsMatch(path, pattern, MatchFlags.Unix)) + return false; + + foreach (var pattern in patterns) + if (Matcher.IsMatch(path, pattern, MatchFlags.Unix)) + return true; + + return false; + } + + /// + /// Returns a cloud storage compatible prefix for the specified directory path. + /// + /// The directory path. + /// + /// A string formatted for cloud storage prefix filtering: + /// + /// Empty string for the root directory ("/") + /// Path without leading slash but with trailing slash for subdirectories + /// + /// + /// + /// + /// GetPrefix("/") // returns "" + /// GetPrefix("/folder") // returns "folder/" + /// GetPrefix("/sub/folder") // returns "sub/folder/" + /// + /// + private static string GetPrefix(string path) + { + Debug.Assert(VirtualPath.IsNormalized(path)); + return path == "/" ? "" : $"{path[1..]}/"; + } +} diff --git a/src/Ramstack.FileSystem.Amazon/S3File.cs b/src/Ramstack.FileSystem.Amazon/S3File.cs index 77df096..b0b564a 100644 --- a/src/Ramstack.FileSystem.Amazon/S3File.cs +++ b/src/Ramstack.FileSystem.Amazon/S3File.cs @@ -1,176 +1,176 @@ -using System.Net; - -using Amazon.S3; -using Amazon.S3.Model; - -namespace Ramstack.FileSystem.Amazon; - -/// -/// Represents an implementation of that maps a file to an object in Amazon S3. -/// -internal sealed class S3File : VirtualFile -{ - private readonly AmazonS3FileSystem _fs; - private readonly string _key; - - /// - public override IVirtualFileSystem FileSystem => _fs; - - /// - /// Initializes a new instance of the class. - /// - /// The file system associated with this file. - /// The path to the file. - public S3File(AmazonS3FileSystem fileSystem, string path) : base(path) => - (_fs, _key) = (fileSystem, path[1..]); - - /// - /// Initializes a new instance of the class with the specified path and properties. - /// - /// The file system associated with this file. - /// The full path of the file. - /// The properties of the file. - public S3File(AmazonS3FileSystem fileSystem, string path, VirtualNodeProperties? properties) : base(path, properties) => - (_fs, _key) = (fileSystem, path[1..]); - - /// - protected override async ValueTask GetPropertiesCoreAsync(CancellationToken cancellationToken) - { - try - { - var metadata = await _fs.AmazonClient - .GetObjectMetadataAsync( - new GetObjectMetadataRequest { BucketName = _fs.BucketName, Key = _key }, - cancellationToken) - .ConfigureAwait(false); - - return VirtualNodeProperties.CreateFileProperties( - creationTime: default, - lastAccessTime: default, - lastWriteTime: metadata.LastModified.GetValueOrDefault(), - length: metadata.ContentLength); - } - catch (AmazonS3Exception e) when (e.StatusCode == HttpStatusCode.NotFound) - { - return null; - } - } - - /// - protected override async ValueTask OpenReadCoreAsync(CancellationToken cancellationToken) - { - var response = await _fs.AmazonClient - .GetObjectAsync( - new GetObjectRequest { BucketName = _fs.BucketName, Key = _key }, - cancellationToken) - .ConfigureAwait(false); - - return response.ResponseStream; - } - - /// - protected override async ValueTask OpenWriteCoreAsync(CancellationToken cancellationToken) - { - var response = await _fs.AmazonClient - .InitiateMultipartUploadAsync(_fs.BucketName, _key, cancellationToken) - .ConfigureAwait(false); - - return new S3UploadStream(_fs.AmazonClient, _fs.BucketName, _key, response.UploadId); - } - - /// - protected override async ValueTask WriteCoreAsync(Stream stream, bool overwrite, CancellationToken cancellationToken) - { - var request = new PutObjectRequest - { - BucketName = _fs.BucketName, - Key = _key, - InputStream = stream, - AutoCloseStream = false - }; - - if (!overwrite) - request.IfNoneMatch = "*"; - - await _fs.AmazonClient - .PutObjectAsync(request, cancellationToken) - .ConfigureAwait(false); - } - - /// - protected override async ValueTask DeleteCoreAsync(CancellationToken cancellationToken) - { - try - { - await _fs.AmazonClient - .DeleteObjectAsync( - new DeleteObjectRequest { BucketName = _fs.BucketName, Key = _key }, - cancellationToken) - .ConfigureAwait(false); - } - catch (AmazonS3Exception ex) when (ex.StatusCode == HttpStatusCode.NotFound) - { - } - } - - /// - protected override ValueTask CopyToCoreAsync(string destinationPath, bool overwrite, CancellationToken cancellationToken) => - CopyObjectAsync(_fs.BucketName, _key, _fs.BucketName, destinationPath[1..], overwrite, cancellationToken); - - /// - protected override ValueTask CopyToCoreAsync(VirtualFile destination, bool overwrite, CancellationToken cancellationToken) - { - if (destination is S3File file) - return CopyObjectAsync(_fs.BucketName, _key, file._fs.BucketName, file._key, overwrite, cancellationToken); - - return base.CopyToCoreAsync(destination, overwrite, cancellationToken); - } - - /// - /// Asynchronously copies an object from the source bucket and key to the destination bucket and key. - /// - /// The name of the source S3 bucket. - /// The key of the source object in the S3 bucket. - /// The name of the destination S3 bucket. - /// The key of the destination object in the S3 bucket. - /// A boolean value indicating whether to overwrite the destination object if it already exists. - /// An optional cancellation token to cancel the operation. - /// - /// A that represents the asynchronous copy operation. - /// - private async ValueTask CopyObjectAsync(string sourceBucket, string sourceKey, string destinationBucket, string destinationKey, bool overwrite, CancellationToken cancellationToken) - { - if (sourceBucket == destinationBucket && sourceKey == destinationKey) - throw new IOException($"Cannot copy a file '{FullName}' to itself."); - - // Unfortunately, Amazon S3 does not support destination conditions, - // so we make a separate request to check for the destination object existence. - - if (!overwrite) - { - try - { - await _fs.AmazonClient - .GetObjectMetadataAsync(destinationBucket, destinationKey, cancellationToken) - .ConfigureAwait(false); - - throw new IOException($"The file '{FullName}' already exists."); - } - catch (AmazonS3Exception e) when (e.StatusCode == HttpStatusCode.NotFound) - { - } - } - - var request = new CopyObjectRequest - { - SourceBucket = sourceBucket, - SourceKey = sourceKey, - DestinationBucket = destinationBucket, - DestinationKey = destinationKey - }; - - await _fs.AmazonClient - .CopyObjectAsync(request, cancellationToken) - .ConfigureAwait(false); - } -} +using System.Net; + +using Amazon.S3; +using Amazon.S3.Model; + +namespace Ramstack.FileSystem.Amazon; + +/// +/// Represents an implementation of that maps a file to an object in Amazon S3. +/// +internal sealed class S3File : VirtualFile +{ + private readonly AmazonS3FileSystem _fs; + private readonly string _key; + + /// + public override IVirtualFileSystem FileSystem => _fs; + + /// + /// Initializes a new instance of the class. + /// + /// The file system associated with this file. + /// The path to the file. + public S3File(AmazonS3FileSystem fileSystem, string path) : base(path) => + (_fs, _key) = (fileSystem, path[1..]); + + /// + /// Initializes a new instance of the class with the specified path and properties. + /// + /// The file system associated with this file. + /// The full path of the file. + /// The properties of the file. + public S3File(AmazonS3FileSystem fileSystem, string path, VirtualNodeProperties? properties) : base(path, properties) => + (_fs, _key) = (fileSystem, path[1..]); + + /// + protected override async ValueTask GetPropertiesCoreAsync(CancellationToken cancellationToken) + { + try + { + var metadata = await _fs.AmazonClient + .GetObjectMetadataAsync( + new GetObjectMetadataRequest { BucketName = _fs.BucketName, Key = _key }, + cancellationToken) + .ConfigureAwait(false); + + return VirtualNodeProperties.CreateFileProperties( + creationTime: default, + lastAccessTime: default, + lastWriteTime: metadata.LastModified.GetValueOrDefault(), + length: metadata.ContentLength); + } + catch (AmazonS3Exception e) when (e.StatusCode == HttpStatusCode.NotFound) + { + return null; + } + } + + /// + protected override async ValueTask OpenReadCoreAsync(CancellationToken cancellationToken) + { + var response = await _fs.AmazonClient + .GetObjectAsync( + new GetObjectRequest { BucketName = _fs.BucketName, Key = _key }, + cancellationToken) + .ConfigureAwait(false); + + return response.ResponseStream; + } + + /// + protected override async ValueTask OpenWriteCoreAsync(CancellationToken cancellationToken) + { + var response = await _fs.AmazonClient + .InitiateMultipartUploadAsync(_fs.BucketName, _key, cancellationToken) + .ConfigureAwait(false); + + return new S3UploadStream(_fs.AmazonClient, _fs.BucketName, _key, response.UploadId); + } + + /// + protected override async ValueTask WriteCoreAsync(Stream stream, bool overwrite, CancellationToken cancellationToken) + { + var request = new PutObjectRequest + { + BucketName = _fs.BucketName, + Key = _key, + InputStream = stream, + AutoCloseStream = false + }; + + if (!overwrite) + request.IfNoneMatch = "*"; + + await _fs.AmazonClient + .PutObjectAsync(request, cancellationToken) + .ConfigureAwait(false); + } + + /// + protected override async ValueTask DeleteCoreAsync(CancellationToken cancellationToken) + { + try + { + await _fs.AmazonClient + .DeleteObjectAsync( + new DeleteObjectRequest { BucketName = _fs.BucketName, Key = _key }, + cancellationToken) + .ConfigureAwait(false); + } + catch (AmazonS3Exception ex) when (ex.StatusCode == HttpStatusCode.NotFound) + { + } + } + + /// + protected override ValueTask CopyToCoreAsync(string destinationPath, bool overwrite, CancellationToken cancellationToken) => + CopyObjectAsync(_fs.BucketName, _key, _fs.BucketName, destinationPath[1..], overwrite, cancellationToken); + + /// + protected override ValueTask CopyToCoreAsync(VirtualFile destination, bool overwrite, CancellationToken cancellationToken) + { + if (destination is S3File file) + return CopyObjectAsync(_fs.BucketName, _key, file._fs.BucketName, file._key, overwrite, cancellationToken); + + return base.CopyToCoreAsync(destination, overwrite, cancellationToken); + } + + /// + /// Asynchronously copies an object from the source bucket and key to the destination bucket and key. + /// + /// The name of the source S3 bucket. + /// The key of the source object in the S3 bucket. + /// The name of the destination S3 bucket. + /// The key of the destination object in the S3 bucket. + /// A boolean value indicating whether to overwrite the destination object if it already exists. + /// An optional cancellation token to cancel the operation. + /// + /// A that represents the asynchronous copy operation. + /// + private async ValueTask CopyObjectAsync(string sourceBucket, string sourceKey, string destinationBucket, string destinationKey, bool overwrite, CancellationToken cancellationToken) + { + if (sourceBucket == destinationBucket && sourceKey == destinationKey) + throw new IOException($"Cannot copy a file '{FullName}' to itself."); + + // Unfortunately, Amazon S3 does not support destination conditions, + // so we make a separate request to check for the destination object existence. + + if (!overwrite) + { + try + { + await _fs.AmazonClient + .GetObjectMetadataAsync(destinationBucket, destinationKey, cancellationToken) + .ConfigureAwait(false); + + throw new IOException($"The file '{FullName}' already exists."); + } + catch (AmazonS3Exception e) when (e.StatusCode == HttpStatusCode.NotFound) + { + } + } + + var request = new CopyObjectRequest + { + SourceBucket = sourceBucket, + SourceKey = sourceKey, + DestinationBucket = destinationBucket, + DestinationKey = destinationKey + }; + + await _fs.AmazonClient + .CopyObjectAsync(request, cancellationToken) + .ConfigureAwait(false); + } +} diff --git a/src/Ramstack.FileSystem.Amazon/S3UploadStream.cs b/src/Ramstack.FileSystem.Amazon/S3UploadStream.cs index 88fa2f9..86c7e8c 100644 --- a/src/Ramstack.FileSystem.Amazon/S3UploadStream.cs +++ b/src/Ramstack.FileSystem.Amazon/S3UploadStream.cs @@ -1,318 +1,318 @@ -using System.Diagnostics.CodeAnalysis; - -using Amazon.S3; -using Amazon.S3.Model; - -using Ramstack.FileSystem.Amazon.Utilities; - -namespace Ramstack.FileSystem.Amazon; - -/// -/// Represents a stream for uploading data to Amazon S3 using multipart upload. -/// -internal sealed class S3UploadStream : Stream -{ - // https://docs.aws.amazon.com/AmazonS3/latest/userguide/qfacts.html - private const long MinPartSize = 5L * 1024 * 1024; - - private readonly IAmazonS3 _client; - private readonly string _bucketName; - private readonly string _key; - private readonly string _uploadId; - private readonly FileStream _stream; - private readonly List _partETags; - - private int _disposed; - - /// - public override bool CanRead => false; - - /// - public override bool CanSeek => false; - - /// - public override bool CanWrite => true; - - /// - public override long Length - { - get - { - Error_NotSupported(); - return 0; - } - } - - /// - public override long Position - { - get - { - Error_NotSupported(); - return 0; - } - // ReSharper disable once ValueParameterNotUsed - set => Error_NotSupported(); - } - - /// - /// Initializes a new instance of the class. - /// - /// The Amazon S3 client used for uploading parts. - /// The name of the S3 bucket where the data will be uploaded. - /// The key (path) in the S3 bucket where the data will be stored. - /// The multipart upload session identifier. - public S3UploadStream(IAmazonS3 client, string bucketName, string key, string uploadId) - { - _client = client; - _bucketName = bucketName; - _key = key; - _uploadId = uploadId; - _partETags = []; - - _stream = new FileStream( - Path.Combine( - Path.GetTempPath(), - Path.GetRandomFileName()), - FileMode.CreateNew, - FileAccess.ReadWrite, - FileShare.None, - bufferSize: 4096, - FileOptions.DeleteOnClose - | FileOptions.Asynchronous); - } - - /// - public override int Read(byte[] buffer, int offset, int count) - { - Error_NotSupported(); - return 0; - } - - /// - public override void Write(byte[] buffer, int offset, int count) => - Write(buffer.AsSpan(offset, count)); - - /// - public override void Write(ReadOnlySpan buffer) - { - try - { - _stream.Write(buffer); - if (_stream.Length >= MinPartSize) - UploadPart(); - } - catch - { - Abort(); - throw; - } - } - - /// - public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => - WriteAsync(buffer.AsMemory(offset, count), cancellationToken).AsTask(); - - /// - public override async ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) - { - try - { - await _stream.WriteAsync(buffer, cancellationToken).ConfigureAwait(false); - if (_stream.Length >= MinPartSize) - await UploadPartAsync(cancellationToken).ConfigureAwait(false); - } - catch - { - await AbortAsync(cancellationToken).ConfigureAwait(false); - throw; - } - } - - /// - public override long Seek(long offset, SeekOrigin origin) - { - Error_NotSupported(); - return 0; - } - - /// - public override void SetLength(long value) => - Error_NotSupported(); - - /// - public override void Flush() - { - } - - /// - public override Task FlushAsync(CancellationToken cancellationToken) => - Task.CompletedTask; - - /// - protected override void Dispose(bool disposing) - { - if (disposing) - { - using var scope = NullSynchronizationContext.CreateScope(); - DisposeAsync().AsTask().Wait(); - } - - base.Dispose(disposing); - } - - /// - public override async ValueTask DisposeAsync() - { - if (Interlocked.Exchange(ref _disposed, 1) != 0) - return; - - try - { - await UploadPartAsync(CancellationToken.None).ConfigureAwait(false); - - var request = new CompleteMultipartUploadRequest - { - BucketName = _bucketName, - Key = _key, - UploadId = _uploadId, - PartETags = _partETags - }; - - await _client - .CompleteMultipartUploadAsync(request) - .ConfigureAwait(false); - } - catch - { - await AbortAsync(CancellationToken.None).ConfigureAwait(false); - throw; - } - finally - { - try - { - await _stream.DisposeAsync().ConfigureAwait(false); - } - catch - { - // Ignore: - // Errors when disposing the temporary buffer are not significant here, because: - // 1) If the upload succeeded, the job is done; a cleanup error can be ignored. - // 2) If the upload failed, preserving the original exception is more important for us. - } - } - } - - /// - /// Uploads the current buffer to Amazon S3 as a part of the multipart upload. - /// This method blocks the calling thread and waits for the upload to complete. - /// - private void UploadPart() - { - using var scope = NullSynchronizationContext.CreateScope(); - UploadPartAsync(CancellationToken.None).AsTask().GetAwaiter().GetResult(); - } - - /// - /// Asynchronously uploads the current buffer to Amazon S3 as a part of the multipart upload. - /// - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation. - /// - private async ValueTask UploadPartAsync(CancellationToken cancellationToken) - { - _stream.Position = 0; - - // https://docs.aws.amazon.com/AmazonS3/latest/userguide/qfacts.html - // The maximum allowed part size is 5 GiB. - // ----------------------------------------------------------------------------------- - // We don't need to worry about S3's 5 GiB part limit because: - // 1. All Write/WriteAsync methods are inherently limited by Array.MaxLength (~2 GiB). - // 2. The upload starts as soon as the buffer reaches MinPartSize (5 MiB). - // Even if a single write matches Array.MaxLength, the data is - // uploaded immediately, staying within AWS limits. - - var request = new UploadPartRequest - { - BucketName = _bucketName, - Key = _key, - UploadId = _uploadId, - PartNumber = _partETags.Count + 1, - InputStream = _stream, - PartSize = _stream.Length - }; - - var response = await _client - .UploadPartAsync(request, cancellationToken) - .ConfigureAwait(false); - - _partETags.Add(new PartETag(response)); - - _stream.Position = 0; - _stream.SetLength(0); - } - - /// - /// Aborts the multipart upload session. - /// - /// - /// This method sends a request to Amazon S3 to abort the multipart upload identified by the - /// . Once aborted, the upload cannot be resumed, and any uploaded parts - /// will be deleted. - /// - private void Abort() - { - using var scope = NullSynchronizationContext.CreateScope(); - AbortAsync(CancellationToken.None).AsTask().GetAwaiter().GetResult(); - } - - /// - /// Asynchronously aborts the multipart upload session. - /// - /// An optional cancellation token to cancel the operation. - /// - /// A that represents the asynchronous abort operation. - /// - /// - /// This method sends a request to Amazon S3 to abort the multipart upload identified by the - /// . Once aborted, the upload cannot be resumed, and any uploaded parts - /// will be deleted. - /// - private async ValueTask AbortAsync(CancellationToken cancellationToken) - { - if (Interlocked.Exchange(ref _disposed, 1) != 0) - return; - - try - { - await using (_stream.ConfigureAwait(false)) - { - var request = new AbortMultipartUploadRequest - { - BucketName = _bucketName, - Key = _key, - UploadId = _uploadId - }; - - await _client - .AbortMultipartUploadAsync(request, cancellationToken) - .ConfigureAwait(false); - } - } - catch - { - // IGNORE: - // Suppressing the exception during abort to preserve - // the original exception that triggered the abort. - } - } - - /// - /// Throws a . - /// - [DoesNotReturn] - private static void Error_NotSupported() => - throw new NotSupportedException(); -} +using System.Diagnostics.CodeAnalysis; + +using Amazon.S3; +using Amazon.S3.Model; + +using Ramstack.FileSystem.Amazon.Utilities; + +namespace Ramstack.FileSystem.Amazon; + +/// +/// Represents a stream for uploading data to Amazon S3 using multipart upload. +/// +internal sealed class S3UploadStream : Stream +{ + // https://docs.aws.amazon.com/AmazonS3/latest/userguide/qfacts.html + private const long MinPartSize = 5L * 1024 * 1024; + + private readonly IAmazonS3 _client; + private readonly string _bucketName; + private readonly string _key; + private readonly string _uploadId; + private readonly FileStream _stream; + private readonly List _partETags; + + private int _disposed; + + /// + public override bool CanRead => false; + + /// + public override bool CanSeek => false; + + /// + public override bool CanWrite => true; + + /// + public override long Length + { + get + { + Error_NotSupported(); + return 0; + } + } + + /// + public override long Position + { + get + { + Error_NotSupported(); + return 0; + } + // ReSharper disable once ValueParameterNotUsed + set => Error_NotSupported(); + } + + /// + /// Initializes a new instance of the class. + /// + /// The Amazon S3 client used for uploading parts. + /// The name of the S3 bucket where the data will be uploaded. + /// The key (path) in the S3 bucket where the data will be stored. + /// The multipart upload session identifier. + public S3UploadStream(IAmazonS3 client, string bucketName, string key, string uploadId) + { + _client = client; + _bucketName = bucketName; + _key = key; + _uploadId = uploadId; + _partETags = []; + + _stream = new FileStream( + Path.Combine( + Path.GetTempPath(), + Path.GetRandomFileName()), + FileMode.CreateNew, + FileAccess.ReadWrite, + FileShare.None, + bufferSize: 4096, + FileOptions.DeleteOnClose + | FileOptions.Asynchronous); + } + + /// + public override int Read(byte[] buffer, int offset, int count) + { + Error_NotSupported(); + return 0; + } + + /// + public override void Write(byte[] buffer, int offset, int count) => + Write(buffer.AsSpan(offset, count)); + + /// + public override void Write(ReadOnlySpan buffer) + { + try + { + _stream.Write(buffer); + if (_stream.Length >= MinPartSize) + UploadPart(); + } + catch + { + Abort(); + throw; + } + } + + /// + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => + WriteAsync(buffer.AsMemory(offset, count), cancellationToken).AsTask(); + + /// + public override async ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + { + try + { + await _stream.WriteAsync(buffer, cancellationToken).ConfigureAwait(false); + if (_stream.Length >= MinPartSize) + await UploadPartAsync(cancellationToken).ConfigureAwait(false); + } + catch + { + await AbortAsync(cancellationToken).ConfigureAwait(false); + throw; + } + } + + /// + public override long Seek(long offset, SeekOrigin origin) + { + Error_NotSupported(); + return 0; + } + + /// + public override void SetLength(long value) => + Error_NotSupported(); + + /// + public override void Flush() + { + } + + /// + public override Task FlushAsync(CancellationToken cancellationToken) => + Task.CompletedTask; + + /// + protected override void Dispose(bool disposing) + { + if (disposing) + { + using var scope = NullSynchronizationContext.CreateScope(); + DisposeAsync().AsTask().Wait(); + } + + base.Dispose(disposing); + } + + /// + public override async ValueTask DisposeAsync() + { + if (Interlocked.Exchange(ref _disposed, 1) != 0) + return; + + try + { + await UploadPartAsync(CancellationToken.None).ConfigureAwait(false); + + var request = new CompleteMultipartUploadRequest + { + BucketName = _bucketName, + Key = _key, + UploadId = _uploadId, + PartETags = _partETags + }; + + await _client + .CompleteMultipartUploadAsync(request) + .ConfigureAwait(false); + } + catch + { + await AbortAsync(CancellationToken.None).ConfigureAwait(false); + throw; + } + finally + { + try + { + await _stream.DisposeAsync().ConfigureAwait(false); + } + catch + { + // Ignore: + // Errors when disposing the temporary buffer are not significant here, because: + // 1) If the upload succeeded, the job is done; a cleanup error can be ignored. + // 2) If the upload failed, preserving the original exception is more important for us. + } + } + } + + /// + /// Uploads the current buffer to Amazon S3 as a part of the multipart upload. + /// This method blocks the calling thread and waits for the upload to complete. + /// + private void UploadPart() + { + using var scope = NullSynchronizationContext.CreateScope(); + UploadPartAsync(CancellationToken.None).AsTask().GetAwaiter().GetResult(); + } + + /// + /// Asynchronously uploads the current buffer to Amazon S3 as a part of the multipart upload. + /// + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation. + /// + private async ValueTask UploadPartAsync(CancellationToken cancellationToken) + { + _stream.Position = 0; + + // https://docs.aws.amazon.com/AmazonS3/latest/userguide/qfacts.html + // The maximum allowed part size is 5 GiB. + // ----------------------------------------------------------------------------------- + // We don't need to worry about S3's 5 GiB part limit because: + // 1. All Write/WriteAsync methods are inherently limited by Array.MaxLength (~2 GiB). + // 2. The upload starts as soon as the buffer reaches MinPartSize (5 MiB). + // Even if a single write matches Array.MaxLength, the data is + // uploaded immediately, staying within AWS limits. + + var request = new UploadPartRequest + { + BucketName = _bucketName, + Key = _key, + UploadId = _uploadId, + PartNumber = _partETags.Count + 1, + InputStream = _stream, + PartSize = _stream.Length + }; + + var response = await _client + .UploadPartAsync(request, cancellationToken) + .ConfigureAwait(false); + + _partETags.Add(new PartETag(response)); + + _stream.Position = 0; + _stream.SetLength(0); + } + + /// + /// Aborts the multipart upload session. + /// + /// + /// This method sends a request to Amazon S3 to abort the multipart upload identified by the + /// . Once aborted, the upload cannot be resumed, and any uploaded parts + /// will be deleted. + /// + private void Abort() + { + using var scope = NullSynchronizationContext.CreateScope(); + AbortAsync(CancellationToken.None).AsTask().GetAwaiter().GetResult(); + } + + /// + /// Asynchronously aborts the multipart upload session. + /// + /// An optional cancellation token to cancel the operation. + /// + /// A that represents the asynchronous abort operation. + /// + /// + /// This method sends a request to Amazon S3 to abort the multipart upload identified by the + /// . Once aborted, the upload cannot be resumed, and any uploaded parts + /// will be deleted. + /// + private async ValueTask AbortAsync(CancellationToken cancellationToken) + { + if (Interlocked.Exchange(ref _disposed, 1) != 0) + return; + + try + { + await using (_stream.ConfigureAwait(false)) + { + var request = new AbortMultipartUploadRequest + { + BucketName = _bucketName, + Key = _key, + UploadId = _uploadId + }; + + await _client + .AbortMultipartUploadAsync(request, cancellationToken) + .ConfigureAwait(false); + } + } + catch + { + // IGNORE: + // Suppressing the exception during abort to preserve + // the original exception that triggered the abort. + } + } + + /// + /// Throws a . + /// + [DoesNotReturn] + private static void Error_NotSupported() => + throw new NotSupportedException(); +} diff --git a/src/Ramstack.FileSystem.Amazon/Utilities/NullSynchronizationContext.cs b/src/Ramstack.FileSystem.Amazon/Utilities/NullSynchronizationContext.cs index b7404bc..38af654 100644 --- a/src/Ramstack.FileSystem.Amazon/Utilities/NullSynchronizationContext.cs +++ b/src/Ramstack.FileSystem.Amazon/Utilities/NullSynchronizationContext.cs @@ -1,33 +1,33 @@ -namespace Ramstack.FileSystem.Amazon.Utilities; - -/// -/// Provides a mechanism to temporarily set the to . -/// -internal static class NullSynchronizationContext -{ - /// - /// Sets the current to - /// and returns a disposable scope that restores the original context when disposed. - /// - /// - /// A struct that restores the original when disposed. - /// - public static ContextScope CreateScope() - { - var context = SynchronizationContext.Current; - SynchronizationContext.SetSynchronizationContext(null); - return new ContextScope(context); - } - - /// - /// A disposable struct that restores the original when disposed. - /// - public readonly struct ContextScope(SynchronizationContext? context) : IDisposable - { - /// - /// Restores the original that was present when the scope was created. - /// - public void Dispose() => - SynchronizationContext.SetSynchronizationContext(context); - } -} +namespace Ramstack.FileSystem.Amazon.Utilities; + +/// +/// Provides a mechanism to temporarily set the to . +/// +internal static class NullSynchronizationContext +{ + /// + /// Sets the current to + /// and returns a disposable scope that restores the original context when disposed. + /// + /// + /// A struct that restores the original when disposed. + /// + public static ContextScope CreateScope() + { + var context = SynchronizationContext.Current; + SynchronizationContext.SetSynchronizationContext(null); + return new ContextScope(context); + } + + /// + /// A disposable struct that restores the original when disposed. + /// + public readonly struct ContextScope(SynchronizationContext? context) : IDisposable + { + /// + /// Restores the original that was present when the scope was created. + /// + public void Dispose() => + SynchronizationContext.SetSynchronizationContext(context); + } +} diff --git a/src/Ramstack.FileSystem.Azure/AzureDirectory.cs b/src/Ramstack.FileSystem.Azure/AzureDirectory.cs index 59c3a74..7cec68a 100644 --- a/src/Ramstack.FileSystem.Azure/AzureDirectory.cs +++ b/src/Ramstack.FileSystem.Azure/AzureDirectory.cs @@ -1,359 +1,359 @@ -using System.Collections.ObjectModel; -using System.Diagnostics; -using System.Runtime.CompilerServices; - -using Azure; -using Azure.Storage.Blobs.Models; -using Azure.Storage.Blobs.Specialized; - -using Ramstack.Globbing; - -namespace Ramstack.FileSystem.Azure; - -/// -/// Represents an implementation of that maps a directory -/// to the path within an Azure Blob Storage container. -/// -internal sealed class AzureDirectory : VirtualDirectory -{ - private readonly AzureFileSystem _fs; - - /// - public override IVirtualFileSystem FileSystem => _fs; - - /// - /// Initializes a new instance of the class. - /// - /// The file system associated with this directory. - /// The path to the directory within the Azure Blob Storage container. - public AzureDirectory(AzureFileSystem fileSystem, string path) : base(path) => - _fs = fileSystem; - - /// - protected override ValueTask GetPropertiesCoreAsync(CancellationToken cancellationToken) => - new ValueTask(VirtualNodeProperties.None); - - /// - protected override ValueTask ExistsCoreAsync(CancellationToken cancellationToken) => - new ValueTask(true); - - /// - protected override ValueTask CreateCoreAsync(CancellationToken cancellationToken) => - default; - - /// - protected override async ValueTask DeleteCoreAsync(CancellationToken cancellationToken) - { - var collection = _fs.AzureClient - .GetBlobsAsync( - prefix: GetPrefix(FullName), - traits: BlobTraits.None, - states: BlobStates.None, - cancellationToken: cancellationToken); - - var client = _fs.AzureClient.GetBlobBatchClient(); - var batch = client.CreateBatch(); - - // https://learn.microsoft.com/en-us/rest/api/storageservices/blob-batch#remarks - // Each batch request supports a maximum of 256 sub requests. - const int MaxSubRequests = 256; - - await foreach (var page in collection.AsPages().WithCancellation(cancellationToken).ConfigureAwait(false)) - { - foreach (var blob in page.Values) - { - batch.DeleteBlob(_fs.AzureClient.Name, blob.Name, DeleteSnapshotsOption.IncludeSnapshots, conditions: null); - - if (batch.RequestCount != MaxSubRequests) - continue; - - await DeleteBlobsAsync(client, batch, cancellationToken).ConfigureAwait(false); - batch = client.CreateBatch(); - } - } - - if (batch.RequestCount != 0) - await DeleteBlobsAsync(client, batch, cancellationToken).ConfigureAwait(false); - - static async ValueTask DeleteBlobsAsync(BlobBatchClient client, BlobBatch batch, CancellationToken cancellationToken) - { - try - { - await client.SubmitBatchAsync(batch, throwOnAnyFailure: true, cancellationToken).ConfigureAwait(false); - } - catch (AggregateException e) when (Processed(e.InnerExceptions)) - { - } - finally - { - batch.Dispose(); - } - - static bool Processed(ReadOnlyCollection exceptions) - { - for (int count = exceptions.Count, i = 0; i < count; i++) - if (exceptions[i] is not RequestFailedException { Status: 404 }) - return false; - - return true; - } - } - } - - /// - protected override async IAsyncEnumerable GetFileNodesCoreAsync([EnumeratorCancellation] CancellationToken cancellationToken) - { - var collection = _fs.AzureClient - .GetBlobsByHierarchyAsync( - delimiter: "/", - prefix: GetPrefix(FullName), - traits: BlobTraits.None, - states: BlobStates.None, - cancellationToken: cancellationToken); - - await foreach (var page in collection.AsPages().WithCancellation(cancellationToken).ConfigureAwait(false)) - foreach (var item in page.Values) - yield return item.Prefix is not null - ? new AzureDirectory(_fs, VirtualPath.Normalize(item.Prefix)) - : CreateVirtualFile(item.Blob); - } - - /// - protected override async IAsyncEnumerable GetFilesCoreAsync([EnumeratorCancellation] CancellationToken cancellationToken) - { - var collection = _fs.AzureClient - .GetBlobsByHierarchyAsync( - delimiter: "/", - prefix: GetPrefix(FullName), - traits: BlobTraits.None, - states: BlobStates.None, - cancellationToken: cancellationToken); - - await foreach (var page in collection.AsPages().WithCancellation(cancellationToken).ConfigureAwait(false)) - foreach (var item in page.Values) - if (item.Prefix is null) - yield return CreateVirtualFile(item.Blob); - } - - /// - protected override async IAsyncEnumerable GetDirectoriesCoreAsync([EnumeratorCancellation] CancellationToken cancellationToken) - { - var collection = _fs.AzureClient - .GetBlobsByHierarchyAsync( - delimiter: "/", - prefix: GetPrefix(FullName), - traits: BlobTraits.None, - states: BlobStates.None, - cancellationToken: cancellationToken); - - await foreach (var page in collection.AsPages().WithCancellation(cancellationToken).ConfigureAwait(false)) - foreach (var item in page.Values) - if (item.Prefix is not null) - yield return new AzureDirectory(_fs, VirtualPath.Normalize(item.Prefix)); - } - - /// - protected override async IAsyncEnumerable GetFileNodesCoreAsync(string[] patterns, string[]? excludes, [EnumeratorCancellation] CancellationToken cancellationToken) - { - // - // Cloud storage optimization strategy: - // 1. List all blobs in one batch using prefix filtering (matches our virtual directory). - // 2. Perform pattern matching and exclusion filters locally. - // - // Benefits: - // - Single API call instead of per-directory requests - // - Reduced network latency, especially for deep directory structures - // - More efficient than recursive directory scanning - // - - var prefix = GetPrefix(FullName); - var directories = new HashSet { FullName }; - - await foreach (var page in _fs.AzureClient - .GetBlobsAsync( - prefix: prefix, - traits: BlobTraits.None, - states: BlobStates.None, - cancellationToken: cancellationToken) - .AsPages() - .WithCancellation(cancellationToken) - .ConfigureAwait(false)) - { - foreach (var blob in page.Values) - { - var path = VirtualPath.Normalize(blob.Name); - var directoryPath = VirtualPath.GetDirectoryName(path); - - while (directoryPath.Length != 0 && directories.Add(directoryPath)) - { - // - // Directories are yielded in reverse order (deepest first). - // - // Note: We could use a Stack to control the order, - // but since order isn't guaranteed anyway and to avoid - // unnecessary memory allocation, we process them directly. - // - if (IsMatched(directoryPath.AsSpan(FullName.Length), patterns, excludes)) - yield return new AzureDirectory(_fs, directoryPath); - - directoryPath = VirtualPath.GetDirectoryName(directoryPath); - } - - if (IsMatched(blob.Name.AsSpan(prefix.Length), patterns, excludes)) - yield return CreateVirtualFile(blob, path); - } - } - } - - /// - protected override async IAsyncEnumerable GetFilesCoreAsync(string[] patterns, string[]? excludes, [EnumeratorCancellation] CancellationToken cancellationToken) - { - // - // Cloud storage optimization strategy: - // 1. List all blobs in one batch using prefix filtering (matches our virtual directory). - // 2. Perform pattern matching and exclusion filters locally. - // - // Benefits: - // - Single API call instead of per-directory requests - // - Reduced network latency, especially for deep directory structures - // - More efficient than recursive directory scanning - // - - var prefix = GetPrefix(FullName); - - await foreach (var page in _fs.AzureClient - .GetBlobsAsync( - prefix: prefix, - traits: BlobTraits.None, - states: BlobStates.None, - cancellationToken: cancellationToken) - .AsPages() - .WithCancellation(cancellationToken) - .ConfigureAwait(false)) - { - foreach (var blob in page.Values) - if (IsMatched(blob.Name.AsSpan(prefix.Length), patterns, excludes)) - yield return CreateVirtualFile(blob); - } - } - - /// - protected override async IAsyncEnumerable GetDirectoriesCoreAsync(string[] patterns, string[]? excludes, [EnumeratorCancellation] CancellationToken cancellationToken) - { - // - // Cloud storage optimization strategy: - // 1. List all blobs in one batch using prefix filtering (matches our virtual directory). - // 2. Perform pattern matching and exclusion filters locally. - // - // Benefits: - // - Single API call instead of per-directory requests - // - Reduced network latency, especially for deep directory structures - // - More efficient than recursive directory scanning - // - - var prefix = GetPrefix(FullName); - var directories = new HashSet { FullName }; - - await foreach (var page in _fs.AzureClient - .GetBlobsAsync( - prefix: prefix, - traits: BlobTraits.None, - states: BlobStates.None, - cancellationToken: cancellationToken) - .AsPages() - .WithCancellation(cancellationToken) - .ConfigureAwait(false)) - { - foreach (var blob in page.Values) - { - var directoryPath = VirtualPath.GetDirectoryName( - VirtualPath.Normalize(blob.Name)); - - while (directoryPath.Length != 0 && directories.Add(directoryPath)) - { - // - // Directories are yielded in reverse order (deepest first). - // - // Note: We could use a Stack to control the order, - // but since order isn't guaranteed anyway and to avoid - // unnecessary memory allocation, we process them directly. - // - if (IsMatched(directoryPath.AsSpan(FullName.Length), patterns, excludes)) - yield return new AzureDirectory(_fs, directoryPath); - - directoryPath = VirtualPath.GetDirectoryName(directoryPath); - } - } - } - } - - /// - /// Creates a instance based on the specified blob item. - /// - /// The representing the file. - /// The normalized name of the blob. - /// - /// A new instance representing the file. - /// - private AzureFile CreateVirtualFile(BlobItem blob, string? normalizedPath = null) - { - var info = blob.Properties; - var properties = VirtualNodeProperties.CreateFileProperties( - creationTime: info.CreatedOn.GetValueOrDefault(), - lastAccessTime: info.LastAccessedOn.GetValueOrDefault(), - lastWriteTime: info.LastModified.GetValueOrDefault(), - length: info.ContentLength.GetValueOrDefault(defaultValue: -1)); - - var path = normalizedPath ?? VirtualPath.Normalize(blob.Name); - return new AzureFile(_fs, path, properties); - } - - /// - /// Determines whether the specified path matches any of the inclusion patterns and none of the exclusion patterns. - /// - /// The path to match. - /// The inclusion patterns to match against the path. - /// An optional array of exclusion patterns. If the path matches any of these, - /// the method returns . - /// - /// if the path matches at least one inclusion pattern and no exclusion patterns; - /// otherwise, . - /// - private static bool IsMatched(scoped ReadOnlySpan path, string[] patterns, string[]? excludes) - { - if (excludes is not null) - foreach (var pattern in excludes) - if (Matcher.IsMatch(path, pattern, MatchFlags.Unix)) - return false; - - foreach (var pattern in patterns) - if (Matcher.IsMatch(path, pattern, MatchFlags.Unix)) - return true; - - return false; - } - - /// - /// Returns a cloud storage compatible prefix for the specified directory path. - /// - /// The directory path. - /// - /// A string formatted for cloud storage prefix filtering: - /// - /// Empty string for the root directory ("/") - /// Path without leading slash but with trailing slash for subdirectories - /// - /// - /// - /// - /// GetPrefix("/") // returns "" - /// GetPrefix("/folder") // returns "folder/" - /// GetPrefix("/sub/folder") // returns "sub/folder/" - /// - /// - private static string GetPrefix(string path) - { - Debug.Assert(VirtualPath.IsNormalized(path)); - return path == "/" ? "" : $"{path[1..]}/"; - } -} +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Runtime.CompilerServices; + +using Azure; +using Azure.Storage.Blobs.Models; +using Azure.Storage.Blobs.Specialized; + +using Ramstack.Globbing; + +namespace Ramstack.FileSystem.Azure; + +/// +/// Represents an implementation of that maps a directory +/// to the path within an Azure Blob Storage container. +/// +internal sealed class AzureDirectory : VirtualDirectory +{ + private readonly AzureFileSystem _fs; + + /// + public override IVirtualFileSystem FileSystem => _fs; + + /// + /// Initializes a new instance of the class. + /// + /// The file system associated with this directory. + /// The path to the directory within the Azure Blob Storage container. + public AzureDirectory(AzureFileSystem fileSystem, string path) : base(path) => + _fs = fileSystem; + + /// + protected override ValueTask GetPropertiesCoreAsync(CancellationToken cancellationToken) => + new ValueTask(VirtualNodeProperties.None); + + /// + protected override ValueTask ExistsCoreAsync(CancellationToken cancellationToken) => + new ValueTask(true); + + /// + protected override ValueTask CreateCoreAsync(CancellationToken cancellationToken) => + default; + + /// + protected override async ValueTask DeleteCoreAsync(CancellationToken cancellationToken) + { + var collection = _fs.AzureClient + .GetBlobsAsync( + prefix: GetPrefix(FullName), + traits: BlobTraits.None, + states: BlobStates.None, + cancellationToken: cancellationToken); + + var client = _fs.AzureClient.GetBlobBatchClient(); + var batch = client.CreateBatch(); + + // https://learn.microsoft.com/en-us/rest/api/storageservices/blob-batch#remarks + // Each batch request supports a maximum of 256 sub requests. + const int MaxSubRequests = 256; + + await foreach (var page in collection.AsPages().WithCancellation(cancellationToken).ConfigureAwait(false)) + { + foreach (var blob in page.Values) + { + batch.DeleteBlob(_fs.AzureClient.Name, blob.Name, DeleteSnapshotsOption.IncludeSnapshots, conditions: null); + + if (batch.RequestCount != MaxSubRequests) + continue; + + await DeleteBlobsAsync(client, batch, cancellationToken).ConfigureAwait(false); + batch = client.CreateBatch(); + } + } + + if (batch.RequestCount != 0) + await DeleteBlobsAsync(client, batch, cancellationToken).ConfigureAwait(false); + + static async ValueTask DeleteBlobsAsync(BlobBatchClient client, BlobBatch batch, CancellationToken cancellationToken) + { + try + { + await client.SubmitBatchAsync(batch, throwOnAnyFailure: true, cancellationToken).ConfigureAwait(false); + } + catch (AggregateException e) when (Processed(e.InnerExceptions)) + { + } + finally + { + batch.Dispose(); + } + + static bool Processed(ReadOnlyCollection exceptions) + { + for (int count = exceptions.Count, i = 0; i < count; i++) + if (exceptions[i] is not RequestFailedException { Status: 404 }) + return false; + + return true; + } + } + } + + /// + protected override async IAsyncEnumerable GetFileNodesCoreAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + var collection = _fs.AzureClient + .GetBlobsByHierarchyAsync( + delimiter: "/", + prefix: GetPrefix(FullName), + traits: BlobTraits.None, + states: BlobStates.None, + cancellationToken: cancellationToken); + + await foreach (var page in collection.AsPages().WithCancellation(cancellationToken).ConfigureAwait(false)) + foreach (var item in page.Values) + yield return item.Prefix is not null + ? new AzureDirectory(_fs, VirtualPath.Normalize(item.Prefix)) + : CreateVirtualFile(item.Blob); + } + + /// + protected override async IAsyncEnumerable GetFilesCoreAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + var collection = _fs.AzureClient + .GetBlobsByHierarchyAsync( + delimiter: "/", + prefix: GetPrefix(FullName), + traits: BlobTraits.None, + states: BlobStates.None, + cancellationToken: cancellationToken); + + await foreach (var page in collection.AsPages().WithCancellation(cancellationToken).ConfigureAwait(false)) + foreach (var item in page.Values) + if (item.Prefix is null) + yield return CreateVirtualFile(item.Blob); + } + + /// + protected override async IAsyncEnumerable GetDirectoriesCoreAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + var collection = _fs.AzureClient + .GetBlobsByHierarchyAsync( + delimiter: "/", + prefix: GetPrefix(FullName), + traits: BlobTraits.None, + states: BlobStates.None, + cancellationToken: cancellationToken); + + await foreach (var page in collection.AsPages().WithCancellation(cancellationToken).ConfigureAwait(false)) + foreach (var item in page.Values) + if (item.Prefix is not null) + yield return new AzureDirectory(_fs, VirtualPath.Normalize(item.Prefix)); + } + + /// + protected override async IAsyncEnumerable GetFileNodesCoreAsync(string[] patterns, string[]? excludes, [EnumeratorCancellation] CancellationToken cancellationToken) + { + // + // Cloud storage optimization strategy: + // 1. List all blobs in one batch using prefix filtering (matches our virtual directory). + // 2. Perform pattern matching and exclusion filters locally. + // + // Benefits: + // - Single API call instead of per-directory requests + // - Reduced network latency, especially for deep directory structures + // - More efficient than recursive directory scanning + // + + var prefix = GetPrefix(FullName); + var directories = new HashSet { FullName }; + + await foreach (var page in _fs.AzureClient + .GetBlobsAsync( + prefix: prefix, + traits: BlobTraits.None, + states: BlobStates.None, + cancellationToken: cancellationToken) + .AsPages() + .WithCancellation(cancellationToken) + .ConfigureAwait(false)) + { + foreach (var blob in page.Values) + { + var path = VirtualPath.Normalize(blob.Name); + var directoryPath = VirtualPath.GetDirectoryName(path); + + while (directoryPath.Length != 0 && directories.Add(directoryPath)) + { + // + // Directories are yielded in reverse order (deepest first). + // + // Note: We could use a Stack to control the order, + // but since order isn't guaranteed anyway and to avoid + // unnecessary memory allocation, we process them directly. + // + if (IsMatched(directoryPath.AsSpan(FullName.Length), patterns, excludes)) + yield return new AzureDirectory(_fs, directoryPath); + + directoryPath = VirtualPath.GetDirectoryName(directoryPath); + } + + if (IsMatched(blob.Name.AsSpan(prefix.Length), patterns, excludes)) + yield return CreateVirtualFile(blob, path); + } + } + } + + /// + protected override async IAsyncEnumerable GetFilesCoreAsync(string[] patterns, string[]? excludes, [EnumeratorCancellation] CancellationToken cancellationToken) + { + // + // Cloud storage optimization strategy: + // 1. List all blobs in one batch using prefix filtering (matches our virtual directory). + // 2. Perform pattern matching and exclusion filters locally. + // + // Benefits: + // - Single API call instead of per-directory requests + // - Reduced network latency, especially for deep directory structures + // - More efficient than recursive directory scanning + // + + var prefix = GetPrefix(FullName); + + await foreach (var page in _fs.AzureClient + .GetBlobsAsync( + prefix: prefix, + traits: BlobTraits.None, + states: BlobStates.None, + cancellationToken: cancellationToken) + .AsPages() + .WithCancellation(cancellationToken) + .ConfigureAwait(false)) + { + foreach (var blob in page.Values) + if (IsMatched(blob.Name.AsSpan(prefix.Length), patterns, excludes)) + yield return CreateVirtualFile(blob); + } + } + + /// + protected override async IAsyncEnumerable GetDirectoriesCoreAsync(string[] patterns, string[]? excludes, [EnumeratorCancellation] CancellationToken cancellationToken) + { + // + // Cloud storage optimization strategy: + // 1. List all blobs in one batch using prefix filtering (matches our virtual directory). + // 2. Perform pattern matching and exclusion filters locally. + // + // Benefits: + // - Single API call instead of per-directory requests + // - Reduced network latency, especially for deep directory structures + // - More efficient than recursive directory scanning + // + + var prefix = GetPrefix(FullName); + var directories = new HashSet { FullName }; + + await foreach (var page in _fs.AzureClient + .GetBlobsAsync( + prefix: prefix, + traits: BlobTraits.None, + states: BlobStates.None, + cancellationToken: cancellationToken) + .AsPages() + .WithCancellation(cancellationToken) + .ConfigureAwait(false)) + { + foreach (var blob in page.Values) + { + var directoryPath = VirtualPath.GetDirectoryName( + VirtualPath.Normalize(blob.Name)); + + while (directoryPath.Length != 0 && directories.Add(directoryPath)) + { + // + // Directories are yielded in reverse order (deepest first). + // + // Note: We could use a Stack to control the order, + // but since order isn't guaranteed anyway and to avoid + // unnecessary memory allocation, we process them directly. + // + if (IsMatched(directoryPath.AsSpan(FullName.Length), patterns, excludes)) + yield return new AzureDirectory(_fs, directoryPath); + + directoryPath = VirtualPath.GetDirectoryName(directoryPath); + } + } + } + } + + /// + /// Creates a instance based on the specified blob item. + /// + /// The representing the file. + /// The normalized name of the blob. + /// + /// A new instance representing the file. + /// + private AzureFile CreateVirtualFile(BlobItem blob, string? normalizedPath = null) + { + var info = blob.Properties; + var properties = VirtualNodeProperties.CreateFileProperties( + creationTime: info.CreatedOn.GetValueOrDefault(), + lastAccessTime: info.LastAccessedOn.GetValueOrDefault(), + lastWriteTime: info.LastModified.GetValueOrDefault(), + length: info.ContentLength.GetValueOrDefault(defaultValue: -1)); + + var path = normalizedPath ?? VirtualPath.Normalize(blob.Name); + return new AzureFile(_fs, path, properties); + } + + /// + /// Determines whether the specified path matches any of the inclusion patterns and none of the exclusion patterns. + /// + /// The path to match. + /// The inclusion patterns to match against the path. + /// An optional array of exclusion patterns. If the path matches any of these, + /// the method returns . + /// + /// if the path matches at least one inclusion pattern and no exclusion patterns; + /// otherwise, . + /// + private static bool IsMatched(scoped ReadOnlySpan path, string[] patterns, string[]? excludes) + { + if (excludes is not null) + foreach (var pattern in excludes) + if (Matcher.IsMatch(path, pattern, MatchFlags.Unix)) + return false; + + foreach (var pattern in patterns) + if (Matcher.IsMatch(path, pattern, MatchFlags.Unix)) + return true; + + return false; + } + + /// + /// Returns a cloud storage compatible prefix for the specified directory path. + /// + /// The directory path. + /// + /// A string formatted for cloud storage prefix filtering: + /// + /// Empty string for the root directory ("/") + /// Path without leading slash but with trailing slash for subdirectories + /// + /// + /// + /// + /// GetPrefix("/") // returns "" + /// GetPrefix("/folder") // returns "folder/" + /// GetPrefix("/sub/folder") // returns "sub/folder/" + /// + /// + private static string GetPrefix(string path) + { + Debug.Assert(VirtualPath.IsNormalized(path)); + return path == "/" ? "" : $"{path[1..]}/"; + } +} diff --git a/src/Ramstack.FileSystem.Azure/AzureFile.cs b/src/Ramstack.FileSystem.Azure/AzureFile.cs index a80a94e..bba2bb4 100644 --- a/src/Ramstack.FileSystem.Azure/AzureFile.cs +++ b/src/Ramstack.FileSystem.Azure/AzureFile.cs @@ -1,165 +1,165 @@ -using Azure; -using Azure.Storage.Blobs; -using Azure.Storage.Blobs.Models; - -namespace Ramstack.FileSystem.Azure; - -/// -/// Represents an implementation of that maps a file to an Azure Storage blob. -/// -internal sealed class AzureFile : VirtualFile -{ - private readonly AzureFileSystem _fs; - private BlobClient? _client; - - /// - public override IVirtualFileSystem FileSystem => _fs; - - /// - /// Initializes a new instance of the class. - /// - /// The file system associated with this file. - /// The path to the file. - public AzureFile(AzureFileSystem fileSystem, string path) : base(path) => - _fs = fileSystem; - - /// - /// Initializes a new instance of the class. - /// - /// The file system associated with this file. - /// The path to the file. - /// The properties of the file, if available. - public AzureFile(AzureFileSystem fileSystem, string path, VirtualNodeProperties? properties) : base(path, properties) => - _fs = fileSystem; - - /// - protected override async ValueTask GetPropertiesCoreAsync(CancellationToken cancellationToken) - { - try - { - BlobProperties info = await GetBlobClient() - .GetPropertiesAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return VirtualNodeProperties.CreateFileProperties( - creationTime: info.CreatedOn, - lastAccessTime: info.LastAccessed, - lastWriteTime: info.LastModified, - length: info.ContentLength); - } - catch (RequestFailedException e) when (e.Status == 404) - { - return null; - } - } - - /// - protected override ValueTask OpenReadCoreAsync(CancellationToken cancellationToken) - { - var task = GetBlobClient().OpenReadAsync(cancellationToken: cancellationToken); - return new ValueTask(task); - } - - /// - protected override ValueTask OpenWriteCoreAsync(CancellationToken cancellationToken) - { - var task = GetBlobClient().OpenWriteAsync(overwrite: true, cancellationToken: cancellationToken); - return new ValueTask(task); - } - - /// - protected override ValueTask WriteCoreAsync(Stream stream, bool overwrite, CancellationToken cancellationToken) - { - var options = new BlobUploadOptions(); - - if (!overwrite) - { - options.Conditions = new BlobRequestConditions - { - IfNoneMatch = new ETag("*") - }; - } - - var task = GetBlobClient().UploadAsync(stream, options, cancellationToken); - return new ValueTask(task); - } - - /// - protected override ValueTask DeleteCoreAsync(CancellationToken cancellationToken) - { - var task = GetBlobClient().DeleteIfExistsAsync( - DeleteSnapshotsOption.IncludeSnapshots, - cancellationToken: cancellationToken); - - return new ValueTask(task); - } - - /// - protected override ValueTask CopyToCoreAsync(string destinationPath, bool overwrite, CancellationToken cancellationToken) - { - var source = GetBlobClient(); - var destination = _fs.CreateBlobClient(destinationPath); - return CopyBlobAsync(source, destination, overwrite, cancellationToken); - } - - /// - protected override ValueTask CopyToCoreAsync(VirtualFile destination, bool overwrite, CancellationToken cancellationToken) - { - if (destination is AzureFile file) - return CopyBlobAsync(GetBlobClient(), file.GetBlobClient(), overwrite, cancellationToken); - - return base.CopyToCoreAsync(destination, overwrite, cancellationToken); - } - - /// - /// Asynchronously copies a source blob to the specified destination. - /// - /// The source blob client. - /// The destination blob client. - /// A boolean value indicating whether to overwrite the destination blob if it already exists. - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation. - /// - private async ValueTask CopyBlobAsync(BlobClient source, BlobClient destination, bool overwrite, CancellationToken cancellationToken) - { - if (source.Uri == destination.Uri) - throw new IOException($"Cannot copy a file '{FullName}' to itself."); - - var conditions = !overwrite - ? new BlobRequestConditions { IfNoneMatch = new ETag("*") } - : null; - - var operation = await destination - .StartCopyFromUriAsync( - source.Uri, - destinationConditions: conditions, - cancellationToken: cancellationToken) - .ConfigureAwait(false); - - await operation - .WaitForCompletionAsync( - pollingInterval: TimeSpan.FromMilliseconds(100), - cancellationToken) - .ConfigureAwait(false); - - BlobProperties properties = await destination - .GetPropertiesAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - - if (properties.CopyStatus != CopyStatus.Success) - { - var message = $"Error while copying file. {properties.CopyStatus}: {properties.CopyStatusDescription}"; - throw new InvalidOperationException(message); - } - } - - /// - /// Returns the associated with this file. - /// - /// - /// The instance used to manage this blob. - /// - private BlobClient GetBlobClient() => - _client ??= _fs.CreateBlobClient(FullName); -} +using Azure; +using Azure.Storage.Blobs; +using Azure.Storage.Blobs.Models; + +namespace Ramstack.FileSystem.Azure; + +/// +/// Represents an implementation of that maps a file to an Azure Storage blob. +/// +internal sealed class AzureFile : VirtualFile +{ + private readonly AzureFileSystem _fs; + private BlobClient? _client; + + /// + public override IVirtualFileSystem FileSystem => _fs; + + /// + /// Initializes a new instance of the class. + /// + /// The file system associated with this file. + /// The path to the file. + public AzureFile(AzureFileSystem fileSystem, string path) : base(path) => + _fs = fileSystem; + + /// + /// Initializes a new instance of the class. + /// + /// The file system associated with this file. + /// The path to the file. + /// The properties of the file, if available. + public AzureFile(AzureFileSystem fileSystem, string path, VirtualNodeProperties? properties) : base(path, properties) => + _fs = fileSystem; + + /// + protected override async ValueTask GetPropertiesCoreAsync(CancellationToken cancellationToken) + { + try + { + BlobProperties info = await GetBlobClient() + .GetPropertiesAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return VirtualNodeProperties.CreateFileProperties( + creationTime: info.CreatedOn, + lastAccessTime: info.LastAccessed, + lastWriteTime: info.LastModified, + length: info.ContentLength); + } + catch (RequestFailedException e) when (e.Status == 404) + { + return null; + } + } + + /// + protected override ValueTask OpenReadCoreAsync(CancellationToken cancellationToken) + { + var task = GetBlobClient().OpenReadAsync(cancellationToken: cancellationToken); + return new ValueTask(task); + } + + /// + protected override ValueTask OpenWriteCoreAsync(CancellationToken cancellationToken) + { + var task = GetBlobClient().OpenWriteAsync(overwrite: true, cancellationToken: cancellationToken); + return new ValueTask(task); + } + + /// + protected override ValueTask WriteCoreAsync(Stream stream, bool overwrite, CancellationToken cancellationToken) + { + var options = new BlobUploadOptions(); + + if (!overwrite) + { + options.Conditions = new BlobRequestConditions + { + IfNoneMatch = new ETag("*") + }; + } + + var task = GetBlobClient().UploadAsync(stream, options, cancellationToken); + return new ValueTask(task); + } + + /// + protected override ValueTask DeleteCoreAsync(CancellationToken cancellationToken) + { + var task = GetBlobClient().DeleteIfExistsAsync( + DeleteSnapshotsOption.IncludeSnapshots, + cancellationToken: cancellationToken); + + return new ValueTask(task); + } + + /// + protected override ValueTask CopyToCoreAsync(string destinationPath, bool overwrite, CancellationToken cancellationToken) + { + var source = GetBlobClient(); + var destination = _fs.CreateBlobClient(destinationPath); + return CopyBlobAsync(source, destination, overwrite, cancellationToken); + } + + /// + protected override ValueTask CopyToCoreAsync(VirtualFile destination, bool overwrite, CancellationToken cancellationToken) + { + if (destination is AzureFile file) + return CopyBlobAsync(GetBlobClient(), file.GetBlobClient(), overwrite, cancellationToken); + + return base.CopyToCoreAsync(destination, overwrite, cancellationToken); + } + + /// + /// Asynchronously copies a source blob to the specified destination. + /// + /// The source blob client. + /// The destination blob client. + /// A boolean value indicating whether to overwrite the destination blob if it already exists. + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation. + /// + private async ValueTask CopyBlobAsync(BlobClient source, BlobClient destination, bool overwrite, CancellationToken cancellationToken) + { + if (source.Uri == destination.Uri) + throw new IOException($"Cannot copy a file '{FullName}' to itself."); + + var conditions = !overwrite + ? new BlobRequestConditions { IfNoneMatch = new ETag("*") } + : null; + + var operation = await destination + .StartCopyFromUriAsync( + source.Uri, + destinationConditions: conditions, + cancellationToken: cancellationToken) + .ConfigureAwait(false); + + await operation + .WaitForCompletionAsync( + pollingInterval: TimeSpan.FromMilliseconds(100), + cancellationToken) + .ConfigureAwait(false); + + BlobProperties properties = await destination + .GetPropertiesAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); + + if (properties.CopyStatus != CopyStatus.Success) + { + var message = $"Error while copying file. {properties.CopyStatus}: {properties.CopyStatusDescription}"; + throw new InvalidOperationException(message); + } + } + + /// + /// Returns the associated with this file. + /// + /// + /// The instance used to manage this blob. + /// + private BlobClient GetBlobClient() => + _client ??= _fs.CreateBlobClient(FullName); +} diff --git a/src/Ramstack.FileSystem.Azure/AzureFileSystem.cs b/src/Ramstack.FileSystem.Azure/AzureFileSystem.cs index a756947..f33d96f 100644 --- a/src/Ramstack.FileSystem.Azure/AzureFileSystem.cs +++ b/src/Ramstack.FileSystem.Azure/AzureFileSystem.cs @@ -1,164 +1,164 @@ -using System.Diagnostics; - -using Azure.Core; -using Azure.Storage; -using Azure.Storage.Blobs; -using Azure.Storage.Blobs.Models; - -namespace Ramstack.FileSystem.Azure; - -/// -/// Represents a file system implementation using Azure Blob Storage. -/// -public sealed class AzureFileSystem : IVirtualFileSystem -{ - /// - /// Gets or sets a value indicating whether the file system is read-only. - /// - public bool IsReadOnly { get; init; } - - /// - /// Gets the instance. - /// - internal BlobContainerClient AzureClient { get; } - - /// - /// Initializes a new instance of the class. - /// - /// A connection string includes the authentication information required for your application - /// to access data in an Azure Storage account at runtime. - /// - /// For more information, - /// Configure Azure Storage connection strings - /// - /// The name of the blob container in the storage account to reference. - public AzureFileSystem(string connectionString, string containerName) => - AzureClient = new BlobContainerClient(connectionString, containerName); - - /// - /// Initializes a new instance of the class. - /// - /// A connection string includes the authentication information required for your application - /// to access data in an Azure Storage account at runtime. - /// - /// For more information, - /// Configure Azure Storage connection strings - /// - /// The name of the blob container in the storage account to reference. - /// Optional client options that define the transport pipeline policies - /// for authentication, retries, etc., that are applied to every request. - public AzureFileSystem(string connectionString, string containerName, BlobClientOptions? options) => - AzureClient = new BlobContainerClient(connectionString, containerName, options); - - /// - /// Initializes a new instance of the class. - /// - /// A referencing the blob container - /// that includes the name of the account and the name of the container. This is likely to be similar - /// to https://{account_name}.blob.core.windows.net/{container_name}. - /// Optional client options that define the transport pipeline policies - /// for authentication, retries, etc., that are applied to every request. - public AzureFileSystem(Uri containerUri, BlobClientOptions? options = null) => - AzureClient = new BlobContainerClient(containerUri, options); - - /// - /// Initializes a new instance of the class. - /// - /// A referencing the blob container - /// that includes the name of the account and the name of the container. This is likely to be similar - /// to https://{account_name}.blob.core.windows.net/{container_name}. - /// The shared key credential used to sign requests. - /// Optional client options that define the transport pipeline policies - /// for authentication, retries, etc., that are applied to every request. - public AzureFileSystem(Uri containerUri, StorageSharedKeyCredential credential, BlobClientOptions? options = null) => - AzureClient = new BlobContainerClient(containerUri, credential, options); - - /// - /// Initializes a new instance of the class. - /// - /// A referencing the blob container - /// that includes the name of the account and the name of the container. This is likely to be similar - /// to https://{account_name}.blob.core.windows.net/{container_name}. - /// The token credential used to sign requests. - /// Optional client options that define the transport pipeline policies - /// for authentication, retries, etc., that are applied to every request. - public AzureFileSystem(Uri containerUri, TokenCredential credential, BlobClientOptions? options = null) => - AzureClient = new BlobContainerClient(containerUri, credential, options); - - /// - /// Initializes a new instance of the class. - /// - /// The instance used to interact with the Azure Blob storage container. - public AzureFileSystem(BlobContainerClient client) => - AzureClient = client; - - /// - public VirtualDirectory GetDirectory(string path) => - new AzureDirectory(this, VirtualPath.Normalize(path)); - - /// - public VirtualFile GetFile(string path) => - new AzureFile(this, VirtualPath.Normalize(path)); - - /// - /// Asynchronously creates the container in Azure Blob Storage if it does not already exist. - /// - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation. - /// - public ValueTask CreateContainerAsync(CancellationToken cancellationToken = default) => - CreateContainerAsync(PublicAccessType.None, cancellationToken); - - /// - /// Asynchronously creates the container in Azure Blob Storage if it does not already exist. - /// - /// Specifies whether data in the container may be accessed publicly and the level of access. - /// - /// - /// - /// : Specifies full public read access for both the container and blob data. - /// Clients can enumerate blobs within the container via anonymous requests but cannot enumerate containers within the storage account. - /// - /// - /// - /// - /// : Specifies public read access for blobs only. Blob data within this container can be - /// read via anonymous requests, but container data is not available. Clients cannot enumerate blobs within the container via anonymous requests. - /// - /// - /// - /// - /// : Specifies that the container data is private to the account owner. - /// - /// - /// - /// - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation. - /// - public ValueTask CreateContainerAsync(PublicAccessType accessType, CancellationToken cancellationToken = default) - { - var task = AzureClient.CreateIfNotExistsAsync(accessType, cancellationToken: cancellationToken); - return new ValueTask(task); - } - - /// - void IDisposable.Dispose() - { - } - - /// - /// Creates a new object for the specified path. - /// - /// The path of the file. - /// - /// The object. - /// - internal BlobClient CreateBlobClient(string path) - { - Debug.Assert(VirtualPath.IsNormalized(path)); - return AzureClient.GetBlobClient(path[1..]); - } -} +using System.Diagnostics; + +using Azure.Core; +using Azure.Storage; +using Azure.Storage.Blobs; +using Azure.Storage.Blobs.Models; + +namespace Ramstack.FileSystem.Azure; + +/// +/// Represents a file system implementation using Azure Blob Storage. +/// +public sealed class AzureFileSystem : IVirtualFileSystem +{ + /// + /// Gets or sets a value indicating whether the file system is read-only. + /// + public bool IsReadOnly { get; init; } + + /// + /// Gets the instance. + /// + internal BlobContainerClient AzureClient { get; } + + /// + /// Initializes a new instance of the class. + /// + /// A connection string includes the authentication information required for your application + /// to access data in an Azure Storage account at runtime. + /// + /// For more information, + /// Configure Azure Storage connection strings + /// + /// The name of the blob container in the storage account to reference. + public AzureFileSystem(string connectionString, string containerName) => + AzureClient = new BlobContainerClient(connectionString, containerName); + + /// + /// Initializes a new instance of the class. + /// + /// A connection string includes the authentication information required for your application + /// to access data in an Azure Storage account at runtime. + /// + /// For more information, + /// Configure Azure Storage connection strings + /// + /// The name of the blob container in the storage account to reference. + /// Optional client options that define the transport pipeline policies + /// for authentication, retries, etc., that are applied to every request. + public AzureFileSystem(string connectionString, string containerName, BlobClientOptions? options) => + AzureClient = new BlobContainerClient(connectionString, containerName, options); + + /// + /// Initializes a new instance of the class. + /// + /// A referencing the blob container + /// that includes the name of the account and the name of the container. This is likely to be similar + /// to https://{account_name}.blob.core.windows.net/{container_name}. + /// Optional client options that define the transport pipeline policies + /// for authentication, retries, etc., that are applied to every request. + public AzureFileSystem(Uri containerUri, BlobClientOptions? options = null) => + AzureClient = new BlobContainerClient(containerUri, options); + + /// + /// Initializes a new instance of the class. + /// + /// A referencing the blob container + /// that includes the name of the account and the name of the container. This is likely to be similar + /// to https://{account_name}.blob.core.windows.net/{container_name}. + /// The shared key credential used to sign requests. + /// Optional client options that define the transport pipeline policies + /// for authentication, retries, etc., that are applied to every request. + public AzureFileSystem(Uri containerUri, StorageSharedKeyCredential credential, BlobClientOptions? options = null) => + AzureClient = new BlobContainerClient(containerUri, credential, options); + + /// + /// Initializes a new instance of the class. + /// + /// A referencing the blob container + /// that includes the name of the account and the name of the container. This is likely to be similar + /// to https://{account_name}.blob.core.windows.net/{container_name}. + /// The token credential used to sign requests. + /// Optional client options that define the transport pipeline policies + /// for authentication, retries, etc., that are applied to every request. + public AzureFileSystem(Uri containerUri, TokenCredential credential, BlobClientOptions? options = null) => + AzureClient = new BlobContainerClient(containerUri, credential, options); + + /// + /// Initializes a new instance of the class. + /// + /// The instance used to interact with the Azure Blob storage container. + public AzureFileSystem(BlobContainerClient client) => + AzureClient = client; + + /// + public VirtualDirectory GetDirectory(string path) => + new AzureDirectory(this, VirtualPath.Normalize(path)); + + /// + public VirtualFile GetFile(string path) => + new AzureFile(this, VirtualPath.Normalize(path)); + + /// + /// Asynchronously creates the container in Azure Blob Storage if it does not already exist. + /// + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation. + /// + public ValueTask CreateContainerAsync(CancellationToken cancellationToken = default) => + CreateContainerAsync(PublicAccessType.None, cancellationToken); + + /// + /// Asynchronously creates the container in Azure Blob Storage if it does not already exist. + /// + /// Specifies whether data in the container may be accessed publicly and the level of access. + /// + /// + /// + /// : Specifies full public read access for both the container and blob data. + /// Clients can enumerate blobs within the container via anonymous requests but cannot enumerate containers within the storage account. + /// + /// + /// + /// + /// : Specifies public read access for blobs only. Blob data within this container can be + /// read via anonymous requests, but container data is not available. Clients cannot enumerate blobs within the container via anonymous requests. + /// + /// + /// + /// + /// : Specifies that the container data is private to the account owner. + /// + /// + /// + /// + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation. + /// + public ValueTask CreateContainerAsync(PublicAccessType accessType, CancellationToken cancellationToken = default) + { + var task = AzureClient.CreateIfNotExistsAsync(accessType, cancellationToken: cancellationToken); + return new ValueTask(task); + } + + /// + void IDisposable.Dispose() + { + } + + /// + /// Creates a new object for the specified path. + /// + /// The path of the file. + /// + /// The object. + /// + internal BlobClient CreateBlobClient(string path) + { + Debug.Assert(VirtualPath.IsNormalized(path)); + return AzureClient.GetBlobClient(path[1..]); + } +} diff --git a/src/Ramstack.FileSystem.Azure/README.md b/src/Ramstack.FileSystem.Azure/README.md index 9a5301b..4e8aa7a 100644 --- a/src/Ramstack.FileSystem.Azure/README.md +++ b/src/Ramstack.FileSystem.Azure/README.md @@ -1,64 +1,64 @@ -# Ramstack.FileSystem.Azure -[![NuGet](https://img.shields.io/nuget/v/Ramstack.FileSystem.Azure.svg)](https://nuget.org/packages/Ramstack.FileSystem.Azure) -[![MIT](https://img.shields.io/github/license/rameel/ramstack.filesystem)](https://github.com/rameel/ramstack.filesystem/blob/main/LICENSE) - -Provides an implementation of `Ramstack.FileSystem` based on Azure Blob Storage. - -## Getting Started - -To install the `Ramstack.FileSystem.Azure` [NuGet package](https://www.nuget.org/packages/Ramstack.FileSystem.Azure) -in your project, run the following command: -```console -dotnet add package Ramstack.FileSystem.Azure -``` -## Usage - -```csharp -using Ramstack.FileSystem.Azure; - -string connectionString = "..."; -AzureFileSystem fs = new AzureFileSystem(connectionString, containerName: "storage"); - -// Create Azure container if it doesn't exist -await fs.CreateContainerAsync(PublicAccessType.None); - -await foreach (VirtualFile file in fs.GetFilesAsync("/")) -{ - Console.WriteLine(node.Name); -} -``` - -You can also configure the file system to be read-only: -```csharp -AzureFileSystem fs = new AzureFileSystem(connectionString, containerName: "storage") -{ - IsReadOnly = true -}; -``` - -## Related Projects -- [Ramstack.FileSystem.Abstractions](https://www.nuget.org/packages/Ramstack.FileSystem.Abstractions) - Provides a virtual file system abstraction. -- [Ramstack.FileSystem.Physical](https://www.nuget.org/packages/Ramstack.FileSystem.Physical) - Provides an implementation based on the local file system. -- [Ramstack.FileSystem.Amazon](https://www.nuget.org/packages/Ramstack.FileSystem.Amazon) - Provides an implementation using Amazon S3 storage. -- [Ramstack.FileSystem.Google](https://www.nuget.org/packages/Ramstack.FileSystem.Google) - Provides an implementation using Google Cloud Storage. -- [Ramstack.FileSystem.Readonly](https://www.nuget.org/packages/Ramstack.FileSystem.Readonly) - Provides a read-only wrapper for the underlying file system. -- [Ramstack.FileSystem.Globbing](https://www.nuget.org/packages/Ramstack.FileSystem.Globbing) - Wraps the file system, filtering files and directories using glob patterns. -- [Ramstack.FileSystem.Prefixed](https://www.nuget.org/packages/Ramstack.FileSystem.Prefixed) - Adds a prefix to file paths within the underlying file system. -- [Ramstack.FileSystem.Sub](https://www.nuget.org/packages/Ramstack.FileSystem.Sub) - Wraps the underlying file system, restricting access to a specific subpath. -- [Ramstack.FileSystem.Adapters](https://www.nuget.org/packages/Ramstack.FileSystem.Adapters) - Provides integration with `Microsoft.Extensions.FileProviders`. -- [Ramstack.FileSystem.Composite](https://www.nuget.org/packages/Ramstack.FileSystem.Composite) - Provides an implementation that combines multiple file systems into a single composite file system. - -## 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.virtualfiles/blob/main/LICENSE) file for more details. +# Ramstack.FileSystem.Azure +[![NuGet](https://img.shields.io/nuget/v/Ramstack.FileSystem.Azure.svg)](https://nuget.org/packages/Ramstack.FileSystem.Azure) +[![MIT](https://img.shields.io/github/license/rameel/ramstack.filesystem)](https://github.com/rameel/ramstack.filesystem/blob/main/LICENSE) + +Provides an implementation of `Ramstack.FileSystem` based on Azure Blob Storage. + +## Getting Started + +To install the `Ramstack.FileSystem.Azure` [NuGet package](https://www.nuget.org/packages/Ramstack.FileSystem.Azure) +in your project, run the following command: +```console +dotnet add package Ramstack.FileSystem.Azure +``` +## Usage + +```csharp +using Ramstack.FileSystem.Azure; + +string connectionString = "..."; +AzureFileSystem fs = new AzureFileSystem(connectionString, containerName: "storage"); + +// Create Azure container if it doesn't exist +await fs.CreateContainerAsync(PublicAccessType.None); + +await foreach (VirtualFile file in fs.GetFilesAsync("/")) +{ + Console.WriteLine(node.Name); +} +``` + +You can also configure the file system to be read-only: +```csharp +AzureFileSystem fs = new AzureFileSystem(connectionString, containerName: "storage") +{ + IsReadOnly = true +}; +``` + +## Related Projects +- [Ramstack.FileSystem.Abstractions](https://www.nuget.org/packages/Ramstack.FileSystem.Abstractions) - Provides a virtual file system abstraction. +- [Ramstack.FileSystem.Physical](https://www.nuget.org/packages/Ramstack.FileSystem.Physical) - Provides an implementation based on the local file system. +- [Ramstack.FileSystem.Amazon](https://www.nuget.org/packages/Ramstack.FileSystem.Amazon) - Provides an implementation using Amazon S3 storage. +- [Ramstack.FileSystem.Google](https://www.nuget.org/packages/Ramstack.FileSystem.Google) - Provides an implementation using Google Cloud Storage. +- [Ramstack.FileSystem.Readonly](https://www.nuget.org/packages/Ramstack.FileSystem.Readonly) - Provides a read-only wrapper for the underlying file system. +- [Ramstack.FileSystem.Globbing](https://www.nuget.org/packages/Ramstack.FileSystem.Globbing) - Wraps the file system, filtering files and directories using glob patterns. +- [Ramstack.FileSystem.Prefixed](https://www.nuget.org/packages/Ramstack.FileSystem.Prefixed) - Adds a prefix to file paths within the underlying file system. +- [Ramstack.FileSystem.Sub](https://www.nuget.org/packages/Ramstack.FileSystem.Sub) - Wraps the underlying file system, restricting access to a specific subpath. +- [Ramstack.FileSystem.Adapters](https://www.nuget.org/packages/Ramstack.FileSystem.Adapters) - Provides integration with `Microsoft.Extensions.FileProviders`. +- [Ramstack.FileSystem.Composite](https://www.nuget.org/packages/Ramstack.FileSystem.Composite) - Provides an implementation that combines multiple file systems into a single composite file system. + +## 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.virtualfiles/blob/main/LICENSE) file for more details. diff --git a/src/Ramstack.FileSystem.Azure/Ramstack.FileSystem.Azure.csproj b/src/Ramstack.FileSystem.Azure/Ramstack.FileSystem.Azure.csproj index a425fa8..e010189 100644 --- a/src/Ramstack.FileSystem.Azure/Ramstack.FileSystem.Azure.csproj +++ b/src/Ramstack.FileSystem.Azure/Ramstack.FileSystem.Azure.csproj @@ -1,76 +1,76 @@ - - - - net6.0;net8.0 - Provides an implementation of Ramstack.FileSystem based on Azure Blob Storage. - 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.filesystem - Rameel - filesystem;fileprovider;azure - https://github.com/rameel/ramstack.filesystem#readme - README.md - MIT - true - snupkg - true - true - true - true - - - - true - - - - - Properties\AssemblyInfo.cs - - - - - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - Properties\README.md - True - \ - - - - + + + + net6.0;net8.0 + Provides an implementation of Ramstack.FileSystem based on Azure Blob Storage. + 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.filesystem + Rameel + filesystem;fileprovider;azure + https://github.com/rameel/ramstack.filesystem#readme + README.md + MIT + true + snupkg + true + true + true + true + + + + true + + + + + Properties\AssemblyInfo.cs + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + Properties\README.md + True + \ + + + + diff --git a/src/Ramstack.FileSystem.Composite/CompositeDirectory.cs b/src/Ramstack.FileSystem.Composite/CompositeDirectory.cs index 704d918..a3bd9c3 100644 --- a/src/Ramstack.FileSystem.Composite/CompositeDirectory.cs +++ b/src/Ramstack.FileSystem.Composite/CompositeDirectory.cs @@ -1,92 +1,92 @@ -using System.Runtime.CompilerServices; - -using Ramstack.FileSystem.Null; - -namespace Ramstack.FileSystem.Composite; - -/// -/// Represents a implementation for the class. -/// -internal sealed class CompositeDirectory : VirtualDirectory -{ - private readonly CompositeFileSystem _fs; - - /// - public override IVirtualFileSystem FileSystem => _fs; - - /// - /// Initializes a new instance of the class. - /// - /// The file system associated with this directory. - /// The path of the directory. - public CompositeDirectory(CompositeFileSystem fileSystem, string path) : base(path) => - _fs = fileSystem; - - /// - protected override ValueTask GetPropertiesCoreAsync(CancellationToken cancellationToken) => - new ValueTask(VirtualNodeProperties.None); - - /// - protected override ValueTask CreateCoreAsync(CancellationToken cancellationToken) => - default; - - /// - protected override ValueTask DeleteCoreAsync(CancellationToken cancellationToken) => - default; - - /// - protected override async IAsyncEnumerable GetFileNodesCoreAsync([EnumeratorCancellation] CancellationToken cancellationToken) - { - var set = new HashSet(); - - foreach (var fs in _fs.InternalFileSystems) - { - await foreach (var node in fs.GetFileNodesAsync(FullName, cancellationToken).ConfigureAwait(false)) - { - if (node is NotFoundFile or NotFoundDirectory) - continue; - - if (set.Add(node.FullName)) - yield return node is VirtualFile file - ? new CompositeFile(_fs, node.FullName, file) - : new CompositeDirectory(_fs, node.FullName); - } - } - } - - /// - protected override async IAsyncEnumerable GetFilesCoreAsync([EnumeratorCancellation] CancellationToken cancellationToken) - { - var set = new HashSet(); - - foreach (var fs in _fs.InternalFileSystems) - { - await foreach (var file in fs.GetFilesAsync(FullName, cancellationToken).ConfigureAwait(false)) - { - if (file is NotFoundFile) - continue; - - if (set.Add(file.FullName)) - yield return new CompositeFile(_fs, file.FullName, file); - } - } - } - - /// - protected override async IAsyncEnumerable GetDirectoriesCoreAsync([EnumeratorCancellation] CancellationToken cancellationToken) - { - var set = new HashSet(); - - foreach (var fs in _fs.InternalFileSystems) - { - await foreach (var directory in fs.GetDirectoriesAsync(FullName, cancellationToken).ConfigureAwait(false)) - { - if (directory is NotFoundDirectory) - continue; - - if (set.Add(directory.FullName)) - yield return new CompositeDirectory(_fs, directory.FullName); - } - } - } -} +using System.Runtime.CompilerServices; + +using Ramstack.FileSystem.Null; + +namespace Ramstack.FileSystem.Composite; + +/// +/// Represents a implementation for the class. +/// +internal sealed class CompositeDirectory : VirtualDirectory +{ + private readonly CompositeFileSystem _fs; + + /// + public override IVirtualFileSystem FileSystem => _fs; + + /// + /// Initializes a new instance of the class. + /// + /// The file system associated with this directory. + /// The path of the directory. + public CompositeDirectory(CompositeFileSystem fileSystem, string path) : base(path) => + _fs = fileSystem; + + /// + protected override ValueTask GetPropertiesCoreAsync(CancellationToken cancellationToken) => + new ValueTask(VirtualNodeProperties.None); + + /// + protected override ValueTask CreateCoreAsync(CancellationToken cancellationToken) => + default; + + /// + protected override ValueTask DeleteCoreAsync(CancellationToken cancellationToken) => + default; + + /// + protected override async IAsyncEnumerable GetFileNodesCoreAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + var set = new HashSet(); + + foreach (var fs in _fs.InternalFileSystems) + { + await foreach (var node in fs.GetFileNodesAsync(FullName, cancellationToken).ConfigureAwait(false)) + { + if (node is NotFoundFile or NotFoundDirectory) + continue; + + if (set.Add(node.FullName)) + yield return node is VirtualFile file + ? new CompositeFile(_fs, node.FullName, file) + : new CompositeDirectory(_fs, node.FullName); + } + } + } + + /// + protected override async IAsyncEnumerable GetFilesCoreAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + var set = new HashSet(); + + foreach (var fs in _fs.InternalFileSystems) + { + await foreach (var file in fs.GetFilesAsync(FullName, cancellationToken).ConfigureAwait(false)) + { + if (file is NotFoundFile) + continue; + + if (set.Add(file.FullName)) + yield return new CompositeFile(_fs, file.FullName, file); + } + } + } + + /// + protected override async IAsyncEnumerable GetDirectoriesCoreAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + var set = new HashSet(); + + foreach (var fs in _fs.InternalFileSystems) + { + await foreach (var directory in fs.GetDirectoriesAsync(FullName, cancellationToken).ConfigureAwait(false)) + { + if (directory is NotFoundDirectory) + continue; + + if (set.Add(directory.FullName)) + yield return new CompositeDirectory(_fs, directory.FullName); + } + } + } +} diff --git a/src/Ramstack.FileSystem.Composite/CompositeFile.cs b/src/Ramstack.FileSystem.Composite/CompositeFile.cs index 55c4003..58cdd1a 100644 --- a/src/Ramstack.FileSystem.Composite/CompositeFile.cs +++ b/src/Ramstack.FileSystem.Composite/CompositeFile.cs @@ -1,82 +1,82 @@ -using Ramstack.FileSystem.Null; - -namespace Ramstack.FileSystem.Composite; - -/// -/// Represents a implementation for the class. -/// -internal sealed class CompositeFile : VirtualFile -{ - private readonly CompositeFileSystem _fs; - private VirtualFile? _file; - - /// - public override IVirtualFileSystem FileSystem => _fs; - - /// - /// Initializes a new instance of the class. - /// - /// The file system associated with this file. - /// The path of the file. - /// The instance to wrap. - public CompositeFile(CompositeFileSystem fileSystem, string path, VirtualFile? file = null) : base(path) => - (_fs, _file) = (fileSystem, file); - - /// - protected override ValueTask GetPropertiesCoreAsync(CancellationToken cancellationToken) - { - return (_file?.GetPropertiesAsync(cancellationToken) ?? GetPropertiesImplAsync(cancellationToken))!; - - async ValueTask GetPropertiesImplAsync(CancellationToken token) - { - var file = await FindFileAsync(token).ConfigureAwait(false); - return await file.GetPropertiesAsync(token).ConfigureAwait(false); - } - } - - /// - protected override ValueTask OpenReadCoreAsync(CancellationToken cancellationToken) - { - return _file?.OpenReadAsync(cancellationToken) ?? OpenStreamImplAsync(cancellationToken); - - async ValueTask OpenStreamImplAsync(CancellationToken token) - { - var file = await FindFileAsync(token).ConfigureAwait(false); - return await file.OpenReadAsync(token).ConfigureAwait(false); - } - } - - /// - protected override ValueTask OpenWriteCoreAsync(CancellationToken cancellationToken) => - default; - - /// - protected override ValueTask WriteCoreAsync(Stream stream, bool overwrite, CancellationToken cancellationToken) => - default; - - /// - protected override ValueTask DeleteCoreAsync(CancellationToken cancellationToken) => - default; - - /// - protected override void RefreshCore() => - _file = null; - - private async ValueTask FindFileAsync(CancellationToken cancellationToken) - { - if (_file is null) - { - foreach (var fs in _fs.InternalFileSystems) - { - var file = fs.GetFile(FullName); - if (file is NotFoundFile) - continue; - - if (await file.ExistsAsync(cancellationToken).ConfigureAwait(false)) - return _file = file; - } - } - - return _file ??= new NotFoundFile(FileSystem, FullName); - } -} +using Ramstack.FileSystem.Null; + +namespace Ramstack.FileSystem.Composite; + +/// +/// Represents a implementation for the class. +/// +internal sealed class CompositeFile : VirtualFile +{ + private readonly CompositeFileSystem _fs; + private VirtualFile? _file; + + /// + public override IVirtualFileSystem FileSystem => _fs; + + /// + /// Initializes a new instance of the class. + /// + /// The file system associated with this file. + /// The path of the file. + /// The instance to wrap. + public CompositeFile(CompositeFileSystem fileSystem, string path, VirtualFile? file = null) : base(path) => + (_fs, _file) = (fileSystem, file); + + /// + protected override ValueTask GetPropertiesCoreAsync(CancellationToken cancellationToken) + { + return (_file?.GetPropertiesAsync(cancellationToken) ?? GetPropertiesImplAsync(cancellationToken))!; + + async ValueTask GetPropertiesImplAsync(CancellationToken token) + { + var file = await FindFileAsync(token).ConfigureAwait(false); + return await file.GetPropertiesAsync(token).ConfigureAwait(false); + } + } + + /// + protected override ValueTask OpenReadCoreAsync(CancellationToken cancellationToken) + { + return _file?.OpenReadAsync(cancellationToken) ?? OpenStreamImplAsync(cancellationToken); + + async ValueTask OpenStreamImplAsync(CancellationToken token) + { + var file = await FindFileAsync(token).ConfigureAwait(false); + return await file.OpenReadAsync(token).ConfigureAwait(false); + } + } + + /// + protected override ValueTask OpenWriteCoreAsync(CancellationToken cancellationToken) => + default; + + /// + protected override ValueTask WriteCoreAsync(Stream stream, bool overwrite, CancellationToken cancellationToken) => + default; + + /// + protected override ValueTask DeleteCoreAsync(CancellationToken cancellationToken) => + default; + + /// + protected override void RefreshCore() => + _file = null; + + private async ValueTask FindFileAsync(CancellationToken cancellationToken) + { + if (_file is null) + { + foreach (var fs in _fs.InternalFileSystems) + { + var file = fs.GetFile(FullName); + if (file is NotFoundFile) + continue; + + if (await file.ExistsAsync(cancellationToken).ConfigureAwait(false)) + return _file = file; + } + } + + return _file ??= new NotFoundFile(FileSystem, FullName); + } +} diff --git a/src/Ramstack.FileSystem.Composite/CompositeFileSystem.Helpers.cs b/src/Ramstack.FileSystem.Composite/CompositeFileSystem.Helpers.cs index 92ba03b..800467f 100644 --- a/src/Ramstack.FileSystem.Composite/CompositeFileSystem.Helpers.cs +++ b/src/Ramstack.FileSystem.Composite/CompositeFileSystem.Helpers.cs @@ -1,83 +1,83 @@ -using Ramstack.FileSystem.Null; - -namespace Ramstack.FileSystem.Composite; - -partial class CompositeFileSystem -{ - /// - /// Tries to flatten the specified into a flat list of instances. - /// - /// - /// If the is not a , - /// the same instance of the is returned. - /// - /// The to flatten. - /// - /// An that represents the flattened version of the specified . - /// - public static IVirtualFileSystem Flatten(IVirtualFileSystem fileSystem) - { - if (fileSystem is CompositeFileSystem composite) - foreach (var fs in composite.InternalFileSystems) - if (fs is CompositeFileSystem) - return Create(composite.InternalFileSystems); - - return fileSystem; - } - - /// - /// Creates an from the specified list, flattening any nested instances. - /// - /// - /// This method returns a if more than one file system remains after flattening. - /// - /// The list of instances to compose and flatten. - /// - /// An that represents the flattened version of the specified list of instances. - /// - public static IVirtualFileSystem Create(params IVirtualFileSystem[] list) => - list.Length != 1 - ? Create(list.AsEnumerable()) - : Flatten(list[0]); - - /// - /// Creates an from the specified list, flattening any nested instances. - /// - /// - /// This method returns a if more than one file system remains after flattening. - /// - /// The list of instances to compose and flatten. - /// - /// An that represents the flattened version of the specified list of instances. - /// - public static IVirtualFileSystem Create(IEnumerable list) - { - var queue = new Queue(); - var collection = new List(); - - foreach (var fs in list) - { - queue.Enqueue(fs); - - while (queue.TryDequeue(out var current)) - { - if (current is CompositeFileSystem composite) - { - foreach (var v in composite.InternalFileSystems) - queue.Enqueue(v); - } - else if (current is not NullFileSystem) - { - collection.Add(current); - } - } - } - - return collection.Count switch - { - 0 => new NullFileSystem(), - 1 => collection[0], - _ => new CompositeFileSystem(collection.ToArray()) - }; - } -} +using Ramstack.FileSystem.Null; + +namespace Ramstack.FileSystem.Composite; + +partial class CompositeFileSystem +{ + /// + /// Tries to flatten the specified into a flat list of instances. + /// + /// + /// If the is not a , + /// the same instance of the is returned. + /// + /// The to flatten. + /// + /// An that represents the flattened version of the specified . + /// + public static IVirtualFileSystem Flatten(IVirtualFileSystem fileSystem) + { + if (fileSystem is CompositeFileSystem composite) + foreach (var fs in composite.InternalFileSystems) + if (fs is CompositeFileSystem) + return Create(composite.InternalFileSystems); + + return fileSystem; + } + + /// + /// Creates an from the specified list, flattening any nested instances. + /// + /// + /// This method returns a if more than one file system remains after flattening. + /// + /// The list of instances to compose and flatten. + /// + /// An that represents the flattened version of the specified list of instances. + /// + public static IVirtualFileSystem Create(params IVirtualFileSystem[] list) => + list.Length != 1 + ? Create(list.AsEnumerable()) + : Flatten(list[0]); + + /// + /// Creates an from the specified list, flattening any nested instances. + /// + /// + /// This method returns a if more than one file system remains after flattening. + /// + /// The list of instances to compose and flatten. + /// + /// An that represents the flattened version of the specified list of instances. + /// + public static IVirtualFileSystem Create(IEnumerable list) + { + var queue = new Queue(); + var collection = new List(); + + foreach (var fs in list) + { + queue.Enqueue(fs); + + while (queue.TryDequeue(out var current)) + { + if (current is CompositeFileSystem composite) + { + foreach (var v in composite.InternalFileSystems) + queue.Enqueue(v); + } + else if (current is not NullFileSystem) + { + collection.Add(current); + } + } + } + + return collection.Count switch + { + 0 => new NullFileSystem(), + 1 => collection[0], + _ => new CompositeFileSystem(collection.ToArray()) + }; + } +} diff --git a/src/Ramstack.FileSystem.Composite/CompositeFileSystem.cs b/src/Ramstack.FileSystem.Composite/CompositeFileSystem.cs index 1750376..eb7cb53 100644 --- a/src/Ramstack.FileSystem.Composite/CompositeFileSystem.cs +++ b/src/Ramstack.FileSystem.Composite/CompositeFileSystem.cs @@ -1,59 +1,59 @@ -namespace Ramstack.FileSystem.Composite; - -/// -/// Represents an implementation of that combines multiple file systems -/// into a single composite file system. -/// -public sealed partial class CompositeFileSystem : IVirtualFileSystem -{ - /// - /// An array of the underlying file systems. - /// - internal readonly IVirtualFileSystem[] InternalFileSystems; - - /// - public bool IsReadOnly => true; - - /// - /// Gets a read-only list of the underlying file systems. - /// - public IReadOnlyList FileSystems => InternalFileSystems; - - /// - /// Initializes a new instance of the class. - /// - /// An array of instances. - public CompositeFileSystem(params IVirtualFileSystem[] fileSystems) - { - ArgumentNullException.ThrowIfNull(fileSystems); - InternalFileSystems = fileSystems; - } - - /// - /// Initializes a new instance of the class. - /// - /// An array of instances. - public CompositeFileSystem(IEnumerable fileSystems) => - InternalFileSystems = fileSystems.ToArray(); - - /// - public VirtualFile GetFile(string path) - { - path = VirtualPath.Normalize(path); - return new CompositeFile(this, path); - } - - /// - public VirtualDirectory GetDirectory(string path) - { - path = VirtualPath.Normalize(path); - return new CompositeDirectory(this, path); - } - - /// - public void Dispose() - { - foreach (var fs in InternalFileSystems) - fs.Dispose(); - } -} +namespace Ramstack.FileSystem.Composite; + +/// +/// Represents an implementation of that combines multiple file systems +/// into a single composite file system. +/// +public sealed partial class CompositeFileSystem : IVirtualFileSystem +{ + /// + /// An array of the underlying file systems. + /// + internal readonly IVirtualFileSystem[] InternalFileSystems; + + /// + public bool IsReadOnly => true; + + /// + /// Gets a read-only list of the underlying file systems. + /// + public IReadOnlyList FileSystems => InternalFileSystems; + + /// + /// Initializes a new instance of the class. + /// + /// An array of instances. + public CompositeFileSystem(params IVirtualFileSystem[] fileSystems) + { + ArgumentNullException.ThrowIfNull(fileSystems); + InternalFileSystems = fileSystems; + } + + /// + /// Initializes a new instance of the class. + /// + /// An array of instances. + public CompositeFileSystem(IEnumerable fileSystems) => + InternalFileSystems = fileSystems.ToArray(); + + /// + public VirtualFile GetFile(string path) + { + path = VirtualPath.Normalize(path); + return new CompositeFile(this, path); + } + + /// + public VirtualDirectory GetDirectory(string path) + { + path = VirtualPath.Normalize(path); + return new CompositeDirectory(this, path); + } + + /// + public void Dispose() + { + foreach (var fs in InternalFileSystems) + fs.Dispose(); + } +} diff --git a/src/Ramstack.FileSystem.Composite/README.md b/src/Ramstack.FileSystem.Composite/README.md index ff34e94..016669d 100644 --- a/src/Ramstack.FileSystem.Composite/README.md +++ b/src/Ramstack.FileSystem.Composite/README.md @@ -1,55 +1,55 @@ -# Ramstack.FileSystem.Composite -[![NuGet](https://img.shields.io/nuget/v/Ramstack.FileSystem.Composite.svg)](https://nuget.org/packages/Ramstack.FileSystem.Composite) -[![MIT](https://img.shields.io/github/license/rameel/ramstack.filesystem)](https://github.com/rameel/ramstack.filesystem/blob/main/LICENSE) - -Provides an implementation of `Ramstack.FileSystem` that combines multiple file systems into a single composite file system. - -## Getting Started - -To install the `Ramstack.FileSystem.Composite` [NuGet package](https://www.nuget.org/packages/Ramstack.FileSystem.Composite) -in your project, run the following command: -```console -dotnet add package Ramstack.FileSystem.Composite -``` -## Usage - -```csharp -using Ramstack.FileSystem.Composite; - -CompositeFileSystem fs = new CompositeFileSystem( - new PhysicalFileSystem(@"C:\dir-1"), - new PhysicalFileSystem(@"C:\dir-2"), - new PhysicalFileSystem(@"C:\dir-3")); - -await foreach (VirtualFile file in fs.GetFilesAsync("/")) -{ - Console.WriteLine(node.Name); -} -``` - -## Related Projects -- [Ramstack.FileSystem.Abstractions](https://www.nuget.org/packages/Ramstack.FileSystem.Abstractions) - Provides a virtual file system abstraction. -- [Ramstack.FileSystem.Physical](https://www.nuget.org/packages/Ramstack.FileSystem.Physical) - Provides an implementation based on the local file system. -- [Ramstack.FileSystem.Azure](https://www.nuget.org/packages/Ramstack.FileSystem.Azure) - Provides an implementation using Azure Blob Storage. -- [Ramstack.FileSystem.Amazon](https://www.nuget.org/packages/Ramstack.FileSystem.Amazon) - Provides an implementation using Amazon S3 storage. -- [Ramstack.FileSystem.Google](https://www.nuget.org/packages/Ramstack.FileSystem.Google) - Provides an implementation using Google Cloud Storage. -- [Ramstack.FileSystem.Readonly](https://www.nuget.org/packages/Ramstack.FileSystem.Readonly) - Provides a read-only wrapper for the underlying file system. -- [Ramstack.FileSystem.Globbing](https://www.nuget.org/packages/Ramstack.FileSystem.Globbing) - Wraps the file system, filtering files and directories using glob patterns. -- [Ramstack.FileSystem.Prefixed](https://www.nuget.org/packages/Ramstack.FileSystem.Prefixed) - Adds a prefix to file paths within the underlying file system. -- [Ramstack.FileSystem.Sub](https://www.nuget.org/packages/Ramstack.FileSystem.Sub) - Wraps the underlying file system, restricting access to a specific subpath. -- [Ramstack.FileSystem.Adapters](https://www.nuget.org/packages/Ramstack.FileSystem.Adapters) - Provides integration with `Microsoft.Extensions.FileProviders`. - -## 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.virtualfiles/blob/main/LICENSE) file for more details. +# Ramstack.FileSystem.Composite +[![NuGet](https://img.shields.io/nuget/v/Ramstack.FileSystem.Composite.svg)](https://nuget.org/packages/Ramstack.FileSystem.Composite) +[![MIT](https://img.shields.io/github/license/rameel/ramstack.filesystem)](https://github.com/rameel/ramstack.filesystem/blob/main/LICENSE) + +Provides an implementation of `Ramstack.FileSystem` that combines multiple file systems into a single composite file system. + +## Getting Started + +To install the `Ramstack.FileSystem.Composite` [NuGet package](https://www.nuget.org/packages/Ramstack.FileSystem.Composite) +in your project, run the following command: +```console +dotnet add package Ramstack.FileSystem.Composite +``` +## Usage + +```csharp +using Ramstack.FileSystem.Composite; + +CompositeFileSystem fs = new CompositeFileSystem( + new PhysicalFileSystem(@"C:\dir-1"), + new PhysicalFileSystem(@"C:\dir-2"), + new PhysicalFileSystem(@"C:\dir-3")); + +await foreach (VirtualFile file in fs.GetFilesAsync("/")) +{ + Console.WriteLine(node.Name); +} +``` + +## Related Projects +- [Ramstack.FileSystem.Abstractions](https://www.nuget.org/packages/Ramstack.FileSystem.Abstractions) - Provides a virtual file system abstraction. +- [Ramstack.FileSystem.Physical](https://www.nuget.org/packages/Ramstack.FileSystem.Physical) - Provides an implementation based on the local file system. +- [Ramstack.FileSystem.Azure](https://www.nuget.org/packages/Ramstack.FileSystem.Azure) - Provides an implementation using Azure Blob Storage. +- [Ramstack.FileSystem.Amazon](https://www.nuget.org/packages/Ramstack.FileSystem.Amazon) - Provides an implementation using Amazon S3 storage. +- [Ramstack.FileSystem.Google](https://www.nuget.org/packages/Ramstack.FileSystem.Google) - Provides an implementation using Google Cloud Storage. +- [Ramstack.FileSystem.Readonly](https://www.nuget.org/packages/Ramstack.FileSystem.Readonly) - Provides a read-only wrapper for the underlying file system. +- [Ramstack.FileSystem.Globbing](https://www.nuget.org/packages/Ramstack.FileSystem.Globbing) - Wraps the file system, filtering files and directories using glob patterns. +- [Ramstack.FileSystem.Prefixed](https://www.nuget.org/packages/Ramstack.FileSystem.Prefixed) - Adds a prefix to file paths within the underlying file system. +- [Ramstack.FileSystem.Sub](https://www.nuget.org/packages/Ramstack.FileSystem.Sub) - Wraps the underlying file system, restricting access to a specific subpath. +- [Ramstack.FileSystem.Adapters](https://www.nuget.org/packages/Ramstack.FileSystem.Adapters) - Provides integration with `Microsoft.Extensions.FileProviders`. + +## 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.virtualfiles/blob/main/LICENSE) file for more details. diff --git a/src/Ramstack.FileSystem.Composite/Ramstack.FileSystem.Composite.csproj b/src/Ramstack.FileSystem.Composite/Ramstack.FileSystem.Composite.csproj index fb06cd0..7d705ba 100644 --- a/src/Ramstack.FileSystem.Composite/Ramstack.FileSystem.Composite.csproj +++ b/src/Ramstack.FileSystem.Composite/Ramstack.FileSystem.Composite.csproj @@ -1,66 +1,66 @@ - - - - net6.0;net8.0 - Provides an implementation of Ramstack.FileSystem that combines multiple file systems into a single composite file system. - 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.filesystem - Rameel - filesystem;fileprovider - https://github.com/rameel/ramstack.filesystem#readme - README.md - MIT - true - snupkg - true - true - true - true - - - - true - - - - - Properties\AssemblyInfo.cs - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - Properties\README.md - True - \ - - - - + + + + net6.0;net8.0 + Provides an implementation of Ramstack.FileSystem that combines multiple file systems into a single composite file system. + 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.filesystem + Rameel + filesystem;fileprovider + https://github.com/rameel/ramstack.filesystem#readme + README.md + MIT + true + snupkg + true + true + true + true + + + + true + + + + + Properties\AssemblyInfo.cs + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + Properties\README.md + True + \ + + + + diff --git a/src/Ramstack.FileSystem.Globbing/GlobbingDirectory.cs b/src/Ramstack.FileSystem.Globbing/GlobbingDirectory.cs index 3a0f47a..8224d77 100644 --- a/src/Ramstack.FileSystem.Globbing/GlobbingDirectory.cs +++ b/src/Ramstack.FileSystem.Globbing/GlobbingDirectory.cs @@ -1,141 +1,141 @@ -using System.Runtime.CompilerServices; - -namespace Ramstack.FileSystem.Globbing; - -/// -/// Represents a directory with globbing support for matching file patterns. -/// -internal sealed class GlobbingDirectory : VirtualDirectory -{ - private readonly GlobbingFileSystem _fs; - private readonly VirtualDirectory _directory; - private readonly bool _included; - - /// - public override IVirtualFileSystem FileSystem => _fs; - - /// - /// Initializes a new instance of the class. - /// - /// The file system associated with this directory. - /// The instance to wrap. - /// A boolean value indicating whether the directory matches the specified globbing patterns. - public GlobbingDirectory(GlobbingFileSystem fileSystem, VirtualDirectory directory, bool included) : base(directory.FullName) => - (_fs, _directory, _included) = (fileSystem, directory, included); - - /// - protected override void RefreshCore() => - _directory.Refresh(); - - /// - protected override ValueTask GetPropertiesCoreAsync(CancellationToken cancellationToken) - { - if (_included) - return _directory.GetPropertiesAsync(cancellationToken)!; - - return default; - } - - /// - protected override ValueTask ExistsCoreAsync(CancellationToken cancellationToken) - { - if (_included) - return _directory.ExistsAsync(cancellationToken); - - return default; - } - - /// - protected override ValueTask CreateCoreAsync(CancellationToken cancellationToken) => - default; - - /// - protected override ValueTask DeleteCoreAsync(CancellationToken cancellationToken) => - default; - - /// - protected override async IAsyncEnumerable GetFileNodesCoreAsync([EnumeratorCancellation] CancellationToken cancellationToken) - { - if (_included) - { - await foreach (var node in _directory.GetFileNodesAsync(cancellationToken).ConfigureAwait(false)) - { - if (node is VirtualFile file) - { - if (_fs.IsFileIncluded(file.FullName)) - yield return new GlobbingFile(_fs, file, included: true); - } - else - { - if (_fs.IsDirectoryIncluded(node.FullName)) - yield return new GlobbingDirectory(_fs, (VirtualDirectory)node, included: true); - } - } - } - } - - /// - protected override async IAsyncEnumerable GetFilesCoreAsync([EnumeratorCancellation] CancellationToken cancellationToken) - { - if (_included) - { - await foreach (var file in _directory.GetFilesAsync(cancellationToken).ConfigureAwait(false)) - if (_fs.IsFileIncluded(file.FullName)) - yield return new GlobbingFile(_fs, file, included: true); - } - } - - /// - protected override async IAsyncEnumerable GetDirectoriesCoreAsync([EnumeratorCancellation] CancellationToken cancellationToken) - { - if (_included) - { - await foreach (var directory in _directory.GetDirectoriesAsync(cancellationToken).ConfigureAwait(false)) - if (_fs.IsDirectoryIncluded(directory.FullName)) - yield return new GlobbingDirectory(_fs, directory, included: true); - } - } - - /// - protected override async IAsyncEnumerable GetFileNodesCoreAsync(string[] patterns, string[]? excludes, [EnumeratorCancellation] CancellationToken cancellationToken) - { - if (_included) - { - await foreach (var node in _directory.GetFileNodesAsync(patterns, excludes, cancellationToken).ConfigureAwait(false)) - { - if (node is VirtualFile file) - { - if (_fs.IsFileIncluded(file.FullName)) - yield return new GlobbingFile(_fs, file, included: true); - } - else - { - if (_fs.IsDirectoryIncluded(node.FullName)) - yield return new GlobbingDirectory(_fs, (VirtualDirectory)node, included: true); - } - } - } - } - - /// - protected override async IAsyncEnumerable GetFilesCoreAsync(string[] patterns, string[]? excludes, [EnumeratorCancellation] CancellationToken cancellationToken) - { - if (_included) - { - await foreach (var file in _directory.GetFilesAsync(patterns, excludes, cancellationToken).ConfigureAwait(false)) - if (_fs.IsFileIncluded(file.FullName)) - yield return new GlobbingFile(_fs, file, included: true); - } - } - - /// - protected override async IAsyncEnumerable GetDirectoriesCoreAsync(string[] patterns, string[]? excludes, [EnumeratorCancellation] CancellationToken cancellationToken) - { - if (_included) - { - await foreach (var directory in _directory.GetDirectoriesAsync(patterns, excludes, cancellationToken).ConfigureAwait(false)) - if (_fs.IsDirectoryIncluded(directory.FullName)) - yield return new GlobbingDirectory(_fs, directory, included: true); - } - } -} +using System.Runtime.CompilerServices; + +namespace Ramstack.FileSystem.Globbing; + +/// +/// Represents a directory with globbing support for matching file patterns. +/// +internal sealed class GlobbingDirectory : VirtualDirectory +{ + private readonly GlobbingFileSystem _fs; + private readonly VirtualDirectory _directory; + private readonly bool _included; + + /// + public override IVirtualFileSystem FileSystem => _fs; + + /// + /// Initializes a new instance of the class. + /// + /// The file system associated with this directory. + /// The instance to wrap. + /// A boolean value indicating whether the directory matches the specified globbing patterns. + public GlobbingDirectory(GlobbingFileSystem fileSystem, VirtualDirectory directory, bool included) : base(directory.FullName) => + (_fs, _directory, _included) = (fileSystem, directory, included); + + /// + protected override void RefreshCore() => + _directory.Refresh(); + + /// + protected override ValueTask GetPropertiesCoreAsync(CancellationToken cancellationToken) + { + if (_included) + return _directory.GetPropertiesAsync(cancellationToken)!; + + return default; + } + + /// + protected override ValueTask ExistsCoreAsync(CancellationToken cancellationToken) + { + if (_included) + return _directory.ExistsAsync(cancellationToken); + + return default; + } + + /// + protected override ValueTask CreateCoreAsync(CancellationToken cancellationToken) => + default; + + /// + protected override ValueTask DeleteCoreAsync(CancellationToken cancellationToken) => + default; + + /// + protected override async IAsyncEnumerable GetFileNodesCoreAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + if (_included) + { + await foreach (var node in _directory.GetFileNodesAsync(cancellationToken).ConfigureAwait(false)) + { + if (node is VirtualFile file) + { + if (_fs.IsFileIncluded(file.FullName)) + yield return new GlobbingFile(_fs, file, included: true); + } + else + { + if (_fs.IsDirectoryIncluded(node.FullName)) + yield return new GlobbingDirectory(_fs, (VirtualDirectory)node, included: true); + } + } + } + } + + /// + protected override async IAsyncEnumerable GetFilesCoreAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + if (_included) + { + await foreach (var file in _directory.GetFilesAsync(cancellationToken).ConfigureAwait(false)) + if (_fs.IsFileIncluded(file.FullName)) + yield return new GlobbingFile(_fs, file, included: true); + } + } + + /// + protected override async IAsyncEnumerable GetDirectoriesCoreAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + if (_included) + { + await foreach (var directory in _directory.GetDirectoriesAsync(cancellationToken).ConfigureAwait(false)) + if (_fs.IsDirectoryIncluded(directory.FullName)) + yield return new GlobbingDirectory(_fs, directory, included: true); + } + } + + /// + protected override async IAsyncEnumerable GetFileNodesCoreAsync(string[] patterns, string[]? excludes, [EnumeratorCancellation] CancellationToken cancellationToken) + { + if (_included) + { + await foreach (var node in _directory.GetFileNodesAsync(patterns, excludes, cancellationToken).ConfigureAwait(false)) + { + if (node is VirtualFile file) + { + if (_fs.IsFileIncluded(file.FullName)) + yield return new GlobbingFile(_fs, file, included: true); + } + else + { + if (_fs.IsDirectoryIncluded(node.FullName)) + yield return new GlobbingDirectory(_fs, (VirtualDirectory)node, included: true); + } + } + } + } + + /// + protected override async IAsyncEnumerable GetFilesCoreAsync(string[] patterns, string[]? excludes, [EnumeratorCancellation] CancellationToken cancellationToken) + { + if (_included) + { + await foreach (var file in _directory.GetFilesAsync(patterns, excludes, cancellationToken).ConfigureAwait(false)) + if (_fs.IsFileIncluded(file.FullName)) + yield return new GlobbingFile(_fs, file, included: true); + } + } + + /// + protected override async IAsyncEnumerable GetDirectoriesCoreAsync(string[] patterns, string[]? excludes, [EnumeratorCancellation] CancellationToken cancellationToken) + { + if (_included) + { + await foreach (var directory in _directory.GetDirectoriesAsync(patterns, excludes, cancellationToken).ConfigureAwait(false)) + if (_fs.IsDirectoryIncluded(directory.FullName)) + yield return new GlobbingDirectory(_fs, directory, included: true); + } + } +} diff --git a/src/Ramstack.FileSystem.Globbing/GlobbingFile.cs b/src/Ramstack.FileSystem.Globbing/GlobbingFile.cs index 63cb8d4..a71bd6a 100644 --- a/src/Ramstack.FileSystem.Globbing/GlobbingFile.cs +++ b/src/Ramstack.FileSystem.Globbing/GlobbingFile.cs @@ -1,72 +1,72 @@ -using System.Diagnostics.CodeAnalysis; - -namespace Ramstack.FileSystem.Globbing; - -/// -/// Represents a file with globbing support for matching file patterns. -/// -internal sealed class GlobbingFile : VirtualFile -{ - private readonly GlobbingFileSystem _fs; - private readonly VirtualFile _file; - private readonly bool _included; - - /// - public override IVirtualFileSystem FileSystem => _fs; - - /// - /// Initializes a new instance of the class. - /// - /// The file system associated with this file. - /// The instance to wrap. - /// A boolean value indicating whether the file matches the specified globbing patterns. - public GlobbingFile(GlobbingFileSystem fileSystem, VirtualFile file, bool included) : base(file.FullName) => - (_fs, _file, _included) = (fileSystem, file, included); - - /// - protected override void RefreshCore() => - _file.Refresh(); - - /// - protected override ValueTask GetPropertiesCoreAsync(CancellationToken cancellationToken) - { - if (_included) - return _file.GetPropertiesAsync(cancellationToken)!; - - return default; - } - - /// - protected override ValueTask ExistsCoreAsync(CancellationToken cancellationToken) - { - if (_included) - return _file.ExistsAsync(cancellationToken); - - return default; - } - - /// - protected override ValueTask OpenReadCoreAsync(CancellationToken cancellationToken) - { - if (!_included) - Error_FileNotFound(FullName); - - return _file.OpenReadAsync(cancellationToken); - } - - /// - protected override ValueTask OpenWriteCoreAsync(CancellationToken cancellationToken) => - default; - - /// - protected override ValueTask WriteCoreAsync(Stream stream, bool overwrite, CancellationToken cancellationToken) => - default; - - /// - protected override ValueTask DeleteCoreAsync(CancellationToken cancellationToken) => - default; - - [DoesNotReturn] - private static void Error_FileNotFound(string path) => - throw new FileNotFoundException($"Unable to find file '{path}'.", path); -} +using System.Diagnostics.CodeAnalysis; + +namespace Ramstack.FileSystem.Globbing; + +/// +/// Represents a file with globbing support for matching file patterns. +/// +internal sealed class GlobbingFile : VirtualFile +{ + private readonly GlobbingFileSystem _fs; + private readonly VirtualFile _file; + private readonly bool _included; + + /// + public override IVirtualFileSystem FileSystem => _fs; + + /// + /// Initializes a new instance of the class. + /// + /// The file system associated with this file. + /// The instance to wrap. + /// A boolean value indicating whether the file matches the specified globbing patterns. + public GlobbingFile(GlobbingFileSystem fileSystem, VirtualFile file, bool included) : base(file.FullName) => + (_fs, _file, _included) = (fileSystem, file, included); + + /// + protected override void RefreshCore() => + _file.Refresh(); + + /// + protected override ValueTask GetPropertiesCoreAsync(CancellationToken cancellationToken) + { + if (_included) + return _file.GetPropertiesAsync(cancellationToken)!; + + return default; + } + + /// + protected override ValueTask ExistsCoreAsync(CancellationToken cancellationToken) + { + if (_included) + return _file.ExistsAsync(cancellationToken); + + return default; + } + + /// + protected override ValueTask OpenReadCoreAsync(CancellationToken cancellationToken) + { + if (!_included) + Error_FileNotFound(FullName); + + return _file.OpenReadAsync(cancellationToken); + } + + /// + protected override ValueTask OpenWriteCoreAsync(CancellationToken cancellationToken) => + default; + + /// + protected override ValueTask WriteCoreAsync(Stream stream, bool overwrite, CancellationToken cancellationToken) => + default; + + /// + protected override ValueTask DeleteCoreAsync(CancellationToken cancellationToken) => + default; + + [DoesNotReturn] + private static void Error_FileNotFound(string path) => + throw new FileNotFoundException($"Unable to find file '{path}'.", path); +} diff --git a/src/Ramstack.FileSystem.Globbing/GlobbingFileSystem.cs b/src/Ramstack.FileSystem.Globbing/GlobbingFileSystem.cs index ced4273..cfbe0c9 100644 --- a/src/Ramstack.FileSystem.Globbing/GlobbingFileSystem.cs +++ b/src/Ramstack.FileSystem.Globbing/GlobbingFileSystem.cs @@ -1,107 +1,107 @@ -using Ramstack.FileSystem.Globbing.Internal; - -namespace Ramstack.FileSystem.Globbing; - -/// -/// Provides a file system implementation that filters files and directories of the underlying file system using glob patterns. -/// -/// -/// The class wraps around another and applies glob-based -/// filtering rules to determine which files and directories to include or exclude. This allows for flexible and powerful -/// file and directory selection using standard glob patterns. -/// -/// -/// -/// var underlying = new PhysicalFileSystem(@"C:\MyDirectory"); -/// var fs = new GlobbingFileSystem(underlying, patterns: ["**/*.txt", "docs/*.md"], excludes: ["**/README.md"]); -/// await foreach (var file in fs.GetFilesAsync("/")) -/// Console.WriteLine(file.FullName); -/// -/// -public sealed class GlobbingFileSystem : IVirtualFileSystem -{ - private readonly IVirtualFileSystem _fs; - private readonly string[] _patterns; - private readonly string[] _excludes; - - /// - public bool IsReadOnly => true; - - /// - /// Initializes a new instance of the class with a single include pattern and an optional exclude pattern. - /// - /// The underlying file system. - /// The pattern to include in the enumeration. - /// The optional pattern to exclude from the enumeration. - public GlobbingFileSystem(IVirtualFileSystem fileSystem, string pattern, string? exclude = null) - { - ArgumentNullException.ThrowIfNull(fileSystem); - ArgumentNullException.ThrowIfNull(pattern); - - _fs = fileSystem; - _patterns = [pattern]; - _excludes = exclude is not null ? [exclude] : []; - } - - /// - /// Initializes a new instance of the class with multiple include patterns and optional exclude patterns. - /// - /// The underlying file system. - /// The patterns to include in the enumeration. - /// The optional patterns to exclude from the enumeration. - public GlobbingFileSystem(IVirtualFileSystem fileSystem, string[] patterns, string[]? excludes = null) - { - ArgumentNullException.ThrowIfNull(fileSystem); - ArgumentNullException.ThrowIfNull(patterns); - - _fs = fileSystem; - _patterns = [..patterns]; - _excludes = [..excludes ?? []]; - } - - /// - public VirtualFile GetFile(string path) - { - path = VirtualPath.Normalize(path); - - var file = _fs.GetFile(path); - var fileIncluded = IsFileIncluded(path); - return new GlobbingFile(this, file, fileIncluded); - } - - /// - public VirtualDirectory GetDirectory(string path) - { - path = VirtualPath.Normalize(path); - - var directory = _fs.GetDirectory(path); - var directoryIncluded = IsDirectoryIncluded(path); - return new GlobbingDirectory(this, directory, directoryIncluded); - } - - /// - public void Dispose() => - _fs.Dispose(); - - /// - /// Determines if a file is included based on the specified patterns and exclusions. - /// - /// The path of the file. - /// - /// if the file is included; - /// otherwise, . - /// - internal 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, . - /// - internal bool IsDirectoryIncluded(string path) => - path == "/" || (!PathHelper.IsMatch(path, _excludes) && PathHelper.IsPartialMatch(path, _patterns)); -} +using Ramstack.FileSystem.Globbing.Internal; + +namespace Ramstack.FileSystem.Globbing; + +/// +/// Provides a file system implementation that filters files and directories of the underlying file system using glob patterns. +/// +/// +/// The class wraps around another and applies glob-based +/// filtering rules to determine which files and directories to include or exclude. This allows for flexible and powerful +/// file and directory selection using standard glob patterns. +/// +/// +/// +/// var underlying = new PhysicalFileSystem(@"C:\MyDirectory"); +/// var fs = new GlobbingFileSystem(underlying, patterns: ["**/*.txt", "docs/*.md"], excludes: ["**/README.md"]); +/// await foreach (var file in fs.GetFilesAsync("/")) +/// Console.WriteLine(file.FullName); +/// +/// +public sealed class GlobbingFileSystem : IVirtualFileSystem +{ + private readonly IVirtualFileSystem _fs; + private readonly string[] _patterns; + private readonly string[] _excludes; + + /// + public bool IsReadOnly => true; + + /// + /// Initializes a new instance of the class with a single include pattern and an optional exclude pattern. + /// + /// The underlying file system. + /// The pattern to include in the enumeration. + /// The optional pattern to exclude from the enumeration. + public GlobbingFileSystem(IVirtualFileSystem fileSystem, string pattern, string? exclude = null) + { + ArgumentNullException.ThrowIfNull(fileSystem); + ArgumentNullException.ThrowIfNull(pattern); + + _fs = fileSystem; + _patterns = [pattern]; + _excludes = exclude is not null ? [exclude] : []; + } + + /// + /// Initializes a new instance of the class with multiple include patterns and optional exclude patterns. + /// + /// The underlying file system. + /// The patterns to include in the enumeration. + /// The optional patterns to exclude from the enumeration. + public GlobbingFileSystem(IVirtualFileSystem fileSystem, string[] patterns, string[]? excludes = null) + { + ArgumentNullException.ThrowIfNull(fileSystem); + ArgumentNullException.ThrowIfNull(patterns); + + _fs = fileSystem; + _patterns = [..patterns]; + _excludes = [..excludes ?? []]; + } + + /// + public VirtualFile GetFile(string path) + { + path = VirtualPath.Normalize(path); + + var file = _fs.GetFile(path); + var fileIncluded = IsFileIncluded(path); + return new GlobbingFile(this, file, fileIncluded); + } + + /// + public VirtualDirectory GetDirectory(string path) + { + path = VirtualPath.Normalize(path); + + var directory = _fs.GetDirectory(path); + var directoryIncluded = IsDirectoryIncluded(path); + return new GlobbingDirectory(this, directory, directoryIncluded); + } + + /// + public void Dispose() => + _fs.Dispose(); + + /// + /// Determines if a file is included based on the specified patterns and exclusions. + /// + /// The path of the file. + /// + /// if the file is included; + /// otherwise, . + /// + internal 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, . + /// + internal bool IsDirectoryIncluded(string path) => + path == "/" || (!PathHelper.IsMatch(path, _excludes) && PathHelper.IsPartialMatch(path, _patterns)); +} diff --git a/src/Ramstack.FileSystem.Globbing/Internal/PathHelper.cs b/src/Ramstack.FileSystem.Globbing/Internal/PathHelper.cs index e9ba214..8ba1c4d 100644 --- a/src/Ramstack.FileSystem.Globbing/Internal/PathHelper.cs +++ b/src/Ramstack.FileSystem.Globbing/Internal/PathHelper.cs @@ -1,343 +1,343 @@ -using System.Diagnostics; -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.FileSystem.Globbing.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 test 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. - /// - public 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. - /// - public 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((ushort)'/'); - 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((ushort)'/'); - 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((ushort)'/'); - 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.Diagnostics; +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.FileSystem.Globbing.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 test 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. + /// + public 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. + /// + public 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((ushort)'/'); + 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((ushort)'/'); + 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((ushort)'/'); + 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.FileSystem.Globbing/README.md b/src/Ramstack.FileSystem.Globbing/README.md index f3f62e1..fd06c30 100644 --- a/src/Ramstack.FileSystem.Globbing/README.md +++ b/src/Ramstack.FileSystem.Globbing/README.md @@ -1,60 +1,60 @@ -# Ramstack.FileSystem.Globbing -[![NuGet](https://img.shields.io/nuget/v/Ramstack.FileSystem.Globbing.svg)](https://nuget.org/packages/Ramstack.FileSystem.Globbing) -[![MIT](https://img.shields.io/github/license/rameel/ramstack.filesystem)](https://github.com/rameel/ramstack.filesystem/blob/main/LICENSE) - -Provides an implementation of `Ramstack.FileSystem` that applies glob-based filtering rules to determine -which files and directories of the underlying file system to include or exclude. - -It relies on the [Ramstack.Globbing](https://www.nuget.org/packages/Ramstack.Globbing) package for its globbing capabilities. - -## Getting Started - -To install the `Ramstack.FileSystem.Globbing` [NuGet package](https://www.nuget.org/packages/Ramstack.FileSystem.Globbing) -in your project, run the following command: -```console -dotnet add package Ramstack.FileSystem.Globbing -``` -## Usage - -```csharp -using Ramstack.FileSystem.Globbing; - -PhysicalFileSystem fileSystem = new PhysicalFileSystem(@"C:\path"); - -// Include only "*.txt" files and "*.md" files in the "/docs" directory, excluding any "README.md" file -GlobbingFileSystem fs = new GlobbingFileSystem(fileSystem, - patterns: ["**/*.txt", "docs/*.md"], - excludes: ["**/README.md"]); - -await foreach (VirtualFile file in fs.GetFilesAsync("/")) -{ - Console.WriteLine(file.FullName); -} -``` - -## Related Projects -- [Ramstack.FileSystem.Abstractions](https://www.nuget.org/packages/Ramstack.FileSystem.Abstractions) - Provides a virtual file system abstraction. -- [Ramstack.FileSystem.Physical](https://www.nuget.org/packages/Ramstack.FileSystem.Physical) - Provides an implementation based on the local file system. -- [Ramstack.FileSystem.Azure](https://www.nuget.org/packages/Ramstack.FileSystem.Azure) - Provides an implementation using Azure Blob Storage. -- [Ramstack.FileSystem.Amazon](https://www.nuget.org/packages/Ramstack.FileSystem.Amazon) - Provides an implementation using Amazon S3 storage. -- [Ramstack.FileSystem.Google](https://www.nuget.org/packages/Ramstack.FileSystem.Google) - Provides an implementation using Google Cloud Storage. -- [Ramstack.FileSystem.Readonly](https://www.nuget.org/packages/Ramstack.FileSystem.Readonly) - Provides a read-only wrapper for the underlying file system. -- [Ramstack.FileSystem.Prefixed](https://www.nuget.org/packages/Ramstack.FileSystem.Prefixed) - Adds a prefix to file paths within the underlying file system. -- [Ramstack.FileSystem.Sub](https://www.nuget.org/packages/Ramstack.FileSystem.Sub) - Wraps the underlying file system, restricting access to a specific subpath. -- [Ramstack.FileSystem.Adapters](https://www.nuget.org/packages/Ramstack.FileSystem.Adapters) - Provides integration with `Microsoft.Extensions.FileProviders`. -- [Ramstack.FileSystem.Composite](https://www.nuget.org/packages/Ramstack.FileSystem.Composite) - Provides an implementation that combines multiple file systems into a single composite file system. - -## 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.virtualfiles/blob/main/LICENSE) file for more details. +# Ramstack.FileSystem.Globbing +[![NuGet](https://img.shields.io/nuget/v/Ramstack.FileSystem.Globbing.svg)](https://nuget.org/packages/Ramstack.FileSystem.Globbing) +[![MIT](https://img.shields.io/github/license/rameel/ramstack.filesystem)](https://github.com/rameel/ramstack.filesystem/blob/main/LICENSE) + +Provides an implementation of `Ramstack.FileSystem` that applies glob-based filtering rules to determine +which files and directories of the underlying file system to include or exclude. + +It relies on the [Ramstack.Globbing](https://www.nuget.org/packages/Ramstack.Globbing) package for its globbing capabilities. + +## Getting Started + +To install the `Ramstack.FileSystem.Globbing` [NuGet package](https://www.nuget.org/packages/Ramstack.FileSystem.Globbing) +in your project, run the following command: +```console +dotnet add package Ramstack.FileSystem.Globbing +``` +## Usage + +```csharp +using Ramstack.FileSystem.Globbing; + +PhysicalFileSystem fileSystem = new PhysicalFileSystem(@"C:\path"); + +// Include only "*.txt" files and "*.md" files in the "/docs" directory, excluding any "README.md" file +GlobbingFileSystem fs = new GlobbingFileSystem(fileSystem, + patterns: ["**/*.txt", "docs/*.md"], + excludes: ["**/README.md"]); + +await foreach (VirtualFile file in fs.GetFilesAsync("/")) +{ + Console.WriteLine(file.FullName); +} +``` + +## Related Projects +- [Ramstack.FileSystem.Abstractions](https://www.nuget.org/packages/Ramstack.FileSystem.Abstractions) - Provides a virtual file system abstraction. +- [Ramstack.FileSystem.Physical](https://www.nuget.org/packages/Ramstack.FileSystem.Physical) - Provides an implementation based on the local file system. +- [Ramstack.FileSystem.Azure](https://www.nuget.org/packages/Ramstack.FileSystem.Azure) - Provides an implementation using Azure Blob Storage. +- [Ramstack.FileSystem.Amazon](https://www.nuget.org/packages/Ramstack.FileSystem.Amazon) - Provides an implementation using Amazon S3 storage. +- [Ramstack.FileSystem.Google](https://www.nuget.org/packages/Ramstack.FileSystem.Google) - Provides an implementation using Google Cloud Storage. +- [Ramstack.FileSystem.Readonly](https://www.nuget.org/packages/Ramstack.FileSystem.Readonly) - Provides a read-only wrapper for the underlying file system. +- [Ramstack.FileSystem.Prefixed](https://www.nuget.org/packages/Ramstack.FileSystem.Prefixed) - Adds a prefix to file paths within the underlying file system. +- [Ramstack.FileSystem.Sub](https://www.nuget.org/packages/Ramstack.FileSystem.Sub) - Wraps the underlying file system, restricting access to a specific subpath. +- [Ramstack.FileSystem.Adapters](https://www.nuget.org/packages/Ramstack.FileSystem.Adapters) - Provides integration with `Microsoft.Extensions.FileProviders`. +- [Ramstack.FileSystem.Composite](https://www.nuget.org/packages/Ramstack.FileSystem.Composite) - Provides an implementation that combines multiple file systems into a single composite file system. + +## 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.virtualfiles/blob/main/LICENSE) file for more details. diff --git a/src/Ramstack.FileSystem.Globbing/Ramstack.FileSystem.Globbing.csproj b/src/Ramstack.FileSystem.Globbing/Ramstack.FileSystem.Globbing.csproj index e771eb5..c52f674 100644 --- a/src/Ramstack.FileSystem.Globbing/Ramstack.FileSystem.Globbing.csproj +++ b/src/Ramstack.FileSystem.Globbing/Ramstack.FileSystem.Globbing.csproj @@ -1,67 +1,67 @@ - - - - net6.0;net8.0 - Provides an implementation of Ramstack.FileSystem that applies glob-based filtering rules to the underlying file system. - 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.filesystem - Rameel - filesystem;fileprovider - https://github.com/rameel/ramstack.filesystem#readme - README.md - MIT - true - snupkg - true - true - true - true - - - - true - - - - - Properties\AssemblyInfo.cs - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - Properties\README.md - True - \ - - - - + + + + net6.0;net8.0 + Provides an implementation of Ramstack.FileSystem that applies glob-based filtering rules to the underlying file system. + 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.filesystem + Rameel + filesystem;fileprovider + https://github.com/rameel/ramstack.filesystem#readme + README.md + MIT + true + snupkg + true + true + true + true + + + + true + + + + + Properties\AssemblyInfo.cs + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + Properties\README.md + True + \ + + + + diff --git a/src/Ramstack.FileSystem.Google/GcsDirectory.cs b/src/Ramstack.FileSystem.Google/GcsDirectory.cs index a66c20f..3283caa 100644 --- a/src/Ramstack.FileSystem.Google/GcsDirectory.cs +++ b/src/Ramstack.FileSystem.Google/GcsDirectory.cs @@ -1,315 +1,315 @@ -using System.Diagnostics; -using System.Net; -using System.Runtime.CompilerServices; - -using Google; -using Google.Cloud.Storage.V1; - -using Ramstack.Globbing; - -using GcsObject = Google.Apis.Storage.v1.Data.Object; - -namespace Ramstack.FileSystem.Google; - -/// -/// Represents an implementation of that maps a directory -/// to the path within a Google Cloud Storage bucket. -/// -internal sealed class GcsDirectory : VirtualDirectory -{ - private readonly GoogleFileSystem _fs; - - /// - public override IVirtualFileSystem FileSystem => _fs; - - /// - /// Initializes a new instance of the class. - /// - /// The file system associated with this directory. - /// The path to the directory within the Google Cloud Storage bucket. - public GcsDirectory(GoogleFileSystem fileSystem, string path) : base(path) => - _fs = fileSystem; - - /// - protected override ValueTask GetPropertiesCoreAsync(CancellationToken cancellationToken) => - new ValueTask(VirtualNodeProperties.None); - - /// - protected override ValueTask ExistsCoreAsync(CancellationToken cancellationToken) => - new ValueTask(true); - - /// - protected override ValueTask CreateCoreAsync(CancellationToken cancellationToken) => - default; - - /// - protected override async ValueTask DeleteCoreAsync(CancellationToken cancellationToken) - { - await foreach (var obj in _fs.StorageClient.ListObjectsAsync(_fs.BucketName, GetPrefix(FullName)).WithCancellation(cancellationToken).ConfigureAwait(false)) - { - try - { - await _fs.StorageClient - .DeleteObjectAsync(_fs.BucketName, obj.Name, cancellationToken: cancellationToken) - .ConfigureAwait(false); - } - catch (GoogleApiException e) when (e.HttpStatusCode == HttpStatusCode.NotFound) - { - } - } - } - - /// - protected override async IAsyncEnumerable GetFileNodesCoreAsync([EnumeratorCancellation] CancellationToken cancellationToken) - { - var options = new ListObjectsOptions - { - Delimiter = "/", - IncludeFoldersAsPrefixes = true, - IncludeTrailingDelimiter = true - }; - - var responses = _fs.StorageClient - .ListObjectsAsync(_fs.BucketName, GetPrefix(FullName), options) - .AsRawResponses(); - - await foreach (var page in responses.WithCancellation(cancellationToken).ConfigureAwait(false)) - { - if (page.Prefixes is not null) - foreach (var prefix in page.Prefixes) - yield return new GcsDirectory(_fs, VirtualPath.Normalize(prefix)); - - if (page.Items is null) - continue; - - foreach (var obj in page.Items) - if (!obj.Name.EndsWith('/')) - yield return CreateVirtualFile(obj); - } - } - - /// - protected override async IAsyncEnumerable GetFilesCoreAsync([EnumeratorCancellation] CancellationToken cancellationToken) - { - var options = new ListObjectsOptions - { - Delimiter = "/", - IncludeFoldersAsPrefixes = true, - IncludeTrailingDelimiter = true - }; - - var response = _fs.StorageClient.ListObjectsAsync(_fs.BucketName, GetPrefix(FullName), options); - - await foreach (var obj in response.WithCancellation(cancellationToken).ConfigureAwait(false)) - if (!obj.Name.EndsWith('/')) - yield return CreateVirtualFile(obj); - } - - /// - protected override async IAsyncEnumerable GetDirectoriesCoreAsync([EnumeratorCancellation] CancellationToken cancellationToken) - { - var options = new ListObjectsOptions - { - Delimiter = "/", - IncludeFoldersAsPrefixes = true, - IncludeTrailingDelimiter = true - }; - - var response = _fs.StorageClient - .ListObjectsAsync(_fs.BucketName, GetPrefix(FullName), options) - .AsRawResponses(); - - await foreach (var page in response.WithCancellation(cancellationToken).ConfigureAwait(false)) - { - if (page.Prefixes is null) - continue; - - foreach (var prefix in page.Prefixes) - yield return new GcsDirectory(_fs, VirtualPath.Normalize(prefix)); - } - } - - /// - protected override async IAsyncEnumerable GetFileNodesCoreAsync(string[] patterns, string[]? excludes, [EnumeratorCancellation] CancellationToken cancellationToken) - { - // - // Cloud storage optimization strategy: - // 1. List all objects in one batch using prefix filtering (matches our virtual directory). - // 2. Perform pattern matching and exclusion filters locally. - // - // Benefits: - // - Single API call instead of per-directory requests - // - Reduced network latency, especially for deep directory structures - // - More efficient than recursive directory scanning - // - - var prefix = GetPrefix(FullName); - var directories = new HashSet { FullName }; - var response = _fs.StorageClient.ListObjectsAsync(_fs.BucketName, prefix); - - await foreach (var obj in response.WithCancellation(cancellationToken).ConfigureAwait(false)) - { - var path = VirtualPath.Normalize(obj.Name); - var directoryPath = obj.Name.EndsWith('/') ? path : VirtualPath.GetDirectoryName(path); - - while (directoryPath.Length != 0 && directories.Add(directoryPath)) - { - // - // Directories are yielded in reverse order (deepest first). - // - // Note: We could use a Stack to control the order, - // but since order isn't guaranteed anyway and to avoid - // unnecessary memory allocation, we process them directly. - // - if (IsMatched(directoryPath.AsSpan(FullName.Length), patterns, excludes)) - yield return new GcsDirectory(_fs, VirtualPath.Normalize(directoryPath)); - - directoryPath = VirtualPath.GetDirectoryName(directoryPath); - } - - if (!obj.Name.EndsWith('/')) - if (IsMatched(obj.Name.AsSpan(prefix.Length), patterns, excludes)) - yield return CreateVirtualFile(obj, path); - } - } - - /// - protected override async IAsyncEnumerable GetFilesCoreAsync(string[] patterns, string[]? excludes, [EnumeratorCancellation] CancellationToken cancellationToken) - { - // - // Cloud storage optimization strategy: - // 1. List all objects in one batch using prefix filtering (matches our virtual directory). - // 2. Perform pattern matching and exclusion filters locally. - // - // Benefits: - // - Single API call instead of per-directory requests - // - Reduced network latency, especially for deep directory structures - // - More efficient than recursive directory scanning - // - - var prefix = GetPrefix(FullName); - - // - // Google Cloud Storage supports native glob matching in ListObjects, - // but we can't use this feature due to some limitation: - // GCS's MatchGlob doesn't support brace expansion in the middle of paths. - // For example, patterns like "project/{images,styles}/*.{svg,css}" won't work. - // - - var response = _fs.StorageClient.ListObjectsAsync(_fs.BucketName, prefix); - - await foreach (var obj in response.WithCancellation(cancellationToken).ConfigureAwait(false)) - if (!obj.Name.EndsWith('/')) - if (IsMatched(obj.Name.AsSpan(prefix.Length), patterns, excludes)) - yield return CreateVirtualFile(obj); - } - - /// - protected override async IAsyncEnumerable GetDirectoriesCoreAsync(string[] patterns, string[]? excludes, [EnumeratorCancellation] CancellationToken cancellationToken) - { - // - // Cloud storage optimization strategy: - // 1. List all objects in one batch using prefix filtering (matches our virtual directory). - // 2. Perform pattern matching and exclusion filters locally. - // - // Benefits: - // - Single API call instead of per-directory requests - // - Reduced network latency, especially for deep directory structures - // - More efficient than recursive directory scanning - // - - var response = _fs.StorageClient.ListObjectsAsync(_fs.BucketName, GetPrefix(FullName)); - var directories = new HashSet { FullName }; - - await foreach (var obj in response.WithCancellation(cancellationToken).ConfigureAwait(false)) - { - var path = VirtualPath.Normalize(obj.Name); - var directoryPath = obj.Name.EndsWith('/') ? path : VirtualPath.GetDirectoryName(path); - - while (directoryPath.Length != 0 && directories.Add(directoryPath)) - { - // - // Directories are yielded in reverse order (deepest first). - // - // Note: We could use a Stack to control the order, - // but since order isn't guaranteed anyway and to avoid - // unnecessary memory allocation, we process them directly. - // - if (IsMatched(directoryPath.AsSpan(FullName.Length), patterns, excludes)) - yield return new GcsDirectory(_fs, VirtualPath.Normalize(directoryPath)); - - directoryPath = VirtualPath.GetDirectoryName(directoryPath); - } - } - } - - /// - /// Creates a instance based on the specified object item. - /// - /// The representing the file. - /// The normalized name of the object. - /// - /// A new instance representing the file. - /// - private GcsFile CreateVirtualFile(GcsObject obj, string? normalizedPath = null) - { - var properties = VirtualNodeProperties.CreateFileProperties( - obj.TimeCreatedDateTimeOffset.GetValueOrDefault(), - DateTimeOffset.MinValue, - obj.UpdatedDateTimeOffset.GetValueOrDefault(), - (long)(obj.Size ?? 0)); - - var path = normalizedPath ?? VirtualPath.Normalize(obj.Name); - return new GcsFile(_fs, path, properties); - } - - /// - /// Determines whether the specified path matches any of the inclusion patterns and none of the exclusion patterns. - /// - /// The path to match. - /// The inclusion patterns to match against the path. - /// An optional array of exclusion patterns. If the path matches any of these, - /// the method returns . - /// - /// if the path matches at least one inclusion pattern and no exclusion patterns; - /// otherwise, . - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsMatched(scoped ReadOnlySpan path, string[] patterns, string[]? excludes) - { - if (excludes is not null) - foreach (var pattern in excludes) - if (Matcher.IsMatch(path, pattern, MatchFlags.Unix)) - return false; - - foreach (var pattern in patterns) - if (Matcher.IsMatch(path, pattern, MatchFlags.Unix)) - return true; - - return false; - } - - /// - /// Returns a cloud storage compatible prefix for the specified directory path. - /// - /// The directory path. - /// - /// A string formatted for cloud storage prefix filtering: - /// - /// Empty string for the root directory ("/") - /// Path without leading slash but with trailing slash for subdirectories - /// - /// - /// - /// - /// GetPrefix("/") // returns "" - /// GetPrefix("/folder") // returns "folder/" - /// GetPrefix("/sub/folder") // returns "sub/folder/" - /// - /// - private static string GetPrefix(string path) - { - Debug.Assert(VirtualPath.IsNormalized(path)); - return path == "/" ? "" : $"{path[1..]}/"; - } -} +using System.Diagnostics; +using System.Net; +using System.Runtime.CompilerServices; + +using Google; +using Google.Cloud.Storage.V1; + +using Ramstack.Globbing; + +using GcsObject = Google.Apis.Storage.v1.Data.Object; + +namespace Ramstack.FileSystem.Google; + +/// +/// Represents an implementation of that maps a directory +/// to the path within a Google Cloud Storage bucket. +/// +internal sealed class GcsDirectory : VirtualDirectory +{ + private readonly GoogleFileSystem _fs; + + /// + public override IVirtualFileSystem FileSystem => _fs; + + /// + /// Initializes a new instance of the class. + /// + /// The file system associated with this directory. + /// The path to the directory within the Google Cloud Storage bucket. + public GcsDirectory(GoogleFileSystem fileSystem, string path) : base(path) => + _fs = fileSystem; + + /// + protected override ValueTask GetPropertiesCoreAsync(CancellationToken cancellationToken) => + new ValueTask(VirtualNodeProperties.None); + + /// + protected override ValueTask ExistsCoreAsync(CancellationToken cancellationToken) => + new ValueTask(true); + + /// + protected override ValueTask CreateCoreAsync(CancellationToken cancellationToken) => + default; + + /// + protected override async ValueTask DeleteCoreAsync(CancellationToken cancellationToken) + { + await foreach (var obj in _fs.StorageClient.ListObjectsAsync(_fs.BucketName, GetPrefix(FullName)).WithCancellation(cancellationToken).ConfigureAwait(false)) + { + try + { + await _fs.StorageClient + .DeleteObjectAsync(_fs.BucketName, obj.Name, cancellationToken: cancellationToken) + .ConfigureAwait(false); + } + catch (GoogleApiException e) when (e.HttpStatusCode == HttpStatusCode.NotFound) + { + } + } + } + + /// + protected override async IAsyncEnumerable GetFileNodesCoreAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + var options = new ListObjectsOptions + { + Delimiter = "/", + IncludeFoldersAsPrefixes = true, + IncludeTrailingDelimiter = true + }; + + var responses = _fs.StorageClient + .ListObjectsAsync(_fs.BucketName, GetPrefix(FullName), options) + .AsRawResponses(); + + await foreach (var page in responses.WithCancellation(cancellationToken).ConfigureAwait(false)) + { + if (page.Prefixes is not null) + foreach (var prefix in page.Prefixes) + yield return new GcsDirectory(_fs, VirtualPath.Normalize(prefix)); + + if (page.Items is null) + continue; + + foreach (var obj in page.Items) + if (!obj.Name.EndsWith('/')) + yield return CreateVirtualFile(obj); + } + } + + /// + protected override async IAsyncEnumerable GetFilesCoreAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + var options = new ListObjectsOptions + { + Delimiter = "/", + IncludeFoldersAsPrefixes = true, + IncludeTrailingDelimiter = true + }; + + var response = _fs.StorageClient.ListObjectsAsync(_fs.BucketName, GetPrefix(FullName), options); + + await foreach (var obj in response.WithCancellation(cancellationToken).ConfigureAwait(false)) + if (!obj.Name.EndsWith('/')) + yield return CreateVirtualFile(obj); + } + + /// + protected override async IAsyncEnumerable GetDirectoriesCoreAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + var options = new ListObjectsOptions + { + Delimiter = "/", + IncludeFoldersAsPrefixes = true, + IncludeTrailingDelimiter = true + }; + + var response = _fs.StorageClient + .ListObjectsAsync(_fs.BucketName, GetPrefix(FullName), options) + .AsRawResponses(); + + await foreach (var page in response.WithCancellation(cancellationToken).ConfigureAwait(false)) + { + if (page.Prefixes is null) + continue; + + foreach (var prefix in page.Prefixes) + yield return new GcsDirectory(_fs, VirtualPath.Normalize(prefix)); + } + } + + /// + protected override async IAsyncEnumerable GetFileNodesCoreAsync(string[] patterns, string[]? excludes, [EnumeratorCancellation] CancellationToken cancellationToken) + { + // + // Cloud storage optimization strategy: + // 1. List all objects in one batch using prefix filtering (matches our virtual directory). + // 2. Perform pattern matching and exclusion filters locally. + // + // Benefits: + // - Single API call instead of per-directory requests + // - Reduced network latency, especially for deep directory structures + // - More efficient than recursive directory scanning + // + + var prefix = GetPrefix(FullName); + var directories = new HashSet { FullName }; + var response = _fs.StorageClient.ListObjectsAsync(_fs.BucketName, prefix); + + await foreach (var obj in response.WithCancellation(cancellationToken).ConfigureAwait(false)) + { + var path = VirtualPath.Normalize(obj.Name); + var directoryPath = obj.Name.EndsWith('/') ? path : VirtualPath.GetDirectoryName(path); + + while (directoryPath.Length != 0 && directories.Add(directoryPath)) + { + // + // Directories are yielded in reverse order (deepest first). + // + // Note: We could use a Stack to control the order, + // but since order isn't guaranteed anyway and to avoid + // unnecessary memory allocation, we process them directly. + // + if (IsMatched(directoryPath.AsSpan(FullName.Length), patterns, excludes)) + yield return new GcsDirectory(_fs, VirtualPath.Normalize(directoryPath)); + + directoryPath = VirtualPath.GetDirectoryName(directoryPath); + } + + if (!obj.Name.EndsWith('/')) + if (IsMatched(obj.Name.AsSpan(prefix.Length), patterns, excludes)) + yield return CreateVirtualFile(obj, path); + } + } + + /// + protected override async IAsyncEnumerable GetFilesCoreAsync(string[] patterns, string[]? excludes, [EnumeratorCancellation] CancellationToken cancellationToken) + { + // + // Cloud storage optimization strategy: + // 1. List all objects in one batch using prefix filtering (matches our virtual directory). + // 2. Perform pattern matching and exclusion filters locally. + // + // Benefits: + // - Single API call instead of per-directory requests + // - Reduced network latency, especially for deep directory structures + // - More efficient than recursive directory scanning + // + + var prefix = GetPrefix(FullName); + + // + // Google Cloud Storage supports native glob matching in ListObjects, + // but we can't use this feature due to some limitation: + // GCS's MatchGlob doesn't support brace expansion in the middle of paths. + // For example, patterns like "project/{images,styles}/*.{svg,css}" won't work. + // + + var response = _fs.StorageClient.ListObjectsAsync(_fs.BucketName, prefix); + + await foreach (var obj in response.WithCancellation(cancellationToken).ConfigureAwait(false)) + if (!obj.Name.EndsWith('/')) + if (IsMatched(obj.Name.AsSpan(prefix.Length), patterns, excludes)) + yield return CreateVirtualFile(obj); + } + + /// + protected override async IAsyncEnumerable GetDirectoriesCoreAsync(string[] patterns, string[]? excludes, [EnumeratorCancellation] CancellationToken cancellationToken) + { + // + // Cloud storage optimization strategy: + // 1. List all objects in one batch using prefix filtering (matches our virtual directory). + // 2. Perform pattern matching and exclusion filters locally. + // + // Benefits: + // - Single API call instead of per-directory requests + // - Reduced network latency, especially for deep directory structures + // - More efficient than recursive directory scanning + // + + var response = _fs.StorageClient.ListObjectsAsync(_fs.BucketName, GetPrefix(FullName)); + var directories = new HashSet { FullName }; + + await foreach (var obj in response.WithCancellation(cancellationToken).ConfigureAwait(false)) + { + var path = VirtualPath.Normalize(obj.Name); + var directoryPath = obj.Name.EndsWith('/') ? path : VirtualPath.GetDirectoryName(path); + + while (directoryPath.Length != 0 && directories.Add(directoryPath)) + { + // + // Directories are yielded in reverse order (deepest first). + // + // Note: We could use a Stack to control the order, + // but since order isn't guaranteed anyway and to avoid + // unnecessary memory allocation, we process them directly. + // + if (IsMatched(directoryPath.AsSpan(FullName.Length), patterns, excludes)) + yield return new GcsDirectory(_fs, VirtualPath.Normalize(directoryPath)); + + directoryPath = VirtualPath.GetDirectoryName(directoryPath); + } + } + } + + /// + /// Creates a instance based on the specified object item. + /// + /// The representing the file. + /// The normalized name of the object. + /// + /// A new instance representing the file. + /// + private GcsFile CreateVirtualFile(GcsObject obj, string? normalizedPath = null) + { + var properties = VirtualNodeProperties.CreateFileProperties( + obj.TimeCreatedDateTimeOffset.GetValueOrDefault(), + DateTimeOffset.MinValue, + obj.UpdatedDateTimeOffset.GetValueOrDefault(), + (long)(obj.Size ?? 0)); + + var path = normalizedPath ?? VirtualPath.Normalize(obj.Name); + return new GcsFile(_fs, path, properties); + } + + /// + /// Determines whether the specified path matches any of the inclusion patterns and none of the exclusion patterns. + /// + /// The path to match. + /// The inclusion patterns to match against the path. + /// An optional array of exclusion patterns. If the path matches any of these, + /// the method returns . + /// + /// if the path matches at least one inclusion pattern and no exclusion patterns; + /// otherwise, . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsMatched(scoped ReadOnlySpan path, string[] patterns, string[]? excludes) + { + if (excludes is not null) + foreach (var pattern in excludes) + if (Matcher.IsMatch(path, pattern, MatchFlags.Unix)) + return false; + + foreach (var pattern in patterns) + if (Matcher.IsMatch(path, pattern, MatchFlags.Unix)) + return true; + + return false; + } + + /// + /// Returns a cloud storage compatible prefix for the specified directory path. + /// + /// The directory path. + /// + /// A string formatted for cloud storage prefix filtering: + /// + /// Empty string for the root directory ("/") + /// Path without leading slash but with trailing slash for subdirectories + /// + /// + /// + /// + /// GetPrefix("/") // returns "" + /// GetPrefix("/folder") // returns "folder/" + /// GetPrefix("/sub/folder") // returns "sub/folder/" + /// + /// + private static string GetPrefix(string path) + { + Debug.Assert(VirtualPath.IsNormalized(path)); + return path == "/" ? "" : $"{path[1..]}/"; + } +} diff --git a/src/Ramstack.FileSystem.Google/GcsFile.cs b/src/Ramstack.FileSystem.Google/GcsFile.cs index 66113e9..420234d 100644 --- a/src/Ramstack.FileSystem.Google/GcsFile.cs +++ b/src/Ramstack.FileSystem.Google/GcsFile.cs @@ -1,162 +1,162 @@ -using System.Net; - -using Google; -using Google.Cloud.Storage.V1; - -namespace Ramstack.FileSystem.Google; - -/// -/// Represents an implementation of that maps a file to a Google Cloud Storage object. -/// -internal sealed class GcsFile : VirtualFile -{ - private readonly GoogleFileSystem _fs; - private string? _objectName; - - /// - public override IVirtualFileSystem FileSystem => _fs; - - /// - /// Initializes a new instance of the class. - /// - /// The file system associated with this file. - /// The path to the file. - public GcsFile(GoogleFileSystem fileSystem, string path) : base(path) => - _fs = fileSystem; - - /// - /// Initializes a new instance of the class. - /// - /// The file system associated with this file. - /// The path to the file. - /// The properties of the file, if available. - public GcsFile(GoogleFileSystem fileSystem, string path, VirtualNodeProperties? properties) : base(path, properties) => - _fs = fileSystem; - - /// - protected override async ValueTask GetPropertiesCoreAsync(CancellationToken cancellationToken) - { - try - { - var obj = await _fs.StorageClient - .GetObjectAsync(_fs.BucketName, GetObjectName(), cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return VirtualNodeProperties.CreateFileProperties( - creationTime: obj.TimeCreatedDateTimeOffset.GetValueOrDefault(), - lastAccessTime: default, - lastWriteTime: obj.UpdatedDateTimeOffset.GetValueOrDefault(), - length: (long?)obj.Size ?? 0); - } - catch (GoogleApiException e) when (e.HttpStatusCode == HttpStatusCode.NotFound) - { - return null; - } - } - - /// - protected override async ValueTask OpenReadCoreAsync(CancellationToken cancellationToken) - { - var stream = CreateTempFileStream(); - await _fs.StorageClient - .DownloadObjectAsync(_fs.BucketName, GetObjectName(), stream, cancellationToken: cancellationToken) - .ConfigureAwait(false); - - stream.Position = 0; - return stream; - - static FileStream CreateTempFileStream() - { - const int BufferSize = 4096; - const FileOptions Options = FileOptions.DeleteOnClose | FileOptions.Asynchronous; - - var path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); - return new FileStream(path, FileMode.Create, FileAccess.ReadWrite, FileShare.Read, BufferSize, Options); - } - } - - /// - protected override ValueTask OpenWriteCoreAsync(CancellationToken cancellationToken) => - new ValueTask(new GcsWriteStream(_fs, GetObjectName())); - - /// - protected override async ValueTask WriteCoreAsync(Stream stream, bool overwrite, CancellationToken cancellationToken) - { - var obj = new global::Google.Apis.Storage.v1.Data.Object - { - Bucket = _fs.BucketName, - Name = GetObjectName() - }; - - var options = new UploadObjectOptions(); - if (!overwrite) - options.IfGenerationMatch = 0; - - await _fs.StorageClient - .UploadObjectAsync(obj, stream, options, cancellationToken) - .ConfigureAwait(false); - } - - /// - protected override async ValueTask DeleteCoreAsync(CancellationToken cancellationToken) - { - try - { - await _fs.StorageClient - .DeleteObjectAsync(_fs.BucketName, GetObjectName(), cancellationToken: cancellationToken) - .ConfigureAwait(false); - } - catch (GoogleApiException e) when (e.HttpStatusCode == HttpStatusCode.NotFound) - { - // Object doesn't exist, which is fine for this operation - } - } - - /// - protected override ValueTask CopyToCoreAsync(string destinationPath, bool overwrite, CancellationToken cancellationToken) => - CopyObjectAsync(_fs.BucketName, GetObjectName(), _fs.BucketName, destinationPath[1..], overwrite, cancellationToken); - - /// - protected override ValueTask CopyToCoreAsync(VirtualFile destination, bool overwrite, CancellationToken cancellationToken) - { - if (destination is GcsFile file) - return CopyObjectAsync(_fs.BucketName, GetObjectName(), file._fs.BucketName, file.GetObjectName(), overwrite, cancellationToken); - - return base.CopyToCoreAsync(destination, overwrite, cancellationToken); - } - - /// - /// Returns the object name associated with this file. - /// - /// - /// The object name used in Google Cloud Storage. - /// - internal string GetObjectName() => - _objectName ??= FullName[1..]; - - /// - /// Asynchronously copies a source object to the specified destination. - /// - /// The name of the source GCS bucket. - /// The source object name. - /// The name of the destination GCS bucket. - /// The destination object name. - /// A boolean value indicating whether to overwrite the destination object if it already exists. - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation. - /// - private async ValueTask CopyObjectAsync(string sourceBucket, string sourceObjectName, string destinationBucket, string destinationObjectName, bool overwrite, CancellationToken cancellationToken) - { - if (sourceBucket == destinationBucket && sourceObjectName == destinationObjectName) - throw new IOException($"Cannot copy a file '{FullName}' to itself."); - - var options = new CopyObjectOptions(); - if (!overwrite) - options.IfGenerationMatch = 0; - - await _fs.StorageClient - .CopyObjectAsync(sourceBucket, sourceObjectName, destinationBucket, destinationObjectName, options, cancellationToken) - .ConfigureAwait(false); - } -} +using System.Net; + +using Google; +using Google.Cloud.Storage.V1; + +namespace Ramstack.FileSystem.Google; + +/// +/// Represents an implementation of that maps a file to a Google Cloud Storage object. +/// +internal sealed class GcsFile : VirtualFile +{ + private readonly GoogleFileSystem _fs; + private string? _objectName; + + /// + public override IVirtualFileSystem FileSystem => _fs; + + /// + /// Initializes a new instance of the class. + /// + /// The file system associated with this file. + /// The path to the file. + public GcsFile(GoogleFileSystem fileSystem, string path) : base(path) => + _fs = fileSystem; + + /// + /// Initializes a new instance of the class. + /// + /// The file system associated with this file. + /// The path to the file. + /// The properties of the file, if available. + public GcsFile(GoogleFileSystem fileSystem, string path, VirtualNodeProperties? properties) : base(path, properties) => + _fs = fileSystem; + + /// + protected override async ValueTask GetPropertiesCoreAsync(CancellationToken cancellationToken) + { + try + { + var obj = await _fs.StorageClient + .GetObjectAsync(_fs.BucketName, GetObjectName(), cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return VirtualNodeProperties.CreateFileProperties( + creationTime: obj.TimeCreatedDateTimeOffset.GetValueOrDefault(), + lastAccessTime: default, + lastWriteTime: obj.UpdatedDateTimeOffset.GetValueOrDefault(), + length: (long?)obj.Size ?? 0); + } + catch (GoogleApiException e) when (e.HttpStatusCode == HttpStatusCode.NotFound) + { + return null; + } + } + + /// + protected override async ValueTask OpenReadCoreAsync(CancellationToken cancellationToken) + { + var stream = CreateTempFileStream(); + await _fs.StorageClient + .DownloadObjectAsync(_fs.BucketName, GetObjectName(), stream, cancellationToken: cancellationToken) + .ConfigureAwait(false); + + stream.Position = 0; + return stream; + + static FileStream CreateTempFileStream() + { + const int BufferSize = 4096; + const FileOptions Options = FileOptions.DeleteOnClose | FileOptions.Asynchronous; + + var path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + return new FileStream(path, FileMode.Create, FileAccess.ReadWrite, FileShare.Read, BufferSize, Options); + } + } + + /// + protected override ValueTask OpenWriteCoreAsync(CancellationToken cancellationToken) => + new ValueTask(new GcsWriteStream(_fs, GetObjectName())); + + /// + protected override async ValueTask WriteCoreAsync(Stream stream, bool overwrite, CancellationToken cancellationToken) + { + var obj = new global::Google.Apis.Storage.v1.Data.Object + { + Bucket = _fs.BucketName, + Name = GetObjectName() + }; + + var options = new UploadObjectOptions(); + if (!overwrite) + options.IfGenerationMatch = 0; + + await _fs.StorageClient + .UploadObjectAsync(obj, stream, options, cancellationToken) + .ConfigureAwait(false); + } + + /// + protected override async ValueTask DeleteCoreAsync(CancellationToken cancellationToken) + { + try + { + await _fs.StorageClient + .DeleteObjectAsync(_fs.BucketName, GetObjectName(), cancellationToken: cancellationToken) + .ConfigureAwait(false); + } + catch (GoogleApiException e) when (e.HttpStatusCode == HttpStatusCode.NotFound) + { + // Object doesn't exist, which is fine for this operation + } + } + + /// + protected override ValueTask CopyToCoreAsync(string destinationPath, bool overwrite, CancellationToken cancellationToken) => + CopyObjectAsync(_fs.BucketName, GetObjectName(), _fs.BucketName, destinationPath[1..], overwrite, cancellationToken); + + /// + protected override ValueTask CopyToCoreAsync(VirtualFile destination, bool overwrite, CancellationToken cancellationToken) + { + if (destination is GcsFile file) + return CopyObjectAsync(_fs.BucketName, GetObjectName(), file._fs.BucketName, file.GetObjectName(), overwrite, cancellationToken); + + return base.CopyToCoreAsync(destination, overwrite, cancellationToken); + } + + /// + /// Returns the object name associated with this file. + /// + /// + /// The object name used in Google Cloud Storage. + /// + internal string GetObjectName() => + _objectName ??= FullName[1..]; + + /// + /// Asynchronously copies a source object to the specified destination. + /// + /// The name of the source GCS bucket. + /// The source object name. + /// The name of the destination GCS bucket. + /// The destination object name. + /// A boolean value indicating whether to overwrite the destination object if it already exists. + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation. + /// + private async ValueTask CopyObjectAsync(string sourceBucket, string sourceObjectName, string destinationBucket, string destinationObjectName, bool overwrite, CancellationToken cancellationToken) + { + if (sourceBucket == destinationBucket && sourceObjectName == destinationObjectName) + throw new IOException($"Cannot copy a file '{FullName}' to itself."); + + var options = new CopyObjectOptions(); + if (!overwrite) + options.IfGenerationMatch = 0; + + await _fs.StorageClient + .CopyObjectAsync(sourceBucket, sourceObjectName, destinationBucket, destinationObjectName, options, cancellationToken) + .ConfigureAwait(false); + } +} diff --git a/src/Ramstack.FileSystem.Google/GcsWriteStream.cs b/src/Ramstack.FileSystem.Google/GcsWriteStream.cs index 9a6fa15..71755a2 100644 --- a/src/Ramstack.FileSystem.Google/GcsWriteStream.cs +++ b/src/Ramstack.FileSystem.Google/GcsWriteStream.cs @@ -1,190 +1,190 @@ -using System.Diagnostics.CodeAnalysis; - -namespace Ramstack.FileSystem.Google; - -/// -/// Represents a temporary write-only stream that buffers data to a temporary file before uploading it to Google Cloud Storage. -/// Data is committed to the storage bucket when the stream is disposed or closed. -/// -internal sealed class GcsWriteStream : Stream -{ - private readonly GoogleFileSystem _fs; - private readonly string _objectName; - private readonly FileStream _stream; - private bool _disposed; - - /// - public override bool CanRead => false; - - /// - public override bool CanSeek => false; - - /// - public override bool CanWrite => true; - - /// - public override long Length - { - get - { - Error_NotSupported(); - return 0; - } - } - - /// - public override long Position - { - get - { - Error_NotSupported(); - return 0; - } - // ReSharper disable once ValueParameterNotUsed - set => Error_NotSupported(); - } - - /// - /// Initializes a new instance of the class. - /// - /// The instance. - /// The name of the object. - public GcsWriteStream(GoogleFileSystem fs, string objectName) - { - _fs = fs; - _objectName = objectName; - _stream = new FileStream( - Path.Combine( - Path.GetTempPath(), - Path.GetRandomFileName()), - FileMode.CreateNew, - FileAccess.ReadWrite, - FileShare.None, - bufferSize: 4096, - FileOptions.DeleteOnClose - | FileOptions.Asynchronous); - } - - /// - public override int Read(byte[] array, int offset, int count) - { - Error_NotSupported(); - return 0; - } - - /// - public override int Read(Span buffer) - { - Error_NotSupported(); - return 0; - } - - /// - public override void Write(byte[] buffer, int offset, int count) => - Write(buffer.AsSpan(offset, count)); - - /// - public override void Write(ReadOnlySpan buffer) - { - try - { - _stream.Write(buffer); - } - catch - { - _disposed = true; - _stream.Close(); - throw; - } - } - - /// - public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => - WriteAsync(buffer.AsMemory(offset, count), cancellationToken).AsTask(); - - /// - public override async ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) - { - try - { - await _stream.WriteAsync(buffer, cancellationToken).ConfigureAwait(false); - } - catch - { - _disposed = true; - _stream.Close(); - throw; - } - } - - /// - public override long Seek(long offset, SeekOrigin origin) - { - Error_NotSupported(); - return 0; - } - - /// - public override void SetLength(long value) => - Error_NotSupported(); - - /// - public override void Flush() - { - } - - /// - public override Task FlushAsync(CancellationToken cancellationToken) => - Task.CompletedTask; - - /// - protected override void Dispose(bool disposing) - { - if (!_disposed && disposing) - { - try - { - _stream.Position = 0; - - var destination = new global::Google.Apis.Storage.v1.Data.Object { Bucket = _fs.BucketName, Name = _objectName }; - _fs.StorageClient.UploadObject(destination, _stream); - } - finally - { - _disposed = true; - _stream.Close(); - - base.Dispose(disposing); - } - } - } - - /// - public override async ValueTask DisposeAsync() - { - if (!_disposed) - { - try - { - _stream.Position = 0; - - var destination = new global::Google.Apis.Storage.v1.Data.Object { Bucket = _fs.BucketName, Name = _objectName }; - - await _fs.StorageClient - .UploadObjectAsync(destination, _stream) - .ConfigureAwait(false); - } - finally - { - _disposed = true; - - await _stream.DisposeAsync().ConfigureAwait(false); - await base.DisposeAsync().ConfigureAwait(false); - } - } - } - - [DoesNotReturn] - private static void Error_NotSupported() => - throw new NotSupportedException(); -} +using System.Diagnostics.CodeAnalysis; + +namespace Ramstack.FileSystem.Google; + +/// +/// Represents a temporary write-only stream that buffers data to a temporary file before uploading it to Google Cloud Storage. +/// Data is committed to the storage bucket when the stream is disposed or closed. +/// +internal sealed class GcsWriteStream : Stream +{ + private readonly GoogleFileSystem _fs; + private readonly string _objectName; + private readonly FileStream _stream; + private bool _disposed; + + /// + public override bool CanRead => false; + + /// + public override bool CanSeek => false; + + /// + public override bool CanWrite => true; + + /// + public override long Length + { + get + { + Error_NotSupported(); + return 0; + } + } + + /// + public override long Position + { + get + { + Error_NotSupported(); + return 0; + } + // ReSharper disable once ValueParameterNotUsed + set => Error_NotSupported(); + } + + /// + /// Initializes a new instance of the class. + /// + /// The instance. + /// The name of the object. + public GcsWriteStream(GoogleFileSystem fs, string objectName) + { + _fs = fs; + _objectName = objectName; + _stream = new FileStream( + Path.Combine( + Path.GetTempPath(), + Path.GetRandomFileName()), + FileMode.CreateNew, + FileAccess.ReadWrite, + FileShare.None, + bufferSize: 4096, + FileOptions.DeleteOnClose + | FileOptions.Asynchronous); + } + + /// + public override int Read(byte[] array, int offset, int count) + { + Error_NotSupported(); + return 0; + } + + /// + public override int Read(Span buffer) + { + Error_NotSupported(); + return 0; + } + + /// + public override void Write(byte[] buffer, int offset, int count) => + Write(buffer.AsSpan(offset, count)); + + /// + public override void Write(ReadOnlySpan buffer) + { + try + { + _stream.Write(buffer); + } + catch + { + _disposed = true; + _stream.Close(); + throw; + } + } + + /// + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => + WriteAsync(buffer.AsMemory(offset, count), cancellationToken).AsTask(); + + /// + public override async ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + { + try + { + await _stream.WriteAsync(buffer, cancellationToken).ConfigureAwait(false); + } + catch + { + _disposed = true; + _stream.Close(); + throw; + } + } + + /// + public override long Seek(long offset, SeekOrigin origin) + { + Error_NotSupported(); + return 0; + } + + /// + public override void SetLength(long value) => + Error_NotSupported(); + + /// + public override void Flush() + { + } + + /// + public override Task FlushAsync(CancellationToken cancellationToken) => + Task.CompletedTask; + + /// + protected override void Dispose(bool disposing) + { + if (!_disposed && disposing) + { + try + { + _stream.Position = 0; + + var destination = new global::Google.Apis.Storage.v1.Data.Object { Bucket = _fs.BucketName, Name = _objectName }; + _fs.StorageClient.UploadObject(destination, _stream); + } + finally + { + _disposed = true; + _stream.Close(); + + base.Dispose(disposing); + } + } + } + + /// + public override async ValueTask DisposeAsync() + { + if (!_disposed) + { + try + { + _stream.Position = 0; + + var destination = new global::Google.Apis.Storage.v1.Data.Object { Bucket = _fs.BucketName, Name = _objectName }; + + await _fs.StorageClient + .UploadObjectAsync(destination, _stream) + .ConfigureAwait(false); + } + finally + { + _disposed = true; + + await _stream.DisposeAsync().ConfigureAwait(false); + await base.DisposeAsync().ConfigureAwait(false); + } + } + } + + [DoesNotReturn] + private static void Error_NotSupported() => + throw new NotSupportedException(); +} diff --git a/src/Ramstack.FileSystem.Google/GoogleFileSystem.cs b/src/Ramstack.FileSystem.Google/GoogleFileSystem.cs index bb69257..96a437f 100644 --- a/src/Ramstack.FileSystem.Google/GoogleFileSystem.cs +++ b/src/Ramstack.FileSystem.Google/GoogleFileSystem.cs @@ -1,85 +1,85 @@ -using Google; -using Google.Cloud.Storage.V1; -using Google.Apis.Auth.OAuth2; - -namespace Ramstack.FileSystem.Google; - -/// -/// Represents a file system implementation using Google Cloud Storage. -/// -public sealed class GoogleFileSystem : IVirtualFileSystem -{ - /// - /// Gets the instance. - /// - internal StorageClient StorageClient { get; } - - /// - /// Gets the bucket name. - /// - internal string BucketName { get; } - - /// - /// Gets or sets a value indicating whether the file system is read-only. - /// - public bool IsReadOnly { get; init; } - - /// - /// Initializes a new instance of the class. - /// - /// The instance. - /// The name of the Google Cloud Storage bucket. - public GoogleFileSystem(StorageClient client, string bucketName) - { - ArgumentNullException.ThrowIfNull(client); - ArgumentNullException.ThrowIfNull(bucketName); - - StorageClient = client; - BucketName = bucketName; - } - - /// - /// Initializes a new instance of the class. - /// - /// The Google credential to use for authentication. - /// The name of the Google Cloud Storage bucket. - public GoogleFileSystem(GoogleCredential credential, string bucketName) - : this(StorageClient.Create(credential), bucketName) - { - } - - /// - public VirtualDirectory GetDirectory(string path) => - new GcsDirectory(this, VirtualPath.Normalize(path)); - - /// - public VirtualFile GetFile(string path) => - new GcsFile(this, VirtualPath.Normalize(path)); - - /// - /// Asynchronously creates the bucket in Google Cloud Storage if it does not already exist. - /// - /// The Google Cloud project ID. - /// Additional options for the operation. - /// An optional cancellation token to cancel the operation. - /// - /// A representing the asynchronous operation. - /// - public async ValueTask CreateBucketAsync(string projectId, CreateBucketOptions? options = null, CancellationToken cancellationToken = default) - { - try - { - await StorageClient.GetBucketAsync(BucketName, cancellationToken: cancellationToken).ConfigureAwait(false); - } - catch (GoogleApiException ex) when (ex.HttpStatusCode == System.Net.HttpStatusCode.NotFound) - { - await StorageClient - .CreateBucketAsync(projectId, BucketName, options, cancellationToken) - .ConfigureAwait(false); - } - } - - /// - void IDisposable.Dispose() => - StorageClient.Dispose(); -} +using Google; +using Google.Cloud.Storage.V1; +using Google.Apis.Auth.OAuth2; + +namespace Ramstack.FileSystem.Google; + +/// +/// Represents a file system implementation using Google Cloud Storage. +/// +public sealed class GoogleFileSystem : IVirtualFileSystem +{ + /// + /// Gets the instance. + /// + internal StorageClient StorageClient { get; } + + /// + /// Gets the bucket name. + /// + internal string BucketName { get; } + + /// + /// Gets or sets a value indicating whether the file system is read-only. + /// + public bool IsReadOnly { get; init; } + + /// + /// Initializes a new instance of the class. + /// + /// The instance. + /// The name of the Google Cloud Storage bucket. + public GoogleFileSystem(StorageClient client, string bucketName) + { + ArgumentNullException.ThrowIfNull(client); + ArgumentNullException.ThrowIfNull(bucketName); + + StorageClient = client; + BucketName = bucketName; + } + + /// + /// Initializes a new instance of the class. + /// + /// The Google credential to use for authentication. + /// The name of the Google Cloud Storage bucket. + public GoogleFileSystem(GoogleCredential credential, string bucketName) + : this(StorageClient.Create(credential), bucketName) + { + } + + /// + public VirtualDirectory GetDirectory(string path) => + new GcsDirectory(this, VirtualPath.Normalize(path)); + + /// + public VirtualFile GetFile(string path) => + new GcsFile(this, VirtualPath.Normalize(path)); + + /// + /// Asynchronously creates the bucket in Google Cloud Storage if it does not already exist. + /// + /// The Google Cloud project ID. + /// Additional options for the operation. + /// An optional cancellation token to cancel the operation. + /// + /// A representing the asynchronous operation. + /// + public async ValueTask CreateBucketAsync(string projectId, CreateBucketOptions? options = null, CancellationToken cancellationToken = default) + { + try + { + await StorageClient.GetBucketAsync(BucketName, cancellationToken: cancellationToken).ConfigureAwait(false); + } + catch (GoogleApiException ex) when (ex.HttpStatusCode == System.Net.HttpStatusCode.NotFound) + { + await StorageClient + .CreateBucketAsync(projectId, BucketName, options, cancellationToken) + .ConfigureAwait(false); + } + } + + /// + void IDisposable.Dispose() => + StorageClient.Dispose(); +} diff --git a/src/Ramstack.FileSystem.Google/README.md b/src/Ramstack.FileSystem.Google/README.md index 0e643bd..fcf657f 100644 --- a/src/Ramstack.FileSystem.Google/README.md +++ b/src/Ramstack.FileSystem.Google/README.md @@ -1,67 +1,67 @@ -# Ramstack.FileSystem.Google -[![NuGet](https://img.shields.io/nuget/v/Ramstack.FileSystem.Google.svg)](https://nuget.org/packages/Ramstack.FileSystem.Google) -[![MIT](https://img.shields.io/github/license/rameel/ramstack.filesystem)](https://github.com/rameel/ramstack.filesystem/blob/main/LICENSE) - -Provides an implementation of `Ramstack.FileSystem` using Google Cloud Storage. - -## Getting Started - -To install the `Ramstack.FileSystem.Google` [NuGet package](https://www.nuget.org/packages/Ramstack.FileSystem.Google) -in your project, run the following command: -```console -dotnet add package Ramstack.FileSystem.Google -``` - -## Usage - -```csharp -using Ramstack.FileSystem.Google; - -var client = StorageClient.Create( - GoogleCredential.FromFile("/path/to/credentials.json")); - -GoogleFileSystem fs = new GoogleFileSystem(client, bucketName: "my-bucket"); - -// Create GCS bucket if it doesn't exist -await fs.CreateBucketAsync("my-project-id"); - -await foreach (VirtualFile file in fs.GetFilesAsync("/")) -{ - Console.WriteLine(node.Name); -} -``` - -You can also configure the file system to be read-only: -```csharp -GoogleFileSystem fs = new GoogleFileSystem(client, bucketName: "my-bucket") -{ - IsReadOnly = true -}; -``` - -## Related Projects -- [Ramstack.FileSystem.Abstractions](https://www.nuget.org/packages/Ramstack.FileSystem.Abstractions) - Provides a virtual file system abstraction. -- [Ramstack.FileSystem.Physical](https://www.nuget.org/packages/Ramstack.FileSystem.Physical) - Provides an implementation based on the local file system. -- [Ramstack.FileSystem.Azure](https://www.nuget.org/packages/Ramstack.FileSystem.Azure) - Provides an implementation using Azure Blob Storage. -- [Ramstack.FileSystem.Amazon](https://www.nuget.org/packages/Ramstack.FileSystem.Amazon) - Provides an implementation using Amazon S3 storage. -- [Ramstack.FileSystem.Readonly](https://www.nuget.org/packages/Ramstack.FileSystem.Readonly) - Provides a read-only wrapper for the underlying file system. -- [Ramstack.FileSystem.Globbing](https://www.nuget.org/packages/Ramstack.FileSystem.Globbing) - Wraps the file system, filtering files and directories using glob patterns. -- [Ramstack.FileSystem.Prefixed](https://www.nuget.org/packages/Ramstack.FileSystem.Prefixed) - Adds a prefix to file paths within the underlying file system. -- [Ramstack.FileSystem.Sub](https://www.nuget.org/packages/Ramstack.FileSystem.Sub) - Wraps the underlying file system, restricting access to a specific subpath. -- [Ramstack.FileSystem.Adapters](https://www.nuget.org/packages/Ramstack.FileSystem.Adapters) - Provides integration with `Microsoft.Extensions.FileProviders`. -- [Ramstack.FileSystem.Composite](https://www.nuget.org/packages/Ramstack.FileSystem.Composite) - Provides an implementation that combines multiple file systems into a single composite file system. - -## 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.virtualfiles/blob/main/LICENSE) file for more details. +# Ramstack.FileSystem.Google +[![NuGet](https://img.shields.io/nuget/v/Ramstack.FileSystem.Google.svg)](https://nuget.org/packages/Ramstack.FileSystem.Google) +[![MIT](https://img.shields.io/github/license/rameel/ramstack.filesystem)](https://github.com/rameel/ramstack.filesystem/blob/main/LICENSE) + +Provides an implementation of `Ramstack.FileSystem` using Google Cloud Storage. + +## Getting Started + +To install the `Ramstack.FileSystem.Google` [NuGet package](https://www.nuget.org/packages/Ramstack.FileSystem.Google) +in your project, run the following command: +```console +dotnet add package Ramstack.FileSystem.Google +``` + +## Usage + +```csharp +using Ramstack.FileSystem.Google; + +var client = StorageClient.Create( + GoogleCredential.FromFile("/path/to/credentials.json")); + +GoogleFileSystem fs = new GoogleFileSystem(client, bucketName: "my-bucket"); + +// Create GCS bucket if it doesn't exist +await fs.CreateBucketAsync("my-project-id"); + +await foreach (VirtualFile file in fs.GetFilesAsync("/")) +{ + Console.WriteLine(node.Name); +} +``` + +You can also configure the file system to be read-only: +```csharp +GoogleFileSystem fs = new GoogleFileSystem(client, bucketName: "my-bucket") +{ + IsReadOnly = true +}; +``` + +## Related Projects +- [Ramstack.FileSystem.Abstractions](https://www.nuget.org/packages/Ramstack.FileSystem.Abstractions) - Provides a virtual file system abstraction. +- [Ramstack.FileSystem.Physical](https://www.nuget.org/packages/Ramstack.FileSystem.Physical) - Provides an implementation based on the local file system. +- [Ramstack.FileSystem.Azure](https://www.nuget.org/packages/Ramstack.FileSystem.Azure) - Provides an implementation using Azure Blob Storage. +- [Ramstack.FileSystem.Amazon](https://www.nuget.org/packages/Ramstack.FileSystem.Amazon) - Provides an implementation using Amazon S3 storage. +- [Ramstack.FileSystem.Readonly](https://www.nuget.org/packages/Ramstack.FileSystem.Readonly) - Provides a read-only wrapper for the underlying file system. +- [Ramstack.FileSystem.Globbing](https://www.nuget.org/packages/Ramstack.FileSystem.Globbing) - Wraps the file system, filtering files and directories using glob patterns. +- [Ramstack.FileSystem.Prefixed](https://www.nuget.org/packages/Ramstack.FileSystem.Prefixed) - Adds a prefix to file paths within the underlying file system. +- [Ramstack.FileSystem.Sub](https://www.nuget.org/packages/Ramstack.FileSystem.Sub) - Wraps the underlying file system, restricting access to a specific subpath. +- [Ramstack.FileSystem.Adapters](https://www.nuget.org/packages/Ramstack.FileSystem.Adapters) - Provides integration with `Microsoft.Extensions.FileProviders`. +- [Ramstack.FileSystem.Composite](https://www.nuget.org/packages/Ramstack.FileSystem.Composite) - Provides an implementation that combines multiple file systems into a single composite file system. + +## 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.virtualfiles/blob/main/LICENSE) file for more details. diff --git a/src/Ramstack.FileSystem.Google/Ramstack.FileSystem.Google.csproj b/src/Ramstack.FileSystem.Google/Ramstack.FileSystem.Google.csproj index 05f8d69..c385801 100644 --- a/src/Ramstack.FileSystem.Google/Ramstack.FileSystem.Google.csproj +++ b/src/Ramstack.FileSystem.Google/Ramstack.FileSystem.Google.csproj @@ -1,68 +1,68 @@ - - - - net6.0;net8.0 - Provides an implementation of Ramstack.FileSystem using Google Cloud Storage. - 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.filesystem - Rameel - filesystem;fileprovider - https://github.com/rameel/ramstack.filesystem#readme - README.md - MIT - true - snupkg - true - true - true - true - - - - true - - - - - Properties\AssemblyInfo.cs - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - Properties\README.md - True - \ - - - - + + + + net6.0;net8.0 + Provides an implementation of Ramstack.FileSystem using Google Cloud Storage. + 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.filesystem + Rameel + filesystem;fileprovider + https://github.com/rameel/ramstack.filesystem#readme + README.md + MIT + true + snupkg + true + true + true + true + + + + true + + + + + Properties\AssemblyInfo.cs + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + Properties\README.md + True + \ + + + + diff --git a/src/Ramstack.FileSystem.Physical/PhysicalDirectory.cs b/src/Ramstack.FileSystem.Physical/PhysicalDirectory.cs index d9272fb..348c3da 100644 --- a/src/Ramstack.FileSystem.Physical/PhysicalDirectory.cs +++ b/src/Ramstack.FileSystem.Physical/PhysicalDirectory.cs @@ -1,155 +1,155 @@ -using System.Diagnostics; -using System.IO.Enumeration; - -using Ramstack.FileSystem.Utilities; - -namespace Ramstack.FileSystem.Physical; - -/// -/// Represents an implementation of that maps to a physical directory. -/// -internal sealed class PhysicalDirectory : VirtualDirectory -{ - /// - /// Default options for file enumeration, configured to enumerate all files, - /// including system and hidden files, not recurse through subdirectories, - /// and ignore inaccessible files. - /// - private static readonly EnumerationOptions s_defaultOptions = new() - { - AttributesToSkip = 0, - RecurseSubdirectories = false, - IgnoreInaccessible = true - }; - - private readonly PhysicalFileSystem _fs; - private readonly string _physicalPath; - - /// - public override IVirtualFileSystem FileSystem => _fs; - - /// - /// Initializes a new instance of the class. - /// - /// The file system associated with this directory. - /// The path of the directory. - /// The physical path of the directory. - public PhysicalDirectory(PhysicalFileSystem fileSystem, string path, string physicalPath) : base(path) => - (_fs, _physicalPath) = (fileSystem, physicalPath); - - /// - protected override ValueTask GetPropertiesCoreAsync(CancellationToken cancellationToken) - { - var info = new DirectoryInfo(_physicalPath); - - var properties = info.Exists - ? VirtualNodeProperties.CreateDirectoryProperties( - creationTime: info.CreationTime, - lastAccessTime: info.LastAccessTime, - lastWriteTime: info.LastWriteTime) - : null; - - return new ValueTask(properties); - } - - /// - protected override ValueTask CreateCoreAsync(CancellationToken cancellationToken) - { - Directory.CreateDirectory(_physicalPath); - return default; - } - - /// - protected override ValueTask DeleteCoreAsync(CancellationToken cancellationToken) - { - try - { - Directory.Delete(_physicalPath, recursive: true); - } - catch (DirectoryNotFoundException) - { - // The path of the directory being deleted may not exist. - // This is a safe scenario and does not require further handling. - } - - return default; - } - - /// - protected override IAsyncEnumerable GetFileNodesCoreAsync(CancellationToken cancellationToken) - { - // TOCTOU (Time-of-check to time-of-use) - // ------------------------------------- - // We accept that a "DirectoryNotFoundException" may be thrown if - // the directory becomes unavailable, such as being deleted after - // its existence was checked. - - var nodes = Directory.Exists(_physicalPath) - ? new FileSystemEnumerable(_physicalPath, FindTransform, s_defaultOptions) - : Enumerable.Empty(); - - return nodes.ToAsyncEnumerable(); - - VirtualNode FindTransform(ref FileSystemEntry entry) - { - var fullName = VirtualPath.Join(FullName, entry.FileName); - var physicalPath = entry.ToFullPath(); - - return entry.IsDirectory - ? new PhysicalDirectory(_fs, fullName, physicalPath) - : new PhysicalFile(_fs, fullName, physicalPath); - } - } - - /// - protected override IAsyncEnumerable GetFilesCoreAsync(CancellationToken cancellationToken) - { - var nodes = Enumerable.Empty(); - - if (Directory.Exists(_physicalPath)) - { - nodes = new FileSystemEnumerable(_physicalPath, FindTransform, s_defaultOptions) - { - ShouldIncludePredicate = (ref entry) => !entry.IsDirectory - }; - } - - return nodes.ToAsyncEnumerable(); - - VirtualFile FindTransform(ref FileSystemEntry entry) - { - Debug.Assert(!entry.IsDirectory); - - var fullName = VirtualPath.Join(FullName, entry.FileName); - var physicalPath = entry.ToFullPath(); - - return new PhysicalFile(_fs, fullName, physicalPath); - } - } - - /// - protected override IAsyncEnumerable GetDirectoriesCoreAsync(CancellationToken cancellationToken) - { - var nodes = Enumerable.Empty(); - - if (Directory.Exists(_physicalPath)) - { - nodes = new FileSystemEnumerable(_physicalPath, FindTransform, s_defaultOptions) - { - ShouldIncludePredicate = (ref entry) => entry.IsDirectory - }; - } - - return nodes.ToAsyncEnumerable(); - - VirtualDirectory FindTransform(ref FileSystemEntry entry) - { - Debug.Assert(entry.IsDirectory); - - var fullName = VirtualPath.Join(FullName, entry.FileName); - var physicalPath = entry.ToFullPath(); - - return new PhysicalDirectory(_fs, fullName, physicalPath); - } - } -} +using System.Diagnostics; +using System.IO.Enumeration; + +using Ramstack.FileSystem.Utilities; + +namespace Ramstack.FileSystem.Physical; + +/// +/// Represents an implementation of that maps to a physical directory. +/// +internal sealed class PhysicalDirectory : VirtualDirectory +{ + /// + /// Default options for file enumeration, configured to enumerate all files, + /// including system and hidden files, not recurse through subdirectories, + /// and ignore inaccessible files. + /// + private static readonly EnumerationOptions s_defaultOptions = new() + { + AttributesToSkip = 0, + RecurseSubdirectories = false, + IgnoreInaccessible = true + }; + + private readonly PhysicalFileSystem _fs; + private readonly string _physicalPath; + + /// + public override IVirtualFileSystem FileSystem => _fs; + + /// + /// Initializes a new instance of the class. + /// + /// The file system associated with this directory. + /// The path of the directory. + /// The physical path of the directory. + public PhysicalDirectory(PhysicalFileSystem fileSystem, string path, string physicalPath) : base(path) => + (_fs, _physicalPath) = (fileSystem, physicalPath); + + /// + protected override ValueTask GetPropertiesCoreAsync(CancellationToken cancellationToken) + { + var info = new DirectoryInfo(_physicalPath); + + var properties = info.Exists + ? VirtualNodeProperties.CreateDirectoryProperties( + creationTime: info.CreationTime, + lastAccessTime: info.LastAccessTime, + lastWriteTime: info.LastWriteTime) + : null; + + return new ValueTask(properties); + } + + /// + protected override ValueTask CreateCoreAsync(CancellationToken cancellationToken) + { + Directory.CreateDirectory(_physicalPath); + return default; + } + + /// + protected override ValueTask DeleteCoreAsync(CancellationToken cancellationToken) + { + try + { + Directory.Delete(_physicalPath, recursive: true); + } + catch (DirectoryNotFoundException) + { + // The path of the directory being deleted may not exist. + // This is a safe scenario and does not require further handling. + } + + return default; + } + + /// + protected override IAsyncEnumerable GetFileNodesCoreAsync(CancellationToken cancellationToken) + { + // TOCTOU (Time-of-check to time-of-use) + // ------------------------------------- + // We accept that a "DirectoryNotFoundException" may be thrown if + // the directory becomes unavailable, such as being deleted after + // its existence was checked. + + var nodes = Directory.Exists(_physicalPath) + ? new FileSystemEnumerable(_physicalPath, FindTransform, s_defaultOptions) + : Enumerable.Empty(); + + return nodes.ToAsyncEnumerable(); + + VirtualNode FindTransform(ref FileSystemEntry entry) + { + var fullName = VirtualPath.Join(FullName, entry.FileName); + var physicalPath = entry.ToFullPath(); + + return entry.IsDirectory + ? new PhysicalDirectory(_fs, fullName, physicalPath) + : new PhysicalFile(_fs, fullName, physicalPath); + } + } + + /// + protected override IAsyncEnumerable GetFilesCoreAsync(CancellationToken cancellationToken) + { + var nodes = Enumerable.Empty(); + + if (Directory.Exists(_physicalPath)) + { + nodes = new FileSystemEnumerable(_physicalPath, FindTransform, s_defaultOptions) + { + ShouldIncludePredicate = (ref entry) => !entry.IsDirectory + }; + } + + return nodes.ToAsyncEnumerable(); + + VirtualFile FindTransform(ref FileSystemEntry entry) + { + Debug.Assert(!entry.IsDirectory); + + var fullName = VirtualPath.Join(FullName, entry.FileName); + var physicalPath = entry.ToFullPath(); + + return new PhysicalFile(_fs, fullName, physicalPath); + } + } + + /// + protected override IAsyncEnumerable GetDirectoriesCoreAsync(CancellationToken cancellationToken) + { + var nodes = Enumerable.Empty(); + + if (Directory.Exists(_physicalPath)) + { + nodes = new FileSystemEnumerable(_physicalPath, FindTransform, s_defaultOptions) + { + ShouldIncludePredicate = (ref entry) => entry.IsDirectory + }; + } + + return nodes.ToAsyncEnumerable(); + + VirtualDirectory FindTransform(ref FileSystemEntry entry) + { + Debug.Assert(entry.IsDirectory); + + var fullName = VirtualPath.Join(FullName, entry.FileName); + var physicalPath = entry.ToFullPath(); + + return new PhysicalDirectory(_fs, fullName, physicalPath); + } + } +} diff --git a/src/Ramstack.FileSystem.Physical/PhysicalFile.cs b/src/Ramstack.FileSystem.Physical/PhysicalFile.cs index 4870d0c..890b190 100644 --- a/src/Ramstack.FileSystem.Physical/PhysicalFile.cs +++ b/src/Ramstack.FileSystem.Physical/PhysicalFile.cs @@ -1,141 +1,141 @@ -namespace Ramstack.FileSystem.Physical; - -/// -/// Represents an implementation of that maps to a physical file. -/// -internal sealed class PhysicalFile : VirtualFile -{ - /// - /// The default size of the buffer used for file operations. - /// - private const int DefaultBufferSize = 4096; - - private readonly PhysicalFileSystem _fs; - private readonly string _physicalPath; - - /// - public override IVirtualFileSystem FileSystem => _fs; - - /// - /// Initializes a new instance of the class. - /// - /// The file system associated with this file. - /// The path of the file. - /// The physical path of the file. - internal PhysicalFile(PhysicalFileSystem fileSystem, string path, string physicalPath) : base(path) => - (_fs, _physicalPath) = (fileSystem, physicalPath); - - /// - protected override ValueTask GetPropertiesCoreAsync(CancellationToken cancellationToken) - { - var file = new FileInfo(_physicalPath); - var properties = file.Exists - ? VirtualNodeProperties.CreateFileProperties( - creationTime: file.CreationTime, - lastAccessTime: file.LastAccessTime, - lastWriteTime: file.LastWriteTime, - length: file.Length) - : null; - - return new ValueTask(properties); - } - - /// - protected override ValueTask OpenReadCoreAsync(CancellationToken cancellationToken) - { - // SequentialScan is a performance hint that requires an extra system call on non-Windows systems. - // https://github.com/dotnet/runtime/blob/46c9a4fff83f35ec659e6659050440aadccf3201/src/libraries/System.Private.CoreLib/src/System/IO/File.cs#L694 - var options = OperatingSystem.IsWindows() - ? FileOptions.Asynchronous | FileOptions.SequentialScan - : FileOptions.Asynchronous; - - var stream = new FileStream(_physicalPath, FileMode.Open, FileAccess.Read, FileShare.Read, DefaultBufferSize, options); - - return new ValueTask(stream); - } - - /// - protected override ValueTask OpenWriteCoreAsync(CancellationToken cancellationToken) - { - EnsureDirectoryExists(); - - var stream = new FileStream(_physicalPath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None, DefaultBufferSize, FileOptions.Asynchronous); - - // Since FileMode.OpenOrCreate doesn't truncate the file, we manually - // set the file length to zero to remove any leftover data. - stream.SetLength(0); - - return new ValueTask(stream); - } - - /// - protected override async ValueTask WriteCoreAsync(Stream stream, bool overwrite, CancellationToken cancellationToken) - { - EnsureDirectoryExists(); - - try - { - // To overwrite the file, we use FileMode.OpenOrCreate instead of FileMode.Create. - // This avoids a System.UnauthorizedAccessException: Access to the path is denied, - // which can occur if the file has the FileAttributes.Hidden attribute. - var fileMode = overwrite ? FileMode.OpenOrCreate : FileMode.CreateNew; - - await using var fs = new FileStream(_physicalPath, fileMode, FileAccess.Write, FileShare.None, DefaultBufferSize, FileOptions.Asynchronous); - - // Since FileMode.OpenOrCreate doesn't truncate the file, we manually - // set the file length to zero to remove any leftover data. - fs.SetLength(0); - - await stream.CopyToAsync(fs, cancellationToken).ConfigureAwait(false); - } - catch (IOException exception) - { - // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-erref/0642cb2f-2075-4469-918c-4441e69c548a - // https://github.com/github/VisualStudio/blob/master/tools/Debugging%20Tools%20for%20Windows/winext/manifest/winerror.h#L64C9-L64C26 - // - // 0x8007_0050 - // 1000000000000111 00000000 01010000 - // S____________HR_ ____CODE 80 _____ - - const int WIN32_ERROR_FILE_EXISTS = unchecked((int)0x80070050); - const int POSIX_EEXIST = 17; - - var exists = OperatingSystem.IsWindows() - ? exception.HResult == WIN32_ERROR_FILE_EXISTS - : exception.HResult == POSIX_EEXIST; - - throw new IOException(exists - ? $"The file '{FullName}' already exists." - : $"An error occurred while writing to the file '{FullName}'.", exception); - } - } - - /// - protected override ValueTask DeleteCoreAsync(CancellationToken cancellationToken) - { - try - { - File.Delete(_physicalPath); - } - catch (DirectoryNotFoundException) - { - // The path of the file being deleted may not exist. - // This is a safe scenario and does not require further handling. - } - - return default; - } - - /// - /// Ensures that the specified directory exists on disk. If the directory does not exist, it will be created. - /// - /// - /// This method is used to prevent errors when attempting to write to a non-existing directory. - /// - private void EnsureDirectoryExists() - { - var directoryPath = Path.GetDirectoryName(_physicalPath)!; - if (!System.IO.Directory.Exists(directoryPath)) - System.IO.Directory.CreateDirectory(directoryPath); - } -} +namespace Ramstack.FileSystem.Physical; + +/// +/// Represents an implementation of that maps to a physical file. +/// +internal sealed class PhysicalFile : VirtualFile +{ + /// + /// The default size of the buffer used for file operations. + /// + private const int DefaultBufferSize = 4096; + + private readonly PhysicalFileSystem _fs; + private readonly string _physicalPath; + + /// + public override IVirtualFileSystem FileSystem => _fs; + + /// + /// Initializes a new instance of the class. + /// + /// The file system associated with this file. + /// The path of the file. + /// The physical path of the file. + internal PhysicalFile(PhysicalFileSystem fileSystem, string path, string physicalPath) : base(path) => + (_fs, _physicalPath) = (fileSystem, physicalPath); + + /// + protected override ValueTask GetPropertiesCoreAsync(CancellationToken cancellationToken) + { + var file = new FileInfo(_physicalPath); + var properties = file.Exists + ? VirtualNodeProperties.CreateFileProperties( + creationTime: file.CreationTime, + lastAccessTime: file.LastAccessTime, + lastWriteTime: file.LastWriteTime, + length: file.Length) + : null; + + return new ValueTask(properties); + } + + /// + protected override ValueTask OpenReadCoreAsync(CancellationToken cancellationToken) + { + // SequentialScan is a performance hint that requires an extra system call on non-Windows systems. + // https://github.com/dotnet/runtime/blob/46c9a4fff83f35ec659e6659050440aadccf3201/src/libraries/System.Private.CoreLib/src/System/IO/File.cs#L694 + var options = OperatingSystem.IsWindows() + ? FileOptions.Asynchronous | FileOptions.SequentialScan + : FileOptions.Asynchronous; + + var stream = new FileStream(_physicalPath, FileMode.Open, FileAccess.Read, FileShare.Read, DefaultBufferSize, options); + + return new ValueTask(stream); + } + + /// + protected override ValueTask OpenWriteCoreAsync(CancellationToken cancellationToken) + { + EnsureDirectoryExists(); + + var stream = new FileStream(_physicalPath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None, DefaultBufferSize, FileOptions.Asynchronous); + + // Since FileMode.OpenOrCreate doesn't truncate the file, we manually + // set the file length to zero to remove any leftover data. + stream.SetLength(0); + + return new ValueTask(stream); + } + + /// + protected override async ValueTask WriteCoreAsync(Stream stream, bool overwrite, CancellationToken cancellationToken) + { + EnsureDirectoryExists(); + + try + { + // To overwrite the file, we use FileMode.OpenOrCreate instead of FileMode.Create. + // This avoids a System.UnauthorizedAccessException: Access to the path is denied, + // which can occur if the file has the FileAttributes.Hidden attribute. + var fileMode = overwrite ? FileMode.OpenOrCreate : FileMode.CreateNew; + + await using var fs = new FileStream(_physicalPath, fileMode, FileAccess.Write, FileShare.None, DefaultBufferSize, FileOptions.Asynchronous); + + // Since FileMode.OpenOrCreate doesn't truncate the file, we manually + // set the file length to zero to remove any leftover data. + fs.SetLength(0); + + await stream.CopyToAsync(fs, cancellationToken).ConfigureAwait(false); + } + catch (IOException exception) + { + // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-erref/0642cb2f-2075-4469-918c-4441e69c548a + // https://github.com/github/VisualStudio/blob/master/tools/Debugging%20Tools%20for%20Windows/winext/manifest/winerror.h#L64C9-L64C26 + // + // 0x8007_0050 + // 1000000000000111 00000000 01010000 + // S____________HR_ ____CODE 80 _____ + + const int WIN32_ERROR_FILE_EXISTS = unchecked((int)0x80070050); + const int POSIX_EEXIST = 17; + + var exists = OperatingSystem.IsWindows() + ? exception.HResult == WIN32_ERROR_FILE_EXISTS + : exception.HResult == POSIX_EEXIST; + + throw new IOException(exists + ? $"The file '{FullName}' already exists." + : $"An error occurred while writing to the file '{FullName}'.", exception); + } + } + + /// + protected override ValueTask DeleteCoreAsync(CancellationToken cancellationToken) + { + try + { + File.Delete(_physicalPath); + } + catch (DirectoryNotFoundException) + { + // The path of the file being deleted may not exist. + // This is a safe scenario and does not require further handling. + } + + return default; + } + + /// + /// Ensures that the specified directory exists on disk. If the directory does not exist, it will be created. + /// + /// + /// This method is used to prevent errors when attempting to write to a non-existing directory. + /// + private void EnsureDirectoryExists() + { + var directoryPath = Path.GetDirectoryName(_physicalPath)!; + if (!System.IO.Directory.Exists(directoryPath)) + System.IO.Directory.CreateDirectory(directoryPath); + } +} diff --git a/src/Ramstack.FileSystem.Physical/PhysicalFileSystem.cs b/src/Ramstack.FileSystem.Physical/PhysicalFileSystem.cs index 71be1c0..1d4c2f4 100644 --- a/src/Ramstack.FileSystem.Physical/PhysicalFileSystem.cs +++ b/src/Ramstack.FileSystem.Physical/PhysicalFileSystem.cs @@ -1,72 +1,72 @@ -using System.Diagnostics; - -namespace Ramstack.FileSystem.Physical; - -/// -/// Represents an implementation of for physical files. -/// -[DebuggerDisplay("Root = {Root,nq}")] -public sealed class PhysicalFileSystem : IVirtualFileSystem -{ - /// - /// Gets the physical path of the root directory for this instance. - /// - public string Root { get; } - - /// - /// Gets or sets a value indicating whether the file system is read-only. - /// - public bool IsReadOnly { get; init; } - - /// - /// Initializes a new instance of the class. - /// - /// The physical path of the root directory. - public PhysicalFileSystem(string path) - { - if (!Path.IsPathRooted(path)) - Error(path); - - Root = Path.GetFullPath(path); - - static void Error(string path) => - throw new ArgumentException($"The path '{path}' must be absolute."); - } - - /// - public VirtualFile GetFile(string path) - { - path = VirtualPath.Normalize(path); - var physicalPath = GetPhysicalPath(path); - - return new PhysicalFile(this, path, physicalPath); - } - - /// - public VirtualDirectory GetDirectory(string path) - { - path = VirtualPath.Normalize(path); - var physicalPath = GetPhysicalPath(path); - - return new PhysicalDirectory(this, path, physicalPath); - } - - /// - void IDisposable.Dispose() - { - } - - /// - /// Converts the specified virtual path within this file system - /// to its corresponding physical path. - /// - /// The virtual path within this file system. - /// - /// The corresponding physical path. - /// - private string GetPhysicalPath(string path) - { - Debug.Assert(VirtualPath.IsNormalized(path)); - return Path.Join(Root, path); - } -} +using System.Diagnostics; + +namespace Ramstack.FileSystem.Physical; + +/// +/// Represents an implementation of for physical files. +/// +[DebuggerDisplay("Root = {Root,nq}")] +public sealed class PhysicalFileSystem : IVirtualFileSystem +{ + /// + /// Gets the physical path of the root directory for this instance. + /// + public string Root { get; } + + /// + /// Gets or sets a value indicating whether the file system is read-only. + /// + public bool IsReadOnly { get; init; } + + /// + /// Initializes a new instance of the class. + /// + /// The physical path of the root directory. + public PhysicalFileSystem(string path) + { + if (!Path.IsPathRooted(path)) + Error(path); + + Root = Path.GetFullPath(path); + + static void Error(string path) => + throw new ArgumentException($"The path '{path}' must be absolute."); + } + + /// + public VirtualFile GetFile(string path) + { + path = VirtualPath.Normalize(path); + var physicalPath = GetPhysicalPath(path); + + return new PhysicalFile(this, path, physicalPath); + } + + /// + public VirtualDirectory GetDirectory(string path) + { + path = VirtualPath.Normalize(path); + var physicalPath = GetPhysicalPath(path); + + return new PhysicalDirectory(this, path, physicalPath); + } + + /// + void IDisposable.Dispose() + { + } + + /// + /// Converts the specified virtual path within this file system + /// to its corresponding physical path. + /// + /// The virtual path within this file system. + /// + /// The corresponding physical path. + /// + private string GetPhysicalPath(string path) + { + Debug.Assert(VirtualPath.IsNormalized(path)); + return Path.Join(Root, path); + } +} diff --git a/src/Ramstack.FileSystem.Physical/README.md b/src/Ramstack.FileSystem.Physical/README.md index 2f12805..a8e62a9 100644 --- a/src/Ramstack.FileSystem.Physical/README.md +++ b/src/Ramstack.FileSystem.Physical/README.md @@ -1,62 +1,62 @@ -# Ramstack.FileSystem.Physical -[![NuGet](https://img.shields.io/nuget/v/Ramstack.FileSystem.Physical.svg)](https://nuget.org/packages/Ramstack.FileSystem.Physical) -[![MIT](https://img.shields.io/github/license/rameel/ramstack.filesystem)](https://github.com/rameel/ramstack.filesystem/blob/main/LICENSE) - -Provides an implementation of `Ramstack.FileSystem` based on the local file system. - -## Getting Started - -To install the `Ramstack.FileSystem.Physical` [NuGet package](https://www.nuget.org/packages/Ramstack.FileSystem.Physical) -in your project, run the following command: -```console -dotnet add package Ramstack.FileSystem.Physical -``` -## Usage - -```csharp -using Ramstack.FileSystem.Physical; - -PhysicalFileSystem fs = new PhysicalFileSystem(@"C:\path\to\directory"); - -await foreach (VirtualFile file in fs.GetFilesAsync("/")) -{ - Console.WriteLine(node.Name); -} -``` - -You can also configure the file system to be read-only: -```csharp -using Ramstack.FileSystem.Physical; - -PhysicalFileSystem fs = new PhysicalFileSystem(@"C:\path\to\directory") -{ - IsReadOnly = true -}; -``` - -## Related Projects -- [Ramstack.FileSystem.Abstractions](https://www.nuget.org/packages/Ramstack.FileSystem.Abstractions) - Provides a virtual file system abstraction. -- [Ramstack.FileSystem.Azure](https://www.nuget.org/packages/Ramstack.FileSystem.Azure) - Provides an implementation using Azure Blob Storage. -- [Ramstack.FileSystem.Amazon](https://www.nuget.org/packages/Ramstack.FileSystem.Amazon) - Provides an implementation using Amazon S3 storage. -- [Ramstack.FileSystem.Google](https://www.nuget.org/packages/Ramstack.FileSystem.Google) - Provides an implementation using Google Cloud Storage. -- [Ramstack.FileSystem.Readonly](https://www.nuget.org/packages/Ramstack.FileSystem.Readonly) - Provides a read-only wrapper for the underlying file system. -- [Ramstack.FileSystem.Globbing](https://www.nuget.org/packages/Ramstack.FileSystem.Globbing) - Wraps the file system, filtering files and directories using glob patterns. -- [Ramstack.FileSystem.Prefixed](https://www.nuget.org/packages/Ramstack.FileSystem.Prefixed) - Adds a prefix to file paths within the underlying file system. -- [Ramstack.FileSystem.Sub](https://www.nuget.org/packages/Ramstack.FileSystem.Sub) - Wraps the underlying file system, restricting access to a specific subpath. -- [Ramstack.FileSystem.Adapters](https://www.nuget.org/packages/Ramstack.FileSystem.Adapters) - Provides integration with `Microsoft.Extensions.FileProviders`. -- [Ramstack.FileSystem.Composite](https://www.nuget.org/packages/Ramstack.FileSystem.Composite) - Provides an implementation that combines multiple file systems into a single composite file system. - -## 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.virtualfiles/blob/main/LICENSE) file for more details. +# Ramstack.FileSystem.Physical +[![NuGet](https://img.shields.io/nuget/v/Ramstack.FileSystem.Physical.svg)](https://nuget.org/packages/Ramstack.FileSystem.Physical) +[![MIT](https://img.shields.io/github/license/rameel/ramstack.filesystem)](https://github.com/rameel/ramstack.filesystem/blob/main/LICENSE) + +Provides an implementation of `Ramstack.FileSystem` based on the local file system. + +## Getting Started + +To install the `Ramstack.FileSystem.Physical` [NuGet package](https://www.nuget.org/packages/Ramstack.FileSystem.Physical) +in your project, run the following command: +```console +dotnet add package Ramstack.FileSystem.Physical +``` +## Usage + +```csharp +using Ramstack.FileSystem.Physical; + +PhysicalFileSystem fs = new PhysicalFileSystem(@"C:\path\to\directory"); + +await foreach (VirtualFile file in fs.GetFilesAsync("/")) +{ + Console.WriteLine(node.Name); +} +``` + +You can also configure the file system to be read-only: +```csharp +using Ramstack.FileSystem.Physical; + +PhysicalFileSystem fs = new PhysicalFileSystem(@"C:\path\to\directory") +{ + IsReadOnly = true +}; +``` + +## Related Projects +- [Ramstack.FileSystem.Abstractions](https://www.nuget.org/packages/Ramstack.FileSystem.Abstractions) - Provides a virtual file system abstraction. +- [Ramstack.FileSystem.Azure](https://www.nuget.org/packages/Ramstack.FileSystem.Azure) - Provides an implementation using Azure Blob Storage. +- [Ramstack.FileSystem.Amazon](https://www.nuget.org/packages/Ramstack.FileSystem.Amazon) - Provides an implementation using Amazon S3 storage. +- [Ramstack.FileSystem.Google](https://www.nuget.org/packages/Ramstack.FileSystem.Google) - Provides an implementation using Google Cloud Storage. +- [Ramstack.FileSystem.Readonly](https://www.nuget.org/packages/Ramstack.FileSystem.Readonly) - Provides a read-only wrapper for the underlying file system. +- [Ramstack.FileSystem.Globbing](https://www.nuget.org/packages/Ramstack.FileSystem.Globbing) - Wraps the file system, filtering files and directories using glob patterns. +- [Ramstack.FileSystem.Prefixed](https://www.nuget.org/packages/Ramstack.FileSystem.Prefixed) - Adds a prefix to file paths within the underlying file system. +- [Ramstack.FileSystem.Sub](https://www.nuget.org/packages/Ramstack.FileSystem.Sub) - Wraps the underlying file system, restricting access to a specific subpath. +- [Ramstack.FileSystem.Adapters](https://www.nuget.org/packages/Ramstack.FileSystem.Adapters) - Provides integration with `Microsoft.Extensions.FileProviders`. +- [Ramstack.FileSystem.Composite](https://www.nuget.org/packages/Ramstack.FileSystem.Composite) - Provides an implementation that combines multiple file systems into a single composite file system. + +## 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.virtualfiles/blob/main/LICENSE) file for more details. diff --git a/src/Ramstack.FileSystem.Physical/Ramstack.FileSystem.Physical.csproj b/src/Ramstack.FileSystem.Physical/Ramstack.FileSystem.Physical.csproj index ea47b73..48d134f 100644 --- a/src/Ramstack.FileSystem.Physical/Ramstack.FileSystem.Physical.csproj +++ b/src/Ramstack.FileSystem.Physical/Ramstack.FileSystem.Physical.csproj @@ -1,69 +1,69 @@ - - - - net6.0;net8.0 - Provides an implementation of Ramstack.FileSystem based on the local file system. - 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.filesystem - Rameel - filesystem;fileprovider - https://github.com/rameel/ramstack.filesystem#readme - README.md - MIT - true - snupkg - true - true - true - true - - - - true - - - - - Properties\AssemblyInfo.cs - - - Utilities\EnumerableExtensions.cs - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - Properties\README.md - True - \ - - - - + + + + net6.0;net8.0 + Provides an implementation of Ramstack.FileSystem based on the local file system. + 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.filesystem + Rameel + filesystem;fileprovider + https://github.com/rameel/ramstack.filesystem#readme + README.md + MIT + true + snupkg + true + true + true + true + + + + true + + + + + Properties\AssemblyInfo.cs + + + Utilities\EnumerableExtensions.cs + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + Properties\README.md + True + \ + + + + diff --git a/src/Ramstack.FileSystem.Prefixed/PrefixedDirectory.cs b/src/Ramstack.FileSystem.Prefixed/PrefixedDirectory.cs index 16dc682..4db5ca8 100644 --- a/src/Ramstack.FileSystem.Prefixed/PrefixedDirectory.cs +++ b/src/Ramstack.FileSystem.Prefixed/PrefixedDirectory.cs @@ -1,114 +1,114 @@ -using System.Runtime.CompilerServices; - -namespace Ramstack.FileSystem.Prefixed; - -/// -/// Represents a directory in a , -/// wrapping an existing instance with a prefixed path. -/// -internal class PrefixedDirectory : VirtualDirectory -{ - private readonly PrefixedFileSystem _fs; - private readonly VirtualDirectory _directory; - - /// - public override IVirtualFileSystem FileSystem => _fs; - - /// - /// Initializes a new instance of the class. - /// - /// The file system associated with this directory. - /// The prefixed path of the directory. - /// The underlying that this instance wraps. - public PrefixedDirectory(PrefixedFileSystem fileSystem, string path, VirtualDirectory directory) : base(path) => - (_fs, _directory) = (fileSystem, directory); - - /// - protected override void RefreshCore() => - _directory.Refresh(); - - /// - protected override ValueTask GetPropertiesCoreAsync(CancellationToken cancellationToken) => - _directory.GetPropertiesAsync(cancellationToken)!; - - /// - protected override ValueTask ExistsCoreAsync(CancellationToken cancellationToken) => - _directory.ExistsAsync(cancellationToken); - - /// - protected override ValueTask CreateCoreAsync(CancellationToken cancellationToken) => - _directory.CreateAsync(cancellationToken); - - /// - protected override ValueTask DeleteCoreAsync(CancellationToken cancellationToken) => - _directory.DeleteAsync(cancellationToken); - - /// - protected override async IAsyncEnumerable GetFileNodesCoreAsync([EnumeratorCancellation] CancellationToken cancellationToken) - { - await foreach (var node in _directory.GetFileNodesAsync(cancellationToken).ConfigureAwait(false)) - { - var path = VirtualPath.Join(FullName, node.Name); - yield return node switch - { - VirtualDirectory directory => new PrefixedDirectory(_fs, path, directory), - _ => new PrefixedFile(_fs, path, (VirtualFile)node) - }; - } - } - - /// - protected override async IAsyncEnumerable GetFilesCoreAsync([EnumeratorCancellation] CancellationToken cancellationToken) - { - await foreach (var file in _directory.GetFilesAsync(cancellationToken).ConfigureAwait(false)) - { - var path = VirtualPath.Join(FullName, file.Name); - yield return new PrefixedFile(_fs, path, file); - } - } - - /// - protected override async IAsyncEnumerable GetDirectoriesCoreAsync([EnumeratorCancellation] CancellationToken cancellationToken) - { - await foreach (var directory in _directory.GetDirectoriesAsync(cancellationToken).ConfigureAwait(false)) - { - var path = VirtualPath.Join(FullName, directory.Name); - yield return new PrefixedDirectory(_fs, path, directory); - } - } - - /// - protected override async IAsyncEnumerable GetFileNodesCoreAsync(string[] patterns, string[]? excludes, [EnumeratorCancellation] CancellationToken cancellationToken) - { - await foreach (var node in _directory.GetFileNodesAsync(patterns, excludes, cancellationToken).ConfigureAwait(false)) - { - var path = _fs.WrapWithPrefix(node.FullName); - - yield return node switch - { - VirtualDirectory directory => new PrefixedDirectory(_fs, path, directory), - _ => new PrefixedFile(_fs, path, (VirtualFile)node) - }; - } - } - - /// - protected override async IAsyncEnumerable GetFilesCoreAsync(string[] patterns, string[]? excludes, [EnumeratorCancellation] CancellationToken cancellationToken) - { - await foreach (var file in _directory.GetFilesAsync(patterns, excludes, cancellationToken).ConfigureAwait(false)) - { - var path = _fs.WrapWithPrefix(file.FullName); - yield return new PrefixedFile(_fs, path, file); - } - } - - /// - protected override async IAsyncEnumerable GetDirectoriesCoreAsync(string[] patterns, string[]? excludes, [EnumeratorCancellation] CancellationToken cancellationToken) - { - await foreach (var directory in _directory.GetDirectoriesAsync(patterns, excludes, cancellationToken).ConfigureAwait(false)) - { - var path = _fs.WrapWithPrefix(directory.FullName); - yield return new PrefixedDirectory(_fs, path, directory); - } - } -} +using System.Runtime.CompilerServices; + +namespace Ramstack.FileSystem.Prefixed; + +/// +/// Represents a directory in a , +/// wrapping an existing instance with a prefixed path. +/// +internal class PrefixedDirectory : VirtualDirectory +{ + private readonly PrefixedFileSystem _fs; + private readonly VirtualDirectory _directory; + + /// + public override IVirtualFileSystem FileSystem => _fs; + + /// + /// Initializes a new instance of the class. + /// + /// The file system associated with this directory. + /// The prefixed path of the directory. + /// The underlying that this instance wraps. + public PrefixedDirectory(PrefixedFileSystem fileSystem, string path, VirtualDirectory directory) : base(path) => + (_fs, _directory) = (fileSystem, directory); + + /// + protected override void RefreshCore() => + _directory.Refresh(); + + /// + protected override ValueTask GetPropertiesCoreAsync(CancellationToken cancellationToken) => + _directory.GetPropertiesAsync(cancellationToken)!; + + /// + protected override ValueTask ExistsCoreAsync(CancellationToken cancellationToken) => + _directory.ExistsAsync(cancellationToken); + + /// + protected override ValueTask CreateCoreAsync(CancellationToken cancellationToken) => + _directory.CreateAsync(cancellationToken); + + /// + protected override ValueTask DeleteCoreAsync(CancellationToken cancellationToken) => + _directory.DeleteAsync(cancellationToken); + + /// + protected override async IAsyncEnumerable GetFileNodesCoreAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + await foreach (var node in _directory.GetFileNodesAsync(cancellationToken).ConfigureAwait(false)) + { + var path = VirtualPath.Join(FullName, node.Name); + yield return node switch + { + VirtualDirectory directory => new PrefixedDirectory(_fs, path, directory), + _ => new PrefixedFile(_fs, path, (VirtualFile)node) + }; + } + } + + /// + protected override async IAsyncEnumerable GetFilesCoreAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + await foreach (var file in _directory.GetFilesAsync(cancellationToken).ConfigureAwait(false)) + { + var path = VirtualPath.Join(FullName, file.Name); + yield return new PrefixedFile(_fs, path, file); + } + } + + /// + protected override async IAsyncEnumerable GetDirectoriesCoreAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + await foreach (var directory in _directory.GetDirectoriesAsync(cancellationToken).ConfigureAwait(false)) + { + var path = VirtualPath.Join(FullName, directory.Name); + yield return new PrefixedDirectory(_fs, path, directory); + } + } + + /// + protected override async IAsyncEnumerable GetFileNodesCoreAsync(string[] patterns, string[]? excludes, [EnumeratorCancellation] CancellationToken cancellationToken) + { + await foreach (var node in _directory.GetFileNodesAsync(patterns, excludes, cancellationToken).ConfigureAwait(false)) + { + var path = _fs.WrapWithPrefix(node.FullName); + + yield return node switch + { + VirtualDirectory directory => new PrefixedDirectory(_fs, path, directory), + _ => new PrefixedFile(_fs, path, (VirtualFile)node) + }; + } + } + + /// + protected override async IAsyncEnumerable GetFilesCoreAsync(string[] patterns, string[]? excludes, [EnumeratorCancellation] CancellationToken cancellationToken) + { + await foreach (var file in _directory.GetFilesAsync(patterns, excludes, cancellationToken).ConfigureAwait(false)) + { + var path = _fs.WrapWithPrefix(file.FullName); + yield return new PrefixedFile(_fs, path, file); + } + } + + /// + protected override async IAsyncEnumerable GetDirectoriesCoreAsync(string[] patterns, string[]? excludes, [EnumeratorCancellation] CancellationToken cancellationToken) + { + await foreach (var directory in _directory.GetDirectoriesAsync(patterns, excludes, cancellationToken).ConfigureAwait(false)) + { + var path = _fs.WrapWithPrefix(directory.FullName); + yield return new PrefixedDirectory(_fs, path, directory); + } + } +} diff --git a/src/Ramstack.FileSystem.Prefixed/PrefixedFile.cs b/src/Ramstack.FileSystem.Prefixed/PrefixedFile.cs index dfaaad6..1d4a9e6 100644 --- a/src/Ramstack.FileSystem.Prefixed/PrefixedFile.cs +++ b/src/Ramstack.FileSystem.Prefixed/PrefixedFile.cs @@ -1,51 +1,51 @@ -namespace Ramstack.FileSystem.Prefixed; - -/// -/// Represents a file in a , -/// wrapping an existing instance with a prefixed path. -/// -internal sealed class PrefixedFile : VirtualFile -{ - private readonly PrefixedFileSystem _fileSystem; - private readonly VirtualFile _file; - - /// - public override IVirtualFileSystem FileSystem => _fileSystem; - - /// - /// Initializes a new instance of the class. - /// - /// The file system associated with this file. - /// The prefixed path of the file. - /// The instance to wrap. - public PrefixedFile(PrefixedFileSystem fileSystem, string path, VirtualFile file) : base(path) => - (_fileSystem, _file) = (fileSystem, file); - - /// - protected override void RefreshCore() => - _file.Refresh(); - - /// - protected override ValueTask GetPropertiesCoreAsync(CancellationToken cancellationToken) => - _file.GetPropertiesAsync(refresh: true, cancellationToken)!; - - /// - protected override ValueTask ExistsCoreAsync(CancellationToken cancellationToken) => - _file.ExistsAsync(cancellationToken); - - /// - protected override ValueTask OpenReadCoreAsync(CancellationToken cancellationToken) => - _file.OpenReadAsync(cancellationToken); - - /// - protected override ValueTask OpenWriteCoreAsync(CancellationToken cancellationToken) => - _file.OpenWriteAsync(cancellationToken); - - /// - protected override ValueTask WriteCoreAsync(Stream stream, bool overwrite, CancellationToken cancellationToken) => - _file.WriteAsync(stream, overwrite, cancellationToken); - - /// - protected override ValueTask DeleteCoreAsync(CancellationToken cancellationToken) => - _file.DeleteAsync(cancellationToken); -} +namespace Ramstack.FileSystem.Prefixed; + +/// +/// Represents a file in a , +/// wrapping an existing instance with a prefixed path. +/// +internal sealed class PrefixedFile : VirtualFile +{ + private readonly PrefixedFileSystem _fileSystem; + private readonly VirtualFile _file; + + /// + public override IVirtualFileSystem FileSystem => _fileSystem; + + /// + /// Initializes a new instance of the class. + /// + /// The file system associated with this file. + /// The prefixed path of the file. + /// The instance to wrap. + public PrefixedFile(PrefixedFileSystem fileSystem, string path, VirtualFile file) : base(path) => + (_fileSystem, _file) = (fileSystem, file); + + /// + protected override void RefreshCore() => + _file.Refresh(); + + /// + protected override ValueTask GetPropertiesCoreAsync(CancellationToken cancellationToken) => + _file.GetPropertiesAsync(refresh: true, cancellationToken)!; + + /// + protected override ValueTask ExistsCoreAsync(CancellationToken cancellationToken) => + _file.ExistsAsync(cancellationToken); + + /// + protected override ValueTask OpenReadCoreAsync(CancellationToken cancellationToken) => + _file.OpenReadAsync(cancellationToken); + + /// + protected override ValueTask OpenWriteCoreAsync(CancellationToken cancellationToken) => + _file.OpenWriteAsync(cancellationToken); + + /// + protected override ValueTask WriteCoreAsync(Stream stream, bool overwrite, CancellationToken cancellationToken) => + _file.WriteAsync(stream, overwrite, cancellationToken); + + /// + protected override ValueTask DeleteCoreAsync(CancellationToken cancellationToken) => + _file.DeleteAsync(cancellationToken); +} diff --git a/src/Ramstack.FileSystem.Prefixed/PrefixedFileSystem.cs b/src/Ramstack.FileSystem.Prefixed/PrefixedFileSystem.cs index a9eb554..ef8dbfb 100644 --- a/src/Ramstack.FileSystem.Prefixed/PrefixedFileSystem.cs +++ b/src/Ramstack.FileSystem.Prefixed/PrefixedFileSystem.cs @@ -1,169 +1,169 @@ -using System.Diagnostics; - -using Ramstack.FileSystem.Null; -using Ramstack.FileSystem.Utilities; - -namespace Ramstack.FileSystem.Prefixed; - -/// -/// Represents an implementation of the that adds a specified prefix -/// to the file paths within the underlying file system. -/// -[DebuggerDisplay("{_prefix,nq}")] -public sealed class PrefixedFileSystem : IVirtualFileSystem -{ - private readonly string _prefix; - private readonly IVirtualFileSystem _fs; - private readonly VirtualDirectory[] _directories; - - /// - public bool IsReadOnly => _fs.IsReadOnly; - - /// - /// Initializes a new instance of the class. - /// - /// The prefix to be applied to the file paths managed by this instance. - /// The underlying file system that manages the files to which the prefix will be applied. - public PrefixedFileSystem(string prefix, IVirtualFileSystem fileSystem) - { - prefix = VirtualPath.Normalize(prefix); - (_prefix, _fs) = (prefix, fileSystem); - - // Create an artificial directory list - _directories = CreateArtificialDirectories(this, prefix); - - static VirtualDirectory[] CreateArtificialDirectories(PrefixedFileSystem fs, string path) - { - var directories = new List(); - VirtualDirectory? directory = null; - - while (!string.IsNullOrEmpty(path)) - { - directory = directory is null - ? new PrefixedDirectory(fs, path, fs._fs.GetDirectory("/")) - : new ArtificialDirectory(fs, path, directory); - - directories.Add(directory); - path = VirtualPath.GetDirectoryName(path); - } - - return directories.ToArray(); - } - } - - /// - public VirtualFile GetFile(string path) - { - path = VirtualPath.Normalize(path); - - var underlying = TryUnwrapPrefix(path, _prefix); - if (underlying is not null) - return new PrefixedFile(this, path, _fs.GetFile(underlying)); - - return new NotFoundFile(this, path); - } - - /// - public VirtualDirectory GetDirectory(string path) - { - path = VirtualPath.Normalize(path); - - foreach (var directory in _directories) - if (directory.FullName == path) - return directory; - - var underlying = TryUnwrapPrefix(path, _prefix); - if (underlying is not null) - return new PrefixedDirectory(this, path, _fs.GetDirectory(underlying)); - - return new NotFoundDirectory(this, path); - } - - /// - public void Dispose() => - _fs.Dispose(); - - /// - /// Converts a path from the wrapped file system to a full path in this prefixed file system. - /// - /// The path relative to the wrapped file system (must be normalized). - /// - /// The full path including this file system's prefix. - /// - internal string WrapWithPrefix(string underlyingPath) - { - Debug.Assert(VirtualPath.IsNormalized(underlyingPath)); - - if (underlyingPath == "/") - return _prefix; - - return VirtualPath.Join(_prefix, underlyingPath); - } - - /// - /// Attempts to extract the underlying path by removing this file system's prefix from a full path. - /// - /// The full path that may include this file system's prefix (must be normalized). - /// The prefix to compare against the path. - /// - /// The underlying path relative to the wrapped file system if the prefix matches; - /// otherwise, if the path doesn't belong to this prefixed file system. - /// - private static string? TryUnwrapPrefix(string path, string prefix) - { - Debug.Assert(VirtualPath.IsNormalized(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; - } - - #region Inner type: ArtificialDirectory - - /// - /// Represents an artificial directory within a file system. - /// - private sealed class ArtificialDirectory : VirtualDirectory - { - private readonly PrefixedFileSystem _fs; - private readonly VirtualDirectory _directory; - - /// - public override IVirtualFileSystem FileSystem => _fs; - - /// - /// Initializes a new instance of the class. - /// - /// The file system associated with this directory. - /// The path of the directory. - /// The child directory. - public ArtificialDirectory(PrefixedFileSystem fileSystem, string path, VirtualDirectory directory) : base(path) => - (_fs, _directory) = (fileSystem, directory); - - /// - protected override ValueTask GetPropertiesCoreAsync(CancellationToken cancellationToken) => - new ValueTask(VirtualNodeProperties.None); - - /// - protected override ValueTask CreateCoreAsync(CancellationToken cancellationToken) => - default; - - /// - protected override ValueTask DeleteCoreAsync(CancellationToken cancellationToken) => - throw new UnauthorizedAccessException($"Access to the path '{FullName}' is denied."); - - /// - protected override IAsyncEnumerable GetFileNodesCoreAsync(CancellationToken cancellationToken) => - new VirtualNode[] { _directory }.ToAsyncEnumerable(); - } - - #endregion -} +using System.Diagnostics; + +using Ramstack.FileSystem.Null; +using Ramstack.FileSystem.Utilities; + +namespace Ramstack.FileSystem.Prefixed; + +/// +/// Represents an implementation of the that adds a specified prefix +/// to the file paths within the underlying file system. +/// +[DebuggerDisplay("{_prefix,nq}")] +public sealed class PrefixedFileSystem : IVirtualFileSystem +{ + private readonly string _prefix; + private readonly IVirtualFileSystem _fs; + private readonly VirtualDirectory[] _directories; + + /// + public bool IsReadOnly => _fs.IsReadOnly; + + /// + /// Initializes a new instance of the class. + /// + /// The prefix to be applied to the file paths managed by this instance. + /// The underlying file system that manages the files to which the prefix will be applied. + public PrefixedFileSystem(string prefix, IVirtualFileSystem fileSystem) + { + prefix = VirtualPath.Normalize(prefix); + (_prefix, _fs) = (prefix, fileSystem); + + // Create an artificial directory list + _directories = CreateArtificialDirectories(this, prefix); + + static VirtualDirectory[] CreateArtificialDirectories(PrefixedFileSystem fs, string path) + { + var directories = new List(); + VirtualDirectory? directory = null; + + while (!string.IsNullOrEmpty(path)) + { + directory = directory is null + ? new PrefixedDirectory(fs, path, fs._fs.GetDirectory("/")) + : new ArtificialDirectory(fs, path, directory); + + directories.Add(directory); + path = VirtualPath.GetDirectoryName(path); + } + + return directories.ToArray(); + } + } + + /// + public VirtualFile GetFile(string path) + { + path = VirtualPath.Normalize(path); + + var underlying = TryUnwrapPrefix(path, _prefix); + if (underlying is not null) + return new PrefixedFile(this, path, _fs.GetFile(underlying)); + + return new NotFoundFile(this, path); + } + + /// + public VirtualDirectory GetDirectory(string path) + { + path = VirtualPath.Normalize(path); + + foreach (var directory in _directories) + if (directory.FullName == path) + return directory; + + var underlying = TryUnwrapPrefix(path, _prefix); + if (underlying is not null) + return new PrefixedDirectory(this, path, _fs.GetDirectory(underlying)); + + return new NotFoundDirectory(this, path); + } + + /// + public void Dispose() => + _fs.Dispose(); + + /// + /// Converts a path from the wrapped file system to a full path in this prefixed file system. + /// + /// The path relative to the wrapped file system (must be normalized). + /// + /// The full path including this file system's prefix. + /// + internal string WrapWithPrefix(string underlyingPath) + { + Debug.Assert(VirtualPath.IsNormalized(underlyingPath)); + + if (underlyingPath == "/") + return _prefix; + + return VirtualPath.Join(_prefix, underlyingPath); + } + + /// + /// Attempts to extract the underlying path by removing this file system's prefix from a full path. + /// + /// The full path that may include this file system's prefix (must be normalized). + /// The prefix to compare against the path. + /// + /// The underlying path relative to the wrapped file system if the prefix matches; + /// otherwise, if the path doesn't belong to this prefixed file system. + /// + private static string? TryUnwrapPrefix(string path, string prefix) + { + Debug.Assert(VirtualPath.IsNormalized(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; + } + + #region Inner type: ArtificialDirectory + + /// + /// Represents an artificial directory within a file system. + /// + private sealed class ArtificialDirectory : VirtualDirectory + { + private readonly PrefixedFileSystem _fs; + private readonly VirtualDirectory _directory; + + /// + public override IVirtualFileSystem FileSystem => _fs; + + /// + /// Initializes a new instance of the class. + /// + /// The file system associated with this directory. + /// The path of the directory. + /// The child directory. + public ArtificialDirectory(PrefixedFileSystem fileSystem, string path, VirtualDirectory directory) : base(path) => + (_fs, _directory) = (fileSystem, directory); + + /// + protected override ValueTask GetPropertiesCoreAsync(CancellationToken cancellationToken) => + new ValueTask(VirtualNodeProperties.None); + + /// + protected override ValueTask CreateCoreAsync(CancellationToken cancellationToken) => + default; + + /// + protected override ValueTask DeleteCoreAsync(CancellationToken cancellationToken) => + throw new UnauthorizedAccessException($"Access to the path '{FullName}' is denied."); + + /// + protected override IAsyncEnumerable GetFileNodesCoreAsync(CancellationToken cancellationToken) => + new VirtualNode[] { _directory }.ToAsyncEnumerable(); + } + + #endregion +} diff --git a/src/Ramstack.FileSystem.Prefixed/README.md b/src/Ramstack.FileSystem.Prefixed/README.md index 71d8da7..0edcc9b 100644 --- a/src/Ramstack.FileSystem.Prefixed/README.md +++ b/src/Ramstack.FileSystem.Prefixed/README.md @@ -1,59 +1,59 @@ -# Ramstack.FileSystem.Prefixed -[![NuGet](https://img.shields.io/nuget/v/Ramstack.FileSystem.Prefixed.svg)](https://nuget.org/packages/Ramstack.FileSystem.Prefixed) -[![MIT](https://img.shields.io/github/license/rameel/ramstack.filesystem)](https://github.com/rameel/ramstack.filesystem/blob/main/LICENSE) - -Provides an implementation of `Ramstack.FileSystem` that adds a specified prefix to the file paths within the underlying file system. - -## Getting Started - -To install the `Ramstack.FileSystem.Prefixed` [NuGet package](https://www.nuget.org/packages/Ramstack.FileSystem.Prefixed) -in your project, run the following command: -```console -dotnet add package Ramstack.FileSystem.Prefixed -``` -## Usage - -```csharp -using Ramstack.FileSystem.Prefixed; - -IVirtualFileSystem fileSystem = new PhysicalFileSystem(@"C:\path"); - -// Make all original files and directories accessible with the "/public/assets" prefix, -// as if they were originally located at that path. -// -// As an example, a file originally at "/hello.txt" will be accessed as "/public/assets/hello.txt". - -PrefixedFileSystem fs = new PrefixedFileSystem(prefix: "/public/assets", fileSystem); - -await foreach (VirtualFile file in fs.GetFilesAsync("/public/assets")) -{ - Console.WriteLine(file.FullName); -} -``` - -## Related Projects -- [Ramstack.FileSystem.Abstractions](https://www.nuget.org/packages/Ramstack.FileSystem.Abstractions) - Provides a virtual file system abstraction. -- [Ramstack.FileSystem.Physical](https://www.nuget.org/packages/Ramstack.FileSystem.Physical) - Provides an implementation based on the local file system. -- [Ramstack.FileSystem.Azure](https://www.nuget.org/packages/Ramstack.FileSystem.Azure) - Provides an implementation using Azure Blob Storage. -- [Ramstack.FileSystem.Amazon](https://www.nuget.org/packages/Ramstack.FileSystem.Amazon) - Provides an implementation using Amazon S3 storage. -- [Ramstack.FileSystem.Google](https://www.nuget.org/packages/Ramstack.FileSystem.Google) - Provides an implementation using Google Cloud Storage. -- [Ramstack.FileSystem.Readonly](https://www.nuget.org/packages/Ramstack.FileSystem.Readonly) - Provides a read-only wrapper for the underlying file system. -- [Ramstack.FileSystem.Globbing](https://www.nuget.org/packages/Ramstack.FileSystem.Globbing) - Wraps the file system, filtering files and directories using glob patterns. -- [Ramstack.FileSystem.Sub](https://www.nuget.org/packages/Ramstack.FileSystem.Sub) - Wraps the underlying file system, restricting access to a specific subpath. -- [Ramstack.FileSystem.Adapters](https://www.nuget.org/packages/Ramstack.FileSystem.Adapters) - Provides integration with `Microsoft.Extensions.FileProviders`. -- [Ramstack.FileSystem.Composite](https://www.nuget.org/packages/Ramstack.FileSystem.Composite) - Provides an implementation that combines multiple file systems into a single composite file system. - -## 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.virtualfiles/blob/main/LICENSE) file for more details. +# Ramstack.FileSystem.Prefixed +[![NuGet](https://img.shields.io/nuget/v/Ramstack.FileSystem.Prefixed.svg)](https://nuget.org/packages/Ramstack.FileSystem.Prefixed) +[![MIT](https://img.shields.io/github/license/rameel/ramstack.filesystem)](https://github.com/rameel/ramstack.filesystem/blob/main/LICENSE) + +Provides an implementation of `Ramstack.FileSystem` that adds a specified prefix to the file paths within the underlying file system. + +## Getting Started + +To install the `Ramstack.FileSystem.Prefixed` [NuGet package](https://www.nuget.org/packages/Ramstack.FileSystem.Prefixed) +in your project, run the following command: +```console +dotnet add package Ramstack.FileSystem.Prefixed +``` +## Usage + +```csharp +using Ramstack.FileSystem.Prefixed; + +IVirtualFileSystem fileSystem = new PhysicalFileSystem(@"C:\path"); + +// Make all original files and directories accessible with the "/public/assets" prefix, +// as if they were originally located at that path. +// +// As an example, a file originally at "/hello.txt" will be accessed as "/public/assets/hello.txt". + +PrefixedFileSystem fs = new PrefixedFileSystem(prefix: "/public/assets", fileSystem); + +await foreach (VirtualFile file in fs.GetFilesAsync("/public/assets")) +{ + Console.WriteLine(file.FullName); +} +``` + +## Related Projects +- [Ramstack.FileSystem.Abstractions](https://www.nuget.org/packages/Ramstack.FileSystem.Abstractions) - Provides a virtual file system abstraction. +- [Ramstack.FileSystem.Physical](https://www.nuget.org/packages/Ramstack.FileSystem.Physical) - Provides an implementation based on the local file system. +- [Ramstack.FileSystem.Azure](https://www.nuget.org/packages/Ramstack.FileSystem.Azure) - Provides an implementation using Azure Blob Storage. +- [Ramstack.FileSystem.Amazon](https://www.nuget.org/packages/Ramstack.FileSystem.Amazon) - Provides an implementation using Amazon S3 storage. +- [Ramstack.FileSystem.Google](https://www.nuget.org/packages/Ramstack.FileSystem.Google) - Provides an implementation using Google Cloud Storage. +- [Ramstack.FileSystem.Readonly](https://www.nuget.org/packages/Ramstack.FileSystem.Readonly) - Provides a read-only wrapper for the underlying file system. +- [Ramstack.FileSystem.Globbing](https://www.nuget.org/packages/Ramstack.FileSystem.Globbing) - Wraps the file system, filtering files and directories using glob patterns. +- [Ramstack.FileSystem.Sub](https://www.nuget.org/packages/Ramstack.FileSystem.Sub) - Wraps the underlying file system, restricting access to a specific subpath. +- [Ramstack.FileSystem.Adapters](https://www.nuget.org/packages/Ramstack.FileSystem.Adapters) - Provides integration with `Microsoft.Extensions.FileProviders`. +- [Ramstack.FileSystem.Composite](https://www.nuget.org/packages/Ramstack.FileSystem.Composite) - Provides an implementation that combines multiple file systems into a single composite file system. + +## 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.virtualfiles/blob/main/LICENSE) file for more details. diff --git a/src/Ramstack.FileSystem.Prefixed/Ramstack.FileSystem.Prefixed.csproj b/src/Ramstack.FileSystem.Prefixed/Ramstack.FileSystem.Prefixed.csproj index 9a911d8..dad5ac7 100644 --- a/src/Ramstack.FileSystem.Prefixed/Ramstack.FileSystem.Prefixed.csproj +++ b/src/Ramstack.FileSystem.Prefixed/Ramstack.FileSystem.Prefixed.csproj @@ -1,69 +1,69 @@ - - - - net6.0;net8.0 - Provides an implementation of Ramstack.FileSystem that adds a specified prefix to the file paths within the underlying file system. - 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.filesystem - Rameel - filesystem;fileprovider - https://github.com/rameel/ramstack.filesystem#readme - README.md - MIT - true - snupkg - true - true - true - true - - - - true - - - - - Properties\AssemblyInfo.cs - - - Utilities\EnumerableExtensions.cs - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - Properties\README.md - True - \ - - - - + + + + net6.0;net8.0 + Provides an implementation of Ramstack.FileSystem that adds a specified prefix to the file paths within the underlying file system. + 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.filesystem + Rameel + filesystem;fileprovider + https://github.com/rameel/ramstack.filesystem#readme + README.md + MIT + true + snupkg + true + true + true + true + + + + true + + + + + Properties\AssemblyInfo.cs + + + Utilities\EnumerableExtensions.cs + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + Properties\README.md + True + \ + + + + diff --git a/src/Ramstack.FileSystem.Readonly/README.md b/src/Ramstack.FileSystem.Readonly/README.md index 218b82e..f872c98 100644 --- a/src/Ramstack.FileSystem.Readonly/README.md +++ b/src/Ramstack.FileSystem.Readonly/README.md @@ -1,56 +1,56 @@ -# Ramstack.FileSystem.Readonly -[![NuGet](https://img.shields.io/nuget/v/Ramstack.FileSystem.Readonly.svg)](https://nuget.org/packages/Ramstack.FileSystem.Readonly) -[![MIT](https://img.shields.io/github/license/rameel/ramstack.filesystem)](https://github.com/rameel/ramstack.filesystem/blob/main/LICENSE) - -Provides an implementation of `Ramstack.FileSystem` that wraps the underlying file system, preventing any destructive operations. - -## Getting Started - -To install the `Ramstack.FileSystem.Readonly` [NuGet package](https://www.nuget.org/packages/Ramstack.FileSystem.Readonly) -in your project, run the following command: -```console -dotnet add package Ramstack.FileSystem.Readonly -``` -## Usage - -```csharp -using Ramstack.FileSystem.Readonly; - -IVirtualFileSystem fileSystem = new PhysicalFileSystem(@"C:\path"); -ReadonlyFileSystem fs = new ReadonlyFileSystem(fileSystem); - -await foreach (VirtualFile file in fs.GetFilesAsync("/")) -{ - Console.WriteLine(node.Name); -} - -// Throws an exception because the ReadonlyFileSystem does not support modifying operations -await fs.DeleteFileAsync("/hello.txt"); -``` - -## Related Projects -- [Ramstack.FileSystem.Abstractions](https://www.nuget.org/packages/Ramstack.FileSystem.Abstractions) - Provides a virtual file system abstraction. -- [Ramstack.FileSystem.Physical](https://www.nuget.org/packages/Ramstack.FileSystem.Physical) - Provides an implementation based on the local file system. -- [Ramstack.FileSystem.Azure](https://www.nuget.org/packages/Ramstack.FileSystem.Azure) - Provides an implementation using Azure Blob Storage. -- [Ramstack.FileSystem.Amazon](https://www.nuget.org/packages/Ramstack.FileSystem.Amazon) - Provides an implementation using Amazon S3 storage. -- [Ramstack.FileSystem.Google](https://www.nuget.org/packages/Ramstack.FileSystem.Google) - Provides an implementation using Google Cloud Storage. -- [Ramstack.FileSystem.Globbing](https://www.nuget.org/packages/Ramstack.FileSystem.Globbing) - Wraps the file system, filtering files and directories using glob patterns. -- [Ramstack.FileSystem.Prefixed](https://www.nuget.org/packages/Ramstack.FileSystem.Prefixed) - Adds a prefix to file paths within the underlying file system. -- [Ramstack.FileSystem.Sub](https://www.nuget.org/packages/Ramstack.FileSystem.Sub) - Wraps the underlying file system, restricting access to a specific subpath. -- [Ramstack.FileSystem.Adapters](https://www.nuget.org/packages/Ramstack.FileSystem.Adapters) - Provides integration with `Microsoft.Extensions.FileProviders`. -- [Ramstack.FileSystem.Composite](https://www.nuget.org/packages/Ramstack.FileSystem.Composite) - Provides an implementation that combines multiple file systems into a single composite file system. - -## 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.virtualfiles/blob/main/LICENSE) file for more details. +# Ramstack.FileSystem.Readonly +[![NuGet](https://img.shields.io/nuget/v/Ramstack.FileSystem.Readonly.svg)](https://nuget.org/packages/Ramstack.FileSystem.Readonly) +[![MIT](https://img.shields.io/github/license/rameel/ramstack.filesystem)](https://github.com/rameel/ramstack.filesystem/blob/main/LICENSE) + +Provides an implementation of `Ramstack.FileSystem` that wraps the underlying file system, preventing any destructive operations. + +## Getting Started + +To install the `Ramstack.FileSystem.Readonly` [NuGet package](https://www.nuget.org/packages/Ramstack.FileSystem.Readonly) +in your project, run the following command: +```console +dotnet add package Ramstack.FileSystem.Readonly +``` +## Usage + +```csharp +using Ramstack.FileSystem.Readonly; + +IVirtualFileSystem fileSystem = new PhysicalFileSystem(@"C:\path"); +ReadonlyFileSystem fs = new ReadonlyFileSystem(fileSystem); + +await foreach (VirtualFile file in fs.GetFilesAsync("/")) +{ + Console.WriteLine(node.Name); +} + +// Throws an exception because the ReadonlyFileSystem does not support modifying operations +await fs.DeleteFileAsync("/hello.txt"); +``` + +## Related Projects +- [Ramstack.FileSystem.Abstractions](https://www.nuget.org/packages/Ramstack.FileSystem.Abstractions) - Provides a virtual file system abstraction. +- [Ramstack.FileSystem.Physical](https://www.nuget.org/packages/Ramstack.FileSystem.Physical) - Provides an implementation based on the local file system. +- [Ramstack.FileSystem.Azure](https://www.nuget.org/packages/Ramstack.FileSystem.Azure) - Provides an implementation using Azure Blob Storage. +- [Ramstack.FileSystem.Amazon](https://www.nuget.org/packages/Ramstack.FileSystem.Amazon) - Provides an implementation using Amazon S3 storage. +- [Ramstack.FileSystem.Google](https://www.nuget.org/packages/Ramstack.FileSystem.Google) - Provides an implementation using Google Cloud Storage. +- [Ramstack.FileSystem.Globbing](https://www.nuget.org/packages/Ramstack.FileSystem.Globbing) - Wraps the file system, filtering files and directories using glob patterns. +- [Ramstack.FileSystem.Prefixed](https://www.nuget.org/packages/Ramstack.FileSystem.Prefixed) - Adds a prefix to file paths within the underlying file system. +- [Ramstack.FileSystem.Sub](https://www.nuget.org/packages/Ramstack.FileSystem.Sub) - Wraps the underlying file system, restricting access to a specific subpath. +- [Ramstack.FileSystem.Adapters](https://www.nuget.org/packages/Ramstack.FileSystem.Adapters) - Provides integration with `Microsoft.Extensions.FileProviders`. +- [Ramstack.FileSystem.Composite](https://www.nuget.org/packages/Ramstack.FileSystem.Composite) - Provides an implementation that combines multiple file systems into a single composite file system. + +## 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.virtualfiles/blob/main/LICENSE) file for more details. diff --git a/src/Ramstack.FileSystem.Readonly/Ramstack.FileSystem.Readonly.csproj b/src/Ramstack.FileSystem.Readonly/Ramstack.FileSystem.Readonly.csproj index a015533..e8c756c 100644 --- a/src/Ramstack.FileSystem.Readonly/Ramstack.FileSystem.Readonly.csproj +++ b/src/Ramstack.FileSystem.Readonly/Ramstack.FileSystem.Readonly.csproj @@ -1,66 +1,66 @@ - - - - net6.0;net8.0 - Provides an implementation of Ramstack.FileSystem that wraps the underlying file system, preventing any destructive operations. - 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.filesystem - Rameel - filesystem;fileprovider - https://github.com/rameel/ramstack.filesystem#readme - README.md - MIT - true - snupkg - true - true - true - true - - - - true - - - - - Properties\AssemblyInfo.cs - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - Properties\README.md - True - \ - - - - + + + + net6.0;net8.0 + Provides an implementation of Ramstack.FileSystem that wraps the underlying file system, preventing any destructive operations. + 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.filesystem + Rameel + filesystem;fileprovider + https://github.com/rameel/ramstack.filesystem#readme + README.md + MIT + true + snupkg + true + true + true + true + + + + true + + + + + Properties\AssemblyInfo.cs + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + Properties\README.md + True + \ + + + + diff --git a/src/Ramstack.FileSystem.Readonly/ReadonlyDirectory.cs b/src/Ramstack.FileSystem.Readonly/ReadonlyDirectory.cs index 068c74d..a2eb0b0 100644 --- a/src/Ramstack.FileSystem.Readonly/ReadonlyDirectory.cs +++ b/src/Ramstack.FileSystem.Readonly/ReadonlyDirectory.cs @@ -1,97 +1,97 @@ -using System.Runtime.CompilerServices; - -namespace Ramstack.FileSystem.Readonly; - -/// -/// Represents a read-only wrapper around an existing instance. -/// -internal sealed class ReadonlyDirectory : VirtualDirectory -{ - private readonly ReadonlyFileSystem _fs; - private readonly VirtualDirectory _directory; - - /// - public override IVirtualFileSystem FileSystem => _fs; - - /// - /// Initializes a new instance of the class. - /// - /// The file system associated with this directory. - /// The instance to wrap. - public ReadonlyDirectory(ReadonlyFileSystem fileSystem, VirtualDirectory directory) : base(directory.FullName) => - (_fs, _directory) = (fileSystem, directory); - - /// - protected override void RefreshCore() => - _directory.Refresh(); - - /// - protected override ValueTask GetPropertiesCoreAsync(CancellationToken cancellationToken) => - _directory.GetPropertiesAsync(cancellationToken)!; - - /// - protected override ValueTask ExistsCoreAsync(CancellationToken cancellationToken) => - _directory.ExistsAsync(cancellationToken); - - /// - protected override ValueTask CreateCoreAsync(CancellationToken cancellationToken) => - default; - - /// - protected override ValueTask DeleteCoreAsync(CancellationToken cancellationToken) => - default; - - /// - protected override async IAsyncEnumerable GetFileNodesCoreAsync([EnumeratorCancellation] CancellationToken cancellationToken) - { - await foreach (var node in _directory.GetFileNodesAsync(cancellationToken).ConfigureAwait(false)) - { - yield return node switch - { - VirtualDirectory directory => new ReadonlyDirectory(_fs, directory), - _ => new ReadonlyFile(_fs, (VirtualFile)node) - }; - } - } - - /// - protected override async IAsyncEnumerable GetFilesCoreAsync([EnumeratorCancellation] CancellationToken cancellationToken) - { - await foreach (var file in _directory.GetFilesAsync(cancellationToken).ConfigureAwait(false)) - yield return new ReadonlyFile(_fs, file); - } - - /// - protected override async IAsyncEnumerable GetDirectoriesCoreAsync([EnumeratorCancellation] CancellationToken cancellationToken) - { - await foreach (var directory in _directory.GetDirectoriesAsync(cancellationToken).ConfigureAwait(false)) - yield return new ReadonlyDirectory(_fs, directory); - } - - /// - protected override async IAsyncEnumerable GetFileNodesCoreAsync(string[] patterns, string[]? excludes, [EnumeratorCancellation] CancellationToken cancellationToken) - { - await foreach (var node in _directory.GetFileNodesAsync(patterns, excludes, cancellationToken).ConfigureAwait(false)) - { - yield return node switch - { - VirtualDirectory directory => new ReadonlyDirectory(_fs, directory), - _ => new ReadonlyFile(_fs, (VirtualFile)node) - }; - } - } - - /// - protected override async IAsyncEnumerable GetFilesCoreAsync(string[] patterns, string[]? excludes, [EnumeratorCancellation] CancellationToken cancellationToken) - { - await foreach (var file in _directory.GetFilesAsync(patterns, excludes, cancellationToken).ConfigureAwait(false)) - yield return new ReadonlyFile(_fs, file); - } - - /// - protected override async IAsyncEnumerable GetDirectoriesCoreAsync(string[] patterns, string[]? excludes, [EnumeratorCancellation] CancellationToken cancellationToken) - { - await foreach (var directory in _directory.GetDirectoriesAsync(patterns, excludes, cancellationToken).ConfigureAwait(false)) - yield return new ReadonlyDirectory(_fs, directory); - } -} +using System.Runtime.CompilerServices; + +namespace Ramstack.FileSystem.Readonly; + +/// +/// Represents a read-only wrapper around an existing instance. +/// +internal sealed class ReadonlyDirectory : VirtualDirectory +{ + private readonly ReadonlyFileSystem _fs; + private readonly VirtualDirectory _directory; + + /// + public override IVirtualFileSystem FileSystem => _fs; + + /// + /// Initializes a new instance of the class. + /// + /// The file system associated with this directory. + /// The instance to wrap. + public ReadonlyDirectory(ReadonlyFileSystem fileSystem, VirtualDirectory directory) : base(directory.FullName) => + (_fs, _directory) = (fileSystem, directory); + + /// + protected override void RefreshCore() => + _directory.Refresh(); + + /// + protected override ValueTask GetPropertiesCoreAsync(CancellationToken cancellationToken) => + _directory.GetPropertiesAsync(cancellationToken)!; + + /// + protected override ValueTask ExistsCoreAsync(CancellationToken cancellationToken) => + _directory.ExistsAsync(cancellationToken); + + /// + protected override ValueTask CreateCoreAsync(CancellationToken cancellationToken) => + default; + + /// + protected override ValueTask DeleteCoreAsync(CancellationToken cancellationToken) => + default; + + /// + protected override async IAsyncEnumerable GetFileNodesCoreAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + await foreach (var node in _directory.GetFileNodesAsync(cancellationToken).ConfigureAwait(false)) + { + yield return node switch + { + VirtualDirectory directory => new ReadonlyDirectory(_fs, directory), + _ => new ReadonlyFile(_fs, (VirtualFile)node) + }; + } + } + + /// + protected override async IAsyncEnumerable GetFilesCoreAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + await foreach (var file in _directory.GetFilesAsync(cancellationToken).ConfigureAwait(false)) + yield return new ReadonlyFile(_fs, file); + } + + /// + protected override async IAsyncEnumerable GetDirectoriesCoreAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + await foreach (var directory in _directory.GetDirectoriesAsync(cancellationToken).ConfigureAwait(false)) + yield return new ReadonlyDirectory(_fs, directory); + } + + /// + protected override async IAsyncEnumerable GetFileNodesCoreAsync(string[] patterns, string[]? excludes, [EnumeratorCancellation] CancellationToken cancellationToken) + { + await foreach (var node in _directory.GetFileNodesAsync(patterns, excludes, cancellationToken).ConfigureAwait(false)) + { + yield return node switch + { + VirtualDirectory directory => new ReadonlyDirectory(_fs, directory), + _ => new ReadonlyFile(_fs, (VirtualFile)node) + }; + } + } + + /// + protected override async IAsyncEnumerable GetFilesCoreAsync(string[] patterns, string[]? excludes, [EnumeratorCancellation] CancellationToken cancellationToken) + { + await foreach (var file in _directory.GetFilesAsync(patterns, excludes, cancellationToken).ConfigureAwait(false)) + yield return new ReadonlyFile(_fs, file); + } + + /// + protected override async IAsyncEnumerable GetDirectoriesCoreAsync(string[] patterns, string[]? excludes, [EnumeratorCancellation] CancellationToken cancellationToken) + { + await foreach (var directory in _directory.GetDirectoriesAsync(patterns, excludes, cancellationToken).ConfigureAwait(false)) + yield return new ReadonlyDirectory(_fs, directory); + } +} diff --git a/src/Ramstack.FileSystem.Readonly/ReadonlyFile.cs b/src/Ramstack.FileSystem.Readonly/ReadonlyFile.cs index 2a9522b..3140b9c 100644 --- a/src/Ramstack.FileSystem.Readonly/ReadonlyFile.cs +++ b/src/Ramstack.FileSystem.Readonly/ReadonlyFile.cs @@ -1,49 +1,49 @@ -namespace Ramstack.FileSystem.Readonly; - -/// -/// Represents a read-only wrapper around an existing instance. -/// -internal sealed class ReadonlyFile : VirtualFile -{ - private readonly ReadonlyFileSystem _fileSystem; - private readonly VirtualFile _file; - - /// - public override IVirtualFileSystem FileSystem => _fileSystem; - - /// - /// Initializes a new instance of the class. - /// - /// The file system associated with this file. - /// The instance to wrap. - public ReadonlyFile(ReadonlyFileSystem fileSystem, VirtualFile file) : base(file.FullName) => - (_fileSystem, _file) = (fileSystem, file); - - /// - protected override void RefreshCore() => - _file.Refresh(); - - /// - protected override ValueTask GetPropertiesCoreAsync(CancellationToken cancellationToken) => - _file.GetPropertiesAsync(true, cancellationToken)!; - - /// - protected override ValueTask ExistsCoreAsync(CancellationToken cancellationToken) => - _file.ExistsAsync(cancellationToken); - - /// - protected override ValueTask OpenReadCoreAsync(CancellationToken cancellationToken) => - _file.OpenReadAsync(cancellationToken); - - /// - protected override ValueTask OpenWriteCoreAsync(CancellationToken cancellationToken) => - default; - - /// - protected override ValueTask WriteCoreAsync(Stream stream, bool overwrite, CancellationToken cancellationToken) => - default; - - /// - protected override ValueTask DeleteCoreAsync(CancellationToken cancellationToken) => - default; -} +namespace Ramstack.FileSystem.Readonly; + +/// +/// Represents a read-only wrapper around an existing instance. +/// +internal sealed class ReadonlyFile : VirtualFile +{ + private readonly ReadonlyFileSystem _fileSystem; + private readonly VirtualFile _file; + + /// + public override IVirtualFileSystem FileSystem => _fileSystem; + + /// + /// Initializes a new instance of the class. + /// + /// The file system associated with this file. + /// The instance to wrap. + public ReadonlyFile(ReadonlyFileSystem fileSystem, VirtualFile file) : base(file.FullName) => + (_fileSystem, _file) = (fileSystem, file); + + /// + protected override void RefreshCore() => + _file.Refresh(); + + /// + protected override ValueTask GetPropertiesCoreAsync(CancellationToken cancellationToken) => + _file.GetPropertiesAsync(true, cancellationToken)!; + + /// + protected override ValueTask ExistsCoreAsync(CancellationToken cancellationToken) => + _file.ExistsAsync(cancellationToken); + + /// + protected override ValueTask OpenReadCoreAsync(CancellationToken cancellationToken) => + _file.OpenReadAsync(cancellationToken); + + /// + protected override ValueTask OpenWriteCoreAsync(CancellationToken cancellationToken) => + default; + + /// + protected override ValueTask WriteCoreAsync(Stream stream, bool overwrite, CancellationToken cancellationToken) => + default; + + /// + protected override ValueTask DeleteCoreAsync(CancellationToken cancellationToken) => + default; +} diff --git a/src/Ramstack.FileSystem.Readonly/ReadonlyFileSystem.cs b/src/Ramstack.FileSystem.Readonly/ReadonlyFileSystem.cs index 32c6f02..0b76590 100644 --- a/src/Ramstack.FileSystem.Readonly/ReadonlyFileSystem.cs +++ b/src/Ramstack.FileSystem.Readonly/ReadonlyFileSystem.cs @@ -1,32 +1,32 @@ -namespace Ramstack.FileSystem.Readonly; - -/// -/// Represents a read-only wrapper around an existing instance. -/// This wrapper prevents modifications to the underlying file system. -/// -public sealed class ReadonlyFileSystem : IVirtualFileSystem -{ - private readonly IVirtualFileSystem _fs; - - /// - public bool IsReadOnly => true; - - /// - /// Initializes a new instance of the class. - /// - /// The underlying file system to wrap as read-only. - public ReadonlyFileSystem(IVirtualFileSystem fileSystem) => - _fs = fileSystem; - - /// - public VirtualDirectory GetDirectory(string path) => - new ReadonlyDirectory(this, _fs.GetDirectory(path)); - - /// - public VirtualFile GetFile(string path) => - new ReadonlyFile(this, _fs.GetFile(path)); - - /// - public void Dispose() => - _fs.Dispose(); -} +namespace Ramstack.FileSystem.Readonly; + +/// +/// Represents a read-only wrapper around an existing instance. +/// This wrapper prevents modifications to the underlying file system. +/// +public sealed class ReadonlyFileSystem : IVirtualFileSystem +{ + private readonly IVirtualFileSystem _fs; + + /// + public bool IsReadOnly => true; + + /// + /// Initializes a new instance of the class. + /// + /// The underlying file system to wrap as read-only. + public ReadonlyFileSystem(IVirtualFileSystem fileSystem) => + _fs = fileSystem; + + /// + public VirtualDirectory GetDirectory(string path) => + new ReadonlyDirectory(this, _fs.GetDirectory(path)); + + /// + public VirtualFile GetFile(string path) => + new ReadonlyFile(this, _fs.GetFile(path)); + + /// + public void Dispose() => + _fs.Dispose(); +} diff --git a/src/Ramstack.FileSystem.Sub/README.md b/src/Ramstack.FileSystem.Sub/README.md index ffdf81d..2b6ff6b 100644 --- a/src/Ramstack.FileSystem.Sub/README.md +++ b/src/Ramstack.FileSystem.Sub/README.md @@ -1,56 +1,56 @@ -# Ramstack.FileSystem.Sub -[![NuGet](https://img.shields.io/nuget/v/Ramstack.FileSystem.Sub.svg)](https://nuget.org/packages/Ramstack.FileSystem.Sub) -[![MIT](https://img.shields.io/github/license/rameel/ramstack.filesystem)](https://github.com/rameel/ramstack.filesystem/blob/main/LICENSE) - -Provides an implementation of `Ramstack.FileSystem` that wraps an underlying file system for managing files under a specific subpath. - -## Getting Started - -To install the `Ramstack.FileSystem.Sub` [NuGet package](https://www.nuget.org/packages/Ramstack.FileSystem.Sub) -in your project, run the following command: -```console -dotnet add package Ramstack.FileSystem.Sub -``` -## Usage - -```csharp -using Ramstack.FileSystem.Sub; - -IVirtualFileSystem fileSystem = new PhysicalFileSystem(@"C:\project"); - -// Restrict the file system to only the contents within the "/app/assets" directory -// of the parent file system. Files and directories above the specified path will be inaccessible. -SubFileSystem fs = new SubFileSystem(path: "/app/assets", fileSystem); - -await foreach (VirtualFile file in fs.GetFilesAsync("/")) -{ - Console.WriteLine(file.FullName); -} -``` - -## Related Projects -- [Ramstack.FileSystem.Abstractions](https://www.nuget.org/packages/Ramstack.FileSystem.Abstractions) - Provides a virtual file system abstraction. -- [Ramstack.FileSystem.Physical](https://www.nuget.org/packages/Ramstack.FileSystem.Physical) - Provides an implementation based on the local file system. -- [Ramstack.FileSystem.Azure](https://www.nuget.org/packages/Ramstack.FileSystem.Azure) - Provides an implementation using Azure Blob Storage. -- [Ramstack.FileSystem.Amazon](https://www.nuget.org/packages/Ramstack.FileSystem.Amazon) - Provides an implementation using Amazon S3 storage. -- [Ramstack.FileSystem.Google](https://www.nuget.org/packages/Ramstack.FileSystem.Google) - Provides an implementation using Google Cloud Storage. -- [Ramstack.FileSystem.Readonly](https://www.nuget.org/packages/Ramstack.FileSystem.Readonly) - Provides a read-only wrapper for the underlying file system. -- [Ramstack.FileSystem.Globbing](https://www.nuget.org/packages/Ramstack.FileSystem.Globbing) - Wraps the file system, filtering files and directories using glob patterns. -- [Ramstack.FileSystem.Prefixed](https://www.nuget.org/packages/Ramstack.FileSystem.Prefixed) - Adds a prefix to file paths within the underlying file system. -- [Ramstack.FileSystem.Adapters](https://www.nuget.org/packages/Ramstack.FileSystem.Adapters) - Provides integration with `Microsoft.Extensions.FileProviders`. -- [Ramstack.FileSystem.Composite](https://www.nuget.org/packages/Ramstack.FileSystem.Composite) - Provides an implementation that combines multiple file systems into a single composite file system. - -## 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.virtualfiles/blob/main/LICENSE) file for more details. +# Ramstack.FileSystem.Sub +[![NuGet](https://img.shields.io/nuget/v/Ramstack.FileSystem.Sub.svg)](https://nuget.org/packages/Ramstack.FileSystem.Sub) +[![MIT](https://img.shields.io/github/license/rameel/ramstack.filesystem)](https://github.com/rameel/ramstack.filesystem/blob/main/LICENSE) + +Provides an implementation of `Ramstack.FileSystem` that wraps an underlying file system for managing files under a specific subpath. + +## Getting Started + +To install the `Ramstack.FileSystem.Sub` [NuGet package](https://www.nuget.org/packages/Ramstack.FileSystem.Sub) +in your project, run the following command: +```console +dotnet add package Ramstack.FileSystem.Sub +``` +## Usage + +```csharp +using Ramstack.FileSystem.Sub; + +IVirtualFileSystem fileSystem = new PhysicalFileSystem(@"C:\project"); + +// Restrict the file system to only the contents within the "/app/assets" directory +// of the parent file system. Files and directories above the specified path will be inaccessible. +SubFileSystem fs = new SubFileSystem(path: "/app/assets", fileSystem); + +await foreach (VirtualFile file in fs.GetFilesAsync("/")) +{ + Console.WriteLine(file.FullName); +} +``` + +## Related Projects +- [Ramstack.FileSystem.Abstractions](https://www.nuget.org/packages/Ramstack.FileSystem.Abstractions) - Provides a virtual file system abstraction. +- [Ramstack.FileSystem.Physical](https://www.nuget.org/packages/Ramstack.FileSystem.Physical) - Provides an implementation based on the local file system. +- [Ramstack.FileSystem.Azure](https://www.nuget.org/packages/Ramstack.FileSystem.Azure) - Provides an implementation using Azure Blob Storage. +- [Ramstack.FileSystem.Amazon](https://www.nuget.org/packages/Ramstack.FileSystem.Amazon) - Provides an implementation using Amazon S3 storage. +- [Ramstack.FileSystem.Google](https://www.nuget.org/packages/Ramstack.FileSystem.Google) - Provides an implementation using Google Cloud Storage. +- [Ramstack.FileSystem.Readonly](https://www.nuget.org/packages/Ramstack.FileSystem.Readonly) - Provides a read-only wrapper for the underlying file system. +- [Ramstack.FileSystem.Globbing](https://www.nuget.org/packages/Ramstack.FileSystem.Globbing) - Wraps the file system, filtering files and directories using glob patterns. +- [Ramstack.FileSystem.Prefixed](https://www.nuget.org/packages/Ramstack.FileSystem.Prefixed) - Adds a prefix to file paths within the underlying file system. +- [Ramstack.FileSystem.Adapters](https://www.nuget.org/packages/Ramstack.FileSystem.Adapters) - Provides integration with `Microsoft.Extensions.FileProviders`. +- [Ramstack.FileSystem.Composite](https://www.nuget.org/packages/Ramstack.FileSystem.Composite) - Provides an implementation that combines multiple file systems into a single composite file system. + +## 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.virtualfiles/blob/main/LICENSE) file for more details. diff --git a/src/Ramstack.FileSystem.Sub/Ramstack.FileSystem.Sub.csproj b/src/Ramstack.FileSystem.Sub/Ramstack.FileSystem.Sub.csproj index 62806de..ade88ac 100644 --- a/src/Ramstack.FileSystem.Sub/Ramstack.FileSystem.Sub.csproj +++ b/src/Ramstack.FileSystem.Sub/Ramstack.FileSystem.Sub.csproj @@ -1,66 +1,66 @@ - - - - net6.0;net8.0 - Provides an implementation of Ramstack.FileSystem that wraps an underlying file system for managing files under a specific subpath. - 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.filesystem - Rameel - filesystem;fileprovider - https://github.com/rameel/ramstack.filesystem#readme - README.md - MIT - true - snupkg - true - true - true - true - - - - true - - - - - Properties\AssemblyInfo.cs - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - Properties\README.md - True - \ - - - - + + + + net6.0;net8.0 + Provides an implementation of Ramstack.FileSystem that wraps an underlying file system for managing files under a specific subpath. + 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.filesystem + Rameel + filesystem;fileprovider + https://github.com/rameel/ramstack.filesystem#readme + README.md + MIT + true + snupkg + true + true + true + true + + + + true + + + + + Properties\AssemblyInfo.cs + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + Properties\README.md + True + \ + + + + diff --git a/src/Ramstack.FileSystem.Sub/SubDirectory.cs b/src/Ramstack.FileSystem.Sub/SubDirectory.cs index 0fdd339..e555239 100644 --- a/src/Ramstack.FileSystem.Sub/SubDirectory.cs +++ b/src/Ramstack.FileSystem.Sub/SubDirectory.cs @@ -1,114 +1,114 @@ -using System.Runtime.CompilerServices; - -namespace Ramstack.FileSystem.Sub; - -/// -/// Represents a wrapper around an existing instance. -/// -internal sealed class SubDirectory : VirtualDirectory -{ - private readonly SubFileSystem _fs; - private readonly VirtualDirectory _directory; - - /// - public override IVirtualFileSystem FileSystem => _fs; - - /// - /// Initializes a new instance of the class. - /// - /// The file system associated with this directory. - /// The path of the directory. - /// The underlying instance to wrap. - public SubDirectory(SubFileSystem fileSystem, string path, VirtualDirectory directory) : base(path) => - (_fs, _directory) = (fileSystem, directory); - - /// - protected override void RefreshCore() => - _directory.Refresh(); - - /// - protected override ValueTask GetPropertiesCoreAsync(CancellationToken cancellationToken) => - _directory.GetPropertiesAsync(cancellationToken)!; - - /// - protected override ValueTask ExistsCoreAsync(CancellationToken cancellationToken) => - _directory.ExistsAsync(cancellationToken); - - /// - protected override ValueTask CreateCoreAsync(CancellationToken cancellationToken) => - _directory.CreateAsync(cancellationToken); - - /// - protected override ValueTask DeleteCoreAsync(CancellationToken cancellationToken) => - _directory.DeleteAsync(cancellationToken); - - /// - protected override async IAsyncEnumerable GetFileNodesCoreAsync([EnumeratorCancellation] CancellationToken cancellationToken) - { - await foreach (var node in _directory.GetFileNodesAsync(cancellationToken).ConfigureAwait(false)) - { - var path = VirtualPath.Join(FullName, node.Name); - - yield return node switch - { - VirtualDirectory directory => new SubDirectory(_fs, path, directory), - _ => new SubFile(_fs, path, (VirtualFile)node) - }; - } - } - - /// - protected override async IAsyncEnumerable GetFilesCoreAsync([EnumeratorCancellation] CancellationToken cancellationToken) - { - await foreach (var file in _directory.GetFilesAsync(cancellationToken).ConfigureAwait(false)) - { - var path = VirtualPath.Join(FullName, file.Name); - yield return new SubFile(_fs, path, file); - } - } - - /// - protected override async IAsyncEnumerable GetDirectoriesCoreAsync([EnumeratorCancellation] CancellationToken cancellationToken) - { - await foreach (var directory in _directory.GetDirectoriesAsync(cancellationToken).ConfigureAwait(false)) - { - var path = VirtualPath.Join(FullName, directory.Name); - yield return new SubDirectory(_fs, path, directory); - } - } - - /// - protected override async IAsyncEnumerable GetFileNodesCoreAsync(string[] patterns, string[]? excludes, [EnumeratorCancellation] CancellationToken cancellationToken) - { - await foreach (var node in _directory.GetFileNodesAsync(patterns, excludes, cancellationToken).ConfigureAwait(false)) - { - var path = _fs.ConvertToSubPath(node.FullName); - - yield return node switch - { - VirtualDirectory directory => new SubDirectory(_fs, path, directory), - _ => new SubFile(_fs, path, (VirtualFile)node) - }; - } - } - - /// - protected override async IAsyncEnumerable GetFilesCoreAsync(string[] patterns, string[]? excludes, [EnumeratorCancellation] CancellationToken cancellationToken) - { - await foreach (var file in _directory.GetFilesAsync(patterns, excludes, cancellationToken).ConfigureAwait(false)) - { - var path = _fs.ConvertToSubPath(file.FullName); - yield return new SubFile(_fs, path, file); - } - } - - /// - protected override async IAsyncEnumerable GetDirectoriesCoreAsync(string[] patterns, string[]? excludes, [EnumeratorCancellation] CancellationToken cancellationToken) - { - await foreach (var directory in _directory.GetDirectoriesAsync(patterns, excludes, cancellationToken).ConfigureAwait(false)) - { - var path = _fs.ConvertToSubPath(directory.FullName); - yield return new SubDirectory(_fs, path, directory); - } - } -} +using System.Runtime.CompilerServices; + +namespace Ramstack.FileSystem.Sub; + +/// +/// Represents a wrapper around an existing instance. +/// +internal sealed class SubDirectory : VirtualDirectory +{ + private readonly SubFileSystem _fs; + private readonly VirtualDirectory _directory; + + /// + public override IVirtualFileSystem FileSystem => _fs; + + /// + /// Initializes a new instance of the class. + /// + /// The file system associated with this directory. + /// The path of the directory. + /// The underlying instance to wrap. + public SubDirectory(SubFileSystem fileSystem, string path, VirtualDirectory directory) : base(path) => + (_fs, _directory) = (fileSystem, directory); + + /// + protected override void RefreshCore() => + _directory.Refresh(); + + /// + protected override ValueTask GetPropertiesCoreAsync(CancellationToken cancellationToken) => + _directory.GetPropertiesAsync(cancellationToken)!; + + /// + protected override ValueTask ExistsCoreAsync(CancellationToken cancellationToken) => + _directory.ExistsAsync(cancellationToken); + + /// + protected override ValueTask CreateCoreAsync(CancellationToken cancellationToken) => + _directory.CreateAsync(cancellationToken); + + /// + protected override ValueTask DeleteCoreAsync(CancellationToken cancellationToken) => + _directory.DeleteAsync(cancellationToken); + + /// + protected override async IAsyncEnumerable GetFileNodesCoreAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + await foreach (var node in _directory.GetFileNodesAsync(cancellationToken).ConfigureAwait(false)) + { + var path = VirtualPath.Join(FullName, node.Name); + + yield return node switch + { + VirtualDirectory directory => new SubDirectory(_fs, path, directory), + _ => new SubFile(_fs, path, (VirtualFile)node) + }; + } + } + + /// + protected override async IAsyncEnumerable GetFilesCoreAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + await foreach (var file in _directory.GetFilesAsync(cancellationToken).ConfigureAwait(false)) + { + var path = VirtualPath.Join(FullName, file.Name); + yield return new SubFile(_fs, path, file); + } + } + + /// + protected override async IAsyncEnumerable GetDirectoriesCoreAsync([EnumeratorCancellation] CancellationToken cancellationToken) + { + await foreach (var directory in _directory.GetDirectoriesAsync(cancellationToken).ConfigureAwait(false)) + { + var path = VirtualPath.Join(FullName, directory.Name); + yield return new SubDirectory(_fs, path, directory); + } + } + + /// + protected override async IAsyncEnumerable GetFileNodesCoreAsync(string[] patterns, string[]? excludes, [EnumeratorCancellation] CancellationToken cancellationToken) + { + await foreach (var node in _directory.GetFileNodesAsync(patterns, excludes, cancellationToken).ConfigureAwait(false)) + { + var path = _fs.ConvertToSubPath(node.FullName); + + yield return node switch + { + VirtualDirectory directory => new SubDirectory(_fs, path, directory), + _ => new SubFile(_fs, path, (VirtualFile)node) + }; + } + } + + /// + protected override async IAsyncEnumerable GetFilesCoreAsync(string[] patterns, string[]? excludes, [EnumeratorCancellation] CancellationToken cancellationToken) + { + await foreach (var file in _directory.GetFilesAsync(patterns, excludes, cancellationToken).ConfigureAwait(false)) + { + var path = _fs.ConvertToSubPath(file.FullName); + yield return new SubFile(_fs, path, file); + } + } + + /// + protected override async IAsyncEnumerable GetDirectoriesCoreAsync(string[] patterns, string[]? excludes, [EnumeratorCancellation] CancellationToken cancellationToken) + { + await foreach (var directory in _directory.GetDirectoriesAsync(patterns, excludes, cancellationToken).ConfigureAwait(false)) + { + var path = _fs.ConvertToSubPath(directory.FullName); + yield return new SubDirectory(_fs, path, directory); + } + } +} diff --git a/src/Ramstack.FileSystem.Sub/SubFile.cs b/src/Ramstack.FileSystem.Sub/SubFile.cs index 7799915..14e320c 100644 --- a/src/Ramstack.FileSystem.Sub/SubFile.cs +++ b/src/Ramstack.FileSystem.Sub/SubFile.cs @@ -1,50 +1,50 @@ -namespace Ramstack.FileSystem.Sub; - -/// -/// Represents a wrapper around an existing instance. -/// -internal sealed class SubFile : VirtualFile -{ - private readonly SubFileSystem _fileSystem; - private readonly VirtualFile _file; - - /// - public override IVirtualFileSystem FileSystem => _fileSystem; - - /// - /// Initializes a new instance of the class. - /// - /// The file system associated with this file. - /// The path of the file. - /// The instance to wrap. - public SubFile(SubFileSystem fileSystem, string path, VirtualFile file) : base(path) => - (_fileSystem, _file) = (fileSystem, file); - - /// - protected override void RefreshCore() => - _file.Refresh(); - - /// - protected override ValueTask GetPropertiesCoreAsync(CancellationToken cancellationToken) => - _file.GetPropertiesAsync(refresh: true, cancellationToken)!; - - /// - protected override ValueTask ExistsCoreAsync(CancellationToken cancellationToken) => - _file.ExistsAsync(cancellationToken); - - /// - protected override ValueTask OpenReadCoreAsync(CancellationToken cancellationToken) => - _file.OpenReadAsync(cancellationToken); - - /// - protected override ValueTask OpenWriteCoreAsync(CancellationToken cancellationToken) => - _file.OpenWriteAsync(cancellationToken); - - /// - protected override ValueTask WriteCoreAsync(Stream stream, bool overwrite, CancellationToken cancellationToken) => - _file.WriteAsync(stream, overwrite, cancellationToken); - - /// - protected override ValueTask DeleteCoreAsync(CancellationToken cancellationToken) => - _file.DeleteAsync(cancellationToken); -} +namespace Ramstack.FileSystem.Sub; + +/// +/// Represents a wrapper around an existing instance. +/// +internal sealed class SubFile : VirtualFile +{ + private readonly SubFileSystem _fileSystem; + private readonly VirtualFile _file; + + /// + public override IVirtualFileSystem FileSystem => _fileSystem; + + /// + /// Initializes a new instance of the class. + /// + /// The file system associated with this file. + /// The path of the file. + /// The instance to wrap. + public SubFile(SubFileSystem fileSystem, string path, VirtualFile file) : base(path) => + (_fileSystem, _file) = (fileSystem, file); + + /// + protected override void RefreshCore() => + _file.Refresh(); + + /// + protected override ValueTask GetPropertiesCoreAsync(CancellationToken cancellationToken) => + _file.GetPropertiesAsync(refresh: true, cancellationToken)!; + + /// + protected override ValueTask ExistsCoreAsync(CancellationToken cancellationToken) => + _file.ExistsAsync(cancellationToken); + + /// + protected override ValueTask OpenReadCoreAsync(CancellationToken cancellationToken) => + _file.OpenReadAsync(cancellationToken); + + /// + protected override ValueTask OpenWriteCoreAsync(CancellationToken cancellationToken) => + _file.OpenWriteAsync(cancellationToken); + + /// + protected override ValueTask WriteCoreAsync(Stream stream, bool overwrite, CancellationToken cancellationToken) => + _file.WriteAsync(stream, overwrite, cancellationToken); + + /// + protected override ValueTask DeleteCoreAsync(CancellationToken cancellationToken) => + _file.DeleteAsync(cancellationToken); +} diff --git a/src/Ramstack.FileSystem.Sub/SubFileSystem.cs b/src/Ramstack.FileSystem.Sub/SubFileSystem.cs index 77cce48..d67f294 100644 --- a/src/Ramstack.FileSystem.Sub/SubFileSystem.cs +++ b/src/Ramstack.FileSystem.Sub/SubFileSystem.cs @@ -1,100 +1,100 @@ -using System.Diagnostics; - -namespace Ramstack.FileSystem.Sub; - -/// -/// Represents a file system that manages a subset of files -/// located 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 system. -/// -[DebuggerDisplay("{_root,nq}")] -public sealed class SubFileSystem : IVirtualFileSystem -{ - private readonly IVirtualFileSystem _fs; - private readonly string _root; - - /// - public bool IsReadOnly => _fs.IsReadOnly; - - /// - /// Initializes a new instance of the class. - /// - /// The path under the root directory of the . - /// The underlying file system. - public SubFileSystem(string path, IVirtualFileSystem fileSystem) => - (_root, _fs) = (VirtualPath.Normalize(path), fileSystem); - - /// - public VirtualFile GetFile(string path) - { - path = VirtualPath.Normalize(path); - - var underlyingPath = ConvertToUnderlyingPath(path); - var file = _fs.GetFile(underlyingPath); - - return new SubFile(this, path, file); - } - - /// - public VirtualDirectory GetDirectory(string path) - { - path = VirtualPath.Normalize(path); - - var underlyingPath = ConvertToUnderlyingPath(path); - var directory = _fs.GetDirectory(underlyingPath); - - return new SubDirectory(this, path, directory); - } - - /// - public void Dispose() => - _fs.Dispose(); - - /// - /// Converts a path from the underlying file system to a path relative to this file system's root. - /// - /// The absolute path within the parent file system. - /// Must be normalized and start with this file system's root path. - /// - /// The corresponding path within this file system. - /// - /// - /// For example, converts "/app/assets/images/logo.png" to "/images/logo.png", - /// where "/app/assets" is this file system's root. - /// - internal string ConvertToSubPath(string underlyingPath) - { - Debug.Assert(VirtualPath.IsNormalized(underlyingPath)); - Debug.Assert(underlyingPath.StartsWith(_root, StringComparison.Ordinal)); - - if (underlyingPath == _root) - return "/"; - - return new string(underlyingPath.AsSpan(_root.Length)); - } - - /// - /// Converts a path from this file system to the corresponding absolute path in the underlying file system. - /// - /// The path within this file system. - /// Must be normalized (e.g., "/" or "/images/logo.png"). - /// - /// The absolute path in the underlying file system that corresponds to this file system path. - /// - /// - /// For example, converts "/images/logo.png" to "/app/assets/images/logo.png", - /// where "/app/assets" is this file system's root. - /// - private string ConvertToUnderlyingPath(string path) - { - Debug.Assert(VirtualPath.IsNormalized(path)); - - if (path == "/") - return _root; - - return VirtualPath.Join(_root, path); - } -} +using System.Diagnostics; + +namespace Ramstack.FileSystem.Sub; + +/// +/// Represents a file system that manages a subset of files +/// located 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 system. +/// +[DebuggerDisplay("{_root,nq}")] +public sealed class SubFileSystem : IVirtualFileSystem +{ + private readonly IVirtualFileSystem _fs; + private readonly string _root; + + /// + public bool IsReadOnly => _fs.IsReadOnly; + + /// + /// Initializes a new instance of the class. + /// + /// The path under the root directory of the . + /// The underlying file system. + public SubFileSystem(string path, IVirtualFileSystem fileSystem) => + (_root, _fs) = (VirtualPath.Normalize(path), fileSystem); + + /// + public VirtualFile GetFile(string path) + { + path = VirtualPath.Normalize(path); + + var underlyingPath = ConvertToUnderlyingPath(path); + var file = _fs.GetFile(underlyingPath); + + return new SubFile(this, path, file); + } + + /// + public VirtualDirectory GetDirectory(string path) + { + path = VirtualPath.Normalize(path); + + var underlyingPath = ConvertToUnderlyingPath(path); + var directory = _fs.GetDirectory(underlyingPath); + + return new SubDirectory(this, path, directory); + } + + /// + public void Dispose() => + _fs.Dispose(); + + /// + /// Converts a path from the underlying file system to a path relative to this file system's root. + /// + /// The absolute path within the parent file system. + /// Must be normalized and start with this file system's root path. + /// + /// The corresponding path within this file system. + /// + /// + /// For example, converts "/app/assets/images/logo.png" to "/images/logo.png", + /// where "/app/assets" is this file system's root. + /// + internal string ConvertToSubPath(string underlyingPath) + { + Debug.Assert(VirtualPath.IsNormalized(underlyingPath)); + Debug.Assert(underlyingPath.StartsWith(_root, StringComparison.Ordinal)); + + if (underlyingPath == _root) + return "/"; + + return new string(underlyingPath.AsSpan(_root.Length)); + } + + /// + /// Converts a path from this file system to the corresponding absolute path in the underlying file system. + /// + /// The path within this file system. + /// Must be normalized (e.g., "/" or "/images/logo.png"). + /// + /// The absolute path in the underlying file system that corresponds to this file system path. + /// + /// + /// For example, converts "/images/logo.png" to "/app/assets/images/logo.png", + /// where "/app/assets" is this file system's root. + /// + private string ConvertToUnderlyingPath(string path) + { + Debug.Assert(VirtualPath.IsNormalized(path)); + + if (path == "/") + return _root; + + return VirtualPath.Join(_root, path); + } +} diff --git a/tests/Ramstack.FileSystem.Abstractions.Tests/Ramstack.FileSystem.Abstractions.Tests.csproj b/tests/Ramstack.FileSystem.Abstractions.Tests/Ramstack.FileSystem.Abstractions.Tests.csproj index c2df622..058b3dd 100644 --- a/tests/Ramstack.FileSystem.Abstractions.Tests/Ramstack.FileSystem.Abstractions.Tests.csproj +++ b/tests/Ramstack.FileSystem.Abstractions.Tests/Ramstack.FileSystem.Abstractions.Tests.csproj @@ -1,26 +1,26 @@ - - - - net6.0;net8.0 - enable - enable - preview - Ramstack.FileSystem - - - - - - - - - - - - - - - - - - + + + + net6.0;net8.0 + enable + enable + preview + Ramstack.FileSystem + + + + + + + + + + + + + + + + + + diff --git a/tests/Ramstack.FileSystem.Abstractions.Tests/VirtualPathTests.cs b/tests/Ramstack.FileSystem.Abstractions.Tests/VirtualPathTests.cs index 8ccae68..c146cec 100644 --- a/tests/Ramstack.FileSystem.Abstractions.Tests/VirtualPathTests.cs +++ b/tests/Ramstack.FileSystem.Abstractions.Tests/VirtualPathTests.cs @@ -1,148 +1,148 @@ -namespace Ramstack.FileSystem; - -[TestFixture] -public class VirtualPathTests -{ - [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(VirtualPath.GetExtension(p), Is.EqualTo(expected)); - Assert.That(VirtualPath.GetExtension(p.AsSpan()).ToString(), 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(VirtualPath.GetFileName(p), Is.EqualTo(expected)); - Assert.That(VirtualPath.GetFileName(p.AsSpan()).ToString(), 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(VirtualPath.GetDirectoryName(p), Is.EqualTo(expected)); - Assert.That(VirtualPath.GetDirectoryName(p.AsSpan()).ToString(), 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) => - VirtualPath.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(VirtualPath.Normalize(p), Is.EqualTo(expected)); - } - - [TestCase("/", "/", "//")] - [TestCase("", "/", "/")] - [TestCase("/", "", "/")] - [TestCase("a", "b", "a/b")] - [TestCase("a/", "b", "a/b")] - [TestCase("a", "/b", "a/b")] - [TestCase("a/", "/b", "a//b")] - [TestCase("a", "", "a")] - [TestCase("", "a", "a")] - [TestCase("/a", "/b", "/a/b")] - [TestCase("a", "/b", "a/b")] - [TestCase("/a/", "/b/", "/a//b/")] - - [TestCase("a\\", "b", "a\\b")] - [TestCase("a", "\\b", "a\\b")] - [TestCase("a\\", "\\b", "a\\\\b")] - [TestCase("a\\", "/b", "a\\/b")] - [TestCase("a/", "\\b", "a/\\b")] - - [TestCase("a/", "/", "a//")] - [TestCase("/", "/a", "//a")] - public void Join(string a, string b, string expected) => - Assert.That(VirtualPath.Join(a, b), Is.EqualTo(expected)); - - private static string[] GetPathVariations(string path) => - [path, path.Replace('/', '\\')]; -} +namespace Ramstack.FileSystem; + +[TestFixture] +public class VirtualPathTests +{ + [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(VirtualPath.GetExtension(p), Is.EqualTo(expected)); + Assert.That(VirtualPath.GetExtension(p.AsSpan()).ToString(), 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(VirtualPath.GetFileName(p), Is.EqualTo(expected)); + Assert.That(VirtualPath.GetFileName(p.AsSpan()).ToString(), 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(VirtualPath.GetDirectoryName(p), Is.EqualTo(expected)); + Assert.That(VirtualPath.GetDirectoryName(p.AsSpan()).ToString(), 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) => + VirtualPath.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(VirtualPath.Normalize(p), Is.EqualTo(expected)); + } + + [TestCase("/", "/", "//")] + [TestCase("", "/", "/")] + [TestCase("/", "", "/")] + [TestCase("a", "b", "a/b")] + [TestCase("a/", "b", "a/b")] + [TestCase("a", "/b", "a/b")] + [TestCase("a/", "/b", "a//b")] + [TestCase("a", "", "a")] + [TestCase("", "a", "a")] + [TestCase("/a", "/b", "/a/b")] + [TestCase("a", "/b", "a/b")] + [TestCase("/a/", "/b/", "/a//b/")] + + [TestCase("a\\", "b", "a\\b")] + [TestCase("a", "\\b", "a\\b")] + [TestCase("a\\", "\\b", "a\\\\b")] + [TestCase("a\\", "/b", "a\\/b")] + [TestCase("a/", "\\b", "a/\\b")] + + [TestCase("a/", "/", "a//")] + [TestCase("/", "/a", "//a")] + public void Join(string a, string b, string expected) => + Assert.That(VirtualPath.Join(a, b), Is.EqualTo(expected)); + + private static string[] GetPathVariations(string path) => + [path, path.Replace('/', '\\')]; +} diff --git a/tests/Ramstack.FileSystem.Amazon.Tests/Ramstack.FileSystem.Amazon.Tests.csproj b/tests/Ramstack.FileSystem.Amazon.Tests/Ramstack.FileSystem.Amazon.Tests.csproj index 1d3871d..b62f21b 100644 --- a/tests/Ramstack.FileSystem.Amazon.Tests/Ramstack.FileSystem.Amazon.Tests.csproj +++ b/tests/Ramstack.FileSystem.Amazon.Tests/Ramstack.FileSystem.Amazon.Tests.csproj @@ -1,28 +1,28 @@ - - - - net6.0;net8.0 - enable - enable - preview - Ramstack.FileSystem.Amazon - - - - - - - - - - - - - - - - - - - - + + + + net6.0;net8.0 + enable + enable + preview + Ramstack.FileSystem.Amazon + + + + + + + + + + + + + + + + + + + + diff --git a/tests/Ramstack.FileSystem.Amazon.Tests/ReadonlyAmazonFileSystemTests.cs b/tests/Ramstack.FileSystem.Amazon.Tests/ReadonlyAmazonFileSystemTests.cs index 0267c0b..27fa805 100644 --- a/tests/Ramstack.FileSystem.Amazon.Tests/ReadonlyAmazonFileSystemTests.cs +++ b/tests/Ramstack.FileSystem.Amazon.Tests/ReadonlyAmazonFileSystemTests.cs @@ -1,60 +1,60 @@ -using Amazon; -using Amazon.Runtime; -using Amazon.S3; - -using Ramstack.FileSystem.Specification.Tests; -using Ramstack.FileSystem.Specification.Tests.Utilities; - -namespace Ramstack.FileSystem.Amazon; - -[TestFixture] -[Category("Cloud:Amazon")] -public class ReadonlyAmazonFileSystemTests : VirtualFileSystemSpecificationTests -{ - private readonly TempFileStorage _storage = new TempFileStorage(); - private readonly string _storageName = Guid.NewGuid().ToString("N"); - - [OneTimeSetUp] - public async Task Setup() - { - using var fs = CreateFileSystem(isReadOnly: false); - await fs.CreateBucketAsync(); - - foreach (var path in Directory.EnumerateFiles(_storage.Root, "*", SearchOption.AllDirectories)) - { - await using var stream = File.OpenRead(path); - await fs.WriteAsync(path[_storage.Root.Length..], stream, overwrite: true); - } - } - - [OneTimeTearDown] - public async Task Cleanup() - { - _storage.Dispose(); - - using var fs = CreateFileSystem(isReadOnly: false); - await fs.DeleteDirectoryAsync("/"); - } - - protected override AmazonS3FileSystem GetFileSystem() => - CreateFileSystem(isReadOnly: true); - - protected override DirectoryInfo GetDirectoryInfo() => - new DirectoryInfo(_storage.Root); - - private AmazonS3FileSystem CreateFileSystem(bool isReadOnly) - { - var credentials = new BasicAWSCredentials("rustfsadmin", "rustfsadmin"); - var config = new AmazonS3Config - { - RegionEndpoint = RegionEndpoint.USEast1, - ServiceURL = "http://localhost:9000", - ForcePathStyle = true, - }; - - return new AmazonS3FileSystem(credentials, config, bucketName: _storageName) - { - IsReadOnly = isReadOnly - }; - } -} +using Amazon; +using Amazon.Runtime; +using Amazon.S3; + +using Ramstack.FileSystem.Specification.Tests; +using Ramstack.FileSystem.Specification.Tests.Utilities; + +namespace Ramstack.FileSystem.Amazon; + +[TestFixture] +[Category("Cloud:Amazon")] +public class ReadonlyAmazonFileSystemTests : VirtualFileSystemSpecificationTests +{ + private readonly TempFileStorage _storage = new TempFileStorage(); + private readonly string _storageName = Guid.NewGuid().ToString("N"); + + [OneTimeSetUp] + public async Task Setup() + { + using var fs = CreateFileSystem(isReadOnly: false); + await fs.CreateBucketAsync(); + + foreach (var path in Directory.EnumerateFiles(_storage.Root, "*", SearchOption.AllDirectories)) + { + await using var stream = File.OpenRead(path); + await fs.WriteAsync(path[_storage.Root.Length..], stream, overwrite: true); + } + } + + [OneTimeTearDown] + public async Task Cleanup() + { + _storage.Dispose(); + + using var fs = CreateFileSystem(isReadOnly: false); + await fs.DeleteDirectoryAsync("/"); + } + + protected override AmazonS3FileSystem GetFileSystem() => + CreateFileSystem(isReadOnly: true); + + protected override DirectoryInfo GetDirectoryInfo() => + new DirectoryInfo(_storage.Root); + + private AmazonS3FileSystem CreateFileSystem(bool isReadOnly) + { + var credentials = new BasicAWSCredentials("rustfsadmin", "rustfsadmin"); + var config = new AmazonS3Config + { + RegionEndpoint = RegionEndpoint.USEast1, + ServiceURL = "http://localhost:9000", + ForcePathStyle = true, + }; + + return new AmazonS3FileSystem(credentials, config, bucketName: _storageName) + { + IsReadOnly = isReadOnly + }; + } +} diff --git a/tests/Ramstack.FileSystem.Amazon.Tests/WritableAmazonFileSystemTests.cs b/tests/Ramstack.FileSystem.Amazon.Tests/WritableAmazonFileSystemTests.cs index 64d7eff..6fdbc49 100644 --- a/tests/Ramstack.FileSystem.Amazon.Tests/WritableAmazonFileSystemTests.cs +++ b/tests/Ramstack.FileSystem.Amazon.Tests/WritableAmazonFileSystemTests.cs @@ -1,330 +1,330 @@ -using System.Reflection; - -using Amazon; -using Amazon.Runtime; -using Amazon.S3; - -using Ramstack.FileSystem.Physical; -using Ramstack.FileSystem.Specification.Tests; -using Ramstack.FileSystem.Specification.Tests.Utilities; - -namespace Ramstack.FileSystem.Amazon; - -[TestFixture] -[Category("Cloud:Amazon")] -public class WritableAmazonFileSystemTests : VirtualFileSystemSpecificationTests -{ - private readonly HashSet _buckets = []; - private readonly TempFileStorage _storage = new TempFileStorage(); - private readonly string _storageName = Guid.NewGuid().ToString("N"); - - [OneTimeSetUp] - public async Task Setup() - { - using var fs = GetFileSystem(); - await fs.CreateBucketAsync(); - - foreach (var path in Directory.EnumerateFiles(_storage.Root, "*", SearchOption.AllDirectories)) - { - await using var stream = File.OpenRead(path); - await fs.WriteAsync(path[_storage.Root.Length..], stream, overwrite: true); - } - } - - [OneTimeTearDown] - public async Task Cleanup() - { - _storage.Dispose(); - - foreach (var name in _buckets.ToArray()) - { - using var fs = CreateFileSystem(name); - - try - { - await fs.DeleteDirectoryAsync("/"); - } - catch (Exception e) - { - Console.WriteLine(e); - } - } - } - - [Test] - public async Task File_OpenWrite_InternalBufferWriteError_DoesNotCreateFile() - { - using var fs = GetFileSystem(); - - await using (var stream = await fs.OpenWriteAsync("/error.log")) - { - var underlying = (FileStream)stream.GetType().GetField("_stream", BindingFlags.NonPublic | BindingFlags.Instance)!.GetValue(stream)!; - Assert.That(underlying, Is.Not.Null); - - // Write enough data to trigger automatic part upload (>= 5 MiB). - await stream.WriteAsync(new byte[6 * 1024 * 1024]); - - // Simulates an internal buffer write error. - await underlying.DisposeAsync(); - - try - { - await stream.WriteAsync(new byte[1024]); - Assert.Fail(); - } - catch - { - // Ignore - } - } - - Assert.That( - await fs.FileExistsAsync("/error.log"), - Is.False); - } - - [Test] - public async Task File_WriteHugeFile() - { - const int TotalLines = 500; - - using var fs = GetFileSystem(); - - var value = new string('a', 100 * 1024); - - { - await using var stream = await fs.OpenWriteAsync("/test.txt"); - await using var writer = new StreamWriter(stream); - - for (var i = 0; i < TotalLines; i++) - await writer.WriteLineAsync(value); - } - - { - var file = fs.GetFile("/test.txt"); - - Assert.That( - await file.ExistsAsync(), - Is.True); - - Assert.That( - await file.GetLengthAsync(), - Is.EqualTo(TotalLines * value.Length + TotalLines * Environment.NewLine.Length)); - - await using var stream = await file.OpenReadAsync(); - using var reader = new StreamReader(stream); - - var count = 0; - - while (!reader.EndOfStream) - { - Assert.That( - await reader.ReadLineAsync(), - Is.EqualTo(value)); - - count++; - } - - Assert.That(count, Is.EqualTo(TotalLines)); - - await file.DeleteAsync(); - - Assert.That( - await file.ExistsAsync(), - Is.False); - } - } - - [Test] - public async Task File_CopyTo_File_DifferentFileSystems() - { - using var fs1 = new PhysicalFileSystem(_storage.Root); - using var fs2 = GetFileSystem(); - - var source = fs1.GetFile("/0000.txt"); - var destination = fs2.GetFile("/0000.txt"); - - var content = Guid.NewGuid().ToString(); - - await using (var stream = await source.OpenWriteAsync()) - await using (var writer = new StreamWriter(stream)) - await writer.WriteAsync(content); - - Assert.That(await destination.ExistsAsync(), Is.False); - - await source.CopyToAsync(destination); - Assert.That(await destination.ExistsAsync(), Is.True); - - using var reader = await destination.OpenTextAsync(); - Assert.That( - await reader.ReadToEndAsync(), - Is.EqualTo(content)); - - await source.DeleteAsync(); - await destination.DeleteAsync(); - } - - [Test] - public async Task File_CopyTo_File_DifferentStorages() - { - using var fs1 = CreateFileSystem(Guid.NewGuid().ToString("N")); - using var fs2 = GetFileSystem(); - - await fs1.CreateBucketAsync(); - - var source = fs1.GetFile("/1111.txt"); - var destination = fs2.GetFile("/1111.txt"); - - var content = Guid.NewGuid().ToString(); - - await using (var stream = await source.OpenWriteAsync()) - await using (var writer = new StreamWriter(stream)) - await writer.WriteAsync(content); - - Assert.That(await destination.ExistsAsync(), Is.False); - - await source.CopyToAsync(destination); - Assert.That(await destination.ExistsAsync(), Is.True); - - using var reader = await destination.OpenTextAsync(); - Assert.That( - await reader.ReadToEndAsync(), - Is.EqualTo(content)); - - await source.DeleteAsync(); - await destination.DeleteAsync(); - } - - [Test] - public async Task File_OpenWrite_FlushDoesNotCauseUndersizedParts() - { - using var fs = GetFileSystem(); - - const string Content = "Hello, World!"; - - { - await using var stream = await fs.OpenWriteAsync("/flush-test.txt"); - await using var writer = new StreamWriter(stream); - - // Write small data and flush multiple times. - // Flush should be a no-op and not upload undersized parts. - foreach (var ch in Content) - { - await writer.WriteAsync(ch); - await writer.FlushAsync(); - } - } - { - // ReSharper disable once UseAwaitUsing - using var stream = await fs.OpenReadAsync("/flush-test.txt"); - using var reader = new StreamReader(stream); - - Assert.That(await reader.ReadToEndAsync(), Is.EqualTo(Content)); - } - - await fs.DeleteFileAsync("/flush-test.txt"); - } - - [Test] - public async Task File_OpenWrite_FlushWithMultipartUpload() - { - using var fs = GetFileSystem(); - - const int Count = 5; - const string FileName = "/flush-multipart-test.bin"; - - var chunk = new byte[3 * 1024 * 1024]; - Random.Shared.NextBytes(chunk); - - { - await using var stream = await fs.OpenWriteAsync(FileName); - for (var i = 0; i < Count; i++) - await stream.WriteAsync(chunk); - } - { - var file = fs.GetFile(FileName); - - Assert.That(await file.ExistsAsync(), Is.True); - Assert.That(await file.GetLengthAsync(), Is.EqualTo(chunk.Length * Count)); - - // ReSharper disable once UseAwaitUsing - using var stream = await file.OpenReadAsync(); - - var bytes = new byte[chunk.Length]; - - for (var i = 0; i < Count; i++) - { - var n = await ReadBlockAsync(stream, bytes); - Assert.That(n, Is.EqualTo(bytes.Length)); - - Assert.That( - bytes.AsSpan().SequenceEqual(chunk), - Is.True); - } - } - - await fs.DeleteFileAsync(FileName); - - static async Task ReadBlockAsync(Stream stream, Memory memory) - { - var count = memory.Length; - - while (!memory.IsEmpty) - { - var n = await stream.ReadAsync(memory); - if (n == 0) - return 0; - - memory = memory[n..]; - } - - return count; - } - } - - [Test] - public async Task Directory_BatchDeleting() - { - // 1. The maximum list page size is 1000 items. - // 2. The delete batch supports a maximum of 1000 keys. - // - // We'll set the count slightly higher to test pagination - // and batching functionality. - const int Count = 1100; - - using var fs = GetFileSystem(); - for (var i = 0; i < Count; i++) - await fs.WriteAsync($"/temp/{i:0000}", Stream.Null); - - Assert.That( - await fs.GetFilesAsync("/temp").CountAsync(), - Is.EqualTo(Count)); - - await fs.DeleteDirectoryAsync("/temp"); - - Assert.That( - await fs.GetFilesAsync("/temp").AnyAsync(), - Is.False); - } - - protected override AmazonS3FileSystem GetFileSystem() => - CreateFileSystem(_storageName); - - protected override DirectoryInfo GetDirectoryInfo() => - new DirectoryInfo(_storage.Root); - - private AmazonS3FileSystem CreateFileSystem(string storageName) - { - _buckets.Add(storageName); - - return new AmazonS3FileSystem( - new BasicAWSCredentials("rustfsadmin", "rustfsadmin"), - new AmazonS3Config - { - RegionEndpoint = RegionEndpoint.USEast1, - ServiceURL = "http://localhost:9000", - ForcePathStyle = true - }, - bucketName: storageName); - } -} +using System.Reflection; + +using Amazon; +using Amazon.Runtime; +using Amazon.S3; + +using Ramstack.FileSystem.Physical; +using Ramstack.FileSystem.Specification.Tests; +using Ramstack.FileSystem.Specification.Tests.Utilities; + +namespace Ramstack.FileSystem.Amazon; + +[TestFixture] +[Category("Cloud:Amazon")] +public class WritableAmazonFileSystemTests : VirtualFileSystemSpecificationTests +{ + private readonly HashSet _buckets = []; + private readonly TempFileStorage _storage = new TempFileStorage(); + private readonly string _storageName = Guid.NewGuid().ToString("N"); + + [OneTimeSetUp] + public async Task Setup() + { + using var fs = GetFileSystem(); + await fs.CreateBucketAsync(); + + foreach (var path in Directory.EnumerateFiles(_storage.Root, "*", SearchOption.AllDirectories)) + { + await using var stream = File.OpenRead(path); + await fs.WriteAsync(path[_storage.Root.Length..], stream, overwrite: true); + } + } + + [OneTimeTearDown] + public async Task Cleanup() + { + _storage.Dispose(); + + foreach (var name in _buckets.ToArray()) + { + using var fs = CreateFileSystem(name); + + try + { + await fs.DeleteDirectoryAsync("/"); + } + catch (Exception e) + { + Console.WriteLine(e); + } + } + } + + [Test] + public async Task File_OpenWrite_InternalBufferWriteError_DoesNotCreateFile() + { + using var fs = GetFileSystem(); + + await using (var stream = await fs.OpenWriteAsync("/error.log")) + { + var underlying = (FileStream)stream.GetType().GetField("_stream", BindingFlags.NonPublic | BindingFlags.Instance)!.GetValue(stream)!; + Assert.That(underlying, Is.Not.Null); + + // Write enough data to trigger automatic part upload (>= 5 MiB). + await stream.WriteAsync(new byte[6 * 1024 * 1024]); + + // Simulates an internal buffer write error. + await underlying.DisposeAsync(); + + try + { + await stream.WriteAsync(new byte[1024]); + Assert.Fail(); + } + catch + { + // Ignore + } + } + + Assert.That( + await fs.FileExistsAsync("/error.log"), + Is.False); + } + + [Test] + public async Task File_WriteHugeFile() + { + const int TotalLines = 500; + + using var fs = GetFileSystem(); + + var value = new string('a', 100 * 1024); + + { + await using var stream = await fs.OpenWriteAsync("/test.txt"); + await using var writer = new StreamWriter(stream); + + for (var i = 0; i < TotalLines; i++) + await writer.WriteLineAsync(value); + } + + { + var file = fs.GetFile("/test.txt"); + + Assert.That( + await file.ExistsAsync(), + Is.True); + + Assert.That( + await file.GetLengthAsync(), + Is.EqualTo(TotalLines * value.Length + TotalLines * Environment.NewLine.Length)); + + await using var stream = await file.OpenReadAsync(); + using var reader = new StreamReader(stream); + + var count = 0; + + while (!reader.EndOfStream) + { + Assert.That( + await reader.ReadLineAsync(), + Is.EqualTo(value)); + + count++; + } + + Assert.That(count, Is.EqualTo(TotalLines)); + + await file.DeleteAsync(); + + Assert.That( + await file.ExistsAsync(), + Is.False); + } + } + + [Test] + public async Task File_CopyTo_File_DifferentFileSystems() + { + using var fs1 = new PhysicalFileSystem(_storage.Root); + using var fs2 = GetFileSystem(); + + var source = fs1.GetFile("/0000.txt"); + var destination = fs2.GetFile("/0000.txt"); + + var content = Guid.NewGuid().ToString(); + + await using (var stream = await source.OpenWriteAsync()) + await using (var writer = new StreamWriter(stream)) + await writer.WriteAsync(content); + + Assert.That(await destination.ExistsAsync(), Is.False); + + await source.CopyToAsync(destination); + Assert.That(await destination.ExistsAsync(), Is.True); + + using var reader = await destination.OpenTextAsync(); + Assert.That( + await reader.ReadToEndAsync(), + Is.EqualTo(content)); + + await source.DeleteAsync(); + await destination.DeleteAsync(); + } + + [Test] + public async Task File_CopyTo_File_DifferentStorages() + { + using var fs1 = CreateFileSystem(Guid.NewGuid().ToString("N")); + using var fs2 = GetFileSystem(); + + await fs1.CreateBucketAsync(); + + var source = fs1.GetFile("/1111.txt"); + var destination = fs2.GetFile("/1111.txt"); + + var content = Guid.NewGuid().ToString(); + + await using (var stream = await source.OpenWriteAsync()) + await using (var writer = new StreamWriter(stream)) + await writer.WriteAsync(content); + + Assert.That(await destination.ExistsAsync(), Is.False); + + await source.CopyToAsync(destination); + Assert.That(await destination.ExistsAsync(), Is.True); + + using var reader = await destination.OpenTextAsync(); + Assert.That( + await reader.ReadToEndAsync(), + Is.EqualTo(content)); + + await source.DeleteAsync(); + await destination.DeleteAsync(); + } + + [Test] + public async Task File_OpenWrite_FlushDoesNotCauseUndersizedParts() + { + using var fs = GetFileSystem(); + + const string Content = "Hello, World!"; + + { + await using var stream = await fs.OpenWriteAsync("/flush-test.txt"); + await using var writer = new StreamWriter(stream); + + // Write small data and flush multiple times. + // Flush should be a no-op and not upload undersized parts. + foreach (var ch in Content) + { + await writer.WriteAsync(ch); + await writer.FlushAsync(); + } + } + { + // ReSharper disable once UseAwaitUsing + using var stream = await fs.OpenReadAsync("/flush-test.txt"); + using var reader = new StreamReader(stream); + + Assert.That(await reader.ReadToEndAsync(), Is.EqualTo(Content)); + } + + await fs.DeleteFileAsync("/flush-test.txt"); + } + + [Test] + public async Task File_OpenWrite_FlushWithMultipartUpload() + { + using var fs = GetFileSystem(); + + const int Count = 5; + const string FileName = "/flush-multipart-test.bin"; + + var chunk = new byte[3 * 1024 * 1024]; + Random.Shared.NextBytes(chunk); + + { + await using var stream = await fs.OpenWriteAsync(FileName); + for (var i = 0; i < Count; i++) + await stream.WriteAsync(chunk); + } + { + var file = fs.GetFile(FileName); + + Assert.That(await file.ExistsAsync(), Is.True); + Assert.That(await file.GetLengthAsync(), Is.EqualTo(chunk.Length * Count)); + + // ReSharper disable once UseAwaitUsing + using var stream = await file.OpenReadAsync(); + + var bytes = new byte[chunk.Length]; + + for (var i = 0; i < Count; i++) + { + var n = await ReadBlockAsync(stream, bytes); + Assert.That(n, Is.EqualTo(bytes.Length)); + + Assert.That( + bytes.AsSpan().SequenceEqual(chunk), + Is.True); + } + } + + await fs.DeleteFileAsync(FileName); + + static async Task ReadBlockAsync(Stream stream, Memory memory) + { + var count = memory.Length; + + while (!memory.IsEmpty) + { + var n = await stream.ReadAsync(memory); + if (n == 0) + return 0; + + memory = memory[n..]; + } + + return count; + } + } + + [Test] + public async Task Directory_BatchDeleting() + { + // 1. The maximum list page size is 1000 items. + // 2. The delete batch supports a maximum of 1000 keys. + // + // We'll set the count slightly higher to test pagination + // and batching functionality. + const int Count = 1100; + + using var fs = GetFileSystem(); + for (var i = 0; i < Count; i++) + await fs.WriteAsync($"/temp/{i:0000}", Stream.Null); + + Assert.That( + await fs.GetFilesAsync("/temp").CountAsync(), + Is.EqualTo(Count)); + + await fs.DeleteDirectoryAsync("/temp"); + + Assert.That( + await fs.GetFilesAsync("/temp").AnyAsync(), + Is.False); + } + + protected override AmazonS3FileSystem GetFileSystem() => + CreateFileSystem(_storageName); + + protected override DirectoryInfo GetDirectoryInfo() => + new DirectoryInfo(_storage.Root); + + private AmazonS3FileSystem CreateFileSystem(string storageName) + { + _buckets.Add(storageName); + + return new AmazonS3FileSystem( + new BasicAWSCredentials("rustfsadmin", "rustfsadmin"), + new AmazonS3Config + { + RegionEndpoint = RegionEndpoint.USEast1, + ServiceURL = "http://localhost:9000", + ForcePathStyle = true + }, + bucketName: storageName); + } +} diff --git a/tests/Ramstack.FileSystem.Azure.Tests/Ramstack.FileSystem.Azure.Tests.csproj b/tests/Ramstack.FileSystem.Azure.Tests/Ramstack.FileSystem.Azure.Tests.csproj index 1365519..ef81ea0 100644 --- a/tests/Ramstack.FileSystem.Azure.Tests/Ramstack.FileSystem.Azure.Tests.csproj +++ b/tests/Ramstack.FileSystem.Azure.Tests/Ramstack.FileSystem.Azure.Tests.csproj @@ -1,28 +1,28 @@ - - - - net6.0;net8.0 - enable - enable - preview - Ramstack.FileSystem.Azure - - - - - - - - - - - - - - - - - - - - + + + + net6.0;net8.0 + enable + enable + preview + Ramstack.FileSystem.Azure + + + + + + + + + + + + + + + + + + + + diff --git a/tests/Ramstack.FileSystem.Azure.Tests/ReadonlyAzureFileSystemTests.cs b/tests/Ramstack.FileSystem.Azure.Tests/ReadonlyAzureFileSystemTests.cs index 8d51ba2..2b72d30 100644 --- a/tests/Ramstack.FileSystem.Azure.Tests/ReadonlyAzureFileSystemTests.cs +++ b/tests/Ramstack.FileSystem.Azure.Tests/ReadonlyAzureFileSystemTests.cs @@ -1,51 +1,51 @@ -using Ramstack.FileSystem.Specification.Tests; -using Ramstack.FileSystem.Specification.Tests.Utilities; - -namespace Ramstack.FileSystem.Azure; - -[TestFixture] -[Category("Cloud:Azure")] -public class ReadonlyAzureFileSystemTests : VirtualFileSystemSpecificationTests -{ - private readonly TempFileStorage _storage = new(); - private readonly string _storageName = Guid.NewGuid().ToString("N"); - - [OneTimeSetUp] - public async Task Setup() - { - using var fs = CreateFileSystem(isReadonly: false); - - await fs.CreateContainerAsync(); - - foreach (var path in Directory.EnumerateFiles(_storage.Root, "*", SearchOption.AllDirectories)) - { - await using var stream = File.OpenRead(path); - await fs.WriteAsync(path[_storage.Root.Length..], stream, overwrite: true); - } - } - - [OneTimeTearDown] - public async Task Cleanup() - { - _storage.Dispose(); - - using var fs = CreateFileSystem(isReadonly: false); - await fs.DeleteDirectoryAsync("/"); - } - - protected override IVirtualFileSystem GetFileSystem() => - CreateFileSystem(isReadonly: true); - - protected override DirectoryInfo GetDirectoryInfo() => - new DirectoryInfo(_storage.Root); - - private AzureFileSystem CreateFileSystem(bool isReadonly) - { - const string ConnectionString = "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;"; - - return new AzureFileSystem(ConnectionString, _storageName) - { - IsReadOnly = isReadonly - }; - } -} +using Ramstack.FileSystem.Specification.Tests; +using Ramstack.FileSystem.Specification.Tests.Utilities; + +namespace Ramstack.FileSystem.Azure; + +[TestFixture] +[Category("Cloud:Azure")] +public class ReadonlyAzureFileSystemTests : VirtualFileSystemSpecificationTests +{ + private readonly TempFileStorage _storage = new(); + private readonly string _storageName = Guid.NewGuid().ToString("N"); + + [OneTimeSetUp] + public async Task Setup() + { + using var fs = CreateFileSystem(isReadonly: false); + + await fs.CreateContainerAsync(); + + foreach (var path in Directory.EnumerateFiles(_storage.Root, "*", SearchOption.AllDirectories)) + { + await using var stream = File.OpenRead(path); + await fs.WriteAsync(path[_storage.Root.Length..], stream, overwrite: true); + } + } + + [OneTimeTearDown] + public async Task Cleanup() + { + _storage.Dispose(); + + using var fs = CreateFileSystem(isReadonly: false); + await fs.DeleteDirectoryAsync("/"); + } + + protected override IVirtualFileSystem GetFileSystem() => + CreateFileSystem(isReadonly: true); + + protected override DirectoryInfo GetDirectoryInfo() => + new DirectoryInfo(_storage.Root); + + private AzureFileSystem CreateFileSystem(bool isReadonly) + { + const string ConnectionString = "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;"; + + return new AzureFileSystem(ConnectionString, _storageName) + { + IsReadOnly = isReadonly + }; + } +} diff --git a/tests/Ramstack.FileSystem.Azure.Tests/WritableAzureFileSystemTests.cs b/tests/Ramstack.FileSystem.Azure.Tests/WritableAzureFileSystemTests.cs index a411b71..167350a 100644 --- a/tests/Ramstack.FileSystem.Azure.Tests/WritableAzureFileSystemTests.cs +++ b/tests/Ramstack.FileSystem.Azure.Tests/WritableAzureFileSystemTests.cs @@ -1,144 +1,144 @@ -using Ramstack.FileSystem.Physical; -using Ramstack.FileSystem.Specification.Tests; -using Ramstack.FileSystem.Specification.Tests.Utilities; - -namespace Ramstack.FileSystem.Azure; - -[TestFixture] -[Category("Cloud:Azure")] -public class WritableAzureFileSystemTests : VirtualFileSystemSpecificationTests -{ - private readonly HashSet _list = []; - private readonly TempFileStorage _storage = new(); - private readonly string _storageName = Guid.NewGuid().ToString("N"); - - [OneTimeSetUp] - public async Task Setup() - { - using var fs = GetFileSystem(); - - await fs.CreateContainerAsync(); - - foreach (var path in Directory.EnumerateFiles(_storage.Root, "*", SearchOption.AllDirectories)) - { - await using var stream = File.OpenRead(path); - await fs.WriteAsync(path[_storage.Root.Length..], stream, overwrite: true); - } - } - - [OneTimeTearDown] - public async Task Cleanup() - { - _storage.Dispose(); - - foreach (var name in _list.ToArray()) - { - using var fs = CreateFileSystem(name); - - try - { - await fs.DeleteDirectoryAsync("/"); - } - catch (Exception exception) - { - Console.WriteLine(exception); - } - } - } - - [Test] - public async Task File_CopyTo_File_DifferentFileSystems() - { - using var fs1 = new PhysicalFileSystem(_storage.Root); - using var fs2 = GetFileSystem(); - - var source = fs1.GetFile("/0000.txt"); - var destination = fs2.GetFile("/0000.txt"); - - var content = Guid.NewGuid().ToString(); - - await using (var stream = await source.OpenWriteAsync()) - await using (var writer = new StreamWriter(stream)) - await writer.WriteAsync(content); - - Assert.That(await destination.ExistsAsync(), Is.False); - - await source.CopyToAsync(destination); - Assert.That(await destination.ExistsAsync(), Is.True); - - using var reader = await destination.OpenTextAsync(); - Assert.That( - await reader.ReadToEndAsync(), - Is.EqualTo(content)); - } - - [Test] - public async Task File_CopyTo_File_DifferentStorages() - { - using var fs1 = CreateFileSystem("temp-storage"); - using var fs2 = GetFileSystem(); - - await fs1.CreateContainerAsync(); - - var source = fs1.GetFile("/1111.txt"); - var destination = fs2.GetFile("/1111.txt"); - - var content = Guid.NewGuid().ToString(); - - await using (var stream = await source.OpenWriteAsync()) - await using (var writer = new StreamWriter(stream)) - await writer.WriteAsync(content); - - Assert.That(await destination.ExistsAsync(), Is.False); - - await source.CopyToAsync(destination); - Assert.That(await destination.ExistsAsync(), Is.True); - - using var reader = await destination.OpenTextAsync(); - Assert.That( - await reader.ReadToEndAsync(), - Is.EqualTo(content)); - } - - [Test] - public async Task Directory_BatchDeleting() - { - // 1. Page size is a maximum of 5000 items. - // 2. Each batch request supports a maximum of 256 sub-requests. - // - // We'll set it slightly higher to test pagination and batching functionality. - const int Count = 5100; - - using var fs = GetFileSystem(); - for (var i = 0; i < Count; i++) - await fs.WriteAsync($"/temp/{i:0000}", Stream.Null); - - Assert.That( - await fs.GetFilesAsync("/temp").CountAsync(), - Is.EqualTo(Count)); - - await fs.DeleteDirectoryAsync("/temp"); - - Assert.That( - await fs.GetFilesAsync("/temp").AnyAsync(), - Is.False); - } - - protected override AzureFileSystem GetFileSystem() => - CreateFileSystem(_storageName); - - protected override DirectoryInfo GetDirectoryInfo() => - new DirectoryInfo(_storage.Root); - - private AzureFileSystem CreateFileSystem(string storageName) - { - _list.Add(storageName); - - const string ConnectionString = "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;"; - - return new AzureFileSystem(ConnectionString, storageName) - { - IsReadOnly = false - }; - } -} +using Ramstack.FileSystem.Physical; +using Ramstack.FileSystem.Specification.Tests; +using Ramstack.FileSystem.Specification.Tests.Utilities; + +namespace Ramstack.FileSystem.Azure; + +[TestFixture] +[Category("Cloud:Azure")] +public class WritableAzureFileSystemTests : VirtualFileSystemSpecificationTests +{ + private readonly HashSet _list = []; + private readonly TempFileStorage _storage = new(); + private readonly string _storageName = Guid.NewGuid().ToString("N"); + + [OneTimeSetUp] + public async Task Setup() + { + using var fs = GetFileSystem(); + + await fs.CreateContainerAsync(); + + foreach (var path in Directory.EnumerateFiles(_storage.Root, "*", SearchOption.AllDirectories)) + { + await using var stream = File.OpenRead(path); + await fs.WriteAsync(path[_storage.Root.Length..], stream, overwrite: true); + } + } + + [OneTimeTearDown] + public async Task Cleanup() + { + _storage.Dispose(); + + foreach (var name in _list.ToArray()) + { + using var fs = CreateFileSystem(name); + + try + { + await fs.DeleteDirectoryAsync("/"); + } + catch (Exception exception) + { + Console.WriteLine(exception); + } + } + } + + [Test] + public async Task File_CopyTo_File_DifferentFileSystems() + { + using var fs1 = new PhysicalFileSystem(_storage.Root); + using var fs2 = GetFileSystem(); + + var source = fs1.GetFile("/0000.txt"); + var destination = fs2.GetFile("/0000.txt"); + + var content = Guid.NewGuid().ToString(); + + await using (var stream = await source.OpenWriteAsync()) + await using (var writer = new StreamWriter(stream)) + await writer.WriteAsync(content); + + Assert.That(await destination.ExistsAsync(), Is.False); + + await source.CopyToAsync(destination); + Assert.That(await destination.ExistsAsync(), Is.True); + + using var reader = await destination.OpenTextAsync(); + Assert.That( + await reader.ReadToEndAsync(), + Is.EqualTo(content)); + } + + [Test] + public async Task File_CopyTo_File_DifferentStorages() + { + using var fs1 = CreateFileSystem("temp-storage"); + using var fs2 = GetFileSystem(); + + await fs1.CreateContainerAsync(); + + var source = fs1.GetFile("/1111.txt"); + var destination = fs2.GetFile("/1111.txt"); + + var content = Guid.NewGuid().ToString(); + + await using (var stream = await source.OpenWriteAsync()) + await using (var writer = new StreamWriter(stream)) + await writer.WriteAsync(content); + + Assert.That(await destination.ExistsAsync(), Is.False); + + await source.CopyToAsync(destination); + Assert.That(await destination.ExistsAsync(), Is.True); + + using var reader = await destination.OpenTextAsync(); + Assert.That( + await reader.ReadToEndAsync(), + Is.EqualTo(content)); + } + + [Test] + public async Task Directory_BatchDeleting() + { + // 1. Page size is a maximum of 5000 items. + // 2. Each batch request supports a maximum of 256 sub-requests. + // + // We'll set it slightly higher to test pagination and batching functionality. + const int Count = 5100; + + using var fs = GetFileSystem(); + for (var i = 0; i < Count; i++) + await fs.WriteAsync($"/temp/{i:0000}", Stream.Null); + + Assert.That( + await fs.GetFilesAsync("/temp").CountAsync(), + Is.EqualTo(Count)); + + await fs.DeleteDirectoryAsync("/temp"); + + Assert.That( + await fs.GetFilesAsync("/temp").AnyAsync(), + Is.False); + } + + protected override AzureFileSystem GetFileSystem() => + CreateFileSystem(_storageName); + + protected override DirectoryInfo GetDirectoryInfo() => + new DirectoryInfo(_storage.Root); + + private AzureFileSystem CreateFileSystem(string storageName) + { + _list.Add(storageName); + + const string ConnectionString = "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;"; + + return new AzureFileSystem(ConnectionString, storageName) + { + IsReadOnly = false + }; + } +} diff --git a/tests/Ramstack.FileSystem.Composite.Tests/CompositeFileSystemTests.cs b/tests/Ramstack.FileSystem.Composite.Tests/CompositeFileSystemTests.cs index 17fc98e..15067a6 100644 --- a/tests/Ramstack.FileSystem.Composite.Tests/CompositeFileSystemTests.cs +++ b/tests/Ramstack.FileSystem.Composite.Tests/CompositeFileSystemTests.cs @@ -1,59 +1,59 @@ -using Ramstack.FileSystem.Physical; -using Ramstack.FileSystem.Prefixed; -using Ramstack.FileSystem.Specification.Tests; -using Ramstack.FileSystem.Specification.Tests.Utilities; - -namespace Ramstack.FileSystem.Composite; - -[TestFixture] -public class CompositeFileSystemTests : VirtualFileSystemSpecificationTests -{ - private readonly TempFileStorage _storage1; - private readonly TempFileStorage _storage2; - private readonly TempFileStorage _storage3; - private readonly CompositeFileSystem _fs; - - public CompositeFileSystemTests() - { - _storage1 = new TempFileStorage(); - _storage2 = new TempFileStorage(_storage1); - _storage3 = new TempFileStorage(_storage1); - - foreach (var directory in Directory.GetDirectories(Path.Join(_storage2.Root, "project"))) - Directory.Delete(directory, recursive: true); - - foreach (var file in Directory.GetFiles(Path.Join(_storage3.Root, "project"))) - File.Delete(file); - - var list = new List - { - new PhysicalFileSystem(_storage2.Root) - }; - - foreach (var directory in Directory.GetDirectories(Path.Join(_storage3.Root, "project"))) - { - var fs = new PrefixedFileSystem( - $"/project/{Path.GetFileName(directory)}", - new PhysicalFileSystem(directory) - ); - list.Add(fs); - } - - _fs = new CompositeFileSystem(list); - } - - [OneTimeTearDown] - public void Cleanup() - { - _fs.Dispose(); - _storage1.Dispose(); - _storage2.Dispose(); - _storage3.Dispose(); - } - - protected override IVirtualFileSystem GetFileSystem() => - _fs; - - protected override DirectoryInfo GetDirectoryInfo() => - new DirectoryInfo(_storage1.Root); -} +using Ramstack.FileSystem.Physical; +using Ramstack.FileSystem.Prefixed; +using Ramstack.FileSystem.Specification.Tests; +using Ramstack.FileSystem.Specification.Tests.Utilities; + +namespace Ramstack.FileSystem.Composite; + +[TestFixture] +public class CompositeFileSystemTests : VirtualFileSystemSpecificationTests +{ + private readonly TempFileStorage _storage1; + private readonly TempFileStorage _storage2; + private readonly TempFileStorage _storage3; + private readonly CompositeFileSystem _fs; + + public CompositeFileSystemTests() + { + _storage1 = new TempFileStorage(); + _storage2 = new TempFileStorage(_storage1); + _storage3 = new TempFileStorage(_storage1); + + foreach (var directory in Directory.GetDirectories(Path.Join(_storage2.Root, "project"))) + Directory.Delete(directory, recursive: true); + + foreach (var file in Directory.GetFiles(Path.Join(_storage3.Root, "project"))) + File.Delete(file); + + var list = new List + { + new PhysicalFileSystem(_storage2.Root) + }; + + foreach (var directory in Directory.GetDirectories(Path.Join(_storage3.Root, "project"))) + { + var fs = new PrefixedFileSystem( + $"/project/{Path.GetFileName(directory)}", + new PhysicalFileSystem(directory) + ); + list.Add(fs); + } + + _fs = new CompositeFileSystem(list); + } + + [OneTimeTearDown] + public void Cleanup() + { + _fs.Dispose(); + _storage1.Dispose(); + _storage2.Dispose(); + _storage3.Dispose(); + } + + protected override IVirtualFileSystem GetFileSystem() => + _fs; + + protected override DirectoryInfo GetDirectoryInfo() => + new DirectoryInfo(_storage1.Root); +} diff --git a/tests/Ramstack.FileSystem.Composite.Tests/CompositionHelperTests.cs b/tests/Ramstack.FileSystem.Composite.Tests/CompositionHelperTests.cs index 905cb75..e835c05 100644 --- a/tests/Ramstack.FileSystem.Composite.Tests/CompositionHelperTests.cs +++ b/tests/Ramstack.FileSystem.Composite.Tests/CompositionHelperTests.cs @@ -1,230 +1,230 @@ -using Ramstack.FileSystem.Null; - -namespace Ramstack.FileSystem.Composite; - -[TestFixture] -public class CompositionHelperTests -{ - [Test] - public void Flatten_ReturnsAsIs_WhenNoComposite() - { - var fs = new TestFileSystem(); - var result = CompositeFileSystem.Flatten(fs); - Assert.That(result, Is.SameAs(fs)); - } - - [Test] - public void Flatten_ReturnsCompositeFileSystem_WhenNeedComposite() - { - var fs = new CompositeFileSystem(new TestFileSystem(), new TestFileSystem()); - - var result = CompositeFileSystem.Flatten(fs); - Assert.That(result, Is.InstanceOf()); - } - - [Test] - public void Flatten_ReturnsAsIs_WhenAlreadyFlat() - { - var fs = new CompositeFileSystem(new TestFileSystem(), new TestFileSystem()); - - var result = CompositeFileSystem.Flatten(fs); - Assert.That(result, Is.SameAs(fs)); - } - - [Test] - public void Flatten_ReturnsCompositeFileSystem_Flattened() - { - var fs = new CompositeFileSystem( - new TestFileSystem(), - new CompositeFileSystem( - new TestFileSystem(), - new TestFileSystem(), - new CompositeFileSystem( - new TestFileSystem()))); - - var result = CompositeFileSystem.Flatten(fs); - - Assert.That(result, Is.InstanceOf()); - Assert.That(((CompositeFileSystem)result).FileSystems, Is.All.InstanceOf()); - Assert.That(((CompositeFileSystem)result).FileSystems.Count, Is.EqualTo(4)); - } - - [Test] - public void Flatten_RemovesNullFileSystem() - { - var provider = new CompositeFileSystem( - new TestFileSystem(), - new CompositeFileSystem( - new TestFileSystem(), - new NullFileSystem(), - new TestFileSystem()), - new NullFileSystem()); - - var result = CompositeFileSystem.Flatten(provider); - - Assert.That(result, Is.InstanceOf()); - Assert.That(((CompositeFileSystem)result).FileSystems.Count, Is.EqualTo(3)); - Assert.That(((CompositeFileSystem)result).FileSystems, Is.All.InstanceOf()); - } - - [Test] - public void Flatten_ReturnsNullFileSystem_WhenNothingReturn() - { - var fs = new CompositeFileSystem( - new CompositeFileSystem( - new CompositeFileSystem(), - new CompositeFileSystem(), - new CompositeFileSystem( - new CompositeFileSystem( - new CompositeFileSystem(), - new CompositeFileSystem()), - new CompositeFileSystem()), - new CompositeFileSystem( - new CompositeFileSystem(), - new CompositeFileSystem(), - new CompositeFileSystem( - new CompositeFileSystem( - new CompositeFileSystem( - new NullFileSystem(), - new NullFileSystem(), - new NullFileSystem(), - new NullFileSystem(), - new NullFileSystem()), - new CompositeFileSystem()), - new CompositeFileSystem()))), - new CompositeFileSystem(), - new CompositeFileSystem(), - new CompositeFileSystem( - new CompositeFileSystem(), - new CompositeFileSystem(), - new CompositeFileSystem( - new CompositeFileSystem( - new CompositeFileSystem(), - new CompositeFileSystem()), - new CompositeFileSystem())), - new CompositeFileSystem(), - new CompositeFileSystem( - new CompositeFileSystem( - new CompositeFileSystem(), - new CompositeFileSystem()), - new CompositeFileSystem())); - - var result = CompositeFileSystem.Flatten(fs); - Assert.That(result, Is.InstanceOf()); - } - - [Test] - public void Flatten_ReturnsSingleFileSystem_WhenRemainOneFileSystem() - { - var fs = new CompositeFileSystem( - new CompositeFileSystem( - new CompositeFileSystem(), - new CompositeFileSystem(), - new CompositeFileSystem( - new CompositeFileSystem( - new CompositeFileSystem(), - new CompositeFileSystem()), - new CompositeFileSystem()), - new CompositeFileSystem( - new CompositeFileSystem(), - new CompositeFileSystem(), - new CompositeFileSystem( - new CompositeFileSystem( - new CompositeFileSystem( - new TestFileSystem()), - new CompositeFileSystem( - new NullFileSystem(), - new NullFileSystem(), - new NullFileSystem())), - new CompositeFileSystem()))), - new CompositeFileSystem(), - new CompositeFileSystem(), - new CompositeFileSystem( - new CompositeFileSystem(), - new CompositeFileSystem(), - new CompositeFileSystem( - new CompositeFileSystem( - new CompositeFileSystem(), - new CompositeFileSystem( - new NullFileSystem())), - new CompositeFileSystem())), - new CompositeFileSystem(), - new CompositeFileSystem( - new CompositeFileSystem( - new CompositeFileSystem(), - new CompositeFileSystem()), - new CompositeFileSystem())); - - var result = CompositeFileSystem.Flatten(fs); - Assert.That(result, Is.InstanceOf()); - } - - [Test] - public void Flatten_MaintainOrder() - { - var fs1 = new TestFileSystem(); - var fs2 = new TestFileSystem(); - var fs3 = new TestFileSystem(); - var fs4 = new TestFileSystem(); - var fs5 = new TestFileSystem(); - var fs6 = new TestFileSystem(); - var fs7 = new TestFileSystem(); - var fs8 = new TestFileSystem(); - var fs9 = new TestFileSystem(); - - var fs = CompositeFileSystem.Create( - new CompositeFileSystem( - new CompositeFileSystem(), - new CompositeFileSystem(), - new CompositeFileSystem( - fs1, - new CompositeFileSystem( - fs2, - new CompositeFileSystem(), - fs3), - fs4, - new CompositeFileSystem( - new NullFileSystem(), - new NullFileSystem(), - new NullFileSystem() - )), - fs5, - new CompositeFileSystem( - new CompositeFileSystem(), - new CompositeFileSystem(), - new CompositeFileSystem( - new CompositeFileSystem( - new CompositeFileSystem(), - new NullFileSystem(), - new NullFileSystem(), - new NullFileSystem(), - new NullFileSystem(), - fs6), - new CompositeFileSystem())), - fs7), - new CompositeFileSystem(), - new CompositeFileSystem(), - new CompositeFileSystem( - new CompositeFileSystem(), - new CompositeFileSystem(), - new CompositeFileSystem( - new CompositeFileSystem( - new CompositeFileSystem(), - new CompositeFileSystem()), - new CompositeFileSystem(), - fs8)), - new CompositeFileSystem(), - new CompositeFileSystem( - new CompositeFileSystem( - new CompositeFileSystem(), - new CompositeFileSystem()), - new CompositeFileSystem(), - fs9)); - - var compositeFileSystem = (CompositeFileSystem)fs; - var list = new IVirtualFileSystem[] { fs1, fs2, fs3, fs4, fs5, fs6, fs7, fs8, fs9 }; - - Assert.That(fs, Is.InstanceOf()); - Assert.That(compositeFileSystem.FileSystems, Is.EquivalentTo(list)); - } -} +using Ramstack.FileSystem.Null; + +namespace Ramstack.FileSystem.Composite; + +[TestFixture] +public class CompositionHelperTests +{ + [Test] + public void Flatten_ReturnsAsIs_WhenNoComposite() + { + var fs = new TestFileSystem(); + var result = CompositeFileSystem.Flatten(fs); + Assert.That(result, Is.SameAs(fs)); + } + + [Test] + public void Flatten_ReturnsCompositeFileSystem_WhenNeedComposite() + { + var fs = new CompositeFileSystem(new TestFileSystem(), new TestFileSystem()); + + var result = CompositeFileSystem.Flatten(fs); + Assert.That(result, Is.InstanceOf()); + } + + [Test] + public void Flatten_ReturnsAsIs_WhenAlreadyFlat() + { + var fs = new CompositeFileSystem(new TestFileSystem(), new TestFileSystem()); + + var result = CompositeFileSystem.Flatten(fs); + Assert.That(result, Is.SameAs(fs)); + } + + [Test] + public void Flatten_ReturnsCompositeFileSystem_Flattened() + { + var fs = new CompositeFileSystem( + new TestFileSystem(), + new CompositeFileSystem( + new TestFileSystem(), + new TestFileSystem(), + new CompositeFileSystem( + new TestFileSystem()))); + + var result = CompositeFileSystem.Flatten(fs); + + Assert.That(result, Is.InstanceOf()); + Assert.That(((CompositeFileSystem)result).FileSystems, Is.All.InstanceOf()); + Assert.That(((CompositeFileSystem)result).FileSystems.Count, Is.EqualTo(4)); + } + + [Test] + public void Flatten_RemovesNullFileSystem() + { + var provider = new CompositeFileSystem( + new TestFileSystem(), + new CompositeFileSystem( + new TestFileSystem(), + new NullFileSystem(), + new TestFileSystem()), + new NullFileSystem()); + + var result = CompositeFileSystem.Flatten(provider); + + Assert.That(result, Is.InstanceOf()); + Assert.That(((CompositeFileSystem)result).FileSystems.Count, Is.EqualTo(3)); + Assert.That(((CompositeFileSystem)result).FileSystems, Is.All.InstanceOf()); + } + + [Test] + public void Flatten_ReturnsNullFileSystem_WhenNothingReturn() + { + var fs = new CompositeFileSystem( + new CompositeFileSystem( + new CompositeFileSystem(), + new CompositeFileSystem(), + new CompositeFileSystem( + new CompositeFileSystem( + new CompositeFileSystem(), + new CompositeFileSystem()), + new CompositeFileSystem()), + new CompositeFileSystem( + new CompositeFileSystem(), + new CompositeFileSystem(), + new CompositeFileSystem( + new CompositeFileSystem( + new CompositeFileSystem( + new NullFileSystem(), + new NullFileSystem(), + new NullFileSystem(), + new NullFileSystem(), + new NullFileSystem()), + new CompositeFileSystem()), + new CompositeFileSystem()))), + new CompositeFileSystem(), + new CompositeFileSystem(), + new CompositeFileSystem( + new CompositeFileSystem(), + new CompositeFileSystem(), + new CompositeFileSystem( + new CompositeFileSystem( + new CompositeFileSystem(), + new CompositeFileSystem()), + new CompositeFileSystem())), + new CompositeFileSystem(), + new CompositeFileSystem( + new CompositeFileSystem( + new CompositeFileSystem(), + new CompositeFileSystem()), + new CompositeFileSystem())); + + var result = CompositeFileSystem.Flatten(fs); + Assert.That(result, Is.InstanceOf()); + } + + [Test] + public void Flatten_ReturnsSingleFileSystem_WhenRemainOneFileSystem() + { + var fs = new CompositeFileSystem( + new CompositeFileSystem( + new CompositeFileSystem(), + new CompositeFileSystem(), + new CompositeFileSystem( + new CompositeFileSystem( + new CompositeFileSystem(), + new CompositeFileSystem()), + new CompositeFileSystem()), + new CompositeFileSystem( + new CompositeFileSystem(), + new CompositeFileSystem(), + new CompositeFileSystem( + new CompositeFileSystem( + new CompositeFileSystem( + new TestFileSystem()), + new CompositeFileSystem( + new NullFileSystem(), + new NullFileSystem(), + new NullFileSystem())), + new CompositeFileSystem()))), + new CompositeFileSystem(), + new CompositeFileSystem(), + new CompositeFileSystem( + new CompositeFileSystem(), + new CompositeFileSystem(), + new CompositeFileSystem( + new CompositeFileSystem( + new CompositeFileSystem(), + new CompositeFileSystem( + new NullFileSystem())), + new CompositeFileSystem())), + new CompositeFileSystem(), + new CompositeFileSystem( + new CompositeFileSystem( + new CompositeFileSystem(), + new CompositeFileSystem()), + new CompositeFileSystem())); + + var result = CompositeFileSystem.Flatten(fs); + Assert.That(result, Is.InstanceOf()); + } + + [Test] + public void Flatten_MaintainOrder() + { + var fs1 = new TestFileSystem(); + var fs2 = new TestFileSystem(); + var fs3 = new TestFileSystem(); + var fs4 = new TestFileSystem(); + var fs5 = new TestFileSystem(); + var fs6 = new TestFileSystem(); + var fs7 = new TestFileSystem(); + var fs8 = new TestFileSystem(); + var fs9 = new TestFileSystem(); + + var fs = CompositeFileSystem.Create( + new CompositeFileSystem( + new CompositeFileSystem(), + new CompositeFileSystem(), + new CompositeFileSystem( + fs1, + new CompositeFileSystem( + fs2, + new CompositeFileSystem(), + fs3), + fs4, + new CompositeFileSystem( + new NullFileSystem(), + new NullFileSystem(), + new NullFileSystem() + )), + fs5, + new CompositeFileSystem( + new CompositeFileSystem(), + new CompositeFileSystem(), + new CompositeFileSystem( + new CompositeFileSystem( + new CompositeFileSystem(), + new NullFileSystem(), + new NullFileSystem(), + new NullFileSystem(), + new NullFileSystem(), + fs6), + new CompositeFileSystem())), + fs7), + new CompositeFileSystem(), + new CompositeFileSystem(), + new CompositeFileSystem( + new CompositeFileSystem(), + new CompositeFileSystem(), + new CompositeFileSystem( + new CompositeFileSystem( + new CompositeFileSystem(), + new CompositeFileSystem()), + new CompositeFileSystem(), + fs8)), + new CompositeFileSystem(), + new CompositeFileSystem( + new CompositeFileSystem( + new CompositeFileSystem(), + new CompositeFileSystem()), + new CompositeFileSystem(), + fs9)); + + var compositeFileSystem = (CompositeFileSystem)fs; + var list = new IVirtualFileSystem[] { fs1, fs2, fs3, fs4, fs5, fs6, fs7, fs8, fs9 }; + + Assert.That(fs, Is.InstanceOf()); + Assert.That(compositeFileSystem.FileSystems, Is.EquivalentTo(list)); + } +} diff --git a/tests/Ramstack.FileSystem.Composite.Tests/Ramstack.FileSystem.Composite.Tests.csproj b/tests/Ramstack.FileSystem.Composite.Tests/Ramstack.FileSystem.Composite.Tests.csproj index 609d57e..8792907 100644 --- a/tests/Ramstack.FileSystem.Composite.Tests/Ramstack.FileSystem.Composite.Tests.csproj +++ b/tests/Ramstack.FileSystem.Composite.Tests/Ramstack.FileSystem.Composite.Tests.csproj @@ -1,29 +1,29 @@ - - - - net6.0;net8.0 - enable - enable - preview - Ramstack.FileSystem.Composite - - - - - - - - - - - - - - - - - - - - - + + + + net6.0;net8.0 + enable + enable + preview + Ramstack.FileSystem.Composite + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/Ramstack.FileSystem.Composite.Tests/TestFileSystem.cs b/tests/Ramstack.FileSystem.Composite.Tests/TestFileSystem.cs index 5ff2180..ef0970b 100644 --- a/tests/Ramstack.FileSystem.Composite.Tests/TestFileSystem.cs +++ b/tests/Ramstack.FileSystem.Composite.Tests/TestFileSystem.cs @@ -1,9 +1,9 @@ -namespace Ramstack.FileSystem.Composite; - -internal sealed class TestFileSystem : IVirtualFileSystem -{ - public bool IsReadOnly => throw new NotImplementedException(); - public VirtualFile GetFile(string path) => throw new NotImplementedException(); - public VirtualDirectory GetDirectory(string path) => throw new NotImplementedException(); - public void Dispose() => throw new NotImplementedException(); -} +namespace Ramstack.FileSystem.Composite; + +internal sealed class TestFileSystem : IVirtualFileSystem +{ + public bool IsReadOnly => throw new NotImplementedException(); + public VirtualFile GetFile(string path) => throw new NotImplementedException(); + public VirtualDirectory GetDirectory(string path) => throw new NotImplementedException(); + public void Dispose() => throw new NotImplementedException(); +} diff --git a/tests/Ramstack.FileSystem.FileProvider.Tests/Ramstack.FileSystem.FileProvider.Tests.csproj b/tests/Ramstack.FileSystem.FileProvider.Tests/Ramstack.FileSystem.FileProvider.Tests.csproj index 45d4bb1..7800568 100644 --- a/tests/Ramstack.FileSystem.FileProvider.Tests/Ramstack.FileSystem.FileProvider.Tests.csproj +++ b/tests/Ramstack.FileSystem.FileProvider.Tests/Ramstack.FileSystem.FileProvider.Tests.csproj @@ -1,28 +1,28 @@ - - - - net6.0;net8.0 - enable - enable - preview - Ramstack.FileSystem.Adapters - - - - - - - - - - - - - - - - - - - - + + + + net6.0;net8.0 + enable + enable + preview + Ramstack.FileSystem.Adapters + + + + + + + + + + + + + + + + + + + + diff --git a/tests/Ramstack.FileSystem.FileProvider.Tests/VirtualFileSystemAdapterTests.cs b/tests/Ramstack.FileSystem.FileProvider.Tests/VirtualFileSystemAdapterTests.cs index 7209d70..f8989ed 100644 --- a/tests/Ramstack.FileSystem.FileProvider.Tests/VirtualFileSystemAdapterTests.cs +++ b/tests/Ramstack.FileSystem.FileProvider.Tests/VirtualFileSystemAdapterTests.cs @@ -1,25 +1,25 @@ -using Microsoft.Extensions.FileProviders; -using Microsoft.Extensions.FileProviders.Physical; - -using Ramstack.FileSystem.Specification.Tests; -using Ramstack.FileSystem.Specification.Tests.Utilities; - -namespace Ramstack.FileSystem.Adapters; - -[TestFixture] -public class VirtualFileSystemAdapterTests : VirtualFileSystemSpecificationTests -{ - private readonly TempFileStorage _storage = new TempFileStorage(); - - [OneTimeTearDown] - public void Cleanup() => - _storage.Dispose(); - - /// - protected override IVirtualFileSystem GetFileSystem() => - new VirtualFileSystemAdapter(new PhysicalFileProvider(_storage.Root, ExclusionFilters.None)); - - /// - protected override DirectoryInfo GetDirectoryInfo() => - new DirectoryInfo(_storage.Root); -} +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.FileProviders.Physical; + +using Ramstack.FileSystem.Specification.Tests; +using Ramstack.FileSystem.Specification.Tests.Utilities; + +namespace Ramstack.FileSystem.Adapters; + +[TestFixture] +public class VirtualFileSystemAdapterTests : VirtualFileSystemSpecificationTests +{ + private readonly TempFileStorage _storage = new TempFileStorage(); + + [OneTimeTearDown] + public void Cleanup() => + _storage.Dispose(); + + /// + protected override IVirtualFileSystem GetFileSystem() => + new VirtualFileSystemAdapter(new PhysicalFileProvider(_storage.Root, ExclusionFilters.None)); + + /// + protected override DirectoryInfo GetDirectoryInfo() => + new DirectoryInfo(_storage.Root); +} diff --git a/tests/Ramstack.FileSystem.Globbing.Tests/GlobingFileSystemTests.cs b/tests/Ramstack.FileSystem.Globbing.Tests/GlobingFileSystemTests.cs index 0fed676..a629b4e 100644 --- a/tests/Ramstack.FileSystem.Globbing.Tests/GlobingFileSystemTests.cs +++ b/tests/Ramstack.FileSystem.Globbing.Tests/GlobingFileSystemTests.cs @@ -1,139 +1,139 @@ -using Ramstack.FileSystem.Physical; -using Ramstack.FileSystem.Specification.Tests; -using Ramstack.FileSystem.Specification.Tests.Utilities; - -namespace Ramstack.FileSystem.Globbing; - -[TestFixture] -public class GlobingFileSystemTests : VirtualFileSystemSpecificationTests -{ - private readonly TempFileStorage _storage1; - private readonly TempFileStorage _storage2; - - public GlobingFileSystemTests() - { - _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 async Task Glob_MatchStructures() - { - using var fs = GetFileSystem(); - - Assert.That( - await fs - .GetDirectoriesAsync("/", "**") - .OrderBy(f => f.FullName) - .Select(f => f.FullName) - .ToArrayAsync(), - Is.EquivalentTo( - [ - "/project", - "/project/assets", - "/project/assets/fonts", - "/project/assets/images", - "/project/assets/images/backgrounds", - "/project/assets/styles" - ])); - - Assert.That( - await fs - .GetFilesAsync("/", "**") - .OrderBy(f => f.FullName) - .Select(f => f.FullName) - .ToArrayAsync(), - 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 async Task ExcludedDirectory_HasNoFileNodes() - { - using var fs = new GlobbingFileSystem( - new PhysicalFileSystem(_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) - { - Assert.That( - await fs.DirectoryExistsAsync(path), - Is.False); - - Assert.That( - await fs.GetFileNodesAsync(path, "**").AnyAsync(), - 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) - Assert.That(await fs.FileExistsAsync(path), Is.False); - } - - protected override IVirtualFileSystem GetFileSystem() - { - var fs = new PhysicalFileSystem(_storage2.Root); - return new GlobbingFileSystem(fs, - patterns: ["project/assets/**", "project/README.md", "project/global.json"], - excludes: ["project/*.json"]); - } - - protected override DirectoryInfo GetDirectoryInfo() => - new(_storage1.Root); -} +using Ramstack.FileSystem.Physical; +using Ramstack.FileSystem.Specification.Tests; +using Ramstack.FileSystem.Specification.Tests.Utilities; + +namespace Ramstack.FileSystem.Globbing; + +[TestFixture] +public class GlobingFileSystemTests : VirtualFileSystemSpecificationTests +{ + private readonly TempFileStorage _storage1; + private readonly TempFileStorage _storage2; + + public GlobingFileSystemTests() + { + _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 async Task Glob_MatchStructures() + { + using var fs = GetFileSystem(); + + Assert.That( + await fs + .GetDirectoriesAsync("/", "**") + .OrderBy(f => f.FullName) + .Select(f => f.FullName) + .ToArrayAsync(), + Is.EquivalentTo( + [ + "/project", + "/project/assets", + "/project/assets/fonts", + "/project/assets/images", + "/project/assets/images/backgrounds", + "/project/assets/styles" + ])); + + Assert.That( + await fs + .GetFilesAsync("/", "**") + .OrderBy(f => f.FullName) + .Select(f => f.FullName) + .ToArrayAsync(), + 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 async Task ExcludedDirectory_HasNoFileNodes() + { + using var fs = new GlobbingFileSystem( + new PhysicalFileSystem(_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) + { + Assert.That( + await fs.DirectoryExistsAsync(path), + Is.False); + + Assert.That( + await fs.GetFileNodesAsync(path, "**").AnyAsync(), + 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) + Assert.That(await fs.FileExistsAsync(path), Is.False); + } + + protected override IVirtualFileSystem GetFileSystem() + { + var fs = new PhysicalFileSystem(_storage2.Root); + return new GlobbingFileSystem(fs, + patterns: ["project/assets/**", "project/README.md", "project/global.json"], + excludes: ["project/*.json"]); + } + + protected override DirectoryInfo GetDirectoryInfo() => + new(_storage1.Root); +} diff --git a/tests/Ramstack.FileSystem.Globbing.Tests/Ramstack.FileSystem.Globbing.Tests.csproj b/tests/Ramstack.FileSystem.Globbing.Tests/Ramstack.FileSystem.Globbing.Tests.csproj index 9b7a8fe..fec8390 100644 --- a/tests/Ramstack.FileSystem.Globbing.Tests/Ramstack.FileSystem.Globbing.Tests.csproj +++ b/tests/Ramstack.FileSystem.Globbing.Tests/Ramstack.FileSystem.Globbing.Tests.csproj @@ -1,28 +1,28 @@ - - - - net6.0;net8.0 - enable - enable - preview - Ramstack.FileSystem.Globbing - - - - - - - - - - - - - - - - - - - - + + + + net6.0;net8.0 + enable + enable + preview + Ramstack.FileSystem.Globbing + + + + + + + + + + + + + + + + + + + + diff --git a/tests/Ramstack.FileSystem.Google.Tests/Ramstack.FileSystem.Google.Tests.csproj b/tests/Ramstack.FileSystem.Google.Tests/Ramstack.FileSystem.Google.Tests.csproj index 3541529..a3ed44e 100644 --- a/tests/Ramstack.FileSystem.Google.Tests/Ramstack.FileSystem.Google.Tests.csproj +++ b/tests/Ramstack.FileSystem.Google.Tests/Ramstack.FileSystem.Google.Tests.csproj @@ -1,28 +1,28 @@ - - - - net6.0;net8.0 - enable - enable - preview - Ramstack.FileSystem.Google - - - - - - - - - - - - - - - - - - - - + + + + net6.0;net8.0 + enable + enable + preview + Ramstack.FileSystem.Google + + + + + + + + + + + + + + + + + + + + diff --git a/tests/Ramstack.FileSystem.Google.Tests/ReadonlyGoogleFileSystemTests.cs b/tests/Ramstack.FileSystem.Google.Tests/ReadonlyGoogleFileSystemTests.cs index 6fe037e..8ac028d 100644 --- a/tests/Ramstack.FileSystem.Google.Tests/ReadonlyGoogleFileSystemTests.cs +++ b/tests/Ramstack.FileSystem.Google.Tests/ReadonlyGoogleFileSystemTests.cs @@ -1,56 +1,56 @@ -using Google.Apis.Auth.OAuth2; -using Google.Cloud.Storage.V1; - -using Ramstack.FileSystem.Specification.Tests; -using Ramstack.FileSystem.Specification.Tests.Utilities; - -namespace Ramstack.FileSystem.Google; - -[TestFixture] -[Category("Cloud:Google")] -[Ignore("This test requires a running Google Cloud Storage emulator")] -public class ReadonlyGoogleFileSystemTests : VirtualFileSystemSpecificationTests -{ - private readonly TempFileStorage _storage = new TempFileStorage(); - - [OneTimeSetUp] - public async Task Setup() - { - using var fs = CreateFileSystem(isReadOnly: false); - await fs.CreateBucketAsync("ramstack-project"); - - foreach (var path in Directory.EnumerateFiles(_storage.Root, "*", SearchOption.AllDirectories)) - { - await using var stream = File.OpenRead(path); - await fs.WriteAsync(path[_storage.Root.Length..], stream, overwrite: true); - } - } - - [OneTimeTearDown] - public async Task Cleanup() - { - _storage.Dispose(); - - using var fs = CreateFileSystem(isReadOnly: false); - await fs.DeleteDirectoryAsync("/"); - } - - protected override GoogleFileSystem GetFileSystem() => - CreateFileSystem(isReadOnly: true); - - protected override DirectoryInfo GetDirectoryInfo() => - new DirectoryInfo(_storage.Root); - - private static GoogleFileSystem CreateFileSystem(bool isReadOnly) - { - #pragma warning disable CS0618 // Type or member is obsolete - var client = StorageClient.Create( - GoogleCredential.FromFile("credentials.json")); - #pragma warning restore CS0618 // Type or member is obsolete - - return new GoogleFileSystem(client, "ramstack-test-bucket") - { - IsReadOnly = isReadOnly - }; - } -} +using Google.Apis.Auth.OAuth2; +using Google.Cloud.Storage.V1; + +using Ramstack.FileSystem.Specification.Tests; +using Ramstack.FileSystem.Specification.Tests.Utilities; + +namespace Ramstack.FileSystem.Google; + +[TestFixture] +[Category("Cloud:Google")] +[Ignore("This test requires a running Google Cloud Storage emulator")] +public class ReadonlyGoogleFileSystemTests : VirtualFileSystemSpecificationTests +{ + private readonly TempFileStorage _storage = new TempFileStorage(); + + [OneTimeSetUp] + public async Task Setup() + { + using var fs = CreateFileSystem(isReadOnly: false); + await fs.CreateBucketAsync("ramstack-project"); + + foreach (var path in Directory.EnumerateFiles(_storage.Root, "*", SearchOption.AllDirectories)) + { + await using var stream = File.OpenRead(path); + await fs.WriteAsync(path[_storage.Root.Length..], stream, overwrite: true); + } + } + + [OneTimeTearDown] + public async Task Cleanup() + { + _storage.Dispose(); + + using var fs = CreateFileSystem(isReadOnly: false); + await fs.DeleteDirectoryAsync("/"); + } + + protected override GoogleFileSystem GetFileSystem() => + CreateFileSystem(isReadOnly: true); + + protected override DirectoryInfo GetDirectoryInfo() => + new DirectoryInfo(_storage.Root); + + private static GoogleFileSystem CreateFileSystem(bool isReadOnly) + { + #pragma warning disable CS0618 // Type or member is obsolete + var client = StorageClient.Create( + GoogleCredential.FromFile("credentials.json")); + #pragma warning restore CS0618 // Type or member is obsolete + + return new GoogleFileSystem(client, "ramstack-test-bucket") + { + IsReadOnly = isReadOnly + }; + } +} diff --git a/tests/Ramstack.FileSystem.Google.Tests/WritableGoogleFileSystemTests.cs b/tests/Ramstack.FileSystem.Google.Tests/WritableGoogleFileSystemTests.cs index e4c2894..8657e17 100644 --- a/tests/Ramstack.FileSystem.Google.Tests/WritableGoogleFileSystemTests.cs +++ b/tests/Ramstack.FileSystem.Google.Tests/WritableGoogleFileSystemTests.cs @@ -1,214 +1,214 @@ -using System.Reflection; - -using Google.Apis.Auth.OAuth2; -using Google.Cloud.Storage.V1; - -using Ramstack.FileSystem.Physical; -using Ramstack.FileSystem.Specification.Tests; -using Ramstack.FileSystem.Specification.Tests.Utilities; - -namespace Ramstack.FileSystem.Google; - -[TestFixture] -[Category("Cloud:Google")] -[Ignore("This test requires a running Google Cloud Storage emulator")] -public class WritableGoogleFileSystemTests : VirtualFileSystemSpecificationTests -{ - private readonly HashSet _buckets = []; - private readonly TempFileStorage _storage = new TempFileStorage(); - - [OneTimeSetUp] - public async Task Setup() - { - using var fs = GetFileSystem(); - await fs.CreateBucketAsync("ramstack-project"); - - foreach (var path in Directory.EnumerateFiles(_storage.Root, "*", SearchOption.AllDirectories)) - { - await using var stream = File.OpenRead(path); - await fs.WriteAsync(path[_storage.Root.Length..], stream, overwrite: true); - } - } - - [OneTimeTearDown] - public async Task Cleanup() - { - _storage.Dispose(); - - foreach (var bucket in _buckets.ToArray()) - { - using var fs = CreateFileSystem(bucket); - - try - { - await fs.DeleteDirectoryAsync("/"); - } - catch (Exception e) - { - Console.WriteLine(e); - } - } - } - - [Test] - public async Task File_OpenWrite_InternalBufferWriteError_DoesNotCreateFile() - { - using var fs = GetFileSystem(); - - await using (var stream = await fs.OpenWriteAsync("/error.log")) - { - var underlying = (FileStream)stream.GetType().GetField("_stream", BindingFlags.NonPublic | BindingFlags.Instance)!.GetValue(stream)!; - Assert.That(underlying, Is.Not.Null); - - await stream.WriteAsync(new byte[1024]); - - // Simulates an internal buffer write error. - await underlying.DisposeAsync(); - - try - { - await stream.WriteAsync(new byte[1024]); - Assert.Fail(); - } - catch - { - // Ignore - } - } - - Assert.That( - await fs.FileExistsAsync("/error.log"), - Is.False); - } - - [Test] - public async Task File_WriteHugeFile() - { - const int TotalLines = 500; - - using var fs = GetFileSystem(); - - var value = new string('a', 100 * 1024); - - { - await using var stream = await fs.OpenWriteAsync("/test.txt"); - await using var writer = new StreamWriter(stream); - - for (var i = 0; i < TotalLines; i++) - await writer.WriteLineAsync(value); - } - - { - var file = fs.GetFile("/test.txt"); - - Assert.That( - await file.ExistsAsync(), - Is.True); - - Assert.That( - await file.GetLengthAsync(), - Is.EqualTo(TotalLines * value.Length + TotalLines * Environment.NewLine.Length)); - - await using var stream = await file.OpenReadAsync(); - using var reader = new StreamReader(stream); - - var count = 0; - - while (!reader.EndOfStream) - { - Assert.That( - await reader.ReadLineAsync(), - Is.EqualTo(value)); - - count++; - } - - Assert.That(count, Is.EqualTo(TotalLines)); - - await file.DeleteAsync(); - - Assert.That( - await file.ExistsAsync(), - Is.False); - } - } - - [Test] - public async Task File_CopyTo_File_DifferentFileSystems() - { - using var fs1 = new PhysicalFileSystem(_storage.Root); - using var fs2 = GetFileSystem(); - - var source = fs1.GetFile("/0000.txt"); - var destination = fs2.GetFile("/0000.txt"); - - var content = Guid.NewGuid().ToString(); - - await using (var stream = await source.OpenWriteAsync()) - await using (var writer = new StreamWriter(stream)) - await writer.WriteAsync(content); - - Assert.That(await destination.ExistsAsync(), Is.False); - - await source.CopyToAsync(destination); - Assert.That(await destination.ExistsAsync(), Is.True); - - using var reader = await destination.OpenTextAsync(); - Assert.That( - await reader.ReadToEndAsync(), - Is.EqualTo(content)); - - await source.DeleteAsync(); - await destination.DeleteAsync(); - } - - [Test] - public async Task File_CopyTo_File_DifferentStorages() - { - using var fs1 = CreateFileSystem("ramstack-temp-bucket-1"); - using var fs2 = CreateFileSystem("ramstack-temp-bucket-2"); - - await fs1.CreateBucketAsync("ramstack-project"); - await fs2.CreateBucketAsync("ramstack-project"); - - var source = fs1.GetFile("/1111.txt"); - var destination = fs2.GetFile("/1111.txt"); - - var content = Guid.NewGuid().ToString(); - - await using (var stream = await source.OpenWriteAsync()) - await using (var writer = new StreamWriter(stream)) - await writer.WriteAsync(content); - - Assert.That(await destination.ExistsAsync(), Is.False); - - await source.CopyToAsync(destination); - Assert.That(await destination.ExistsAsync(), Is.True); - - using var reader = await destination.OpenTextAsync(); - Assert.That( - await reader.ReadToEndAsync(), - Is.EqualTo(content)); - - await source.DeleteAsync(); - await destination.DeleteAsync(); - } - - protected override GoogleFileSystem GetFileSystem() => - CreateFileSystem("ramstack-test-bucket"); - - protected override DirectoryInfo GetDirectoryInfo() => - new DirectoryInfo(_storage.Root); - - private GoogleFileSystem CreateFileSystem(string bucket) - { - _buckets.Add(bucket); - - #pragma warning disable CS0618 // Type or member is obsolete - var client = StorageClient.Create( - GoogleCredential.FromFile("credentials.json")); - #pragma warning restore CS0618 // Type or member is obsolete - - return new GoogleFileSystem(client, bucketName: bucket); - } -} +using System.Reflection; + +using Google.Apis.Auth.OAuth2; +using Google.Cloud.Storage.V1; + +using Ramstack.FileSystem.Physical; +using Ramstack.FileSystem.Specification.Tests; +using Ramstack.FileSystem.Specification.Tests.Utilities; + +namespace Ramstack.FileSystem.Google; + +[TestFixture] +[Category("Cloud:Google")] +[Ignore("This test requires a running Google Cloud Storage emulator")] +public class WritableGoogleFileSystemTests : VirtualFileSystemSpecificationTests +{ + private readonly HashSet _buckets = []; + private readonly TempFileStorage _storage = new TempFileStorage(); + + [OneTimeSetUp] + public async Task Setup() + { + using var fs = GetFileSystem(); + await fs.CreateBucketAsync("ramstack-project"); + + foreach (var path in Directory.EnumerateFiles(_storage.Root, "*", SearchOption.AllDirectories)) + { + await using var stream = File.OpenRead(path); + await fs.WriteAsync(path[_storage.Root.Length..], stream, overwrite: true); + } + } + + [OneTimeTearDown] + public async Task Cleanup() + { + _storage.Dispose(); + + foreach (var bucket in _buckets.ToArray()) + { + using var fs = CreateFileSystem(bucket); + + try + { + await fs.DeleteDirectoryAsync("/"); + } + catch (Exception e) + { + Console.WriteLine(e); + } + } + } + + [Test] + public async Task File_OpenWrite_InternalBufferWriteError_DoesNotCreateFile() + { + using var fs = GetFileSystem(); + + await using (var stream = await fs.OpenWriteAsync("/error.log")) + { + var underlying = (FileStream)stream.GetType().GetField("_stream", BindingFlags.NonPublic | BindingFlags.Instance)!.GetValue(stream)!; + Assert.That(underlying, Is.Not.Null); + + await stream.WriteAsync(new byte[1024]); + + // Simulates an internal buffer write error. + await underlying.DisposeAsync(); + + try + { + await stream.WriteAsync(new byte[1024]); + Assert.Fail(); + } + catch + { + // Ignore + } + } + + Assert.That( + await fs.FileExistsAsync("/error.log"), + Is.False); + } + + [Test] + public async Task File_WriteHugeFile() + { + const int TotalLines = 500; + + using var fs = GetFileSystem(); + + var value = new string('a', 100 * 1024); + + { + await using var stream = await fs.OpenWriteAsync("/test.txt"); + await using var writer = new StreamWriter(stream); + + for (var i = 0; i < TotalLines; i++) + await writer.WriteLineAsync(value); + } + + { + var file = fs.GetFile("/test.txt"); + + Assert.That( + await file.ExistsAsync(), + Is.True); + + Assert.That( + await file.GetLengthAsync(), + Is.EqualTo(TotalLines * value.Length + TotalLines * Environment.NewLine.Length)); + + await using var stream = await file.OpenReadAsync(); + using var reader = new StreamReader(stream); + + var count = 0; + + while (!reader.EndOfStream) + { + Assert.That( + await reader.ReadLineAsync(), + Is.EqualTo(value)); + + count++; + } + + Assert.That(count, Is.EqualTo(TotalLines)); + + await file.DeleteAsync(); + + Assert.That( + await file.ExistsAsync(), + Is.False); + } + } + + [Test] + public async Task File_CopyTo_File_DifferentFileSystems() + { + using var fs1 = new PhysicalFileSystem(_storage.Root); + using var fs2 = GetFileSystem(); + + var source = fs1.GetFile("/0000.txt"); + var destination = fs2.GetFile("/0000.txt"); + + var content = Guid.NewGuid().ToString(); + + await using (var stream = await source.OpenWriteAsync()) + await using (var writer = new StreamWriter(stream)) + await writer.WriteAsync(content); + + Assert.That(await destination.ExistsAsync(), Is.False); + + await source.CopyToAsync(destination); + Assert.That(await destination.ExistsAsync(), Is.True); + + using var reader = await destination.OpenTextAsync(); + Assert.That( + await reader.ReadToEndAsync(), + Is.EqualTo(content)); + + await source.DeleteAsync(); + await destination.DeleteAsync(); + } + + [Test] + public async Task File_CopyTo_File_DifferentStorages() + { + using var fs1 = CreateFileSystem("ramstack-temp-bucket-1"); + using var fs2 = CreateFileSystem("ramstack-temp-bucket-2"); + + await fs1.CreateBucketAsync("ramstack-project"); + await fs2.CreateBucketAsync("ramstack-project"); + + var source = fs1.GetFile("/1111.txt"); + var destination = fs2.GetFile("/1111.txt"); + + var content = Guid.NewGuid().ToString(); + + await using (var stream = await source.OpenWriteAsync()) + await using (var writer = new StreamWriter(stream)) + await writer.WriteAsync(content); + + Assert.That(await destination.ExistsAsync(), Is.False); + + await source.CopyToAsync(destination); + Assert.That(await destination.ExistsAsync(), Is.True); + + using var reader = await destination.OpenTextAsync(); + Assert.That( + await reader.ReadToEndAsync(), + Is.EqualTo(content)); + + await source.DeleteAsync(); + await destination.DeleteAsync(); + } + + protected override GoogleFileSystem GetFileSystem() => + CreateFileSystem("ramstack-test-bucket"); + + protected override DirectoryInfo GetDirectoryInfo() => + new DirectoryInfo(_storage.Root); + + private GoogleFileSystem CreateFileSystem(string bucket) + { + _buckets.Add(bucket); + + #pragma warning disable CS0618 // Type or member is obsolete + var client = StorageClient.Create( + GoogleCredential.FromFile("credentials.json")); + #pragma warning restore CS0618 // Type or member is obsolete + + return new GoogleFileSystem(client, bucketName: bucket); + } +} diff --git a/tests/Ramstack.FileSystem.Physical.Tests/Ramstack.FileSystem.Physical.Tests.csproj b/tests/Ramstack.FileSystem.Physical.Tests/Ramstack.FileSystem.Physical.Tests.csproj index 7fc3ba7..7b21f50 100644 --- a/tests/Ramstack.FileSystem.Physical.Tests/Ramstack.FileSystem.Physical.Tests.csproj +++ b/tests/Ramstack.FileSystem.Physical.Tests/Ramstack.FileSystem.Physical.Tests.csproj @@ -1,27 +1,27 @@ - - - - net6.0;net8.0 - enable - enable - preview - Ramstack.FileSystem.Physical - - - - - - - - - - - - - - - - - - - + + + + net6.0;net8.0 + enable + enable + preview + Ramstack.FileSystem.Physical + + + + + + + + + + + + + + + + + + + diff --git a/tests/Ramstack.FileSystem.Physical.Tests/ReadonlyPhysicalFileSystemTests.cs b/tests/Ramstack.FileSystem.Physical.Tests/ReadonlyPhysicalFileSystemTests.cs index 3e5b431..67d4976 100644 --- a/tests/Ramstack.FileSystem.Physical.Tests/ReadonlyPhysicalFileSystemTests.cs +++ b/tests/Ramstack.FileSystem.Physical.Tests/ReadonlyPhysicalFileSystemTests.cs @@ -1,27 +1,27 @@ -using Ramstack.FileSystem.Specification.Tests; -using Ramstack.FileSystem.Specification.Tests.Utilities; - -namespace Ramstack.FileSystem.Physical; - -[TestFixture] -public class ReadonlyPhysicalFileSystemTests : VirtualFileSystemSpecificationTests -{ - private readonly TempFileStorage _storage = new TempFileStorage(); - - [OneTimeTearDown] - public void Cleanup() => - _storage.Dispose(); - - /// - protected override IVirtualFileSystem GetFileSystem() - { - return new PhysicalFileSystem(_storage.Root) - { - IsReadOnly = true - }; - } - - /// - protected override DirectoryInfo GetDirectoryInfo() => - new DirectoryInfo(_storage.Root); -} +using Ramstack.FileSystem.Specification.Tests; +using Ramstack.FileSystem.Specification.Tests.Utilities; + +namespace Ramstack.FileSystem.Physical; + +[TestFixture] +public class ReadonlyPhysicalFileSystemTests : VirtualFileSystemSpecificationTests +{ + private readonly TempFileStorage _storage = new TempFileStorage(); + + [OneTimeTearDown] + public void Cleanup() => + _storage.Dispose(); + + /// + protected override IVirtualFileSystem GetFileSystem() + { + return new PhysicalFileSystem(_storage.Root) + { + IsReadOnly = true + }; + } + + /// + protected override DirectoryInfo GetDirectoryInfo() => + new DirectoryInfo(_storage.Root); +} diff --git a/tests/Ramstack.FileSystem.Physical.Tests/WriteablePhysicalFileSystemTests.cs b/tests/Ramstack.FileSystem.Physical.Tests/WriteablePhysicalFileSystemTests.cs index fde8a9a..d03a9c5 100644 --- a/tests/Ramstack.FileSystem.Physical.Tests/WriteablePhysicalFileSystemTests.cs +++ b/tests/Ramstack.FileSystem.Physical.Tests/WriteablePhysicalFileSystemTests.cs @@ -1,58 +1,58 @@ -using Ramstack.FileSystem.Specification.Tests; -using Ramstack.FileSystem.Specification.Tests.Utilities; - -namespace Ramstack.FileSystem.Physical; - -[TestFixture] -public class WriteablePhysicalFileSystemTests : VirtualFileSystemSpecificationTests -{ - private readonly TempFileStorage _storage = new TempFileStorage(); - - [Test] - public async Task Directory_GetProperties_ReturnsConsistentTimezones() - { - using var fs = GetFileSystem(); - - var directory = await fs - .GetDirectory("/project") - .GetPropertiesAsync(); - - Assert.That( - directory.LastAccessTime.Offset, - Is.EqualTo(directory.LastWriteTime.Offset)); - - Assert.That( - directory.CreationTime.Offset, - Is.EqualTo(directory.LastWriteTime.Offset)); - } - - [Test] - public async Task File_GetProperties_ReturnsConsistentTimezones() - { - using var fs = GetFileSystem(); - - var file = await fs - .GetFile("/project/docs/api_reference.md") - .GetPropertiesAsync(); - - Assert.That( - file.LastAccessTime.Offset, - Is.EqualTo(file.LastWriteTime.Offset)); - - Assert.That( - file.CreationTime.Offset, - Is.EqualTo(file.LastWriteTime.Offset)); - } - - [OneTimeTearDown] - public void Cleanup() => - _storage.Dispose(); - - /// - protected override IVirtualFileSystem GetFileSystem() => - new PhysicalFileSystem(_storage.Root); - - /// - protected override DirectoryInfo GetDirectoryInfo() => - new DirectoryInfo(_storage.Root); -} +using Ramstack.FileSystem.Specification.Tests; +using Ramstack.FileSystem.Specification.Tests.Utilities; + +namespace Ramstack.FileSystem.Physical; + +[TestFixture] +public class WriteablePhysicalFileSystemTests : VirtualFileSystemSpecificationTests +{ + private readonly TempFileStorage _storage = new TempFileStorage(); + + [Test] + public async Task Directory_GetProperties_ReturnsConsistentTimezones() + { + using var fs = GetFileSystem(); + + var directory = await fs + .GetDirectory("/project") + .GetPropertiesAsync(); + + Assert.That( + directory.LastAccessTime.Offset, + Is.EqualTo(directory.LastWriteTime.Offset)); + + Assert.That( + directory.CreationTime.Offset, + Is.EqualTo(directory.LastWriteTime.Offset)); + } + + [Test] + public async Task File_GetProperties_ReturnsConsistentTimezones() + { + using var fs = GetFileSystem(); + + var file = await fs + .GetFile("/project/docs/api_reference.md") + .GetPropertiesAsync(); + + Assert.That( + file.LastAccessTime.Offset, + Is.EqualTo(file.LastWriteTime.Offset)); + + Assert.That( + file.CreationTime.Offset, + Is.EqualTo(file.LastWriteTime.Offset)); + } + + [OneTimeTearDown] + public void Cleanup() => + _storage.Dispose(); + + /// + protected override IVirtualFileSystem GetFileSystem() => + new PhysicalFileSystem(_storage.Root); + + /// + protected override DirectoryInfo GetDirectoryInfo() => + new DirectoryInfo(_storage.Root); +} diff --git a/tests/Ramstack.FileSystem.Prefixed.Tests/PrefixedFileSystemTests.cs b/tests/Ramstack.FileSystem.Prefixed.Tests/PrefixedFileSystemTests.cs index c0264e0..4e4a098 100644 --- a/tests/Ramstack.FileSystem.Prefixed.Tests/PrefixedFileSystemTests.cs +++ b/tests/Ramstack.FileSystem.Prefixed.Tests/PrefixedFileSystemTests.cs @@ -1,122 +1,122 @@ -using System.Reflection; - -using Ramstack.FileSystem.Physical; -using Ramstack.FileSystem.Specification.Tests; -using Ramstack.FileSystem.Specification.Tests.Utilities; - -namespace Ramstack.FileSystem.Prefixed; - -[TestFixture] -public class PrefixedFileSystemTests : VirtualFileSystemSpecificationTests -{ - private static readonly Func s_unwrapPrefix = - typeof(PrefixedFileSystem) - .GetMethod("TryUnwrapPrefix", BindingFlags.Static | BindingFlags.NonPublic)! - .CreateDelegate>(); - - private readonly TempFileStorage _storage = new TempFileStorage(); - - [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? UnwrapPrefix(string prefix, string path) => - s_unwrapPrefix(path, prefix); - - [Test] - public async Task File_RootPrefix_DelegatesToInnerProvider() - { - using var fs = new PrefixedFileSystem("/", - new PhysicalFileSystem(_storage.Root)); - - var file = fs.GetFile("/project/README.md"); - Assert.That(await file.ExistsAsync(), Is.True); - } - - [Test] - public async Task File_RootPrefix_MissingFile_ReturnsNotFound() - { - using var fs = new PrefixedFileSystem("/", - new PhysicalFileSystem(_storage.Root)); - - var file = fs.GetFile("/project/nonexistent.txt"); - Assert.That(await file.ExistsAsync(), Is.False); - } - - [Test] - public async Task Directory_RootPrefix_DelegatesToInnerProvider() - { - using var fs = new PrefixedFileSystem("/", - new PhysicalFileSystem(_storage.Root)); - - var file = fs.GetDirectory("/project"); - Assert.That(await file.ExistsAsync(), Is.True); - } - - [Test] - public async Task File_Create_InsideArtificialDirectory_ThrowsException() - { - using var fs = GetFileSystem(); - - await Assert.ThatAsync( - async () => await fs.WriteAsync("/ea5fd219.txt", Stream.Null), - Throws.Exception); - - Assert.That( - await fs.FileExistsAsync("/ea5fd219.txt"), - Is.False); - } - - [Test] - public async Task Directory_Delete_ArtificialDirectory_ThrowsException() - { - using var storage = new TempFileStorage(); - using var fs = new PrefixedFileSystem("/bin/apps/myapp1", new PhysicalFileSystem(storage.Root)); - - await Assert.ThatAsync( - async () => await fs.DeleteDirectoryAsync("/"), - Throws.Exception); - - await Assert.ThatAsync( - async () => await fs.DeleteDirectoryAsync("/bin"), - Throws.Exception); - - await Assert.ThatAsync( - async () => await fs.DeleteDirectoryAsync("/bin/apps"), - Throws.Exception); - - Assert.That( - await fs.FileExistsAsync("/bin/apps/myapp1/project/README.md"), - Is.True); - - await Assert.ThatAsync( - async () => await fs.DeleteDirectoryAsync("/bin/apps/myapp1"), - Throws.Nothing); - - Assert.That( - await fs.FileExistsAsync("/bin/apps/myapp1/project/README.md"), - Is.False); - } - - [OneTimeTearDown] - public void Cleanup() => - _storage.Dispose(); - - protected override IVirtualFileSystem GetFileSystem() => - new PrefixedFileSystem("/project", - new PhysicalFileSystem( - Path.Join(_storage.Root, "project"))); - - /// - protected override DirectoryInfo GetDirectoryInfo() => - new DirectoryInfo(_storage.Root); -} +using System.Reflection; + +using Ramstack.FileSystem.Physical; +using Ramstack.FileSystem.Specification.Tests; +using Ramstack.FileSystem.Specification.Tests.Utilities; + +namespace Ramstack.FileSystem.Prefixed; + +[TestFixture] +public class PrefixedFileSystemTests : VirtualFileSystemSpecificationTests +{ + private static readonly Func s_unwrapPrefix = + typeof(PrefixedFileSystem) + .GetMethod("TryUnwrapPrefix", BindingFlags.Static | BindingFlags.NonPublic)! + .CreateDelegate>(); + + private readonly TempFileStorage _storage = new TempFileStorage(); + + [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? UnwrapPrefix(string prefix, string path) => + s_unwrapPrefix(path, prefix); + + [Test] + public async Task File_RootPrefix_DelegatesToInnerProvider() + { + using var fs = new PrefixedFileSystem("/", + new PhysicalFileSystem(_storage.Root)); + + var file = fs.GetFile("/project/README.md"); + Assert.That(await file.ExistsAsync(), Is.True); + } + + [Test] + public async Task File_RootPrefix_MissingFile_ReturnsNotFound() + { + using var fs = new PrefixedFileSystem("/", + new PhysicalFileSystem(_storage.Root)); + + var file = fs.GetFile("/project/nonexistent.txt"); + Assert.That(await file.ExistsAsync(), Is.False); + } + + [Test] + public async Task Directory_RootPrefix_DelegatesToInnerProvider() + { + using var fs = new PrefixedFileSystem("/", + new PhysicalFileSystem(_storage.Root)); + + var file = fs.GetDirectory("/project"); + Assert.That(await file.ExistsAsync(), Is.True); + } + + [Test] + public async Task File_Create_InsideArtificialDirectory_ThrowsException() + { + using var fs = GetFileSystem(); + + await Assert.ThatAsync( + async () => await fs.WriteAsync("/ea5fd219.txt", Stream.Null), + Throws.Exception); + + Assert.That( + await fs.FileExistsAsync("/ea5fd219.txt"), + Is.False); + } + + [Test] + public async Task Directory_Delete_ArtificialDirectory_ThrowsException() + { + using var storage = new TempFileStorage(); + using var fs = new PrefixedFileSystem("/bin/apps/myapp1", new PhysicalFileSystem(storage.Root)); + + await Assert.ThatAsync( + async () => await fs.DeleteDirectoryAsync("/"), + Throws.Exception); + + await Assert.ThatAsync( + async () => await fs.DeleteDirectoryAsync("/bin"), + Throws.Exception); + + await Assert.ThatAsync( + async () => await fs.DeleteDirectoryAsync("/bin/apps"), + Throws.Exception); + + Assert.That( + await fs.FileExistsAsync("/bin/apps/myapp1/project/README.md"), + Is.True); + + await Assert.ThatAsync( + async () => await fs.DeleteDirectoryAsync("/bin/apps/myapp1"), + Throws.Nothing); + + Assert.That( + await fs.FileExistsAsync("/bin/apps/myapp1/project/README.md"), + Is.False); + } + + [OneTimeTearDown] + public void Cleanup() => + _storage.Dispose(); + + protected override IVirtualFileSystem GetFileSystem() => + new PrefixedFileSystem("/project", + new PhysicalFileSystem( + Path.Join(_storage.Root, "project"))); + + /// + protected override DirectoryInfo GetDirectoryInfo() => + new DirectoryInfo(_storage.Root); +} diff --git a/tests/Ramstack.FileSystem.Prefixed.Tests/Ramstack.FileSystem.Prefixed.Tests.csproj b/tests/Ramstack.FileSystem.Prefixed.Tests/Ramstack.FileSystem.Prefixed.Tests.csproj index e59e6f1..20c8349 100644 --- a/tests/Ramstack.FileSystem.Prefixed.Tests/Ramstack.FileSystem.Prefixed.Tests.csproj +++ b/tests/Ramstack.FileSystem.Prefixed.Tests/Ramstack.FileSystem.Prefixed.Tests.csproj @@ -1,28 +1,28 @@ - - - - net6.0;net8.0 - enable - enable - preview - Ramstack.FileSystem.Prefixed - - - - - - - - - - - - - - - - - - - - + + + + net6.0;net8.0 + enable + enable + preview + Ramstack.FileSystem.Prefixed + + + + + + + + + + + + + + + + + + + + diff --git a/tests/Ramstack.FileSystem.Readonly.Tests/Ramstack.FileSystem.Readonly.Tests.csproj b/tests/Ramstack.FileSystem.Readonly.Tests/Ramstack.FileSystem.Readonly.Tests.csproj index 8be8104..92d5ac1 100644 --- a/tests/Ramstack.FileSystem.Readonly.Tests/Ramstack.FileSystem.Readonly.Tests.csproj +++ b/tests/Ramstack.FileSystem.Readonly.Tests/Ramstack.FileSystem.Readonly.Tests.csproj @@ -1,28 +1,28 @@ - - - - net6.0;net8.0 - enable - enable - preview - Ramstack.FileSystem.Readonly - - - - - - - - - - - - - - - - - - - - + + + + net6.0;net8.0 + enable + enable + preview + Ramstack.FileSystem.Readonly + + + + + + + + + + + + + + + + + + + + diff --git a/tests/Ramstack.FileSystem.Readonly.Tests/ReadOnlyFileSystemTests.cs b/tests/Ramstack.FileSystem.Readonly.Tests/ReadOnlyFileSystemTests.cs index cc1918c..f6a9fd5 100644 --- a/tests/Ramstack.FileSystem.Readonly.Tests/ReadOnlyFileSystemTests.cs +++ b/tests/Ramstack.FileSystem.Readonly.Tests/ReadOnlyFileSystemTests.cs @@ -1,23 +1,23 @@ -using Ramstack.FileSystem.Physical; -using Ramstack.FileSystem.Specification.Tests; -using Ramstack.FileSystem.Specification.Tests.Utilities; - -namespace Ramstack.FileSystem.Readonly; - -[TestFixture] -public class ReadOnlyFileSystemTests : VirtualFileSystemSpecificationTests -{ - private readonly TempFileStorage _storage = new TempFileStorage(); - - [OneTimeTearDown] - public void Cleanup() => - _storage.Dispose(); - - /// - protected override IVirtualFileSystem GetFileSystem() => - new ReadonlyFileSystem(new PhysicalFileSystem(_storage.Root)); - - /// - protected override DirectoryInfo GetDirectoryInfo() => - new(_storage.Root); -} +using Ramstack.FileSystem.Physical; +using Ramstack.FileSystem.Specification.Tests; +using Ramstack.FileSystem.Specification.Tests.Utilities; + +namespace Ramstack.FileSystem.Readonly; + +[TestFixture] +public class ReadOnlyFileSystemTests : VirtualFileSystemSpecificationTests +{ + private readonly TempFileStorage _storage = new TempFileStorage(); + + [OneTimeTearDown] + public void Cleanup() => + _storage.Dispose(); + + /// + protected override IVirtualFileSystem GetFileSystem() => + new ReadonlyFileSystem(new PhysicalFileSystem(_storage.Root)); + + /// + protected override DirectoryInfo GetDirectoryInfo() => + new(_storage.Root); +} diff --git a/tests/Ramstack.FileSystem.Specification.Tests/README.md b/tests/Ramstack.FileSystem.Specification.Tests/README.md index f84b3c8..16f423c 100644 --- a/tests/Ramstack.FileSystem.Specification.Tests/README.md +++ b/tests/Ramstack.FileSystem.Specification.Tests/README.md @@ -1,59 +1,59 @@ -# Ramstack.FileSystem.Specification.Tests -[![NuGet](https://img.shields.io/nuget/v/Ramstack.FileSystem.Specification.Tests.svg)](https://nuget.org/packages/Ramstack.FileSystem.Specification.Tests) -[![MIT](https://img.shields.io/github/license/rameel/ramstack.filesystem)](https://github.com/rameel/ramstack.filesystem/blob/main/LICENSE) - -Provides a suite of `NUnit` tests to validate specifications for `Ramstack.FileSystem`. - -## Getting Started - -To install the `Ramstack.FileSystem.Specification.Tests` [NuGet package](https://www.nuget.org/packages/Ramstack.FileSystem.Specification.Tests) -in your project, run the following command: -```console -dotnet add package Ramstack.FileSystem.Specification.Tests -``` - -## Usage - -```csharp -using Ramstack.FileSystem.Specification.Tests; -using Ramstack.FileSystem.Specification.Tests.Utilities; - -namespace Ramstack.FileSystem.Physical; - -[TestFixture] -public class PhysicalFileSystemSpecificationTests : VirtualFileSystemSpecificationTests -{ - // Test content for the file system structure - private readonly TempFileStorage _storage = new(); - - [OneTimeTearDown] - public void Cleanup() - { - // Remove temporary files - _storage.Dispose(); - } - - // Create a file system - protected override IVirtualFileSystem GetFileSystem() => - new PhysicalFileSystem(_storage.Root); - - // Return DirectoryInfo to check file system structure - protected override DirectoryInfo GetDirectoryInfo() => - new DirectoryInfo(_storage.Root); -} -``` - -## 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.virtualfiles/blob/main/LICENSE) file for more details. +# Ramstack.FileSystem.Specification.Tests +[![NuGet](https://img.shields.io/nuget/v/Ramstack.FileSystem.Specification.Tests.svg)](https://nuget.org/packages/Ramstack.FileSystem.Specification.Tests) +[![MIT](https://img.shields.io/github/license/rameel/ramstack.filesystem)](https://github.com/rameel/ramstack.filesystem/blob/main/LICENSE) + +Provides a suite of `NUnit` tests to validate specifications for `Ramstack.FileSystem`. + +## Getting Started + +To install the `Ramstack.FileSystem.Specification.Tests` [NuGet package](https://www.nuget.org/packages/Ramstack.FileSystem.Specification.Tests) +in your project, run the following command: +```console +dotnet add package Ramstack.FileSystem.Specification.Tests +``` + +## Usage + +```csharp +using Ramstack.FileSystem.Specification.Tests; +using Ramstack.FileSystem.Specification.Tests.Utilities; + +namespace Ramstack.FileSystem.Physical; + +[TestFixture] +public class PhysicalFileSystemSpecificationTests : VirtualFileSystemSpecificationTests +{ + // Test content for the file system structure + private readonly TempFileStorage _storage = new(); + + [OneTimeTearDown] + public void Cleanup() + { + // Remove temporary files + _storage.Dispose(); + } + + // Create a file system + protected override IVirtualFileSystem GetFileSystem() => + new PhysicalFileSystem(_storage.Root); + + // Return DirectoryInfo to check file system structure + protected override DirectoryInfo GetDirectoryInfo() => + new DirectoryInfo(_storage.Root); +} +``` + +## 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.virtualfiles/blob/main/LICENSE) file for more details. diff --git a/tests/Ramstack.FileSystem.Specification.Tests/Ramstack.FileSystem.Specification.Tests.csproj b/tests/Ramstack.FileSystem.Specification.Tests/Ramstack.FileSystem.Specification.Tests.csproj index 9f69632..72ffda5 100644 --- a/tests/Ramstack.FileSystem.Specification.Tests/Ramstack.FileSystem.Specification.Tests.csproj +++ b/tests/Ramstack.FileSystem.Specification.Tests/Ramstack.FileSystem.Specification.Tests.csproj @@ -1,59 +1,59 @@ - - - - net6.0;net8.0 - Suite of NUnit tests to check specifications for Ramstack.FileSystem. - enable - enable - preview - true - false - - - - true - https://github.com/rameel/ramstack.filesystem - Rameel - https://github.com/rameel/ramstack.filesystem#readme - README.md - MIT - true - snupkg - true - true - true - true - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - Properties\README.md - True - \ - - - - + + + + net6.0;net8.0 + Suite of NUnit tests to check specifications for Ramstack.FileSystem. + enable + enable + preview + true + false + + + + true + https://github.com/rameel/ramstack.filesystem + Rameel + https://github.com/rameel/ramstack.filesystem#readme + README.md + MIT + true + snupkg + true + true + true + true + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + Properties\README.md + True + \ + + + + diff --git a/tests/Ramstack.FileSystem.Specification.Tests/Utilities/TempFileStorage.cs b/tests/Ramstack.FileSystem.Specification.Tests/Utilities/TempFileStorage.cs index f6403ad..db18781 100644 --- a/tests/Ramstack.FileSystem.Specification.Tests/Utilities/TempFileStorage.cs +++ b/tests/Ramstack.FileSystem.Specification.Tests/Utilities/TempFileStorage.cs @@ -1,127 +1,127 @@ -namespace Ramstack.FileSystem.Specification.Tests.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() - { - if (Directory.Exists(Root)) - Directory.Delete(Root, true); - } -} +namespace Ramstack.FileSystem.Specification.Tests.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() + { + if (Directory.Exists(Root)) + Directory.Delete(Root, true); + } +} diff --git a/tests/Ramstack.FileSystem.Specification.Tests/VirtualFileSystemSpecificationTests.cs b/tests/Ramstack.FileSystem.Specification.Tests/VirtualFileSystemSpecificationTests.cs index a05641b..6713405 100644 --- a/tests/Ramstack.FileSystem.Specification.Tests/VirtualFileSystemSpecificationTests.cs +++ b/tests/Ramstack.FileSystem.Specification.Tests/VirtualFileSystemSpecificationTests.cs @@ -1,1111 +1,1111 @@ -using System.Security.Cryptography; -using System.Text; - -using Ramstack.FileSystem.Physical; - -namespace Ramstack.FileSystem.Specification.Tests; - -/// -/// Represents a base class for specification tests of virtual file systems. -/// -/// -/// This class defines common functionality and setup for tests that validate the behavior -/// of virtual file systems. Derived classes should implement the abstract methods to provide specific -/// details about the virtual file system being tested. -/// -public abstract class VirtualFileSystemSpecificationTests -{ - [Test] - [Order(-1003)] - public async Task FileTree_MatchesStructure() - { - using var fs = GetFileSystem(); - await CompareDirectoriesAsync(fs.GetDirectory("/"), GetDirectoryInfo()); - - static async Task CompareDirectoriesAsync(VirtualDirectory virtualDir, DirectoryInfo localDir) - { - if (await virtualDir.ExistsAsync() != localDir.Exists) - Assert.Fail($"Directory '{virtualDir.FullName}'.Exists ({virtualDir.ExistsAsync().Result}) != '{localDir.FullName}'.Exists ({localDir.Exists})"); - - var virtualFiles = await virtualDir.GetFilesAsync().OrderBy(f => f.Name).ToArrayAsync(); - var localFiles = localDir.EnumerateFiles().OrderBy(f => f.Name).ToArray(); - - Assert.That( - virtualFiles.Select(f => f.Name), - Is.EquivalentTo(localFiles.Select(f => f.Name)), - virtualDir.FullName); - - for (var i = 0; i < virtualFiles.Length; i++) - await CompareFilesAsync(virtualFiles[i], localFiles[i]); - - var virtualDirectories = await virtualDir.GetDirectoriesAsync().OrderBy(d => d.Name).ToArrayAsync(); - var localDirectories = localDir.EnumerateDirectories().OrderBy(d => d.Name).ToArray(); - - Assert.That( - virtualDirectories.Select(f => f.Name), - Is.EquivalentTo(localDirectories.Select(f => f.Name)), - virtualDir.FullName); - - for (var i = 0; i < virtualDirectories.Length; i++) - await CompareDirectoriesAsync(virtualDirectories[i], localDirectories[i]); - } - - static async Task CompareFilesAsync(VirtualFile virtualFile, FileInfo localFile) - { - Assert.That( - await virtualFile.ReadAllTextAsync(), - Is.EqualTo(await File.ReadAllTextAsync(localFile.FullName)), - virtualFile.FullName); - } - } - - [Test] - [Order(-1002)] - public async Task FileTree_VirtualNode_FileSystem_MatchesIssuingFileSystemReference() - { - using var fs = GetFileSystem(); - - Assert.That( - await fs.GetFilesAsync("/", "**").AnyAsync(), - Is.True); - - await foreach (var node in fs.GetFileNodesAsync("/", "**")) - { - VirtualNode byPath = node is VirtualFile - ? fs.GetFile(node.FullName) - : fs.GetDirectory(node.FullName); - - Assert.That(fs, Is.SameAs(node.FileSystem)); - Assert.That(fs, Is.SameAs(byPath.FileSystem)); - } - } - - [Test] - public async Task Exists_ReturnsTrue_For_ExistingFile() - { - using var fs = GetFileSystem(); - - Assert.That(await fs.DirectoryExistsAsync("/project/assets"), Is.True); - Assert.That(await fs.FileExistsAsync("/project/README.md"), Is.True); - } - - [Test] - public async Task File_Exists_ReturnsFalse_For_NonExistingFile() - { - using var fs = GetFileSystem(); - - Assert.That(await fs.FileExistsAsync($"/{Guid.NewGuid()}"), Is.False); - } - - [Test] - public async Task File_OpenRead_ReturnsReadableStream() - { - using var fs = GetFileSystem(); - await using var stream = await fs.OpenReadAsync("/project/README.md"); - - Assert.That(stream.CanRead, Is.True); - stream.ReadByte(); - } - - [Test] - public async Task File_OpenRead_ThrowsException_For_NonExistingFile() - { - using var fs = GetFileSystem(); - - await Assert.ThatAsync(async () => await fs.OpenReadAsync("/6b01ba26.txt"), Throws.Exception); - await Assert.ThatAsync(async () => await fs.OpenReadAsync("/6b01ba26/6b01ba26.txt"), Throws.Exception); - } - - [Test] - public async Task File_OpenWrite_ReturnsWritableStream() - { - using var fs = GetFileSystem(); - - if (fs.IsReadOnly) - return; - - var text = $"Id:{Guid.NewGuid()}"; - - await using (var stream = await fs.OpenWriteAsync("/project/README.md")) - { - Assert.That(stream.CanWrite, Is.True); - await stream.WriteAsync(Encoding.UTF8.GetBytes(text)); - } - - Assert.That( - await fs.ReadAllTextAsync("/project/README.md"), - Is.EqualTo(text)); - } - - [Test] - public async Task File_OpenWrite_NewFile() - { - using var fs = GetFileSystem(); - - if (fs.IsReadOnly) - return; - - var path = "/project/6b01ba2608be"; - var text = $"Automatically generated on {DateTime.Now:s}\n\nNew Id:{Guid.NewGuid()}"; - - await using (var stream = await fs.OpenWriteAsync(path)) - { - Assert.That(stream.CanWrite, Is.True); - await stream.WriteAsync(Encoding.UTF8.GetBytes(text)); - } - - Assert.That( - await fs.ReadAllTextAsync(path), - Is.EqualTo(text)); - - await fs.DeleteFileAsync(path); - - Assert.That( - await fs.FileExistsAsync(path), - Is.False); - } - - [Test] - public async Task File_Write_OverwritesContent_When_OverwriteIsTrue() - { - using var fs = GetFileSystem(); - - if (fs.IsReadOnly) - return; - - var file = fs.GetFile("/project/README.md"); - var text = $"Id:{Guid.NewGuid()}"; - - var ms = new MemoryStream(); - ms.Write(Encoding.UTF8.GetBytes(text)); - ms.Position = 0; - - await file.WriteAsync(ms, overwrite: true); - - Assert.That( - await file.ReadAllTextAsync(), - Is.EqualTo(text)); - } - - [Test] - public async Task File_Write_ThrowsException_For_ExistingFile_When_OverwriteIsFalse() - { - using var fs = GetFileSystem(); - - if (fs.IsReadOnly) - return; - - var file = fs.GetFile("/project/README.md"); - var text = await file.ReadAllTextAsync(); - - var ms = new MemoryStream(); - ms.Write(Encoding.UTF8.GetBytes(Guid.NewGuid().ToString())); - ms.Position = 0; - - Assert.That( - () => file.WriteAsync(ms, overwrite: false), - Throws.Exception); - - Assert.That( - await file.ReadAllTextAsync(), - Is.EqualTo(text)); - } - - [Test] - public async Task File_Write_NewFile() - { - using var fs = GetFileSystem(); - - if (fs.IsReadOnly) - return; - - var path = "/project/0fce49e0.txt"; - var text = $"Automatically generated on {DateTime.Now:s}\n\nNew Id:{Guid.NewGuid()}"; - - var ms = new MemoryStream(); - ms.Write(Encoding.UTF8.GetBytes(text)); - ms.Position = 0; - - var file = fs.GetFile(path); - Assert.That(await file.ExistsAsync(), Is.False); - - await file.WriteAsync(ms); - - Assert.That(await file.ExistsAsync(), Is.True); - Assert.That(await file.ReadAllTextAsync(), Is.EqualTo(text)); - - await file.DeleteAsync(); - - Assert.That(await file.ExistsAsync(), Is.False); - } - - [Test] - public async Task File_Write_EmptyStream_CreatesFile() - { - using var fs = GetFileSystem(); - - if (fs.IsReadOnly) - return; - - var file = fs.GetFile("/project/e3a1c7f0_empty_write.txt"); - - Assert.That(await file.ExistsAsync(), Is.False); - - await file.WriteAsync(Stream.Null); - - Assert.That(await file.ExistsAsync(), Is.True); - Assert.That(await file.GetLengthAsync(), Is.Zero); - - await file.DeleteAsync(); - } - - [Test] - public async Task File_Write_EmptyStream_ExistingFile_ResultsInZeroLength() - { - using var fs = GetFileSystem(); - - if (fs.IsReadOnly) - return; - - var path = "/project/b7d2e9f1_overwrite_empty.txt"; - var file = fs.GetFile(path); - - var ms = new MemoryStream("Some content"u8.ToArray()); - await file.WriteAsync(ms); - - Assert.That(await file.ExistsAsync(), Is.True); - Assert.That(await file.GetLengthAsync(), Is.GreaterThan(0)); - - // Write empty stream over existing file - await file.WriteAsync(Stream.Null, overwrite: true); - - Assert.That(await file.ExistsAsync(), Is.True); - Assert.That(await file.GetLengthAsync(), Is.Zero); - - await file.DeleteAsync(); - } - - [Test] - public async Task File_OpenWrite_NoWrite_CreatesFile() - { - using var fs = GetFileSystem(); - - if (fs.IsReadOnly) - return; - - var file = fs.GetFile("/project/a4f8d2e0_openwrite_empty.txt"); - - Assert.That(await file.ExistsAsync(), Is.False); - - await using (await file.OpenWriteAsync()) - { - // Do not write anything, just close - } - - Assert.That(await file.ExistsAsync(), Is.True); - Assert.That(await file.GetLengthAsync(), Is.Zero); - - await file.DeleteAsync(); - } - - [Test] - public async Task File_OpenWrite_ExistingFile_ResultsInZeroLength() - { - using var fs = GetFileSystem(); - - if (fs.IsReadOnly) - return; - - var file = fs.GetFile("/project/c5e9a3b1_openwrite_truncate.txt"); - - var ms = new MemoryStream("Some content"u8.ToArray()); - await file.WriteAsync(ms); - - Assert.That(await file.ExistsAsync(), Is.True); - Assert.That(await file.GetLengthAsync(), Is.GreaterThan(0)); - - // Open for write and close without writing - await using (await file.OpenWriteAsync()) - { - // Do not write anything, just close - } - - Assert.That(await file.ExistsAsync(), Is.True); - Assert.That(await file.GetLengthAsync(), Is.Zero); - - await file.DeleteAsync(); - } - - [Test] - public async Task File_Delete_For_ExistingFile() - { - using var fs = GetFileSystem(); - - if (fs.IsReadOnly) - return; - - var path = "/project/793cd29d96c4.txt"; - var file = fs.GetFile(path); - - Assert.That(await file.ExistsAsync(), Is.False); - - await file.WriteAsync(Stream.Null); - - Assert.That(await file.ExistsAsync(), Is.True); - - await file.DeleteAsync(); - - Assert.That(await file.ExistsAsync(), Is.False); - } - - [Test] - public async Task File_Delete_For_NonExistingFile() - { - using var fs = GetFileSystem(); - - if (fs.IsReadOnly) - return; - - await fs.DeleteFileAsync("/1861cb25.txt"); - await fs.DeleteFileAsync("/1861cb25/1861cb25.txt"); - } - - [Test] - public async Task File_Readonly_OpenWrite_ThrowsException() - { - using var fs = GetFileSystem(); - - if (!fs.IsReadOnly) - return; - - await Assert.ThatAsync( - async () => { await using var stream = await fs.OpenWriteAsync("/project/README.md"); }, - Throws.Exception); - } - - [Test] - public async Task File_Readonly_OpenWrite_ThrowsException_For_NewFile() - { - using var fs = GetFileSystem(); - - if (!fs.IsReadOnly) - return; - - await Assert.ThatAsync( - async () => await fs.OpenWriteAsync("/project/6d368a9d0e97.md"), - Throws.Exception); - } - - [Test] - public async Task File_Readonly_Write_ThrowsException() - { - using var fs = GetFileSystem(); - - if (!fs.IsReadOnly) - return; - - await Assert.ThatAsync( - async () => await fs.WriteAsync("/project/README.md", Stream.Null), - Throws.Exception); - } - - [Test] - public async Task File_Readonly_Write_ThrowsException_For_NewFile() - { - using var fs = GetFileSystem(); - - if (!fs.IsReadOnly) - return; - - await Assert.ThatAsync( - async () => await fs.WriteAsync("/project/9286d04f.pdf", Stream.Null), - Throws.Exception); - } - - [Test] - public async Task File_Readonly_Delete_ThrowsException() - { - using var fs = GetFileSystem(); - - if (!fs.IsReadOnly) - return; - - await Assert.ThatAsync( - async () => await fs.DeleteFileAsync("/project/README.md"), - Throws.Exception); - - Assert.That( - await fs.FileExistsAsync("/project/README.md"), - Is.True); - } - - [Test] - public async Task File_Readonly_Delete_ThrowsException_For_NonExistingFile() - { - using var fs = GetFileSystem(); - - if (!fs.IsReadOnly) - return; - - await Assert.ThatAsync( - async () => await fs.DeleteFileAsync("/project/c180408e8005.png"), - Throws.Exception); - } - - [Test] - public async Task File_Readonly_ReadonlyToWritable_Succeeds() - { - using var fs = GetFileSystem(); - using var ds = new PhysicalFileSystem( - Path.Combine( - Path.GetTempPath(), - Path.GetRandomFileName())); - - if (!fs.IsReadOnly) - return; - - var src = fs.GetFile("/project/README.md"); - var dst = ds.GetFile("/README.md"); - - await src.CopyToAsync(dst); - - Assert.That( - await dst.ExistsAsync(), - Is.True); - - Assert.That( - await ds.FileExistsAsync("/README.md"), - Is.True); - - Assert.That( - await src.ReadAllTextAsync(), - Is.EqualTo(await src.ReadAllTextAsync())); - - Directory.Delete(ds.Root, true); - } - - [Test] - public async Task File_CopyTo_Path() - { - using var fs = GetFileSystem(); - if (fs.IsReadOnly) - return; - - var sourcePath = "/project/README.md"; - var destinationPath = "/project/README - Copy.md"; - - Assert.That( - await fs.FileExistsAsync(destinationPath), - Is.False); - - await fs.CopyFileAsync(sourcePath, destinationPath); - Assert.That( - await fs.FileExistsAsync(destinationPath), - Is.True); - - Assert.That( - await fs.ReadAllTextAsync(destinationPath), - Is.EqualTo(await fs.ReadAllTextAsync(sourcePath))); - - await fs.DeleteFileAsync(destinationPath); - Assert.That( - await fs.FileExistsAsync(destinationPath), - Is.False); - } - - [Test] - public async Task File_CopyTo_Path_NoOverwrite_ThrowsException() - { - using var fs = GetFileSystem(); - if (fs.IsReadOnly) - return; - - var sourcePath = "/project/README.md"; - var destinationPath = "/project/README - Copy.md"; - - await fs.CopyFileAsync(sourcePath, destinationPath); - - Assert.That( - await fs.FileExistsAsync(destinationPath), - Is.True); - - await Assert.ThatAsync( - async () => await fs.CopyFileAsync(sourcePath, destinationPath), - Throws.Exception); - - await fs.DeleteFileAsync(destinationPath); - Assert.That( - await fs.FileExistsAsync(destinationPath), - Is.False); - } - - [Test] - public async Task File_CopyTo_File_SameFileSystems() - { - using var fs = GetFileSystem(); - if (fs.IsReadOnly) - return; - - var source = fs.GetFile("/project/README.md"); - var destination = fs.GetFile("/project/README - Copy.md"); - - Assert.That( - await destination.ExistsAsync(), - Is.False); - - await source.CopyToAsync(destination); - - Assert.That( - await destination.ExistsAsync(), - Is.True); - - Assert.That( - await destination.ReadAllTextAsync(), - Is.EqualTo(await source.ReadAllTextAsync())); - - await destination.DeleteAsync(); - Assert.That( - await destination.ExistsAsync(), - Is.False); - } - - [Test] - public async Task File_CopyTo_File_NotSameFileSystems() - { - using var fs1 = GetFileSystem(); - using var fs2 = GetFileSystem(); - - if (fs1.IsReadOnly) - return; - - var source = fs1.GetFile("/project/README.md"); - var destination = fs2.GetFile("/project/README - Copy.md"); - - Assert.That( - await destination.ExistsAsync(), - Is.False); - - await source.CopyToAsync(destination); - - Assert.That( - await destination.ExistsAsync(), - Is.True); - - Assert.That( - await destination.ReadAllTextAsync(), - Is.EqualTo(await source.ReadAllTextAsync())); - - await destination.DeleteAsync(); - Assert.That( - await destination.ExistsAsync(), - Is.False); - } - - [Test] - public async Task File_CopyTo_File_NoOverwrite_ThrowsException() - { - using var fs = GetFileSystem(); - if (fs.IsReadOnly) - return; - - var source = fs.GetFile("/project/README.md"); - var destination = fs.GetFile("/project/README - Copy.md"); - - Assert.That( - await destination.ExistsAsync(), - Is.False); - - await source.CopyToAsync(destination); - - Assert.That( - await destination.ExistsAsync(), - Is.True); - - await Assert.ThatAsync( - async () => await source.CopyToAsync(destination), - Throws.Exception); - - await destination.DeleteAsync(); - Assert.That( - await destination.ExistsAsync(), - Is.False); - } - - [Test] - public async Task File_CopyTo_ThrowsException_When_CopyingToItself() - { - using var fs = GetFileSystem(); - - if (fs.IsReadOnly) - return; - - var file = fs.GetFile("/project/README.md"); - await Assert.ThatAsync(async () => await file.CopyToAsync(file.FullName), Throws.Exception); - await Assert.ThatAsync(async () => await file.CopyToAsync(file), Throws.Exception); - await Assert.ThatAsync(async () => await file.CopyToAsync(fs.GetFile(file.FullName)), Throws.Exception); - } - - [Test] - public async Task File_CopyTo_ThrowException_For_NonExistingFile() - { - using var fs = GetFileSystem(); - - if (fs.IsReadOnly) - return; - - await Assert.ThatAsync( - async () => await fs.CopyFileAsync("/project/800569b11aaa0438.txt", "/project/test-b11aaa.txt"), - Throws.Exception); - - Assert.That( - await fs.FileExistsAsync("/project/test-b11aaa.txt"), - Is.False); - } - - [Test] - public async Task File_Readonly_CopyTo_ThrowException_For_NonExistingFile() - { - using var fs = GetFileSystem(); - - if (!fs.IsReadOnly) - return; - - await Assert.ThatAsync( - async () => await fs.CopyFileAsync("/project/README.md", "/project/README - Copy.md"), - Throws.Exception); - - Assert.That( - await fs.FileExistsAsync("/project/README - Copy.md"), - Is.False); - } - - [Test] - public async Task File_ReadAllBytes() - { - using var fs = GetFileSystem(); - - var file = fs.GetFile("/project/README.md"); - using var reader = new BinaryReader(await file.OpenReadAsync()); - - var bytes = await fs.ReadAllBytesAsync(file.FullName); - var expected = reader.ReadBytes(4096); - var properties = await file.GetPropertiesAsync(); - - Assert.That( - bytes.SequenceEqual(expected), - Is.True); - - Assert.That( - bytes.Length, - Is.EqualTo(properties.Length)); - } - - [Test] - public async Task File_ReadAllText() - { - using var fs = GetFileSystem(); - - var file = fs.GetFile("/project/README.md"); - var stream = await file.OpenReadAsync(); - using var reader = new StreamReader(stream); - - Assert.That( - await fs.ReadAllTextAsync(file.FullName), - Is.EqualTo(await reader.ReadToEndAsync())); - } - - [Test] - public async Task File_ReadAllLines() - { - using var fs = GetFileSystem(); - - var file = fs.GetFile("/project/README.md"); - var stream = await file.OpenReadAsync(); - using var reader = new StreamReader(stream); - - var lines = new List(); - while (await reader.ReadLineAsync() is {} line) - lines.Add(line); - - Assert.That( - await fs.ReadAllLinesAsync(file.FullName), - Is.EquivalentTo(lines)); - } - - [Test] - public async Task File_WriteAllBytes() - { - using var fs = GetFileSystem(); - if (fs.IsReadOnly) - return; - - - var path = "/project/b02a67d8.bin"; - var data = RandomNumberGenerator.GetBytes(10_000); - - await fs.WriteAllBytesAsync(path, data); - - Assert.That( - await fs.ReadAllBytesAsync(path), - Is.EquivalentTo(data)); - - await fs.DeleteFileAsync(path); - } - - [Test] - public async Task File_WriteAllText() - { - using var fs = GetFileSystem(); - if (fs.IsReadOnly) - return; - - var list = new List(); - for (var i = 0; i < 1000; i++) - list.Add($"Hello, 世界! Unicode test: café, weiß, Привет, ёжик! こんにちは! {Guid.NewGuid()}"); - - var data = string.Join(Environment.NewLine, list); - var path = "/project/5cc34e90a7c9.txt"; - - await fs.WriteAllTextAsync(path, data); - - Assert.That( - await fs.ReadAllTextAsync(path), - Is.EqualTo(data)); - - await fs.DeleteFileAsync(path); - } - - [Test] - public async Task File_WriteAllLines() - { - using var fs = GetFileSystem(); - if (fs.IsReadOnly) - return; - - var list = new List(); - for (var i = 0; i < 1000; i++) - list.Add($"Hello, 世界! Unicode test: café, weiß, Привет, ёжик! こんにちは! {Guid.NewGuid()}"); - - var path = "/project/76bb51ee6cb7.txt"; - await fs.WriteAllLinesAsync(path, list); - - Assert.That( - await fs.ReadAllLinesAsync(path), - Is.EquivalentTo(list)); - - await fs.DeleteFileAsync(path); - } - - [Test] - public async Task Directory_GetFiles() - { - using var fs = GetFileSystem(); - - Assert.That( - await fs.GetDirectory("/project/assets").GetFilesAsync().AnyAsync(), - Is.False); - - Assert.That( - await fs - .GetDirectory("/project/assets/images") - .GetFilesAsync() - .Select(f => f.FullName) - .OrderBy(p => p) - .ToArrayAsync(), - Is.EquivalentTo( - [ - "/project/assets/images/icon.svg", - "/project/assets/images/logo.png" - ])); - } - - [Test] - public async Task Directory_GetDirectories() - { - using var fs = GetFileSystem(); - - Assert.That( - await fs.GetDirectory("/project/assets/fonts").GetDirectoriesAsync().AnyAsync(), - Is.False); - - Assert.That( - await fs - .GetDirectory("/project/assets") - .GetDirectoriesAsync() - .Select(f => f.FullName) - .OrderBy(p => p) - .ToArrayAsync(), - Is.EquivalentTo( - [ - "/project/assets/fonts", - "/project/assets/images", - "/project/assets/styles" - ]) - ); - } - - [Test] - public async Task Directory_GetFileNodes() - { - using var fs = GetFileSystem(); - - Assert.That( - await fs.GetDirectory("/project/assets/scripts").GetFileNodesAsync().AnyAsync(), - Is.False); - - Assert.That( - await fs - .GetDirectory("/project/assets/images") - .GetFileNodesAsync() - .Select(f => f.FullName) - .OrderBy(p => p) - .ToArrayAsync(), - Is.EquivalentTo( - [ - "/project/assets/images/backgrounds", - "/project/assets/images/icon.svg", - "/project/assets/images/logo.png" - ]) - ); - } - - [Test] - public async Task Directory_Glob_GetFiles() - { - using var fs = GetFileSystem(); - - Assert.That( - await fs.GetDirectory("/project/assets").GetFilesAsync("*/*.otf").AnyAsync(), - Is.False); - - Assert.That( - await fs - .GetDirectory("/project") - .GetFilesAsync( - pattern: "[a][s]set[s]/{images,styles}/{main.css,logo.png}") - .Select(f => f.FullName) - .OrderBy(p => p) - .ToArrayAsync(), - Is.EquivalentTo( - [ - "/project/assets/images/logo.png", - "/project/assets/styles/main.css" - ]) - ); - - Assert.That( - await fs - .GetDirectory("/project") - .GetFilesAsync( - patterns: ["[a]sset{,s}/*/*.{svg,png}", "assets/{images,styles}/*.css"], - excludes: ["assets/imag*/*icon*", "**/style*/print.css"]) - .Select(f => f.FullName) - .OrderBy(p => p) - .ToArrayAsync(), - Is.EquivalentTo( - [ - "/project/assets/images/logo.png", - "/project/assets/styles/main.css" - ]) - ); - } - - [Test] - public async Task Directory_Glob_GetDirectories() - { - using var fs = GetFileSystem(); - - Assert.That( - await fs.GetDirectory("/project/assets").GetDirectoriesAsync("{styles,fonts}/*").AnyAsync(), - Is.False); - - Assert.That( - await fs - .GetDirectory("/project") - .GetDirectoriesAsync("[a]sset{,s}/*/*") - .Select(f => f.FullName) - .OrderBy(p => p) - .ToArrayAsync(), - Is.EquivalentTo(["/project/assets/images/backgrounds"])); - - Assert.That( - await fs - .GetDirectory("/project") - .GetDirectoriesAsync( - patterns: ["[a]sset{,s}/*/*", "*/**/*s"], - excludes: ["src/**", "tests/**/Fixtures"]) - .Select(f => f.FullName) - .OrderBy(p => p) - .ToArrayAsync(), - Is.EquivalentTo( - [ - "/project/assets/fonts", - "/project/assets/images", - "/project/assets/images/backgrounds", - "/project/assets/styles" - ]) - ); - } - - [Test] - public async Task Directory_Glob_GetFileNodes() - { - using var fs = GetFileSystem(); - - Assert.That( - await fs.GetDirectory("/project/assets").GetFileNodesAsync("{styles,fonts}/*/*").AnyAsync(), - Is.False); - - Assert.That( - await fs - .GetDirectory("/project") - .GetFileNodesAsync("[a]sset{,s}/*/*") - .Select(f => f.FullName) - .OrderBy(p => p) - .ToArrayAsync(), - Is.EquivalentTo( - [ - "/project/assets/fonts/Arial.ttf", - "/project/assets/fonts/Roboto.ttf", - "/project/assets/images/backgrounds", - "/project/assets/images/icon.svg", - "/project/assets/images/logo.png", - "/project/assets/styles/main.css", - "/project/assets/styles/print.css" - ])); - - Assert.That( - await fs - .GetDirectory("/project") - .GetFileNodesAsync( - patterns: ["[a]sset{,s}/*/*", "*/**/*s"], - excludes: ["src/**", "tests/**"]) - .Select(f => f.FullName) - .OrderBy(p => p) - .ToArrayAsync(), - Is.EquivalentTo( - [ - "/project/assets/fonts", - "/project/assets/fonts/Arial.ttf", - "/project/assets/fonts/Roboto.ttf", - "/project/assets/images", - "/project/assets/images/backgrounds", - "/project/assets/images/icon.svg", - "/project/assets/images/logo.png", - "/project/assets/styles", - "/project/assets/styles/main.css", - "/project/assets/styles/print.css" - ]) - ); - } - - [Test] - public async Task Directory_Enumerate_ReturnsEmpty_For_NonExistingDirectory() - { - using var fs = GetFileSystem(); - - var directory = fs.GetDirectory("/project/b02a67d85cc34e90"); - - Assert.That(await directory.GetFileNodesAsync().AnyAsync(), Is.False); - Assert.That(await directory.GetFilesAsync().AnyAsync(), Is.False); - Assert.That(await directory.GetDirectoriesAsync().AnyAsync(), Is.False); - } - - [Test] - public async Task Directory_Create_For_ExistingDirectory() - { - using var fs = GetFileSystem(); - - if (fs.IsReadOnly) - return; - - await fs.CreateDirectoryAsync("/project/assets"); - } - - [Test] - public async Task Directory_Create_For_NonExistingDirectory() - { - using var fs = GetFileSystem(); - - if (fs.IsReadOnly) - return; - - await fs.CreateDirectoryAsync("/project/dcc2d926/8ddb/4a79/8246/a60ed6146c53"); - - Assert.That( - await fs.DirectoryExistsAsync("/project/dcc2d926/8ddb/4a79/8246/a60ed6146c53"), - Is.True); - - await fs.DeleteDirectoryAsync("/project/dcc2d926"); - } - - [Test] - public async Task Directory_Delete() - { - using var fs = GetFileSystem(); - - if (fs.IsReadOnly) - return; - - var directory = fs.GetDirectory("/project/1111b18e"); - - await fs.WriteAsync("/project/1111b18e/4bb69411.txt", Stream.Null); - await fs.WriteAsync("/project/1111b18e/f1e5f79eb6be/909c4bb69411.txt", Stream.Null); - - Assert.That( - await directory.GetFilesAsync("**").CountAsync(), - Is.EqualTo(2)); - - await directory.DeleteAsync(); - - Assert.That( - await directory.GetFilesAsync("**").AnyAsync(), - Is.False); - } - - [Test] - public async Task Directory_Delete_For_NonExistingDirectory() - { - using var fs = GetFileSystem(); - - if (fs.IsReadOnly) - return; - - var directory = fs.GetDirectory("/project/73d0e99e"); - - Assert.That( - await directory.GetFileNodesAsync().AnyAsync(), - Is.False); - - await directory.DeleteAsync(); - } - - [Test] - public async Task Directory_Readonly_Delete_ThrowsException_For_NonExistingDirectory() - { - using var fs = GetFileSystem(); - - if (!fs.IsReadOnly) - return; - - await Assert.ThatAsync( - async () => await fs.DeleteDirectoryAsync("/project/22ef456"), - Throws.Exception); - } - - /// - /// Returns an instance of the virtual file system. - /// - /// - /// An instance of . - /// - protected abstract IVirtualFileSystem GetFileSystem(); - - /// - /// Returns a object representing the root of the test directory. - /// - /// - /// A object that points to the root of the test directory. - /// - protected abstract DirectoryInfo GetDirectoryInfo(); -} +using System.Security.Cryptography; +using System.Text; + +using Ramstack.FileSystem.Physical; + +namespace Ramstack.FileSystem.Specification.Tests; + +/// +/// Represents a base class for specification tests of virtual file systems. +/// +/// +/// This class defines common functionality and setup for tests that validate the behavior +/// of virtual file systems. Derived classes should implement the abstract methods to provide specific +/// details about the virtual file system being tested. +/// +public abstract class VirtualFileSystemSpecificationTests +{ + [Test] + [Order(-1003)] + public async Task FileTree_MatchesStructure() + { + using var fs = GetFileSystem(); + await CompareDirectoriesAsync(fs.GetDirectory("/"), GetDirectoryInfo()); + + static async Task CompareDirectoriesAsync(VirtualDirectory virtualDir, DirectoryInfo localDir) + { + if (await virtualDir.ExistsAsync() != localDir.Exists) + Assert.Fail($"Directory '{virtualDir.FullName}'.Exists ({virtualDir.ExistsAsync().Result}) != '{localDir.FullName}'.Exists ({localDir.Exists})"); + + var virtualFiles = await virtualDir.GetFilesAsync().OrderBy(f => f.Name).ToArrayAsync(); + var localFiles = localDir.EnumerateFiles().OrderBy(f => f.Name).ToArray(); + + Assert.That( + virtualFiles.Select(f => f.Name), + Is.EquivalentTo(localFiles.Select(f => f.Name)), + virtualDir.FullName); + + for (var i = 0; i < virtualFiles.Length; i++) + await CompareFilesAsync(virtualFiles[i], localFiles[i]); + + var virtualDirectories = await virtualDir.GetDirectoriesAsync().OrderBy(d => d.Name).ToArrayAsync(); + var localDirectories = localDir.EnumerateDirectories().OrderBy(d => d.Name).ToArray(); + + Assert.That( + virtualDirectories.Select(f => f.Name), + Is.EquivalentTo(localDirectories.Select(f => f.Name)), + virtualDir.FullName); + + for (var i = 0; i < virtualDirectories.Length; i++) + await CompareDirectoriesAsync(virtualDirectories[i], localDirectories[i]); + } + + static async Task CompareFilesAsync(VirtualFile virtualFile, FileInfo localFile) + { + Assert.That( + await virtualFile.ReadAllTextAsync(), + Is.EqualTo(await File.ReadAllTextAsync(localFile.FullName)), + virtualFile.FullName); + } + } + + [Test] + [Order(-1002)] + public async Task FileTree_VirtualNode_FileSystem_MatchesIssuingFileSystemReference() + { + using var fs = GetFileSystem(); + + Assert.That( + await fs.GetFilesAsync("/", "**").AnyAsync(), + Is.True); + + await foreach (var node in fs.GetFileNodesAsync("/", "**")) + { + VirtualNode byPath = node is VirtualFile + ? fs.GetFile(node.FullName) + : fs.GetDirectory(node.FullName); + + Assert.That(fs, Is.SameAs(node.FileSystem)); + Assert.That(fs, Is.SameAs(byPath.FileSystem)); + } + } + + [Test] + public async Task Exists_ReturnsTrue_For_ExistingFile() + { + using var fs = GetFileSystem(); + + Assert.That(await fs.DirectoryExistsAsync("/project/assets"), Is.True); + Assert.That(await fs.FileExistsAsync("/project/README.md"), Is.True); + } + + [Test] + public async Task File_Exists_ReturnsFalse_For_NonExistingFile() + { + using var fs = GetFileSystem(); + + Assert.That(await fs.FileExistsAsync($"/{Guid.NewGuid()}"), Is.False); + } + + [Test] + public async Task File_OpenRead_ReturnsReadableStream() + { + using var fs = GetFileSystem(); + await using var stream = await fs.OpenReadAsync("/project/README.md"); + + Assert.That(stream.CanRead, Is.True); + stream.ReadByte(); + } + + [Test] + public async Task File_OpenRead_ThrowsException_For_NonExistingFile() + { + using var fs = GetFileSystem(); + + await Assert.ThatAsync(async () => await fs.OpenReadAsync("/6b01ba26.txt"), Throws.Exception); + await Assert.ThatAsync(async () => await fs.OpenReadAsync("/6b01ba26/6b01ba26.txt"), Throws.Exception); + } + + [Test] + public async Task File_OpenWrite_ReturnsWritableStream() + { + using var fs = GetFileSystem(); + + if (fs.IsReadOnly) + return; + + var text = $"Id:{Guid.NewGuid()}"; + + await using (var stream = await fs.OpenWriteAsync("/project/README.md")) + { + Assert.That(stream.CanWrite, Is.True); + await stream.WriteAsync(Encoding.UTF8.GetBytes(text)); + } + + Assert.That( + await fs.ReadAllTextAsync("/project/README.md"), + Is.EqualTo(text)); + } + + [Test] + public async Task File_OpenWrite_NewFile() + { + using var fs = GetFileSystem(); + + if (fs.IsReadOnly) + return; + + var path = "/project/6b01ba2608be"; + var text = $"Automatically generated on {DateTime.Now:s}\n\nNew Id:{Guid.NewGuid()}"; + + await using (var stream = await fs.OpenWriteAsync(path)) + { + Assert.That(stream.CanWrite, Is.True); + await stream.WriteAsync(Encoding.UTF8.GetBytes(text)); + } + + Assert.That( + await fs.ReadAllTextAsync(path), + Is.EqualTo(text)); + + await fs.DeleteFileAsync(path); + + Assert.That( + await fs.FileExistsAsync(path), + Is.False); + } + + [Test] + public async Task File_Write_OverwritesContent_When_OverwriteIsTrue() + { + using var fs = GetFileSystem(); + + if (fs.IsReadOnly) + return; + + var file = fs.GetFile("/project/README.md"); + var text = $"Id:{Guid.NewGuid()}"; + + var ms = new MemoryStream(); + ms.Write(Encoding.UTF8.GetBytes(text)); + ms.Position = 0; + + await file.WriteAsync(ms, overwrite: true); + + Assert.That( + await file.ReadAllTextAsync(), + Is.EqualTo(text)); + } + + [Test] + public async Task File_Write_ThrowsException_For_ExistingFile_When_OverwriteIsFalse() + { + using var fs = GetFileSystem(); + + if (fs.IsReadOnly) + return; + + var file = fs.GetFile("/project/README.md"); + var text = await file.ReadAllTextAsync(); + + var ms = new MemoryStream(); + ms.Write(Encoding.UTF8.GetBytes(Guid.NewGuid().ToString())); + ms.Position = 0; + + Assert.That( + () => file.WriteAsync(ms, overwrite: false), + Throws.Exception); + + Assert.That( + await file.ReadAllTextAsync(), + Is.EqualTo(text)); + } + + [Test] + public async Task File_Write_NewFile() + { + using var fs = GetFileSystem(); + + if (fs.IsReadOnly) + return; + + var path = "/project/0fce49e0.txt"; + var text = $"Automatically generated on {DateTime.Now:s}\n\nNew Id:{Guid.NewGuid()}"; + + var ms = new MemoryStream(); + ms.Write(Encoding.UTF8.GetBytes(text)); + ms.Position = 0; + + var file = fs.GetFile(path); + Assert.That(await file.ExistsAsync(), Is.False); + + await file.WriteAsync(ms); + + Assert.That(await file.ExistsAsync(), Is.True); + Assert.That(await file.ReadAllTextAsync(), Is.EqualTo(text)); + + await file.DeleteAsync(); + + Assert.That(await file.ExistsAsync(), Is.False); + } + + [Test] + public async Task File_Write_EmptyStream_CreatesFile() + { + using var fs = GetFileSystem(); + + if (fs.IsReadOnly) + return; + + var file = fs.GetFile("/project/e3a1c7f0_empty_write.txt"); + + Assert.That(await file.ExistsAsync(), Is.False); + + await file.WriteAsync(Stream.Null); + + Assert.That(await file.ExistsAsync(), Is.True); + Assert.That(await file.GetLengthAsync(), Is.Zero); + + await file.DeleteAsync(); + } + + [Test] + public async Task File_Write_EmptyStream_ExistingFile_ResultsInZeroLength() + { + using var fs = GetFileSystem(); + + if (fs.IsReadOnly) + return; + + var path = "/project/b7d2e9f1_overwrite_empty.txt"; + var file = fs.GetFile(path); + + var ms = new MemoryStream("Some content"u8.ToArray()); + await file.WriteAsync(ms); + + Assert.That(await file.ExistsAsync(), Is.True); + Assert.That(await file.GetLengthAsync(), Is.GreaterThan(0)); + + // Write empty stream over existing file + await file.WriteAsync(Stream.Null, overwrite: true); + + Assert.That(await file.ExistsAsync(), Is.True); + Assert.That(await file.GetLengthAsync(), Is.Zero); + + await file.DeleteAsync(); + } + + [Test] + public async Task File_OpenWrite_NoWrite_CreatesFile() + { + using var fs = GetFileSystem(); + + if (fs.IsReadOnly) + return; + + var file = fs.GetFile("/project/a4f8d2e0_openwrite_empty.txt"); + + Assert.That(await file.ExistsAsync(), Is.False); + + await using (await file.OpenWriteAsync()) + { + // Do not write anything, just close + } + + Assert.That(await file.ExistsAsync(), Is.True); + Assert.That(await file.GetLengthAsync(), Is.Zero); + + await file.DeleteAsync(); + } + + [Test] + public async Task File_OpenWrite_ExistingFile_ResultsInZeroLength() + { + using var fs = GetFileSystem(); + + if (fs.IsReadOnly) + return; + + var file = fs.GetFile("/project/c5e9a3b1_openwrite_truncate.txt"); + + var ms = new MemoryStream("Some content"u8.ToArray()); + await file.WriteAsync(ms); + + Assert.That(await file.ExistsAsync(), Is.True); + Assert.That(await file.GetLengthAsync(), Is.GreaterThan(0)); + + // Open for write and close without writing + await using (await file.OpenWriteAsync()) + { + // Do not write anything, just close + } + + Assert.That(await file.ExistsAsync(), Is.True); + Assert.That(await file.GetLengthAsync(), Is.Zero); + + await file.DeleteAsync(); + } + + [Test] + public async Task File_Delete_For_ExistingFile() + { + using var fs = GetFileSystem(); + + if (fs.IsReadOnly) + return; + + var path = "/project/793cd29d96c4.txt"; + var file = fs.GetFile(path); + + Assert.That(await file.ExistsAsync(), Is.False); + + await file.WriteAsync(Stream.Null); + + Assert.That(await file.ExistsAsync(), Is.True); + + await file.DeleteAsync(); + + Assert.That(await file.ExistsAsync(), Is.False); + } + + [Test] + public async Task File_Delete_For_NonExistingFile() + { + using var fs = GetFileSystem(); + + if (fs.IsReadOnly) + return; + + await fs.DeleteFileAsync("/1861cb25.txt"); + await fs.DeleteFileAsync("/1861cb25/1861cb25.txt"); + } + + [Test] + public async Task File_Readonly_OpenWrite_ThrowsException() + { + using var fs = GetFileSystem(); + + if (!fs.IsReadOnly) + return; + + await Assert.ThatAsync( + async () => { await using var stream = await fs.OpenWriteAsync("/project/README.md"); }, + Throws.Exception); + } + + [Test] + public async Task File_Readonly_OpenWrite_ThrowsException_For_NewFile() + { + using var fs = GetFileSystem(); + + if (!fs.IsReadOnly) + return; + + await Assert.ThatAsync( + async () => await fs.OpenWriteAsync("/project/6d368a9d0e97.md"), + Throws.Exception); + } + + [Test] + public async Task File_Readonly_Write_ThrowsException() + { + using var fs = GetFileSystem(); + + if (!fs.IsReadOnly) + return; + + await Assert.ThatAsync( + async () => await fs.WriteAsync("/project/README.md", Stream.Null), + Throws.Exception); + } + + [Test] + public async Task File_Readonly_Write_ThrowsException_For_NewFile() + { + using var fs = GetFileSystem(); + + if (!fs.IsReadOnly) + return; + + await Assert.ThatAsync( + async () => await fs.WriteAsync("/project/9286d04f.pdf", Stream.Null), + Throws.Exception); + } + + [Test] + public async Task File_Readonly_Delete_ThrowsException() + { + using var fs = GetFileSystem(); + + if (!fs.IsReadOnly) + return; + + await Assert.ThatAsync( + async () => await fs.DeleteFileAsync("/project/README.md"), + Throws.Exception); + + Assert.That( + await fs.FileExistsAsync("/project/README.md"), + Is.True); + } + + [Test] + public async Task File_Readonly_Delete_ThrowsException_For_NonExistingFile() + { + using var fs = GetFileSystem(); + + if (!fs.IsReadOnly) + return; + + await Assert.ThatAsync( + async () => await fs.DeleteFileAsync("/project/c180408e8005.png"), + Throws.Exception); + } + + [Test] + public async Task File_Readonly_ReadonlyToWritable_Succeeds() + { + using var fs = GetFileSystem(); + using var ds = new PhysicalFileSystem( + Path.Combine( + Path.GetTempPath(), + Path.GetRandomFileName())); + + if (!fs.IsReadOnly) + return; + + var src = fs.GetFile("/project/README.md"); + var dst = ds.GetFile("/README.md"); + + await src.CopyToAsync(dst); + + Assert.That( + await dst.ExistsAsync(), + Is.True); + + Assert.That( + await ds.FileExistsAsync("/README.md"), + Is.True); + + Assert.That( + await src.ReadAllTextAsync(), + Is.EqualTo(await src.ReadAllTextAsync())); + + Directory.Delete(ds.Root, true); + } + + [Test] + public async Task File_CopyTo_Path() + { + using var fs = GetFileSystem(); + if (fs.IsReadOnly) + return; + + var sourcePath = "/project/README.md"; + var destinationPath = "/project/README - Copy.md"; + + Assert.That( + await fs.FileExistsAsync(destinationPath), + Is.False); + + await fs.CopyFileAsync(sourcePath, destinationPath); + Assert.That( + await fs.FileExistsAsync(destinationPath), + Is.True); + + Assert.That( + await fs.ReadAllTextAsync(destinationPath), + Is.EqualTo(await fs.ReadAllTextAsync(sourcePath))); + + await fs.DeleteFileAsync(destinationPath); + Assert.That( + await fs.FileExistsAsync(destinationPath), + Is.False); + } + + [Test] + public async Task File_CopyTo_Path_NoOverwrite_ThrowsException() + { + using var fs = GetFileSystem(); + if (fs.IsReadOnly) + return; + + var sourcePath = "/project/README.md"; + var destinationPath = "/project/README - Copy.md"; + + await fs.CopyFileAsync(sourcePath, destinationPath); + + Assert.That( + await fs.FileExistsAsync(destinationPath), + Is.True); + + await Assert.ThatAsync( + async () => await fs.CopyFileAsync(sourcePath, destinationPath), + Throws.Exception); + + await fs.DeleteFileAsync(destinationPath); + Assert.That( + await fs.FileExistsAsync(destinationPath), + Is.False); + } + + [Test] + public async Task File_CopyTo_File_SameFileSystems() + { + using var fs = GetFileSystem(); + if (fs.IsReadOnly) + return; + + var source = fs.GetFile("/project/README.md"); + var destination = fs.GetFile("/project/README - Copy.md"); + + Assert.That( + await destination.ExistsAsync(), + Is.False); + + await source.CopyToAsync(destination); + + Assert.That( + await destination.ExistsAsync(), + Is.True); + + Assert.That( + await destination.ReadAllTextAsync(), + Is.EqualTo(await source.ReadAllTextAsync())); + + await destination.DeleteAsync(); + Assert.That( + await destination.ExistsAsync(), + Is.False); + } + + [Test] + public async Task File_CopyTo_File_NotSameFileSystems() + { + using var fs1 = GetFileSystem(); + using var fs2 = GetFileSystem(); + + if (fs1.IsReadOnly) + return; + + var source = fs1.GetFile("/project/README.md"); + var destination = fs2.GetFile("/project/README - Copy.md"); + + Assert.That( + await destination.ExistsAsync(), + Is.False); + + await source.CopyToAsync(destination); + + Assert.That( + await destination.ExistsAsync(), + Is.True); + + Assert.That( + await destination.ReadAllTextAsync(), + Is.EqualTo(await source.ReadAllTextAsync())); + + await destination.DeleteAsync(); + Assert.That( + await destination.ExistsAsync(), + Is.False); + } + + [Test] + public async Task File_CopyTo_File_NoOverwrite_ThrowsException() + { + using var fs = GetFileSystem(); + if (fs.IsReadOnly) + return; + + var source = fs.GetFile("/project/README.md"); + var destination = fs.GetFile("/project/README - Copy.md"); + + Assert.That( + await destination.ExistsAsync(), + Is.False); + + await source.CopyToAsync(destination); + + Assert.That( + await destination.ExistsAsync(), + Is.True); + + await Assert.ThatAsync( + async () => await source.CopyToAsync(destination), + Throws.Exception); + + await destination.DeleteAsync(); + Assert.That( + await destination.ExistsAsync(), + Is.False); + } + + [Test] + public async Task File_CopyTo_ThrowsException_When_CopyingToItself() + { + using var fs = GetFileSystem(); + + if (fs.IsReadOnly) + return; + + var file = fs.GetFile("/project/README.md"); + await Assert.ThatAsync(async () => await file.CopyToAsync(file.FullName), Throws.Exception); + await Assert.ThatAsync(async () => await file.CopyToAsync(file), Throws.Exception); + await Assert.ThatAsync(async () => await file.CopyToAsync(fs.GetFile(file.FullName)), Throws.Exception); + } + + [Test] + public async Task File_CopyTo_ThrowException_For_NonExistingFile() + { + using var fs = GetFileSystem(); + + if (fs.IsReadOnly) + return; + + await Assert.ThatAsync( + async () => await fs.CopyFileAsync("/project/800569b11aaa0438.txt", "/project/test-b11aaa.txt"), + Throws.Exception); + + Assert.That( + await fs.FileExistsAsync("/project/test-b11aaa.txt"), + Is.False); + } + + [Test] + public async Task File_Readonly_CopyTo_ThrowException_For_NonExistingFile() + { + using var fs = GetFileSystem(); + + if (!fs.IsReadOnly) + return; + + await Assert.ThatAsync( + async () => await fs.CopyFileAsync("/project/README.md", "/project/README - Copy.md"), + Throws.Exception); + + Assert.That( + await fs.FileExistsAsync("/project/README - Copy.md"), + Is.False); + } + + [Test] + public async Task File_ReadAllBytes() + { + using var fs = GetFileSystem(); + + var file = fs.GetFile("/project/README.md"); + using var reader = new BinaryReader(await file.OpenReadAsync()); + + var bytes = await fs.ReadAllBytesAsync(file.FullName); + var expected = reader.ReadBytes(4096); + var properties = await file.GetPropertiesAsync(); + + Assert.That( + bytes.SequenceEqual(expected), + Is.True); + + Assert.That( + bytes.Length, + Is.EqualTo(properties.Length)); + } + + [Test] + public async Task File_ReadAllText() + { + using var fs = GetFileSystem(); + + var file = fs.GetFile("/project/README.md"); + var stream = await file.OpenReadAsync(); + using var reader = new StreamReader(stream); + + Assert.That( + await fs.ReadAllTextAsync(file.FullName), + Is.EqualTo(await reader.ReadToEndAsync())); + } + + [Test] + public async Task File_ReadAllLines() + { + using var fs = GetFileSystem(); + + var file = fs.GetFile("/project/README.md"); + var stream = await file.OpenReadAsync(); + using var reader = new StreamReader(stream); + + var lines = new List(); + while (await reader.ReadLineAsync() is {} line) + lines.Add(line); + + Assert.That( + await fs.ReadAllLinesAsync(file.FullName), + Is.EquivalentTo(lines)); + } + + [Test] + public async Task File_WriteAllBytes() + { + using var fs = GetFileSystem(); + if (fs.IsReadOnly) + return; + + + var path = "/project/b02a67d8.bin"; + var data = RandomNumberGenerator.GetBytes(10_000); + + await fs.WriteAllBytesAsync(path, data); + + Assert.That( + await fs.ReadAllBytesAsync(path), + Is.EquivalentTo(data)); + + await fs.DeleteFileAsync(path); + } + + [Test] + public async Task File_WriteAllText() + { + using var fs = GetFileSystem(); + if (fs.IsReadOnly) + return; + + var list = new List(); + for (var i = 0; i < 1000; i++) + list.Add($"Hello, 世界! Unicode test: café, weiß, Привет, ёжик! こんにちは! {Guid.NewGuid()}"); + + var data = string.Join(Environment.NewLine, list); + var path = "/project/5cc34e90a7c9.txt"; + + await fs.WriteAllTextAsync(path, data); + + Assert.That( + await fs.ReadAllTextAsync(path), + Is.EqualTo(data)); + + await fs.DeleteFileAsync(path); + } + + [Test] + public async Task File_WriteAllLines() + { + using var fs = GetFileSystem(); + if (fs.IsReadOnly) + return; + + var list = new List(); + for (var i = 0; i < 1000; i++) + list.Add($"Hello, 世界! Unicode test: café, weiß, Привет, ёжик! こんにちは! {Guid.NewGuid()}"); + + var path = "/project/76bb51ee6cb7.txt"; + await fs.WriteAllLinesAsync(path, list); + + Assert.That( + await fs.ReadAllLinesAsync(path), + Is.EquivalentTo(list)); + + await fs.DeleteFileAsync(path); + } + + [Test] + public async Task Directory_GetFiles() + { + using var fs = GetFileSystem(); + + Assert.That( + await fs.GetDirectory("/project/assets").GetFilesAsync().AnyAsync(), + Is.False); + + Assert.That( + await fs + .GetDirectory("/project/assets/images") + .GetFilesAsync() + .Select(f => f.FullName) + .OrderBy(p => p) + .ToArrayAsync(), + Is.EquivalentTo( + [ + "/project/assets/images/icon.svg", + "/project/assets/images/logo.png" + ])); + } + + [Test] + public async Task Directory_GetDirectories() + { + using var fs = GetFileSystem(); + + Assert.That( + await fs.GetDirectory("/project/assets/fonts").GetDirectoriesAsync().AnyAsync(), + Is.False); + + Assert.That( + await fs + .GetDirectory("/project/assets") + .GetDirectoriesAsync() + .Select(f => f.FullName) + .OrderBy(p => p) + .ToArrayAsync(), + Is.EquivalentTo( + [ + "/project/assets/fonts", + "/project/assets/images", + "/project/assets/styles" + ]) + ); + } + + [Test] + public async Task Directory_GetFileNodes() + { + using var fs = GetFileSystem(); + + Assert.That( + await fs.GetDirectory("/project/assets/scripts").GetFileNodesAsync().AnyAsync(), + Is.False); + + Assert.That( + await fs + .GetDirectory("/project/assets/images") + .GetFileNodesAsync() + .Select(f => f.FullName) + .OrderBy(p => p) + .ToArrayAsync(), + Is.EquivalentTo( + [ + "/project/assets/images/backgrounds", + "/project/assets/images/icon.svg", + "/project/assets/images/logo.png" + ]) + ); + } + + [Test] + public async Task Directory_Glob_GetFiles() + { + using var fs = GetFileSystem(); + + Assert.That( + await fs.GetDirectory("/project/assets").GetFilesAsync("*/*.otf").AnyAsync(), + Is.False); + + Assert.That( + await fs + .GetDirectory("/project") + .GetFilesAsync( + pattern: "[a][s]set[s]/{images,styles}/{main.css,logo.png}") + .Select(f => f.FullName) + .OrderBy(p => p) + .ToArrayAsync(), + Is.EquivalentTo( + [ + "/project/assets/images/logo.png", + "/project/assets/styles/main.css" + ]) + ); + + Assert.That( + await fs + .GetDirectory("/project") + .GetFilesAsync( + patterns: ["[a]sset{,s}/*/*.{svg,png}", "assets/{images,styles}/*.css"], + excludes: ["assets/imag*/*icon*", "**/style*/print.css"]) + .Select(f => f.FullName) + .OrderBy(p => p) + .ToArrayAsync(), + Is.EquivalentTo( + [ + "/project/assets/images/logo.png", + "/project/assets/styles/main.css" + ]) + ); + } + + [Test] + public async Task Directory_Glob_GetDirectories() + { + using var fs = GetFileSystem(); + + Assert.That( + await fs.GetDirectory("/project/assets").GetDirectoriesAsync("{styles,fonts}/*").AnyAsync(), + Is.False); + + Assert.That( + await fs + .GetDirectory("/project") + .GetDirectoriesAsync("[a]sset{,s}/*/*") + .Select(f => f.FullName) + .OrderBy(p => p) + .ToArrayAsync(), + Is.EquivalentTo(["/project/assets/images/backgrounds"])); + + Assert.That( + await fs + .GetDirectory("/project") + .GetDirectoriesAsync( + patterns: ["[a]sset{,s}/*/*", "*/**/*s"], + excludes: ["src/**", "tests/**/Fixtures"]) + .Select(f => f.FullName) + .OrderBy(p => p) + .ToArrayAsync(), + Is.EquivalentTo( + [ + "/project/assets/fonts", + "/project/assets/images", + "/project/assets/images/backgrounds", + "/project/assets/styles" + ]) + ); + } + + [Test] + public async Task Directory_Glob_GetFileNodes() + { + using var fs = GetFileSystem(); + + Assert.That( + await fs.GetDirectory("/project/assets").GetFileNodesAsync("{styles,fonts}/*/*").AnyAsync(), + Is.False); + + Assert.That( + await fs + .GetDirectory("/project") + .GetFileNodesAsync("[a]sset{,s}/*/*") + .Select(f => f.FullName) + .OrderBy(p => p) + .ToArrayAsync(), + Is.EquivalentTo( + [ + "/project/assets/fonts/Arial.ttf", + "/project/assets/fonts/Roboto.ttf", + "/project/assets/images/backgrounds", + "/project/assets/images/icon.svg", + "/project/assets/images/logo.png", + "/project/assets/styles/main.css", + "/project/assets/styles/print.css" + ])); + + Assert.That( + await fs + .GetDirectory("/project") + .GetFileNodesAsync( + patterns: ["[a]sset{,s}/*/*", "*/**/*s"], + excludes: ["src/**", "tests/**"]) + .Select(f => f.FullName) + .OrderBy(p => p) + .ToArrayAsync(), + Is.EquivalentTo( + [ + "/project/assets/fonts", + "/project/assets/fonts/Arial.ttf", + "/project/assets/fonts/Roboto.ttf", + "/project/assets/images", + "/project/assets/images/backgrounds", + "/project/assets/images/icon.svg", + "/project/assets/images/logo.png", + "/project/assets/styles", + "/project/assets/styles/main.css", + "/project/assets/styles/print.css" + ]) + ); + } + + [Test] + public async Task Directory_Enumerate_ReturnsEmpty_For_NonExistingDirectory() + { + using var fs = GetFileSystem(); + + var directory = fs.GetDirectory("/project/b02a67d85cc34e90"); + + Assert.That(await directory.GetFileNodesAsync().AnyAsync(), Is.False); + Assert.That(await directory.GetFilesAsync().AnyAsync(), Is.False); + Assert.That(await directory.GetDirectoriesAsync().AnyAsync(), Is.False); + } + + [Test] + public async Task Directory_Create_For_ExistingDirectory() + { + using var fs = GetFileSystem(); + + if (fs.IsReadOnly) + return; + + await fs.CreateDirectoryAsync("/project/assets"); + } + + [Test] + public async Task Directory_Create_For_NonExistingDirectory() + { + using var fs = GetFileSystem(); + + if (fs.IsReadOnly) + return; + + await fs.CreateDirectoryAsync("/project/dcc2d926/8ddb/4a79/8246/a60ed6146c53"); + + Assert.That( + await fs.DirectoryExistsAsync("/project/dcc2d926/8ddb/4a79/8246/a60ed6146c53"), + Is.True); + + await fs.DeleteDirectoryAsync("/project/dcc2d926"); + } + + [Test] + public async Task Directory_Delete() + { + using var fs = GetFileSystem(); + + if (fs.IsReadOnly) + return; + + var directory = fs.GetDirectory("/project/1111b18e"); + + await fs.WriteAsync("/project/1111b18e/4bb69411.txt", Stream.Null); + await fs.WriteAsync("/project/1111b18e/f1e5f79eb6be/909c4bb69411.txt", Stream.Null); + + Assert.That( + await directory.GetFilesAsync("**").CountAsync(), + Is.EqualTo(2)); + + await directory.DeleteAsync(); + + Assert.That( + await directory.GetFilesAsync("**").AnyAsync(), + Is.False); + } + + [Test] + public async Task Directory_Delete_For_NonExistingDirectory() + { + using var fs = GetFileSystem(); + + if (fs.IsReadOnly) + return; + + var directory = fs.GetDirectory("/project/73d0e99e"); + + Assert.That( + await directory.GetFileNodesAsync().AnyAsync(), + Is.False); + + await directory.DeleteAsync(); + } + + [Test] + public async Task Directory_Readonly_Delete_ThrowsException_For_NonExistingDirectory() + { + using var fs = GetFileSystem(); + + if (!fs.IsReadOnly) + return; + + await Assert.ThatAsync( + async () => await fs.DeleteDirectoryAsync("/project/22ef456"), + Throws.Exception); + } + + /// + /// Returns an instance of the virtual file system. + /// + /// + /// An instance of . + /// + protected abstract IVirtualFileSystem GetFileSystem(); + + /// + /// Returns a object representing the root of the test directory. + /// + /// + /// A object that points to the root of the test directory. + /// + protected abstract DirectoryInfo GetDirectoryInfo(); +} diff --git a/tests/Ramstack.FileSystem.Sub.Tests/Ramstack.FileSystem.Sub.Tests.csproj b/tests/Ramstack.FileSystem.Sub.Tests/Ramstack.FileSystem.Sub.Tests.csproj index 6802ffc..0533103 100644 --- a/tests/Ramstack.FileSystem.Sub.Tests/Ramstack.FileSystem.Sub.Tests.csproj +++ b/tests/Ramstack.FileSystem.Sub.Tests/Ramstack.FileSystem.Sub.Tests.csproj @@ -1,29 +1,29 @@ - - - - net6.0;net8.0 - enable - enable - preview - Ramstack.FileSystem.Sub - - - - - - - - - - - - - - - - - - - - - + + + + net6.0;net8.0 + enable + enable + preview + Ramstack.FileSystem.Sub + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/Ramstack.FileSystem.Sub.Tests/SubFileSystemTests.cs b/tests/Ramstack.FileSystem.Sub.Tests/SubFileSystemTests.cs index a535b39..b67e2f5 100644 --- a/tests/Ramstack.FileSystem.Sub.Tests/SubFileSystemTests.cs +++ b/tests/Ramstack.FileSystem.Sub.Tests/SubFileSystemTests.cs @@ -1,24 +1,24 @@ -using Ramstack.FileSystem.Physical; -using Ramstack.FileSystem.Prefixed; -using Ramstack.FileSystem.Specification.Tests; -using Ramstack.FileSystem.Specification.Tests.Utilities; - -namespace Ramstack.FileSystem.Sub; - -[TestFixture] -public class SubFileSystemTests : VirtualFileSystemSpecificationTests -{ - private readonly TempFileStorage _storage = new TempFileStorage(); - - [OneTimeTearDown] - public void Cleanup() => - _storage.Dispose(); - - protected override IVirtualFileSystem GetFileSystem() => - new SubFileSystem("/bin/app", - new PrefixedFileSystem("/bin/app", - new PhysicalFileSystem(_storage.Root))); - - protected override DirectoryInfo GetDirectoryInfo() => - new DirectoryInfo(_storage.Root); -} +using Ramstack.FileSystem.Physical; +using Ramstack.FileSystem.Prefixed; +using Ramstack.FileSystem.Specification.Tests; +using Ramstack.FileSystem.Specification.Tests.Utilities; + +namespace Ramstack.FileSystem.Sub; + +[TestFixture] +public class SubFileSystemTests : VirtualFileSystemSpecificationTests +{ + private readonly TempFileStorage _storage = new TempFileStorage(); + + [OneTimeTearDown] + public void Cleanup() => + _storage.Dispose(); + + protected override IVirtualFileSystem GetFileSystem() => + new SubFileSystem("/bin/app", + new PrefixedFileSystem("/bin/app", + new PhysicalFileSystem(_storage.Root))); + + protected override DirectoryInfo GetDirectoryInfo() => + new DirectoryInfo(_storage.Root); +}