From 0b8330ef22503b981c61449fe817baf9294eabd4 Mon Sep 17 00:00:00 2001 From: rameel Date: Tue, 31 Mar 2026 14:46:39 +0500 Subject: [PATCH 1/5] Upgrade AWSSDK.S3 to v4 --- Directory.Packages.props | 4 +- src/Ramstack.FileSystem.Amazon/S3Directory.cs | 114 ++++++++++-------- src/Ramstack.FileSystem.Amazon/S3File.cs | 2 +- 3 files changed, 67 insertions(+), 53 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 944c2cc..bd0b923 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,12 +4,12 @@ true - + @@ -18,4 +18,4 @@ - \ No newline at end of file + diff --git a/src/Ramstack.FileSystem.Amazon/S3Directory.cs b/src/Ramstack.FileSystem.Amazon/S3Directory.cs index 517e6a5..43fd1f4 100644 --- a/src/Ramstack.FileSystem.Amazon/S3Directory.cs +++ b/src/Ramstack.FileSystem.Amazon/S3Directory.cs @@ -48,6 +48,8 @@ protected override async ValueTask DeleteCoreAsync(CancellationToken cancellatio BucketName = _fs.BucketName }; + dr.Objects ??= []; + do { // The maximum number of objects returned is MaxKeys, which is 1000, @@ -59,8 +61,9 @@ protected override async ValueTask DeleteCoreAsync(CancellationToken cancellatio .ListObjectsV2Async(lr, cancellationToken) .ConfigureAwait(false); - foreach (var obj in response.S3Objects) - dr.Objects.Add(new KeyVersion { Key = obj.Key }); + 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 @@ -89,11 +92,13 @@ protected override async IAsyncEnumerable GetFileNodesCoreAsync([En .ListObjectsV2Async(request, cancellationToken) .ConfigureAwait(false); - foreach (var prefix in response.CommonPrefixes) - yield return new S3Directory(_fs, VirtualPath.Normalize(prefix)); + if (response.CommonPrefixes is not null) + foreach (var prefix in response.CommonPrefixes) + yield return new S3Directory(_fs, VirtualPath.Normalize(prefix)); - foreach (var obj in response.S3Objects) - yield return CreateVirtualFile(obj); + if (response.S3Objects is not null) + foreach (var obj in response.S3Objects) + yield return CreateVirtualFile(obj); request.ContinuationToken = response.NextContinuationToken; } @@ -116,8 +121,9 @@ protected override async IAsyncEnumerable GetFilesCoreAsync([Enumer .ListObjectsV2Async(request, cancellationToken) .ConfigureAwait(false); - foreach (var obj in response.S3Objects) - yield return CreateVirtualFile(obj); + if (response.S3Objects is not null) + foreach (var obj in response.S3Objects) + yield return CreateVirtualFile(obj); request.ContinuationToken = response.NextContinuationToken; } @@ -140,8 +146,9 @@ protected override async IAsyncEnumerable GetDirectoriesCoreAs .ListObjectsV2Async(request, cancellationToken) .ConfigureAwait(false); - foreach (var prefix in response.CommonPrefixes) - yield return new S3Directory(_fs, VirtualPath.Normalize(prefix)); + if (response.CommonPrefixes is not null) + foreach (var prefix in response.CommonPrefixes) + yield return new S3Directory(_fs, VirtualPath.Normalize(prefix)); request.ContinuationToken = response.NextContinuationToken; } @@ -179,28 +186,31 @@ protected override async IAsyncEnumerable GetFileNodesCoreAsync(str .ListObjectsV2Async(request, cancellationToken) .ConfigureAwait(false); - foreach (var obj in response.S3Objects) + if (response.S3Objects is not null) { - var path = VirtualPath.Normalize(obj.Key); - var directoryPath = VirtualPath.GetDirectoryName(path); - - while (directoryPath.Length != 0 && directories.Add(directoryPath)) + foreach (var obj in response.S3Objects) { - // - // 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); + 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); } - - if (IsMatched(obj.Key.AsSpan(request.Prefix.Length), patterns, excludes)) - yield return CreateVirtualFile(obj, path); } request.ContinuationToken = response.NextContinuationToken; @@ -234,9 +244,10 @@ protected override async IAsyncEnumerable GetFilesCoreAsync(string[ .ListObjectsV2Async(request, cancellationToken) .ConfigureAwait(false); - foreach (var obj in response.S3Objects) - if (IsMatched(obj.Key.AsSpan(request.Prefix.Length), patterns, excludes)) - yield return CreateVirtualFile(obj); + 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; } @@ -274,24 +285,27 @@ protected override async IAsyncEnumerable GetDirectoriesCoreAs .ListObjectsV2Async(request, cancellationToken) .ConfigureAwait(false); - foreach (var obj in response.S3Objects) + if (response.S3Objects is not null) { - var directoryPath = VirtualPath.GetDirectoryName( - VirtualPath.Normalize(obj.Key)); - - while (directoryPath.Length != 0 && directories.Add(directoryPath)) + foreach (var obj in response.S3Objects) { - // - // 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); + 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); + } } } @@ -314,8 +328,8 @@ private S3File CreateVirtualFile(S3Object obj, string? normalizedName = null) .CreateFileProperties( creationTime: default, lastAccessTime: default, - lastWriteTime: obj.LastModified, - length: obj.Size); + lastWriteTime: obj.LastModified.GetValueOrDefault(), + length: obj.Size ?? 0); var path = normalizedName ?? VirtualPath.Normalize(obj.Key); return new S3File(_fs, path, properties); diff --git a/src/Ramstack.FileSystem.Amazon/S3File.cs b/src/Ramstack.FileSystem.Amazon/S3File.cs index cdca028..1420a7b 100644 --- a/src/Ramstack.FileSystem.Amazon/S3File.cs +++ b/src/Ramstack.FileSystem.Amazon/S3File.cs @@ -47,7 +47,7 @@ public S3File(AmazonS3FileSystem fileSystem, string path, VirtualNodeProperties? return VirtualNodeProperties.CreateFileProperties( creationTime: default, lastAccessTime: default, - lastWriteTime: metadata.LastModified, + lastWriteTime: metadata.LastModified.GetValueOrDefault(), length: metadata.ContentLength); } catch (AmazonS3Exception e) when (e.StatusCode == HttpStatusCode.NotFound) From 8d3bb97048325671f06e69ae9ba5d8a1232bf1b8 Mon Sep 17 00:00:00 2001 From: rameel Date: Tue, 31 Mar 2026 14:52:15 +0500 Subject: [PATCH 2/5] Update Google.Cloud.Storage --- Directory.Packages.props | 2 +- .../ReadonlyGoogleFileSystemTests.cs | 2 ++ .../WritableGoogleFileSystemTests.cs | 2 ++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index bd0b923..17f9902 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -6,10 +6,10 @@ - + diff --git a/tests/Ramstack.FileSystem.Google.Tests/ReadonlyGoogleFileSystemTests.cs b/tests/Ramstack.FileSystem.Google.Tests/ReadonlyGoogleFileSystemTests.cs index 261e911..6fe037e 100644 --- a/tests/Ramstack.FileSystem.Google.Tests/ReadonlyGoogleFileSystemTests.cs +++ b/tests/Ramstack.FileSystem.Google.Tests/ReadonlyGoogleFileSystemTests.cs @@ -43,8 +43,10 @@ protected override DirectoryInfo GetDirectoryInfo() => 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") { diff --git a/tests/Ramstack.FileSystem.Google.Tests/WritableGoogleFileSystemTests.cs b/tests/Ramstack.FileSystem.Google.Tests/WritableGoogleFileSystemTests.cs index da61f77..e4c2894 100644 --- a/tests/Ramstack.FileSystem.Google.Tests/WritableGoogleFileSystemTests.cs +++ b/tests/Ramstack.FileSystem.Google.Tests/WritableGoogleFileSystemTests.cs @@ -204,8 +204,10 @@ 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); } From 462696f2e2a5b0f599e5337c46486a3524f097b5 Mon Sep 17 00:00:00 2001 From: rameel Date: Tue, 31 Mar 2026 19:47:23 +0500 Subject: [PATCH 3/5] Update Microsoft.Extensions.FileProviders.Abstractions --- Directory.Packages.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 17f9902..cf3662c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -6,10 +6,10 @@ - - + + From e42cde8c127714ba71cb8f41e8df0a974a4eed2e Mon Sep 17 00:00:00 2001 From: rameel Date: Tue, 31 Mar 2026 19:51:36 +0500 Subject: [PATCH 4/5] Update Azure.Storage.Blobs --- .github/workflows/tests.yml | 20 ++++++++------ Directory.Packages.props | 4 +-- docker-compose.yml | 2 +- .../AzureDirectory.cs | 26 ++++++++++++++++--- .../Ramstack.FileSystem.Azure.csproj | 12 +++++++-- .../Ramstack.FileSystem.Azure.Tests.csproj | 2 +- .../ReadonlyAzureFileSystemTests.cs | 7 ++--- .../WritableAzureFileSystemTests.cs | 5 ++-- 8 files changed, 56 insertions(+), 22 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6603a99..67d762d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -36,14 +36,18 @@ jobs: azure: name: "Test: AzureFileSystem" runs-on: ubuntu-latest - services: - azurite: - image: mcr.microsoft.com/azure-storage/azurite - ports: - - 10000:10000 - - 10001:10001 - - 10002:10002 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: @@ -58,7 +62,7 @@ jobs: - name: Build run: dotnet build -c Debug - name: Test - run: dotnet test --no-build --filter TestCategory=Cloud:Azure + run: dotnet test --no-build --filter TestCategory=Cloud:Azure -maxcpucount:1 s3: name: "Test: S3FileSystem" diff --git a/Directory.Packages.props b/Directory.Packages.props index cf3662c..12c4c02 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,9 +4,9 @@ true - - + + diff --git a/docker-compose.yml b/docker-compose.yml index 1ea5f76..5409b66 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,7 +6,7 @@ services: - "10000:10000" - "10001:10001" - "10002:10002" - command: azurite --blobHost 0.0.0.0 --queueHost 0.0.0.0 --tableHost 0.0.0.0 + command: azurite --skipApiVersionCheck --blobHost 0.0.0.0 --queueHost 0.0.0.0 --tableHost 0.0.0.0 rustfs: image: docker.io/rustfs/rustfs:latest diff --git a/src/Ramstack.FileSystem.Azure/AzureDirectory.cs b/src/Ramstack.FileSystem.Azure/AzureDirectory.cs index f14a92c..59c3a74 100644 --- a/src/Ramstack.FileSystem.Azure/AzureDirectory.cs +++ b/src/Ramstack.FileSystem.Azure/AzureDirectory.cs @@ -47,6 +47,8 @@ protected override async ValueTask DeleteCoreAsync(CancellationToken cancellatio var collection = _fs.AzureClient .GetBlobsAsync( prefix: GetPrefix(FullName), + traits: BlobTraits.None, + states: BlobStates.None, cancellationToken: cancellationToken); var client = _fs.AzureClient.GetBlobBatchClient(); @@ -105,6 +107,8 @@ protected override async IAsyncEnumerable GetFileNodesCoreAsync([En .GetBlobsByHierarchyAsync( delimiter: "/", prefix: GetPrefix(FullName), + traits: BlobTraits.None, + states: BlobStates.None, cancellationToken: cancellationToken); await foreach (var page in collection.AsPages().WithCancellation(cancellationToken).ConfigureAwait(false)) @@ -121,6 +125,8 @@ protected override async IAsyncEnumerable GetFilesCoreAsync([Enumer .GetBlobsByHierarchyAsync( delimiter: "/", prefix: GetPrefix(FullName), + traits: BlobTraits.None, + states: BlobStates.None, cancellationToken: cancellationToken); await foreach (var page in collection.AsPages().WithCancellation(cancellationToken).ConfigureAwait(false)) @@ -136,6 +142,8 @@ protected override async IAsyncEnumerable GetDirectoriesCoreAs .GetBlobsByHierarchyAsync( delimiter: "/", prefix: GetPrefix(FullName), + traits: BlobTraits.None, + states: BlobStates.None, cancellationToken: cancellationToken); await foreach (var page in collection.AsPages().WithCancellation(cancellationToken).ConfigureAwait(false)) @@ -162,7 +170,11 @@ protected override async IAsyncEnumerable GetFileNodesCoreAsync(str var directories = new HashSet { FullName }; await foreach (var page in _fs.AzureClient - .GetBlobsAsync(prefix: prefix, cancellationToken: cancellationToken) + .GetBlobsAsync( + prefix: prefix, + traits: BlobTraits.None, + states: BlobStates.None, + cancellationToken: cancellationToken) .AsPages() .WithCancellation(cancellationToken) .ConfigureAwait(false)) @@ -210,7 +222,11 @@ protected override async IAsyncEnumerable GetFilesCoreAsync(string[ var prefix = GetPrefix(FullName); await foreach (var page in _fs.AzureClient - .GetBlobsAsync(prefix: prefix, cancellationToken: cancellationToken) + .GetBlobsAsync( + prefix: prefix, + traits: BlobTraits.None, + states: BlobStates.None, + cancellationToken: cancellationToken) .AsPages() .WithCancellation(cancellationToken) .ConfigureAwait(false)) @@ -239,7 +255,11 @@ protected override async IAsyncEnumerable GetDirectoriesCoreAs var directories = new HashSet { FullName }; await foreach (var page in _fs.AzureClient - .GetBlobsAsync(prefix: prefix, cancellationToken: cancellationToken) + .GetBlobsAsync( + prefix: prefix, + traits: BlobTraits.None, + states: BlobStates.None, + cancellationToken: cancellationToken) .AsPages() .WithCancellation(cancellationToken) .ConfigureAwait(false)) diff --git a/src/Ramstack.FileSystem.Azure/Ramstack.FileSystem.Azure.csproj b/src/Ramstack.FileSystem.Azure/Ramstack.FileSystem.Azure.csproj index bf5aee4..1462936 100644 --- a/src/Ramstack.FileSystem.Azure/Ramstack.FileSystem.Azure.csproj +++ b/src/Ramstack.FileSystem.Azure/Ramstack.FileSystem.Azure.csproj @@ -1,7 +1,7 @@ - net6.0 + net6.0;net8.0 Provides an implementation of Ramstack.FileSystem based on Azure Blob Storage. enable enable @@ -40,9 +40,17 @@ - + + + + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive 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 de1581a..1365519 100644 --- a/tests/Ramstack.FileSystem.Azure.Tests/Ramstack.FileSystem.Azure.Tests.csproj +++ b/tests/Ramstack.FileSystem.Azure.Tests/Ramstack.FileSystem.Azure.Tests.csproj @@ -1,7 +1,7 @@ - net6.0 + net6.0;net8.0 enable enable preview diff --git a/tests/Ramstack.FileSystem.Azure.Tests/ReadonlyAzureFileSystemTests.cs b/tests/Ramstack.FileSystem.Azure.Tests/ReadonlyAzureFileSystemTests.cs index e3551a5..8d51ba2 100644 --- a/tests/Ramstack.FileSystem.Azure.Tests/ReadonlyAzureFileSystemTests.cs +++ b/tests/Ramstack.FileSystem.Azure.Tests/ReadonlyAzureFileSystemTests.cs @@ -7,7 +7,8 @@ namespace Ramstack.FileSystem.Azure; [Category("Cloud:Azure")] public class ReadonlyAzureFileSystemTests : VirtualFileSystemSpecificationTests { - private readonly TempFileStorage _storage = new TempFileStorage(); + private readonly TempFileStorage _storage = new(); + private readonly string _storageName = Guid.NewGuid().ToString("N"); [OneTimeSetUp] public async Task Setup() @@ -38,11 +39,11 @@ protected override IVirtualFileSystem GetFileSystem() => protected override DirectoryInfo GetDirectoryInfo() => new DirectoryInfo(_storage.Root); - private static AzureFileSystem CreateFileSystem(bool isReadonly) + 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, "storage") + 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 8a81080..a411b71 100644 --- a/tests/Ramstack.FileSystem.Azure.Tests/WritableAzureFileSystemTests.cs +++ b/tests/Ramstack.FileSystem.Azure.Tests/WritableAzureFileSystemTests.cs @@ -9,7 +9,8 @@ namespace Ramstack.FileSystem.Azure; public class WritableAzureFileSystemTests : VirtualFileSystemSpecificationTests { private readonly HashSet _list = []; - private readonly TempFileStorage _storage = new TempFileStorage(); + private readonly TempFileStorage _storage = new(); + private readonly string _storageName = Guid.NewGuid().ToString("N"); [OneTimeSetUp] public async Task Setup() @@ -124,7 +125,7 @@ await fs.GetFilesAsync("/temp").AnyAsync(), } protected override AzureFileSystem GetFileSystem() => - CreateFileSystem("storage"); + CreateFileSystem(_storageName); protected override DirectoryInfo GetDirectoryInfo() => new DirectoryInfo(_storage.Root); From 8adf021e42ccf681a30a881a4bc6a3d1fc4bf052 Mon Sep 17 00:00:00 2001 From: rameel Date: Tue, 31 Mar 2026 20:17:31 +0500 Subject: [PATCH 5/5] Add docker-compose.yml to solution --- Ramstack.FileSystem.slnx | 1 + 1 file changed, 1 insertion(+) diff --git a/Ramstack.FileSystem.slnx b/Ramstack.FileSystem.slnx index 7347946..e14578d 100644 --- a/Ramstack.FileSystem.slnx +++ b/Ramstack.FileSystem.slnx @@ -4,6 +4,7 @@ +