From 7ebef4aca3d6f097be29eaf97f0b18b4962fef82 Mon Sep 17 00:00:00 2001 From: prpeh Date: Fri, 3 Oct 2025 22:52:07 +0700 Subject: [PATCH] feat: Migrate Eclipse Store AFS NIO module to .NET Core - Port all 7 Java source files from Eclipse Store NIO module to C# - Implement NioFileSystem for local file system operations - Implement NioConnector for AFS integration - Add comprehensive test suite with 23 passing tests - Add detailed README with usage examples and documentation Key components: - NioPathResolver: Path resolution with home directory expansion - NioFileWrapper: Thread-safe file stream management - NioIoHandler: Core I/O operations (read, write, copy, move, etc.) - NioConnector: Full IBlobStoreConnector implementation All tests passing, builds successfully with the entire solution. --- NebulaStore.sln | 352 +++++++----- afs/nio/NebulaStore.Afs.Nio.csproj | 34 ++ afs/nio/README.md | 241 +++++++++ afs/nio/src/INioFileWrapper.cs | 88 +++ afs/nio/src/INioItemWrapper.cs | 20 + afs/nio/src/NioConnector.cs | 500 ++++++++++++++++++ afs/nio/src/NioFileSystem.cs | 250 +++++++++ afs/nio/src/NioFileWrapperBase.cs | 325 ++++++++++++ afs/nio/src/NioIoHandler.cs | 460 ++++++++++++++++ afs/nio/src/NioPathResolver.cs | 58 ++ afs/nio/src/NioReadableFile.cs | 106 ++++ afs/nio/src/NioWritableFile.cs | 93 ++++ afs/nio/test/NebulaStore.Afs.Nio.Tests.csproj | 29 + afs/nio/test/NioConnectorTests.cs | 250 +++++++++ afs/nio/test/NioFileSystemTests.cs | 165 ++++++ 15 files changed, 2837 insertions(+), 134 deletions(-) create mode 100644 afs/nio/NebulaStore.Afs.Nio.csproj create mode 100644 afs/nio/README.md create mode 100644 afs/nio/src/INioFileWrapper.cs create mode 100644 afs/nio/src/INioItemWrapper.cs create mode 100644 afs/nio/src/NioConnector.cs create mode 100644 afs/nio/src/NioFileSystem.cs create mode 100644 afs/nio/src/NioFileWrapperBase.cs create mode 100644 afs/nio/src/NioIoHandler.cs create mode 100644 afs/nio/src/NioPathResolver.cs create mode 100644 afs/nio/src/NioReadableFile.cs create mode 100644 afs/nio/src/NioWritableFile.cs create mode 100644 afs/nio/test/NebulaStore.Afs.Nio.Tests.csproj create mode 100644 afs/nio/test/NioConnectorTests.cs create mode 100644 afs/nio/test/NioFileSystemTests.cs diff --git a/NebulaStore.sln b/NebulaStore.sln index 78b335f..d846551 100644 --- a/NebulaStore.sln +++ b/NebulaStore.sln @@ -1,134 +1,218 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.0.31903.59 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "storage", "storage", "{D0E78B5A-0D39-4CE8-A836-5FC8C7D60478}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "afs", "afs", "{F6G7H8I9-J0K1-2345-MNOP-QR6789012345}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "embedded", "embedded", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NebulaStore.Storage.Embedded", "storage\embedded\NebulaStore.Storage.Embedded.csproj", "{08F6A698-C457-4D0E-8618-28836ABF02FD}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NebulaStore.Storage.EmbeddedConfiguration", "storage\embedded-configuration\NebulaStore.Storage.EmbeddedConfiguration.csproj", "{B1C2D3E4-F5G6-7890-HIJK-LM1234567890}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NebulaStore.Storage", "storage\storage\NebulaStore.Storage.csproj", "{C2D3E4F5-G6H7-8901-IJKL-MN2345678901}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NebulaStore.Afs.Blobstore", "afs\blobstore\NebulaStore.Afs.Blobstore.csproj", "{E4F5G6H7-I8J9-0123-KLMN-OP4567890123}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NebulaStore.Storage.Embedded.Tests", "storage\embedded\tests\NebulaStore.Storage.Embedded.Tests.csproj", "{2099EB97-32CB-4403-BF84-AD7F965FA520}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NebulaStore.Afs.Blobstore.Tests", "afs\blobstore\test\NebulaStore.Afs.Blobstore.Tests.csproj", "{F5G6H7I8-J9K0-1234-LMNO-PQ5678901234}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NebulaStore.Afs.Tests", "afs\tests\NebulaStore.Afs.Tests.csproj", "{G6H7I8J9-K0L1-2345-NOPQ-RS6789012345}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NebulaStore.Examples.ConsoleApp", "examples\ConsoleApp\NebulaStore.Examples.ConsoleApp.csproj", "{D3E4F5G6-H7I8-9012-JKLM-NO3456789012}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NebulaStore.GigaMap", "gigamap\NebulaStore.GigaMap.csproj", "{H7I8J9K0-L1M2-3456-NOPQ-RS7890123456}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NebulaStore.GigaMap.Tests", "gigamap\tests\NebulaStore.GigaMap.Tests.csproj", "{I8J9K0L1-M2N3-4567-OPQR-ST8901234567}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "aws", "aws", "{J9K0L1M2-N3O4-5678-PQRS-TU9012345678}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NebulaStore.Afs.Aws.S3", "afs\aws\s3\NebulaStore.Afs.Aws.S3.csproj", "{K0L1M2N3-O4P5-6789-QRST-UV0123456789}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NebulaStore.Afs.Aws.S3.Tests", "afs\aws\s3\test\NebulaStore.Afs.Aws.S3.Tests.csproj", "{L1M2N3O4-P5Q6-7890-RSTU-VW1234567890}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "azure", "azure", "{M2N3O4P5-Q6R7-8901-STUV-WX2345678901}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NebulaStore.Afs.Azure.Storage", "afs\azure\storage\NebulaStore.Afs.Azure.Storage.csproj", "{N3O4P5Q6-R7S8-9012-TUVW-XY3456789012}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NebulaStore.Afs.Azure.Storage.Tests", "afs\azure\storage\test\NebulaStore.Afs.Azure.Storage.Tests.csproj", "{Q7R8S9T0-U1V2-3456-WXYZ-AB4567890123}" -EndProject - - - -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {08F6A698-C457-4D0E-8618-28836ABF02FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {08F6A698-C457-4D0E-8618-28836ABF02FD}.Debug|Any CPU.Build.0 = Debug|Any CPU - {08F6A698-C457-4D0E-8618-28836ABF02FD}.Release|Any CPU.ActiveCfg = Release|Any CPU - {08F6A698-C457-4D0E-8618-28836ABF02FD}.Release|Any CPU.Build.0 = Release|Any CPU - {B1C2D3E4-F5G6-7890-HIJK-LM1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B1C2D3E4-F5G6-7890-HIJK-LM1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B1C2D3E4-F5G6-7890-HIJK-LM1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B1C2D3E4-F5G6-7890-HIJK-LM1234567890}.Release|Any CPU.Build.0 = Release|Any CPU - {C2D3E4F5-G6H7-8901-IJKL-MN2345678901}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C2D3E4F5-G6H7-8901-IJKL-MN2345678901}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C2D3E4F5-G6H7-8901-IJKL-MN2345678901}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C2D3E4F5-G6H7-8901-IJKL-MN2345678901}.Release|Any CPU.Build.0 = Release|Any CPU - {2099EB97-32CB-4403-BF84-AD7F965FA520}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2099EB97-32CB-4403-BF84-AD7F965FA520}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2099EB97-32CB-4403-BF84-AD7F965FA520}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2099EB97-32CB-4403-BF84-AD7F965FA520}.Release|Any CPU.Build.0 = Release|Any CPU - {D3E4F5G6-H7I8-9012-JKLM-NO3456789012}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D3E4F5G6-H7I8-9012-JKLM-NO3456789012}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D3E4F5G6-H7I8-9012-JKLM-NO3456789012}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D3E4F5G6-H7I8-9012-JKLM-NO3456789012}.Release|Any CPU.Build.0 = Release|Any CPU - {E4F5G6H7-I8J9-0123-KLMN-OP4567890123}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E4F5G6H7-I8J9-0123-KLMN-OP4567890123}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E4F5G6H7-I8J9-0123-KLMN-OP4567890123}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E4F5G6H7-I8J9-0123-KLMN-OP4567890123}.Release|Any CPU.Build.0 = Release|Any CPU - {F5G6H7I8-J9K0-1234-LMNO-PQ5678901234}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F5G6H7I8-J9K0-1234-LMNO-PQ5678901234}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F5G6H7I8-J9K0-1234-LMNO-PQ5678901234}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F5G6H7I8-J9K0-1234-LMNO-PQ5678901234}.Release|Any CPU.Build.0 = Release|Any CPU - {G6H7I8J9-K0L1-2345-NOPQ-RS6789012345}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {G6H7I8J9-K0L1-2345-NOPQ-RS6789012345}.Debug|Any CPU.Build.0 = Debug|Any CPU - {G6H7I8J9-K0L1-2345-NOPQ-RS6789012345}.Release|Any CPU.ActiveCfg = Release|Any CPU - {G6H7I8J9-K0L1-2345-NOPQ-RS6789012345}.Release|Any CPU.Build.0 = Release|Any CPU - {H7I8J9K0-L1M2-3456-NOPQ-RS7890123456}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {H7I8J9K0-L1M2-3456-NOPQ-RS7890123456}.Debug|Any CPU.Build.0 = Debug|Any CPU - {H7I8J9K0-L1M2-3456-NOPQ-RS7890123456}.Release|Any CPU.ActiveCfg = Release|Any CPU - {H7I8J9K0-L1M2-3456-NOPQ-RS7890123456}.Release|Any CPU.Build.0 = Release|Any CPU - {I8J9K0L1-M2N3-4567-OPQR-ST8901234567}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {I8J9K0L1-M2N3-4567-OPQR-ST8901234567}.Debug|Any CPU.Build.0 = Debug|Any CPU - {I8J9K0L1-M2N3-4567-OPQR-ST8901234567}.Release|Any CPU.ActiveCfg = Release|Any CPU - {I8J9K0L1-M2N3-4567-OPQR-ST8901234567}.Release|Any CPU.Build.0 = Release|Any CPU - {K0L1M2N3-O4P5-6789-QRST-UV0123456789}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {K0L1M2N3-O4P5-6789-QRST-UV0123456789}.Debug|Any CPU.Build.0 = Debug|Any CPU - {K0L1M2N3-O4P5-6789-QRST-UV0123456789}.Release|Any CPU.ActiveCfg = Release|Any CPU - {K0L1M2N3-O4P5-6789-QRST-UV0123456789}.Release|Any CPU.Build.0 = Release|Any CPU - {L1M2N3O4-P5Q6-7890-RSTU-VW1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {L1M2N3O4-P5Q6-7890-RSTU-VW1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU - {L1M2N3O4-P5Q6-7890-RSTU-VW1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU - {L1M2N3O4-P5Q6-7890-RSTU-VW1234567890}.Release|Any CPU.Build.0 = Release|Any CPU - {N3O4P5Q6-R7S8-9012-TUVW-XY3456789012}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {N3O4P5Q6-R7S8-9012-TUVW-XY3456789012}.Debug|Any CPU.Build.0 = Debug|Any CPU - {N3O4P5Q6-R7S8-9012-TUVW-XY3456789012}.Release|Any CPU.ActiveCfg = Release|Any CPU - {N3O4P5Q6-R7S8-9012-TUVW-XY3456789012}.Release|Any CPU.Build.0 = Release|Any CPU - {Q7R8S9T0-U1V2-3456-WXYZ-AB4567890123}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {Q7R8S9T0-U1V2-3456-WXYZ-AB4567890123}.Debug|Any CPU.Build.0 = Debug|Any CPU - {Q7R8S9T0-U1V2-3456-WXYZ-AB4567890123}.Release|Any CPU.ActiveCfg = Release|Any CPU - {Q7R8S9T0-U1V2-3456-WXYZ-AB4567890123}.Release|Any CPU.Build.0 = Release|Any CPU - - - - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {A1B2C3D4-E5F6-7890-ABCD-EF1234567890} = {D0E78B5A-0D39-4CE8-A836-5FC8C7D60478} - {08F6A698-C457-4D0E-8618-28836ABF02FD} = {A1B2C3D4-E5F6-7890-ABCD-EF1234567890} - {B1C2D3E4-F5G6-7890-HIJK-LM1234567890} = {D0E78B5A-0D39-4CE8-A836-5FC8C7D60478} - {C2D3E4F5-G6H7-8901-IJKL-MN2345678901} = {D0E78B5A-0D39-4CE8-A836-5FC8C7D60478} - {E4F5G6H7-I8J9-0123-KLMN-OP4567890123} = {F6G7H8I9-J0K1-2345-MNOP-QR6789012345} - {F5G6H7I8-J9K0-1234-LMNO-PQ5678901234} = {F6G7H8I9-J0K1-2345-MNOP-QR6789012345} - {G6H7I8J9-K0L1-2345-NOPQ-RS6789012345} = {F6G7H8I9-J0K1-2345-MNOP-QR6789012345} - {2099EB97-32CB-4403-BF84-AD7F965FA520} = {A1B2C3D4-E5F6-7890-ABCD-EF1234567890} - {J9K0L1M2-N3O4-5678-PQRS-TU9012345678} = {F6G7H8I9-J0K1-2345-MNOP-QR6789012345} - {K0L1M2N3-O4P5-6789-QRST-UV0123456789} = {J9K0L1M2-N3O4-5678-PQRS-TU9012345678} - {L1M2N3O4-P5Q6-7890-RSTU-VW1234567890} = {J9K0L1M2-N3O4-5678-PQRS-TU9012345678} - {M2N3O4P5-Q6R7-8901-STUV-WX2345678901} = {F6G7H8I9-J0K1-2345-MNOP-QR6789012345} - {N3O4P5Q6-R7S8-9012-TUVW-XY3456789012} = {M2N3O4P5-Q6R7-8901-STUV-WX2345678901} - {Q7R8S9T0-U1V2-3456-WXYZ-AB4567890123} = {M2N3O4P5-Q6R7-8901-STUV-WX2345678901} - - - EndGlobalSection -EndGlobal + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "storage", "storage", "{D0E78B5A-0D39-4CE8-A836-5FC8C7D60478}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "afs", "afs", "{1213A069-D226-59B6-6383-4AD31C1DF055}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "embedded", "embedded", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NebulaStore.Storage.Embedded", "storage\embedded\NebulaStore.Storage.Embedded.csproj", "{08F6A698-C457-4D0E-8618-28836ABF02FD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NebulaStore.Storage.EmbeddedConfiguration", "storage\embedded-configuration\NebulaStore.Storage.EmbeddedConfiguration.csproj", "{43FE9E5F-E2C8-EBBE-70A7-9BCD3CC8F87A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NebulaStore.Storage", "storage\storage\NebulaStore.Storage.csproj", "{EB3AD552-AEF2-D7E0-AEA1-A7B5756DAB3D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NebulaStore.Afs.Blobstore", "afs\blobstore\NebulaStore.Afs.Blobstore.csproj", "{84059610-AD39-FA20-9653-176A6CFEFACF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NebulaStore.Storage.Embedded.Tests", "storage\embedded\tests\NebulaStore.Storage.Embedded.Tests.csproj", "{2099EB97-32CB-4403-BF84-AD7F965FA520}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NebulaStore.Afs.Blobstore.Tests", "afs\blobstore\test\NebulaStore.Afs.Blobstore.Tests.csproj", "{957AEEFE-FE29-6397-7D08-297EF5D694BE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NebulaStore.Afs.Tests", "afs\tests\NebulaStore.Afs.Tests.csproj", "{1BA08BC3-4B57-48D2-8DC8-88174BF9CBC5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NebulaStore.Examples.ConsoleApp", "examples\ConsoleApp\NebulaStore.Examples.ConsoleApp.csproj", "{D15783DF-B8ED-7DA0-3855-B4BD90314158}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NebulaStore.GigaMap", "gigamap\NebulaStore.GigaMap.csproj", "{E7A0475A-23ED-EB6D-7120-11CD71A2FF7B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NebulaStore.GigaMap.Tests", "gigamap\tests\NebulaStore.GigaMap.Tests.csproj", "{8DD374AE-8266-C419-D016-ABFC2424D8A4}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "aws", "aws", "{E018A2F0-712D-E946-F173-BAD35C4AC283}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NebulaStore.Afs.Aws.S3", "afs\aws\s3\NebulaStore.Afs.Aws.S3.csproj", "{AF6BA56B-2336-DCDD-CFCB-692755A5C692}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NebulaStore.Afs.Aws.S3.Tests", "afs\aws\s3\test\NebulaStore.Afs.Aws.S3.Tests.csproj", "{E5ECD596-2A73-FF9D-F14A-8A0983C807CF}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "azure", "azure", "{C35B3F86-D513-DC96-F441-B40E8BE042A1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NebulaStore.Afs.Azure.Storage", "afs\azure\storage\NebulaStore.Afs.Azure.Storage.csproj", "{451497DA-1269-BC92-0D1A-DC64CB39853E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NebulaStore.Afs.Azure.Storage.Tests", "afs\azure\storage\test\NebulaStore.Afs.Azure.Storage.Tests.csproj", "{13ACF2E0-A4EA-3D0E-D60E-D8CDA95FCF48}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "nio", "nio", "{01885DAA-D6B3-261D-26CC-40273AA28AE8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NebulaStore.Afs.Nio", "afs\nio\NebulaStore.Afs.Nio.csproj", "{1EDD44A6-7440-4D73-907E-5B89EF199012}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{1704E723-975B-8E7A-0A72-66948CF308B6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NebulaStore.Afs.Nio.Tests", "afs\nio\test\NebulaStore.Afs.Nio.Tests.csproj", "{DA9E1E64-1245-4E0D-9C15-80980DAF1623}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {08F6A698-C457-4D0E-8618-28836ABF02FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {08F6A698-C457-4D0E-8618-28836ABF02FD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {08F6A698-C457-4D0E-8618-28836ABF02FD}.Debug|x64.ActiveCfg = Debug|Any CPU + {08F6A698-C457-4D0E-8618-28836ABF02FD}.Debug|x64.Build.0 = Debug|Any CPU + {08F6A698-C457-4D0E-8618-28836ABF02FD}.Debug|x86.ActiveCfg = Debug|Any CPU + {08F6A698-C457-4D0E-8618-28836ABF02FD}.Debug|x86.Build.0 = Debug|Any CPU + {08F6A698-C457-4D0E-8618-28836ABF02FD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {08F6A698-C457-4D0E-8618-28836ABF02FD}.Release|Any CPU.Build.0 = Release|Any CPU + {08F6A698-C457-4D0E-8618-28836ABF02FD}.Release|x64.ActiveCfg = Release|Any CPU + {08F6A698-C457-4D0E-8618-28836ABF02FD}.Release|x64.Build.0 = Release|Any CPU + {08F6A698-C457-4D0E-8618-28836ABF02FD}.Release|x86.ActiveCfg = Release|Any CPU + {08F6A698-C457-4D0E-8618-28836ABF02FD}.Release|x86.Build.0 = Release|Any CPU + {43FE9E5F-E2C8-EBBE-70A7-9BCD3CC8F87A}.Debug|x64.ActiveCfg = Debug|Any CPU + {43FE9E5F-E2C8-EBBE-70A7-9BCD3CC8F87A}.Debug|x64.Build.0 = Debug|Any CPU + {43FE9E5F-E2C8-EBBE-70A7-9BCD3CC8F87A}.Debug|x86.ActiveCfg = Debug|Any CPU + {43FE9E5F-E2C8-EBBE-70A7-9BCD3CC8F87A}.Debug|x86.Build.0 = Debug|Any CPU + {43FE9E5F-E2C8-EBBE-70A7-9BCD3CC8F87A}.Release|x64.ActiveCfg = Release|Any CPU + {43FE9E5F-E2C8-EBBE-70A7-9BCD3CC8F87A}.Release|x64.Build.0 = Release|Any CPU + {43FE9E5F-E2C8-EBBE-70A7-9BCD3CC8F87A}.Release|x86.ActiveCfg = Release|Any CPU + {43FE9E5F-E2C8-EBBE-70A7-9BCD3CC8F87A}.Release|x86.Build.0 = Release|Any CPU + {EB3AD552-AEF2-D7E0-AEA1-A7B5756DAB3D}.Debug|x64.ActiveCfg = Debug|Any CPU + {EB3AD552-AEF2-D7E0-AEA1-A7B5756DAB3D}.Debug|x64.Build.0 = Debug|Any CPU + {EB3AD552-AEF2-D7E0-AEA1-A7B5756DAB3D}.Debug|x86.ActiveCfg = Debug|Any CPU + {EB3AD552-AEF2-D7E0-AEA1-A7B5756DAB3D}.Debug|x86.Build.0 = Debug|Any CPU + {EB3AD552-AEF2-D7E0-AEA1-A7B5756DAB3D}.Release|x64.ActiveCfg = Release|Any CPU + {EB3AD552-AEF2-D7E0-AEA1-A7B5756DAB3D}.Release|x64.Build.0 = Release|Any CPU + {EB3AD552-AEF2-D7E0-AEA1-A7B5756DAB3D}.Release|x86.ActiveCfg = Release|Any CPU + {EB3AD552-AEF2-D7E0-AEA1-A7B5756DAB3D}.Release|x86.Build.0 = Release|Any CPU + {84059610-AD39-FA20-9653-176A6CFEFACF}.Debug|x64.ActiveCfg = Debug|Any CPU + {84059610-AD39-FA20-9653-176A6CFEFACF}.Debug|x64.Build.0 = Debug|Any CPU + {84059610-AD39-FA20-9653-176A6CFEFACF}.Debug|x86.ActiveCfg = Debug|Any CPU + {84059610-AD39-FA20-9653-176A6CFEFACF}.Debug|x86.Build.0 = Debug|Any CPU + {84059610-AD39-FA20-9653-176A6CFEFACF}.Release|x64.ActiveCfg = Release|Any CPU + {84059610-AD39-FA20-9653-176A6CFEFACF}.Release|x64.Build.0 = Release|Any CPU + {84059610-AD39-FA20-9653-176A6CFEFACF}.Release|x86.ActiveCfg = Release|Any CPU + {84059610-AD39-FA20-9653-176A6CFEFACF}.Release|x86.Build.0 = Release|Any CPU + {2099EB97-32CB-4403-BF84-AD7F965FA520}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2099EB97-32CB-4403-BF84-AD7F965FA520}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2099EB97-32CB-4403-BF84-AD7F965FA520}.Debug|x64.ActiveCfg = Debug|Any CPU + {2099EB97-32CB-4403-BF84-AD7F965FA520}.Debug|x64.Build.0 = Debug|Any CPU + {2099EB97-32CB-4403-BF84-AD7F965FA520}.Debug|x86.ActiveCfg = Debug|Any CPU + {2099EB97-32CB-4403-BF84-AD7F965FA520}.Debug|x86.Build.0 = Debug|Any CPU + {2099EB97-32CB-4403-BF84-AD7F965FA520}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2099EB97-32CB-4403-BF84-AD7F965FA520}.Release|Any CPU.Build.0 = Release|Any CPU + {2099EB97-32CB-4403-BF84-AD7F965FA520}.Release|x64.ActiveCfg = Release|Any CPU + {2099EB97-32CB-4403-BF84-AD7F965FA520}.Release|x64.Build.0 = Release|Any CPU + {2099EB97-32CB-4403-BF84-AD7F965FA520}.Release|x86.ActiveCfg = Release|Any CPU + {2099EB97-32CB-4403-BF84-AD7F965FA520}.Release|x86.Build.0 = Release|Any CPU + {957AEEFE-FE29-6397-7D08-297EF5D694BE}.Debug|x64.ActiveCfg = Debug|Any CPU + {957AEEFE-FE29-6397-7D08-297EF5D694BE}.Debug|x64.Build.0 = Debug|Any CPU + {957AEEFE-FE29-6397-7D08-297EF5D694BE}.Debug|x86.ActiveCfg = Debug|Any CPU + {957AEEFE-FE29-6397-7D08-297EF5D694BE}.Debug|x86.Build.0 = Debug|Any CPU + {957AEEFE-FE29-6397-7D08-297EF5D694BE}.Release|x64.ActiveCfg = Release|Any CPU + {957AEEFE-FE29-6397-7D08-297EF5D694BE}.Release|x64.Build.0 = Release|Any CPU + {957AEEFE-FE29-6397-7D08-297EF5D694BE}.Release|x86.ActiveCfg = Release|Any CPU + {957AEEFE-FE29-6397-7D08-297EF5D694BE}.Release|x86.Build.0 = Release|Any CPU + {1BA08BC3-4B57-48D2-8DC8-88174BF9CBC5}.Debug|x64.ActiveCfg = Debug|Any CPU + {1BA08BC3-4B57-48D2-8DC8-88174BF9CBC5}.Debug|x64.Build.0 = Debug|Any CPU + {1BA08BC3-4B57-48D2-8DC8-88174BF9CBC5}.Debug|x86.ActiveCfg = Debug|Any CPU + {1BA08BC3-4B57-48D2-8DC8-88174BF9CBC5}.Debug|x86.Build.0 = Debug|Any CPU + {1BA08BC3-4B57-48D2-8DC8-88174BF9CBC5}.Release|x64.ActiveCfg = Release|Any CPU + {1BA08BC3-4B57-48D2-8DC8-88174BF9CBC5}.Release|x64.Build.0 = Release|Any CPU + {1BA08BC3-4B57-48D2-8DC8-88174BF9CBC5}.Release|x86.ActiveCfg = Release|Any CPU + {1BA08BC3-4B57-48D2-8DC8-88174BF9CBC5}.Release|x86.Build.0 = Release|Any CPU + {D15783DF-B8ED-7DA0-3855-B4BD90314158}.Debug|x64.ActiveCfg = Debug|Any CPU + {D15783DF-B8ED-7DA0-3855-B4BD90314158}.Debug|x64.Build.0 = Debug|Any CPU + {D15783DF-B8ED-7DA0-3855-B4BD90314158}.Debug|x86.ActiveCfg = Debug|Any CPU + {D15783DF-B8ED-7DA0-3855-B4BD90314158}.Debug|x86.Build.0 = Debug|Any CPU + {D15783DF-B8ED-7DA0-3855-B4BD90314158}.Release|x64.ActiveCfg = Release|Any CPU + {D15783DF-B8ED-7DA0-3855-B4BD90314158}.Release|x64.Build.0 = Release|Any CPU + {D15783DF-B8ED-7DA0-3855-B4BD90314158}.Release|x86.ActiveCfg = Release|Any CPU + {D15783DF-B8ED-7DA0-3855-B4BD90314158}.Release|x86.Build.0 = Release|Any CPU + {E7A0475A-23ED-EB6D-7120-11CD71A2FF7B}.Debug|x64.ActiveCfg = Debug|Any CPU + {E7A0475A-23ED-EB6D-7120-11CD71A2FF7B}.Debug|x64.Build.0 = Debug|Any CPU + {E7A0475A-23ED-EB6D-7120-11CD71A2FF7B}.Debug|x86.ActiveCfg = Debug|Any CPU + {E7A0475A-23ED-EB6D-7120-11CD71A2FF7B}.Debug|x86.Build.0 = Debug|Any CPU + {E7A0475A-23ED-EB6D-7120-11CD71A2FF7B}.Release|x64.ActiveCfg = Release|Any CPU + {E7A0475A-23ED-EB6D-7120-11CD71A2FF7B}.Release|x64.Build.0 = Release|Any CPU + {E7A0475A-23ED-EB6D-7120-11CD71A2FF7B}.Release|x86.ActiveCfg = Release|Any CPU + {E7A0475A-23ED-EB6D-7120-11CD71A2FF7B}.Release|x86.Build.0 = Release|Any CPU + {8DD374AE-8266-C419-D016-ABFC2424D8A4}.Debug|x64.ActiveCfg = Debug|Any CPU + {8DD374AE-8266-C419-D016-ABFC2424D8A4}.Debug|x64.Build.0 = Debug|Any CPU + {8DD374AE-8266-C419-D016-ABFC2424D8A4}.Debug|x86.ActiveCfg = Debug|Any CPU + {8DD374AE-8266-C419-D016-ABFC2424D8A4}.Debug|x86.Build.0 = Debug|Any CPU + {8DD374AE-8266-C419-D016-ABFC2424D8A4}.Release|x64.ActiveCfg = Release|Any CPU + {8DD374AE-8266-C419-D016-ABFC2424D8A4}.Release|x64.Build.0 = Release|Any CPU + {8DD374AE-8266-C419-D016-ABFC2424D8A4}.Release|x86.ActiveCfg = Release|Any CPU + {8DD374AE-8266-C419-D016-ABFC2424D8A4}.Release|x86.Build.0 = Release|Any CPU + {AF6BA56B-2336-DCDD-CFCB-692755A5C692}.Debug|x64.ActiveCfg = Debug|Any CPU + {AF6BA56B-2336-DCDD-CFCB-692755A5C692}.Debug|x64.Build.0 = Debug|Any CPU + {AF6BA56B-2336-DCDD-CFCB-692755A5C692}.Debug|x86.ActiveCfg = Debug|Any CPU + {AF6BA56B-2336-DCDD-CFCB-692755A5C692}.Debug|x86.Build.0 = Debug|Any CPU + {AF6BA56B-2336-DCDD-CFCB-692755A5C692}.Release|x64.ActiveCfg = Release|Any CPU + {AF6BA56B-2336-DCDD-CFCB-692755A5C692}.Release|x64.Build.0 = Release|Any CPU + {AF6BA56B-2336-DCDD-CFCB-692755A5C692}.Release|x86.ActiveCfg = Release|Any CPU + {AF6BA56B-2336-DCDD-CFCB-692755A5C692}.Release|x86.Build.0 = Release|Any CPU + {E5ECD596-2A73-FF9D-F14A-8A0983C807CF}.Debug|x64.ActiveCfg = Debug|Any CPU + {E5ECD596-2A73-FF9D-F14A-8A0983C807CF}.Debug|x64.Build.0 = Debug|Any CPU + {E5ECD596-2A73-FF9D-F14A-8A0983C807CF}.Debug|x86.ActiveCfg = Debug|Any CPU + {E5ECD596-2A73-FF9D-F14A-8A0983C807CF}.Debug|x86.Build.0 = Debug|Any CPU + {E5ECD596-2A73-FF9D-F14A-8A0983C807CF}.Release|x64.ActiveCfg = Release|Any CPU + {E5ECD596-2A73-FF9D-F14A-8A0983C807CF}.Release|x64.Build.0 = Release|Any CPU + {E5ECD596-2A73-FF9D-F14A-8A0983C807CF}.Release|x86.ActiveCfg = Release|Any CPU + {E5ECD596-2A73-FF9D-F14A-8A0983C807CF}.Release|x86.Build.0 = Release|Any CPU + {451497DA-1269-BC92-0D1A-DC64CB39853E}.Debug|x64.ActiveCfg = Debug|Any CPU + {451497DA-1269-BC92-0D1A-DC64CB39853E}.Debug|x64.Build.0 = Debug|Any CPU + {451497DA-1269-BC92-0D1A-DC64CB39853E}.Debug|x86.ActiveCfg = Debug|Any CPU + {451497DA-1269-BC92-0D1A-DC64CB39853E}.Debug|x86.Build.0 = Debug|Any CPU + {451497DA-1269-BC92-0D1A-DC64CB39853E}.Release|x64.ActiveCfg = Release|Any CPU + {451497DA-1269-BC92-0D1A-DC64CB39853E}.Release|x64.Build.0 = Release|Any CPU + {451497DA-1269-BC92-0D1A-DC64CB39853E}.Release|x86.ActiveCfg = Release|Any CPU + {451497DA-1269-BC92-0D1A-DC64CB39853E}.Release|x86.Build.0 = Release|Any CPU + {13ACF2E0-A4EA-3D0E-D60E-D8CDA95FCF48}.Debug|x64.ActiveCfg = Debug|Any CPU + {13ACF2E0-A4EA-3D0E-D60E-D8CDA95FCF48}.Debug|x64.Build.0 = Debug|Any CPU + {13ACF2E0-A4EA-3D0E-D60E-D8CDA95FCF48}.Debug|x86.ActiveCfg = Debug|Any CPU + {13ACF2E0-A4EA-3D0E-D60E-D8CDA95FCF48}.Debug|x86.Build.0 = Debug|Any CPU + {13ACF2E0-A4EA-3D0E-D60E-D8CDA95FCF48}.Release|x64.ActiveCfg = Release|Any CPU + {13ACF2E0-A4EA-3D0E-D60E-D8CDA95FCF48}.Release|x64.Build.0 = Release|Any CPU + {13ACF2E0-A4EA-3D0E-D60E-D8CDA95FCF48}.Release|x86.ActiveCfg = Release|Any CPU + {13ACF2E0-A4EA-3D0E-D60E-D8CDA95FCF48}.Release|x86.Build.0 = Release|Any CPU + {1EDD44A6-7440-4D73-907E-5B89EF199012}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1EDD44A6-7440-4D73-907E-5B89EF199012}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1EDD44A6-7440-4D73-907E-5B89EF199012}.Debug|x64.ActiveCfg = Debug|Any CPU + {1EDD44A6-7440-4D73-907E-5B89EF199012}.Debug|x64.Build.0 = Debug|Any CPU + {1EDD44A6-7440-4D73-907E-5B89EF199012}.Debug|x86.ActiveCfg = Debug|Any CPU + {1EDD44A6-7440-4D73-907E-5B89EF199012}.Debug|x86.Build.0 = Debug|Any CPU + {1EDD44A6-7440-4D73-907E-5B89EF199012}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1EDD44A6-7440-4D73-907E-5B89EF199012}.Release|Any CPU.Build.0 = Release|Any CPU + {1EDD44A6-7440-4D73-907E-5B89EF199012}.Release|x64.ActiveCfg = Release|Any CPU + {1EDD44A6-7440-4D73-907E-5B89EF199012}.Release|x64.Build.0 = Release|Any CPU + {1EDD44A6-7440-4D73-907E-5B89EF199012}.Release|x86.ActiveCfg = Release|Any CPU + {1EDD44A6-7440-4D73-907E-5B89EF199012}.Release|x86.Build.0 = Release|Any CPU + {DA9E1E64-1245-4E0D-9C15-80980DAF1623}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DA9E1E64-1245-4E0D-9C15-80980DAF1623}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DA9E1E64-1245-4E0D-9C15-80980DAF1623}.Debug|x64.ActiveCfg = Debug|Any CPU + {DA9E1E64-1245-4E0D-9C15-80980DAF1623}.Debug|x64.Build.0 = Debug|Any CPU + {DA9E1E64-1245-4E0D-9C15-80980DAF1623}.Debug|x86.ActiveCfg = Debug|Any CPU + {DA9E1E64-1245-4E0D-9C15-80980DAF1623}.Debug|x86.Build.0 = Debug|Any CPU + {DA9E1E64-1245-4E0D-9C15-80980DAF1623}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DA9E1E64-1245-4E0D-9C15-80980DAF1623}.Release|Any CPU.Build.0 = Release|Any CPU + {DA9E1E64-1245-4E0D-9C15-80980DAF1623}.Release|x64.ActiveCfg = Release|Any CPU + {DA9E1E64-1245-4E0D-9C15-80980DAF1623}.Release|x64.Build.0 = Release|Any CPU + {DA9E1E64-1245-4E0D-9C15-80980DAF1623}.Release|x86.ActiveCfg = Release|Any CPU + {DA9E1E64-1245-4E0D-9C15-80980DAF1623}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890} = {D0E78B5A-0D39-4CE8-A836-5FC8C7D60478} + {08F6A698-C457-4D0E-8618-28836ABF02FD} = {A1B2C3D4-E5F6-7890-ABCD-EF1234567890} + {01885DAA-D6B3-261D-26CC-40273AA28AE8} = {1213A069-D226-59B6-6383-4AD31C1DF055} + {1EDD44A6-7440-4D73-907E-5B89EF199012} = {01885DAA-D6B3-261D-26CC-40273AA28AE8} + {1704E723-975B-8E7A-0A72-66948CF308B6} = {01885DAA-D6B3-261D-26CC-40273AA28AE8} + {DA9E1E64-1245-4E0D-9C15-80980DAF1623} = {1704E723-975B-8E7A-0A72-66948CF308B6} + EndGlobalSection +EndGlobal diff --git a/afs/nio/NebulaStore.Afs.Nio.csproj b/afs/nio/NebulaStore.Afs.Nio.csproj new file mode 100644 index 0000000..b10fa09 --- /dev/null +++ b/afs/nio/NebulaStore.Afs.Nio.csproj @@ -0,0 +1,34 @@ + + + + net9.0 + enable + enable + latest + true + true + + CS1591 + + + + NebulaStore Abstract File System - NIO + .NET I/O Adapter for the NebulaStore Abstract File System + NebulaStore + NebulaStore + Copyright © NebulaStore 2025 + 1.0.0.0 + 1.0.0.0 + NebulaStore.Afs.Nio + + + + + + + + + + + + diff --git a/afs/nio/README.md b/afs/nio/README.md new file mode 100644 index 0000000..e3c8577 --- /dev/null +++ b/afs/nio/README.md @@ -0,0 +1,241 @@ +# NebulaStore Abstract File System - NIO + +The NIO (New I/O) module provides a .NET I/O adapter for the NebulaStore Abstract File System. This implementation uses standard .NET file system APIs (`System.IO`) to provide file operations, making it the default and most straightforward storage backend for local file systems. + +## Overview + +The NIO module is a direct port of the Eclipse Store AFS NIO module from Java to .NET. It provides: + +- **Local File System Access**: Direct access to the local file system using .NET's `System.IO` +- **File Stream Management**: Efficient file stream handling with automatic positioning +- **Path Resolution**: Flexible path resolution with home directory expansion support +- **Thread-Safe Operations**: All operations are thread-safe with proper synchronization +- **Integration**: Seamless integration with the NebulaStore AFS infrastructure + +## Architecture + +### Core Components + +1. **NioFileSystem**: Main entry point for NIO file system operations +2. **NioIoHandler**: Handles all I/O operations (read, write, copy, move, etc.) +3. **NioConnector**: Implements `IBlobStoreConnector` for AFS integration +4. **NioReadableFile**: Wrapper for read-only file access +5. **NioWritableFile**: Wrapper for read-write file access +6. **NioPathResolver**: Resolves path elements to file system paths + +### Class Hierarchy + +``` +INioFileSystem +└── NioFileSystem + +INioIoHandler +└── NioIoHandler + +INioFileWrapper +├── INioReadableFile +│ └── NioReadableFile +└── INioWritableFile + └── NioWritableFile + +IBlobStoreConnector +└── NioConnector +``` + +## Usage + +### Basic File System Operations + +```csharp +using NebulaStore.Afs.Nio; +using NebulaStore.Afs.Blobstore; + +// Create a NIO file system +using var fileSystem = NioFileSystem.New(); + +// Create a path +var path = BlobStorePath.New("container", "folder", "file.txt"); + +// Wrap for writing +using var writableFile = fileSystem.WrapForWriting(path); + +// Write data +var data = System.Text.Encoding.UTF8.GetBytes("Hello, NIO!"); +fileSystem.IoHandler.WriteBytes(writableFile, data); + +// Wrap for reading +using var readableFile = fileSystem.WrapForReading(path); + +// Read data +var readData = fileSystem.IoHandler.ReadBytes(readableFile); +var content = System.Text.Encoding.UTF8.GetString(readData); +Console.WriteLine(content); // Output: Hello, NIO! +``` + +### Using NioConnector with BlobStoreFileSystem + +```csharp +using NebulaStore.Afs.Nio; +using NebulaStore.Afs.Blobstore; + +// Create a NIO connector +using var connector = NioConnector.New("./my-storage", useCache: false); + +// Create a blob store file system with NIO backend +using var fileSystem = BlobStoreFileSystem.New(connector); + +// Use the file system +var path = BlobStorePath.New("data", "users", "user1.dat"); +var userData = System.Text.Encoding.UTF8.GetBytes("User data"); + +fileSystem.IoHandler.WriteData(path, userData); +var retrievedData = fileSystem.IoHandler.ReadData(path, 0, -1); +``` + +### Integration with EmbeddedStorage + +```csharp +using NebulaStore.Storage.Embedded; +using NebulaStore.Storage.EmbeddedConfiguration; + +// Configure storage to use NIO backend +var config = EmbeddedStorageConfiguration.New() + .SetStorageDirectory("./nio-storage") + .SetUseAfs(true) + .SetAfsStorageType("nio") // Use NIO backend + .Build(); + +using var storage = EmbeddedStorage.Start(config); + +// Use storage normally +var root = storage.Root(); +root.SomeProperty = "value"; +storage.StoreRoot(); +``` + +### Advanced File Operations + +```csharp +using NebulaStore.Afs.Nio; +using NebulaStore.Afs.Blobstore; + +using var fileSystem = NioFileSystem.New(); +var ioHandler = fileSystem.IoHandler; + +// Create directory +var dirPath = BlobStorePath.New("container", "subfolder"); +ioHandler.CreateDirectory(dirPath); + +// List files +var filePath = BlobStorePath.New("container"); +var files = ioHandler.ListFiles(filePath); +foreach (var file in files) +{ + Console.WriteLine($"File: {file}"); +} + +// Copy file +var sourcePath = BlobStorePath.New("container", "source.txt"); +var targetPath = BlobStorePath.New("container", "target.txt"); + +using var sourceFile = fileSystem.WrapForReading(sourcePath); +using var targetFile = fileSystem.WrapForWriting(targetPath); + +ioHandler.CopyFile(sourceFile, targetFile); + +// Move file +var newPath = BlobStorePath.New("container", "moved.txt"); +using var movableFile = fileSystem.WrapForWriting(targetPath); +using var destinationFile = fileSystem.WrapForWriting(newPath); + +ioHandler.MoveFile(movableFile, destinationFile); +``` + +### Path Resolution + +```csharp +using NebulaStore.Afs.Nio; + +var fileSystem = NioFileSystem.New(); + +// Home directory expansion +var pathElements = fileSystem.ResolvePath("~/Documents/data"); +// Returns: ["Users", "username", "Documents", "data"] (on Unix-like systems) + +// Regular path +var elements = fileSystem.ResolvePath("/var/data/files"); +// Returns: ["var", "data", "files"] +``` + +## Features + +### File Stream Management + +- **Automatic Positioning**: File streams are automatically positioned at the end for append operations +- **Stream Lifecycle**: Proper stream opening, closing, and disposal +- **Reopen Support**: Ability to reopen streams with different options +- **Validation**: Validates file access modes and options + +### Thread Safety + +All operations are thread-safe: +- File wrapper operations use internal mutex for synchronization +- I/O handler operations are stateless and thread-safe +- File system operations properly handle concurrent access + +### Error Handling + +Comprehensive error handling: +- `FileNotFoundException` for missing files +- `InvalidOperationException` for retired file wrappers +- `ArgumentException` for invalid parameters +- `IOException` for I/O errors + +## Comparison with Other AFS Backends + +| Feature | NIO | BlobStore | AWS S3 | Azure | Redis | +|---------|-----|-----------|--------|-------|-------| +| Local Files | ✓ | ✓ | ✗ | ✗ | ✗ | +| Cloud Storage | ✗ | ✗ | ✓ | ✓ | ✗ | +| In-Memory | ✗ | ✗ | ✗ | ✗ | ✓ | +| Caching | ✗ | ✓ | ✓ | ✓ | N/A | +| Performance | High | High | Medium | Medium | Very High | +| Setup Complexity | Low | Low | Medium | Medium | Low | + +## Performance Considerations + +- **Direct I/O**: Uses .NET's native file I/O for maximum performance +- **No Overhead**: Minimal abstraction overhead compared to other backends +- **Buffering**: Leverages .NET's built-in buffering mechanisms +- **Streaming**: Supports efficient streaming for large files + +## Best Practices + +1. **Dispose Resources**: Always dispose file wrappers and file systems +2. **Use Using Statements**: Leverage C#'s `using` statement for automatic disposal +3. **Handle Exceptions**: Properly handle file I/O exceptions +4. **Path Validation**: Validate paths before operations +5. **Concurrent Access**: Be aware of file locking when multiple processes access the same files + +## Limitations + +- **Local Only**: Only supports local file system access +- **No Caching**: Does not include built-in caching (use BlobStore with NioConnector for caching) +- **Platform-Specific**: Path handling may vary across platforms (Windows vs. Unix-like) + +## Migration from Java Eclipse Store + +This module is a direct port of the Eclipse Store AFS NIO module. Key differences: + +- `FileChannel` → `FileStream` +- `Path` (Java NIO) → `string` (file system path) +- `OpenOption` → `FileMode`, `FileAccess`, `FileShare` +- `ByteBuffer` → `byte[]` +- Exception types adapted to .NET conventions + +## See Also + +- [AFS Overview](../README.md) +- [BlobStore Module](../blobstore/README.md) +- [Eclipse Store NIO Documentation](https://github.com/eclipse-store/store/tree/main/afs/nio) + diff --git a/afs/nio/src/INioFileWrapper.cs b/afs/nio/src/INioFileWrapper.cs new file mode 100644 index 0000000..551d86c --- /dev/null +++ b/afs/nio/src/INioFileWrapper.cs @@ -0,0 +1,88 @@ +using System; +using System.IO; + +namespace NebulaStore.Afs.Nio; + +/// +/// Interface for NIO file wrappers that provide file channel access. +/// +public interface INioFileWrapper : INioItemWrapper, IDisposable +{ + /// + /// Gets the file stream (equivalent to Java's FileChannel). + /// + FileStream? FileStream { get; } + + /// + /// Gets the user context associated with this file. + /// + object? User { get; } + + /// + /// Retires this file wrapper, preventing further operations. + /// + /// True if the file was retired, false if already retired + bool Retire(); + + /// + /// Gets a value indicating whether this file wrapper is retired. + /// + bool IsRetired { get; } + + /// + /// Gets a value indicating whether the file stream is open. + /// + bool IsStreamOpen { get; } + + /// + /// Checks if the file stream is open and validates the file is not retired. + /// + /// True if the stream is open + bool CheckStreamOpen(); + + /// + /// Ensures the file stream is open, opening it if necessary. + /// + /// The open file stream + FileStream EnsureOpenStream(); + + /// + /// Ensures the file stream is open with specific options. + /// + /// The file mode + /// The file access + /// The file share mode + /// The open file stream + FileStream EnsureOpenStream(FileMode mode, FileAccess access, FileShare share); + + /// + /// Opens the file stream. + /// + /// True if the stream was opened, false if already open + bool OpenStream(); + + /// + /// Opens the file stream with specific options. + /// + /// The file mode + /// The file access + /// The file share mode + /// True if the stream was opened, false if already open + bool OpenStream(FileMode mode, FileAccess access, FileShare share); + + /// + /// Reopens the file stream with specific options. + /// + /// The file mode + /// The file access + /// The file share mode + /// True if the stream was reopened + bool ReopenStream(FileMode mode, FileAccess access, FileShare share); + + /// + /// Closes the file stream. + /// + /// True if the stream was closed, false if already closed + bool CloseStream(); +} + diff --git a/afs/nio/src/INioItemWrapper.cs b/afs/nio/src/INioItemWrapper.cs new file mode 100644 index 0000000..5bb153b --- /dev/null +++ b/afs/nio/src/INioItemWrapper.cs @@ -0,0 +1,20 @@ +using NebulaStore.Afs.Blobstore; + +namespace NebulaStore.Afs.Nio; + +/// +/// Interface for NIO item wrappers that provide access to file system paths. +/// +public interface INioItemWrapper +{ + /// + /// Gets the file system path for this item. + /// + string Path { get; } + + /// + /// Gets the blob store path representation. + /// + BlobStorePath BlobPath { get; } +} + diff --git a/afs/nio/src/NioConnector.cs b/afs/nio/src/NioConnector.cs new file mode 100644 index 0000000..bdfe01a --- /dev/null +++ b/afs/nio/src/NioConnector.cs @@ -0,0 +1,500 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using NebulaStore.Afs.Blobstore; + +namespace NebulaStore.Afs.Nio; + +/// +/// NIO connector that implements blob store connector interface using local file system. +/// +public class NioConnector : IBlobStoreConnector +{ + private readonly string _baseDirectory; + private readonly INioIoHandler _ioHandler; + private readonly bool _useCache; + private bool _disposed; + + /// + /// Creates a new NIO connector. + /// + /// The base directory for storage + /// Whether to use caching + /// A new NIO connector instance + public static NioConnector New(string baseDirectory, bool useCache = false) + { + return new NioConnector(baseDirectory, useCache); + } + + /// + /// Initializes a new instance of the class. + /// + /// The base directory for storage + /// Whether to use caching + public NioConnector(string baseDirectory, bool useCache = false) + { + if (string.IsNullOrWhiteSpace(baseDirectory)) + { + throw new ArgumentException("Base directory cannot be null or empty", nameof(baseDirectory)); + } + + _baseDirectory = Path.GetFullPath(baseDirectory); + _useCache = useCache; + _ioHandler = NioIoHandler.New(); + + // Ensure base directory exists + Directory.CreateDirectory(_baseDirectory); + } + + /// + /// Gets the full file system path for a blob store path. + /// + /// The blob store path + /// The full file system path + private string GetFullPath(BlobStorePath path) + { + var relativePath = _ioHandler.ToPath(path); + return Path.Combine(_baseDirectory, relativePath); + } + + /// + public bool FileExists(BlobStorePath path) + { + EnsureNotDisposed(); + var fullPath = GetFullPath(path); + return File.Exists(fullPath); + } + + /// + public bool DirectoryExists(BlobStorePath path) + { + EnsureNotDisposed(); + var fullPath = GetFullPath(path); + return Directory.Exists(fullPath); + } + + /// + public bool CreateDirectory(BlobStorePath path) + { + EnsureNotDisposed(); + var fullPath = GetFullPath(path); + + if (Directory.Exists(fullPath)) + { + return false; + } + + Directory.CreateDirectory(fullPath); + return true; + } + + /// + public bool CreateFile(BlobStorePath path) + { + EnsureNotDisposed(); + var fullPath = GetFullPath(path); + + if (File.Exists(fullPath)) + { + return false; + } + + // Ensure parent directory exists + var parentDir = Path.GetDirectoryName(fullPath); + if (!string.IsNullOrEmpty(parentDir)) + { + Directory.CreateDirectory(parentDir); + } + + // Create empty file + using var fs = File.Create(fullPath); + return true; + } + + /// + public bool DeleteFile(BlobStorePath path) + { + EnsureNotDisposed(); + var fullPath = GetFullPath(path); + + if (!File.Exists(fullPath)) + { + return false; + } + + File.Delete(fullPath); + return true; + } + + /// + public byte[] ReadData(BlobStorePath path, long offset, long length) + { + EnsureNotDisposed(); + var fullPath = GetFullPath(path); + + if (!File.Exists(fullPath)) + { + throw new FileNotFoundException($"File not found: {path.FullQualifiedName}", fullPath); + } + + using var fs = new FileStream(fullPath, FileMode.Open, FileAccess.Read, FileShare.Read); + + if (offset < 0) + { + offset = 0; + } + + if (length < 0) + { + length = fs.Length - offset; + } + + if (offset >= fs.Length) + { + return Array.Empty(); + } + + var actualLength = Math.Min(length, fs.Length - offset); + var buffer = new byte[actualLength]; + + fs.Position = offset; + var bytesRead = fs.Read(buffer, 0, (int)actualLength); + + if (bytesRead < actualLength) + { + Array.Resize(ref buffer, bytesRead); + } + + return buffer; + } + + /// + public long ReadData(BlobStorePath path, byte[] targetBuffer, long offset, long length) + { + EnsureNotDisposed(); + var fullPath = GetFullPath(path); + + if (!File.Exists(fullPath)) + { + throw new FileNotFoundException($"File not found: {path.FullQualifiedName}", fullPath); + } + + if (targetBuffer == null) + { + throw new ArgumentNullException(nameof(targetBuffer)); + } + + using var fs = new FileStream(fullPath, FileMode.Open, FileAccess.Read, FileShare.Read); + + if (offset < 0) + { + offset = 0; + } + + if (length < 0) + { + length = fs.Length - offset; + } + + if (offset >= fs.Length) + { + return 0; + } + + var actualLength = Math.Min(length, fs.Length - offset); + actualLength = Math.Min(actualLength, targetBuffer.Length); + + fs.Position = offset; + return fs.Read(targetBuffer, 0, (int)actualLength); + } + + /// + public long WriteData(BlobStorePath path, IEnumerable dataChunks) + { + EnsureNotDisposed(); + var fullPath = GetFullPath(path); + + // Ensure parent directory exists + var parentDir = Path.GetDirectoryName(fullPath); + if (!string.IsNullOrEmpty(parentDir)) + { + Directory.CreateDirectory(parentDir); + } + + long totalBytesWritten = 0; + using var fs = new FileStream(fullPath, FileMode.Create, FileAccess.Write, FileShare.None); + + foreach (var chunk in dataChunks) + { + if (chunk != null && chunk.Length > 0) + { + fs.Write(chunk, 0, chunk.Length); + totalBytesWritten += chunk.Length; + } + } + + fs.Flush(); + return totalBytesWritten; + } + + /// + /// Writes data to a file (convenience method). + /// + /// The file path + /// The data to write + public void WriteData(BlobStorePath path, byte[] data) + { + WriteData(path, new[] { data }); + } + + /// + public long GetFileSize(BlobStorePath path) + { + EnsureNotDisposed(); + var fullPath = GetFullPath(path); + + if (!File.Exists(fullPath)) + { + return 0; + } + + var fileInfo = new FileInfo(fullPath); + return fileInfo.Length; + } + + /// + public IEnumerable ListFiles(BlobStorePath path) + { + EnsureNotDisposed(); + var fullPath = GetFullPath(path); + + if (!Directory.Exists(fullPath)) + { + return Enumerable.Empty(); + } + + return Directory.EnumerateFiles(fullPath) + .Select(Path.GetFileName) + .Where(name => !string.IsNullOrEmpty(name))!; + } + + /// + public IEnumerable ListDirectories(BlobStorePath path) + { + EnsureNotDisposed(); + var fullPath = GetFullPath(path); + + if (!Directory.Exists(fullPath)) + { + return Enumerable.Empty(); + } + + return Directory.EnumerateDirectories(fullPath) + .Select(Path.GetFileName) + .Where(name => !string.IsNullOrEmpty(name))!; + } + + /// + public void MoveFile(BlobStorePath sourcePath, BlobStorePath targetPath) + { + EnsureNotDisposed(); + var sourceFullPath = GetFullPath(sourcePath); + var targetFullPath = GetFullPath(targetPath); + + if (!File.Exists(sourceFullPath)) + { + throw new FileNotFoundException($"Source file not found: {sourcePath.FullQualifiedName}", sourceFullPath); + } + + // Ensure target directory exists + var targetDir = Path.GetDirectoryName(targetFullPath); + if (!string.IsNullOrEmpty(targetDir)) + { + Directory.CreateDirectory(targetDir); + } + + File.Move(sourceFullPath, targetFullPath, overwrite: true); + } + + /// + public long CopyFile(BlobStorePath sourcePath, BlobStorePath targetPath, long offset, long length) + { + EnsureNotDisposed(); + var sourceFullPath = GetFullPath(sourcePath); + var targetFullPath = GetFullPath(targetPath); + + if (!File.Exists(sourceFullPath)) + { + throw new FileNotFoundException($"Source file not found: {sourcePath.FullQualifiedName}", sourceFullPath); + } + + // Ensure target directory exists + var targetDir = Path.GetDirectoryName(targetFullPath); + if (!string.IsNullOrEmpty(targetDir)) + { + Directory.CreateDirectory(targetDir); + } + + using var sourceStream = new FileStream(sourceFullPath, FileMode.Open, FileAccess.Read, FileShare.Read); + using var targetStream = new FileStream(targetFullPath, FileMode.Create, FileAccess.Write, FileShare.None); + + if (offset < 0) + { + offset = 0; + } + + if (length < 0) + { + length = sourceStream.Length - offset; + } + + if (offset >= sourceStream.Length) + { + return 0; + } + + var actualLength = Math.Min(length, sourceStream.Length - offset); + sourceStream.Position = offset; + + var buffer = new byte[81920]; // 80KB buffer + long totalCopied = 0; + long remaining = actualLength; + + while (remaining > 0) + { + var toRead = (int)Math.Min(buffer.Length, remaining); + var bytesRead = sourceStream.Read(buffer, 0, toRead); + + if (bytesRead == 0) + { + break; + } + + targetStream.Write(buffer, 0, bytesRead); + totalCopied += bytesRead; + remaining -= bytesRead; + } + + targetStream.Flush(); + return totalCopied; + } + + /// + /// Copies a file (convenience method that copies the entire file). + /// + /// The source file path + /// The target file path + public void CopyFile(BlobStorePath sourcePath, BlobStorePath targetPath) + { + CopyFile(sourcePath, targetPath, 0, -1); + } + + /// + public void VisitChildren(BlobStorePath path, IBlobStorePathVisitor visitor) + { + EnsureNotDisposed(); + + if (visitor == null) + { + throw new ArgumentNullException(nameof(visitor)); + } + + var fullPath = GetFullPath(path); + + if (!Directory.Exists(fullPath)) + { + return; + } + + // Visit directories + foreach (var dir in Directory.EnumerateDirectories(fullPath)) + { + var dirName = Path.GetFileName(dir); + if (!string.IsNullOrEmpty(dirName)) + { + visitor.VisitDirectory(path, dirName); + } + } + + // Visit files + foreach (var file in Directory.EnumerateFiles(fullPath)) + { + var fileName = Path.GetFileName(file); + if (!string.IsNullOrEmpty(fileName)) + { + visitor.VisitFile(path, fileName); + } + } + } + + /// + public bool IsEmpty(BlobStorePath path) + { + EnsureNotDisposed(); + var fullPath = GetFullPath(path); + + if (!Directory.Exists(fullPath)) + { + return true; + } + + return !Directory.EnumerateFileSystemEntries(fullPath).Any(); + } + + /// + public void TruncateFile(BlobStorePath path, long newLength) + { + EnsureNotDisposed(); + var fullPath = GetFullPath(path); + + if (!File.Exists(fullPath)) + { + throw new FileNotFoundException($"File not found: {path.FullQualifiedName}", fullPath); + } + + if (newLength < 0) + { + throw new ArgumentOutOfRangeException(nameof(newLength), "New length must be non-negative"); + } + + using var fs = new FileStream(fullPath, FileMode.Open, FileAccess.Write, FileShare.None); + fs.SetLength(newLength); + fs.Flush(); + } + + private void EnsureNotDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(GetType().Name); + } + } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Releases resources used by this connector. + /// + /// True if disposing managed resources + protected virtual void Dispose(bool disposing) + { + if (_disposed) + { + return; + } + + if (disposing) + { + _ioHandler?.Dispose(); + } + + _disposed = true; + } +} + diff --git a/afs/nio/src/NioFileSystem.cs b/afs/nio/src/NioFileSystem.cs new file mode 100644 index 0000000..84f91fe --- /dev/null +++ b/afs/nio/src/NioFileSystem.cs @@ -0,0 +1,250 @@ +using System; +using System.IO; +using NebulaStore.Afs.Blobstore; + +namespace NebulaStore.Afs.Nio; + +/// +/// Interface for NIO file system. +/// +public interface INioFileSystem : IDisposable +{ + /// + /// Gets the I/O handler for this file system. + /// + INioIoHandler IoHandler { get; } + + /// + /// Gets the default protocol for this file system. + /// + string DefaultProtocol { get; } + + /// + /// Wraps a blob store path for reading. + /// + /// The blob store path + /// The user context + /// A readable file wrapper + INioReadableFile WrapForReading(BlobStorePath path, object? user = null); + + /// + /// Wraps a blob store path for writing. + /// + /// The blob store path + /// The user context + /// A writable file wrapper + INioWritableFile WrapForWriting(BlobStorePath path, object? user = null); + + /// + /// Converts a readable file to a writable file. + /// + /// The readable file + /// A writable file wrapper + INioWritableFile ConvertToWriting(INioReadableFile file); + + /// + /// Converts a writable file to a readable file. + /// + /// The writable file + /// A readable file wrapper + INioReadableFile ConvertToReading(INioWritableFile file); + + /// + /// Resolves a path string to path elements. + /// + /// The full path string + /// The path elements + string[] ResolvePath(string fullPath); +} + +/// +/// Default implementation of NIO file system. +/// +public class NioFileSystem : INioFileSystem +{ + private readonly string _defaultProtocol; + private readonly INioIoHandler _ioHandler; + private bool _disposed; + + /// + /// Gets the default protocol string for file URIs. + /// + public const string DefaultFileProtocol = "file:///"; + + /// + /// Creates a new NIO file system with default settings. + /// + /// A new file system instance + public static NioFileSystem New() + { + return New(DefaultFileProtocol); + } + + /// + /// Creates a new NIO file system with a custom protocol. + /// + /// The default protocol + /// A new file system instance + public static NioFileSystem New(string defaultProtocol) + { + return New(defaultProtocol, NioIoHandler.New()); + } + + /// + /// Creates a new NIO file system with a custom I/O handler. + /// + /// The I/O handler + /// A new file system instance + public static NioFileSystem New(INioIoHandler ioHandler) + { + return New(DefaultFileProtocol, ioHandler); + } + + /// + /// Creates a new NIO file system with custom settings. + /// + /// The default protocol + /// The I/O handler + /// A new file system instance + public static NioFileSystem New(string defaultProtocol, INioIoHandler ioHandler) + { + return new NioFileSystem( + defaultProtocol ?? throw new ArgumentNullException(nameof(defaultProtocol)), + ioHandler ?? throw new ArgumentNullException(nameof(ioHandler))); + } + + /// + /// Initializes a new instance of the class. + /// + /// The default protocol + /// The I/O handler + protected NioFileSystem(string defaultProtocol, INioIoHandler ioHandler) + { + _defaultProtocol = defaultProtocol ?? throw new ArgumentNullException(nameof(defaultProtocol)); + _ioHandler = ioHandler ?? throw new ArgumentNullException(nameof(ioHandler)); + } + + /// + public string DefaultProtocol => _defaultProtocol; + + /// + public INioIoHandler IoHandler => _ioHandler; + + /// + public string[] ResolvePath(string fullPath) + { + if (string.IsNullOrEmpty(fullPath)) + { + return Array.Empty(); + } + + // Handle home directory expansion + var resolved = fullPath; + if (fullPath.StartsWith("~/") || fullPath.StartsWith("~\\")) + { + var homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + resolved = System.IO.Path.Combine(homeDir, fullPath.Substring(2)); + } + + // Split path by directory separator + var separators = new[] { System.IO.Path.DirectorySeparatorChar, System.IO.Path.AltDirectorySeparatorChar }; + return resolved.Split(separators, StringSplitOptions.RemoveEmptyEntries); + } + + /// + public INioReadableFile WrapForReading(BlobStorePath path, object? user = null) + { + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + var fsPath = _ioHandler.ToPath(path); + return NioReadableFile.New(path, user, fsPath); + } + + /// + public INioWritableFile WrapForWriting(BlobStorePath path, object? user = null) + { + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + var fsPath = _ioHandler.ToPath(path); + return NioWritableFile.New(path, user, fsPath); + } + + /// + public INioWritableFile ConvertToWriting(INioReadableFile file) + { + if (file == null) + { + throw new ArgumentNullException(nameof(file)); + } + + // Close the readable file's stream + var actuallyClosedStream = file.CloseStream(); + + // Create a new writable file + var writableFile = NioWritableFile.New(file.BlobPath, file.User, file.Path, null); + + // Replicate opened stream if necessary + if (actuallyClosedStream) + { + writableFile.EnsureOpenStream(); + } + + return writableFile; + } + + /// + public INioReadableFile ConvertToReading(INioWritableFile file) + { + if (file == null) + { + throw new ArgumentNullException(nameof(file)); + } + + // Close the writable file's stream + var actuallyClosedStream = file.CloseStream(); + + // Create a new readable file + var readableFile = NioReadableFile.New(file.BlobPath, file.User, file.Path, null); + + // Replicate opened stream if necessary + if (actuallyClosedStream) + { + readableFile.EnsureOpenStream(); + } + + return readableFile; + } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Releases resources used by this file system. + /// + /// True if disposing managed resources + protected virtual void Dispose(bool disposing) + { + if (_disposed) + { + return; + } + + if (disposing) + { + _ioHandler?.Dispose(); + } + + _disposed = true; + } +} + diff --git a/afs/nio/src/NioFileWrapperBase.cs b/afs/nio/src/NioFileWrapperBase.cs new file mode 100644 index 0000000..89d03ac --- /dev/null +++ b/afs/nio/src/NioFileWrapperBase.cs @@ -0,0 +1,325 @@ +using System; +using System.IO; +using NebulaStore.Afs.Blobstore; + +namespace NebulaStore.Afs.Nio; + +/// +/// Base class for NIO file wrappers. +/// +/// The type of user context +public abstract class NioFileWrapperBase : INioFileWrapper +{ + private readonly object _mutex = new(); + private string? _path; + private FileStream? _fileStream; + private readonly BlobStorePath _blobPath; + private readonly TUser? _user; + private bool _disposed; + + /// + /// Initializes a new instance of the class. + /// + /// The blob store path + /// The user context + /// The file system path + /// The optional file stream + protected NioFileWrapperBase( + BlobStorePath blobPath, + TUser? user, + string path, + FileStream? fileStream = null) + { + _blobPath = blobPath ?? throw new ArgumentNullException(nameof(blobPath)); + _user = user; + _path = path ?? throw new ArgumentNullException(nameof(path)); + _fileStream = fileStream; + EnsurePositionAtFileEnd(); + } + + /// + /// Gets the mutex for thread synchronization. + /// + protected object Mutex => _mutex; + + /// + public string Path + { + get + { + lock (_mutex) + { + ValidateIsNotRetired(); + return _path!; + } + } + } + + /// + public BlobStorePath BlobPath => _blobPath; + + /// + public FileStream? FileStream + { + get + { + lock (_mutex) + { + ValidateIsNotRetired(); + return _fileStream; + } + } + } + + /// + public object? User => _user; + + /// + public bool IsRetired + { + get + { + lock (_mutex) + { + return _path == null; + } + } + } + + /// + public bool Retire() + { + lock (_mutex) + { + if (_path == null) + { + return false; + } + + _path = null; + return true; + } + } + + /// + /// Validates that this file wrapper is not retired. + /// + /// Thrown if the file is retired + protected void ValidateIsNotRetired() + { + if (!IsRetired) + { + return; + } + + throw new InvalidOperationException( + $"File is retired: {GetType().Name}(\"{_blobPath.FullQualifiedName}\")."); + } + + /// + public bool IsStreamOpen + { + get + { + lock (_mutex) + { + return _fileStream != null && _fileStream.CanRead; + } + } + } + + /// + public bool CheckStreamOpen() + { + lock (_mutex) + { + ValidateIsNotRetired(); + return IsStreamOpen; + } + } + + /// + public FileStream EnsureOpenStream() + { + lock (_mutex) + { + ValidateIsNotRetired(); + OpenStream(GetDefaultFileMode(), GetDefaultFileAccess(), GetDefaultFileShare()); + return _fileStream!; + } + } + + /// + public FileStream EnsureOpenStream(FileMode mode, FileAccess access, FileShare share) + { + lock (_mutex) + { + ValidateIsNotRetired(); + OpenStream(mode, access, share); + return _fileStream!; + } + } + + /// + public bool OpenStream() + { + lock (_mutex) + { + return OpenStream(GetDefaultFileMode(), GetDefaultFileAccess(), GetDefaultFileShare()); + } + } + + /// + public bool OpenStream(FileMode mode, FileAccess access, FileShare share) + { + lock (_mutex) + { + if (CheckStreamOpen()) + { + return false; + } + + ValidateOpenOptions(mode, access, share); + + try + { + var fileStream = new FileStream(_path!, mode, access, share); + InternalSetFileStream(fileStream); + } + catch (Exception ex) + { + throw new IOException($"Failed to open file stream: {_path}", ex); + } + + return true; + } + } + + /// + public bool ReopenStream(FileMode mode, FileAccess access, FileShare share) + { + lock (_mutex) + { + CloseStream(); + return OpenStream(mode, access, share); + } + } + + /// + public bool CloseStream() + { + lock (_mutex) + { + if (!IsStreamOpen) + { + return false; + } + + EnsureClearedFileStreamField(); + return true; + } + } + + /// + /// Gets the default file mode for this file wrapper. + /// + /// The default file mode + protected abstract FileMode GetDefaultFileMode(); + + /// + /// Gets the default file access for this file wrapper. + /// + /// The default file access + protected abstract FileAccess GetDefaultFileAccess(); + + /// + /// Gets the default file share mode for this file wrapper. + /// + /// The default file share mode + protected abstract FileShare GetDefaultFileShare(); + + /// + /// Validates the open options for this file wrapper. + /// + /// The file mode + /// The file access + /// The file share mode + protected abstract void ValidateOpenOptions(FileMode mode, FileAccess access, FileShare share); + + private void InternalSetFileStream(FileStream fileStream) + { + _fileStream = fileStream; + EnsurePositionAtFileEnd(); + } + + private void EnsurePositionAtFileEnd() + { + if (_fileStream == null) + { + return; + } + + try + { + var fileSize = _fileStream.Length; + if (_fileStream.Position != fileSize) + { + _fileStream.Position = fileSize; + } + } + catch (Exception ex) + { + EnsureClearedFileStreamField(ex); + throw new IOException("Failed to position stream at file end", ex); + } + } + + private void EnsureClearedFileStreamField(Exception? cause = null) + { + var fs = _fileStream; + _fileStream = null; + + try + { + fs?.Dispose(); + } + catch (Exception ex) + { + if (cause != null) + { + throw new AggregateException(cause, ex); + } + throw; + } + } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Releases resources used by this file wrapper. + /// + /// True if disposing managed resources + protected virtual void Dispose(bool disposing) + { + if (_disposed) + { + return; + } + + if (disposing) + { + lock (_mutex) + { + EnsureClearedFileStreamField(); + } + } + + _disposed = true; + } +} + diff --git a/afs/nio/src/NioIoHandler.cs b/afs/nio/src/NioIoHandler.cs new file mode 100644 index 0000000..fce678d --- /dev/null +++ b/afs/nio/src/NioIoHandler.cs @@ -0,0 +1,460 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using NebulaStore.Afs.Blobstore; + +namespace NebulaStore.Afs.Nio; + +/// +/// Interface for NIO I/O handler. +/// +public interface INioIoHandler : IDisposable +{ + /// + /// Casts a readable file to NIO readable file. + /// + /// The readable file + /// The NIO readable file + INioReadableFile CastReadableFile(IBlobStoreReadableFile file); + + /// + /// Casts a writable file to NIO writable file. + /// + /// The writable file + /// The NIO writable file + INioWritableFile CastWritableFile(IBlobStoreWritableFile file); + + /// + /// Converts a blob store path to a file system path. + /// + /// The blob store path + /// The file system path + string ToPath(BlobStorePath path); + + /// + /// Converts path elements to a file system path. + /// + /// The path elements + /// The file system path + string ToPath(params string[] pathElements); + + /// + /// Checks if a file exists. + /// + /// The file path + /// True if the file exists + bool FileExists(BlobStorePath path); + + /// + /// Checks if a directory exists. + /// + /// The directory path + /// True if the directory exists + bool DirectoryExists(BlobStorePath path); + + /// + /// Gets the size of a file. + /// + /// The file path + /// The file size in bytes + long GetFileSize(BlobStorePath path); + + /// + /// Lists items in a directory. + /// + /// The directory path + /// The list of item names + IEnumerable ListItems(BlobStorePath path); + + /// + /// Lists directories in a directory. + /// + /// The directory path + /// The list of directory names + IEnumerable ListDirectories(BlobStorePath path); + + /// + /// Lists files in a directory. + /// + /// The directory path + /// The list of file names + IEnumerable ListFiles(BlobStorePath path); + + /// + /// Creates a directory. + /// + /// The directory path + void CreateDirectory(BlobStorePath path); + + /// + /// Creates a file. + /// + /// The file path + void CreateFile(BlobStorePath path); + + /// + /// Deletes a file. + /// + /// The file path + /// True if the file was deleted + bool DeleteFile(BlobStorePath path); + + /// + /// Reads bytes from a file. + /// + /// The readable file + /// The read bytes + byte[] ReadBytes(INioReadableFile file); + + /// + /// Reads bytes from a file at a specific position. + /// + /// The readable file + /// The position to read from + /// The number of bytes to read + /// The read bytes + byte[] ReadBytes(INioReadableFile file, long position, long length); + + /// + /// Writes bytes to a file. + /// + /// The writable file + /// The data to write + /// The number of bytes written + long WriteBytes(INioWritableFile file, byte[] data); + + /// + /// Copies a file. + /// + /// The source file + /// The target file + /// The number of bytes copied + long CopyFile(INioReadableFile source, INioWritableFile target); + + /// + /// Moves a file. + /// + /// The source file + /// The target file + void MoveFile(INioWritableFile source, INioWritableFile target); + + /// + /// Truncates a file to a specific size. + /// + /// The writable file + /// The new size + void TruncateFile(INioWritableFile file, long newSize); +} + +/// +/// Default implementation of NIO I/O handler. +/// +public class NioIoHandler : INioIoHandler +{ + private readonly INioPathResolver _pathResolver; + private bool _disposed; + + /// + /// Creates a new NIO I/O handler. + /// + /// A new I/O handler instance + public static NioIoHandler New() + { + return new NioIoHandler(NioPathResolver.New()); + } + + /// + /// Creates a new NIO I/O handler with a custom path resolver. + /// + /// The path resolver + /// A new I/O handler instance + public static NioIoHandler New(INioPathResolver pathResolver) + { + return new NioIoHandler(pathResolver ?? throw new ArgumentNullException(nameof(pathResolver))); + } + + /// + /// Initializes a new instance of the class. + /// + /// The path resolver + protected NioIoHandler(INioPathResolver pathResolver) + { + _pathResolver = pathResolver ?? throw new ArgumentNullException(nameof(pathResolver)); + } + + /// + public INioReadableFile CastReadableFile(IBlobStoreReadableFile file) + { + if (file is INioReadableFile nioFile) + { + return nioFile; + } + + throw new ArgumentException( + $"File is not a NIO readable file: {file?.GetType().Name ?? "null"}", + nameof(file)); + } + + /// + public INioWritableFile CastWritableFile(IBlobStoreWritableFile file) + { + if (file is INioWritableFile nioFile) + { + return nioFile; + } + + throw new ArgumentException( + $"File is not a NIO writable file: {file?.GetType().Name ?? "null"}", + nameof(file)); + } + + /// + public string ToPath(BlobStorePath path) + { + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + return _pathResolver.ResolvePath(path.PathElements); + } + + /// + public string ToPath(params string[] pathElements) + { + return _pathResolver.ResolvePath(pathElements); + } + + /// + public bool FileExists(BlobStorePath path) + { + var fsPath = ToPath(path); + return File.Exists(fsPath); + } + + /// + public bool DirectoryExists(BlobStorePath path) + { + var fsPath = ToPath(path); + return Directory.Exists(fsPath); + } + + /// + public long GetFileSize(BlobStorePath path) + { + var fsPath = ToPath(path); + var fileInfo = new FileInfo(fsPath); + return fileInfo.Exists ? fileInfo.Length : 0; + } + + /// + public IEnumerable ListItems(BlobStorePath path) + { + var fsPath = ToPath(path); + if (!Directory.Exists(fsPath)) + { + return Enumerable.Empty(); + } + + return Directory.EnumerateFileSystemEntries(fsPath) + .Select(System.IO.Path.GetFileName) + .Where(name => !string.IsNullOrEmpty(name))!; + } + + /// + public IEnumerable ListDirectories(BlobStorePath path) + { + var fsPath = ToPath(path); + if (!Directory.Exists(fsPath)) + { + return Enumerable.Empty(); + } + + return Directory.EnumerateDirectories(fsPath) + .Select(System.IO.Path.GetFileName) + .Where(name => !string.IsNullOrEmpty(name))!; + } + + /// + public IEnumerable ListFiles(BlobStorePath path) + { + var fsPath = ToPath(path); + if (!Directory.Exists(fsPath)) + { + return Enumerable.Empty(); + } + + return Directory.EnumerateFiles(fsPath) + .Select(System.IO.Path.GetFileName) + .Where(name => !string.IsNullOrEmpty(name))!; + } + + /// + public void CreateDirectory(BlobStorePath path) + { + var fsPath = ToPath(path); + Directory.CreateDirectory(fsPath); + } + + /// + public void CreateFile(BlobStorePath path) + { + var fsPath = ToPath(path); + + // Ensure parent directory exists + var parentDir = System.IO.Path.GetDirectoryName(fsPath); + if (!string.IsNullOrEmpty(parentDir)) + { + Directory.CreateDirectory(parentDir); + } + + // Create empty file + using var fs = File.Create(fsPath); + } + + /// + public bool DeleteFile(BlobStorePath path) + { + var fsPath = ToPath(path); + if (!File.Exists(fsPath)) + { + return false; + } + + File.Delete(fsPath); + return true; + } + + /// + public byte[] ReadBytes(INioReadableFile file) + { + var stream = file.EnsureOpenStream(); + using var ms = new MemoryStream(); + stream.CopyTo(ms); + return ms.ToArray(); + } + + /// + public byte[] ReadBytes(INioReadableFile file, long position, long length) + { + var stream = file.EnsureOpenStream(); + + if (position < 0) + { + throw new ArgumentOutOfRangeException(nameof(position), "Position must be non-negative"); + } + + if (length < 0) + { + length = stream.Length - position; + } + + if (length == 0) + { + return Array.Empty(); + } + + var buffer = new byte[length]; + stream.Position = position; + var bytesRead = stream.Read(buffer, 0, (int)length); + + if (bytesRead < length) + { + Array.Resize(ref buffer, bytesRead); + } + + return buffer; + } + + /// + public long WriteBytes(INioWritableFile file, byte[] data) + { + if (data == null || data.Length == 0) + { + return 0; + } + + var stream = file.EnsureOpenStream(); + stream.Write(data, 0, data.Length); + stream.Flush(); + return data.Length; + } + + /// + public long CopyFile(INioReadableFile source, INioWritableFile target) + { + var sourceStream = source.EnsureOpenStream(); + var targetStream = target.EnsureOpenStream(); + + var initialPosition = sourceStream.Position; + sourceStream.Position = 0; + + sourceStream.CopyTo(targetStream); + targetStream.Flush(); + + var bytesCopied = sourceStream.Length; + sourceStream.Position = initialPosition; + + return bytesCopied; + } + + /// + public void MoveFile(INioWritableFile source, INioWritableFile target) + { + // Close streams before moving + source.CloseStream(); + target.CloseStream(); + + var sourcePath = source.Path; + var targetPath = target.Path; + + // Ensure target directory exists + var targetDir = System.IO.Path.GetDirectoryName(targetPath); + if (!string.IsNullOrEmpty(targetDir)) + { + Directory.CreateDirectory(targetDir); + } + + File.Move(sourcePath, targetPath, overwrite: true); + } + + /// + public void TruncateFile(INioWritableFile file, long newSize) + { + if (newSize < 0) + { + throw new ArgumentOutOfRangeException(nameof(newSize), "New size must be non-negative"); + } + + var stream = file.EnsureOpenStream(); + stream.SetLength(newSize); + stream.Flush(); + } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Releases resources used by this I/O handler. + /// + /// True if disposing managed resources + protected virtual void Dispose(bool disposing) + { + if (_disposed) + { + return; + } + + if (disposing) + { + // No resources to dispose currently + } + + _disposed = true; + } +} diff --git a/afs/nio/src/NioPathResolver.cs b/afs/nio/src/NioPathResolver.cs new file mode 100644 index 0000000..b51ed68 --- /dev/null +++ b/afs/nio/src/NioPathResolver.cs @@ -0,0 +1,58 @@ +using System; +using System.IO; +using System.Linq; + +namespace NebulaStore.Afs.Nio; + +/// +/// Resolves path elements to file system paths. +/// +public interface INioPathResolver +{ + /// + /// Resolves path elements to a file system path. + /// + /// The path elements to resolve + /// The resolved file system path + string ResolvePath(params string[] pathElements); +} + +/// +/// Default implementation of path resolver using the local file system. +/// +public class NioPathResolver : INioPathResolver +{ + /// + /// Creates a new path resolver using the default file system. + /// + /// A new path resolver instance + public static NioPathResolver New() + { + return new NioPathResolver(); + } + + /// + /// Resolves path elements to a file system path. + /// + /// The path elements to resolve + /// The resolved file system path + public string ResolvePath(params string[] pathElements) + { + if (pathElements == null || pathElements.Length == 0) + { + return string.Empty; + } + + // Filter out null or empty elements + var validElements = pathElements.Where(e => !string.IsNullOrEmpty(e)).ToArray(); + + if (validElements.Length == 0) + { + return string.Empty; + } + + // Combine path elements using the platform-specific separator + return System.IO.Path.Combine(validElements); + } +} + diff --git a/afs/nio/src/NioReadableFile.cs b/afs/nio/src/NioReadableFile.cs new file mode 100644 index 0000000..836d8d3 --- /dev/null +++ b/afs/nio/src/NioReadableFile.cs @@ -0,0 +1,106 @@ +using System; +using System.IO; +using NebulaStore.Afs.Blobstore; + +namespace NebulaStore.Afs.Nio; + +/// +/// Interface for NIO readable files. +/// +public interface INioReadableFile : INioFileWrapper +{ +} + +/// +/// NIO readable file implementation. +/// +/// The type of user context +public class NioReadableFile : NioFileWrapperBase, INioReadableFile +{ + /// + /// Creates a new readable file wrapper. + /// + /// The blob store path + /// The user context + /// The file system path + /// A new readable file instance + public static NioReadableFile New( + BlobStorePath blobPath, + TUser? user, + string path) + { + return New(blobPath, user, path, null); + } + + /// + /// Creates a new readable file wrapper with an existing file stream. + /// + /// The blob store path + /// The user context + /// The file system path + /// The optional file stream + /// A new readable file instance + public static NioReadableFile New( + BlobStorePath blobPath, + TUser? user, + string path, + FileStream? fileStream) + { + return new NioReadableFile(blobPath, user, path, fileStream); + } + + /// + /// Initializes a new instance of the class. + /// + /// The blob store path + /// The user context + /// The file system path + /// The optional file stream + protected NioReadableFile( + BlobStorePath blobPath, + TUser? user, + string path, + FileStream? fileStream) + : base(blobPath, user, path, fileStream) + { + } + + /// + protected override FileMode GetDefaultFileMode() + { + return FileMode.Open; + } + + /// + protected override FileAccess GetDefaultFileAccess() + { + return FileAccess.Read; + } + + /// + protected override FileShare GetDefaultFileShare() + { + return FileShare.Read; + } + + /// + protected override void ValidateOpenOptions(FileMode mode, FileAccess access, FileShare share) + { + // Readable files should not allow write access + if (access.HasFlag(FileAccess.Write)) + { + throw new ArgumentException( + $"Invalid FileAccess for {GetType().Name}: Write access is not allowed for readable files.", + nameof(access)); + } + + // Readable files should not allow create modes + if (mode == FileMode.Create || mode == FileMode.CreateNew || mode == FileMode.Append) + { + throw new ArgumentException( + $"Invalid FileMode for {GetType().Name}: {mode} is not allowed for readable files.", + nameof(mode)); + } + } +} + diff --git a/afs/nio/src/NioWritableFile.cs b/afs/nio/src/NioWritableFile.cs new file mode 100644 index 0000000..79494e7 --- /dev/null +++ b/afs/nio/src/NioWritableFile.cs @@ -0,0 +1,93 @@ +using System; +using System.IO; +using NebulaStore.Afs.Blobstore; + +namespace NebulaStore.Afs.Nio; + +/// +/// Interface for NIO writable files. +/// +public interface INioWritableFile : INioReadableFile +{ +} + +/// +/// NIO writable file implementation. +/// +/// The type of user context +public class NioWritableFile : NioReadableFile, INioWritableFile +{ + /// + /// Creates a new writable file wrapper. + /// + /// The blob store path + /// The user context + /// The file system path + /// A new writable file instance + public new static NioWritableFile New( + BlobStorePath blobPath, + TUser? user, + string path) + { + return New(blobPath, user, path, null); + } + + /// + /// Creates a new writable file wrapper with an existing file stream. + /// + /// The blob store path + /// The user context + /// The file system path + /// The optional file stream + /// A new writable file instance + public new static NioWritableFile New( + BlobStorePath blobPath, + TUser? user, + string path, + FileStream? fileStream) + { + return new NioWritableFile(blobPath, user, path, fileStream); + } + + /// + /// Initializes a new instance of the class. + /// + /// The blob store path + /// The user context + /// The file system path + /// The optional file stream + protected NioWritableFile( + BlobStorePath blobPath, + TUser? user, + string path, + FileStream? fileStream) + : base(blobPath, user, path, fileStream) + { + } + + /// + protected override FileMode GetDefaultFileMode() + { + return FileMode.OpenOrCreate; + } + + /// + protected override FileAccess GetDefaultFileAccess() + { + return FileAccess.ReadWrite; + } + + /// + protected override FileShare GetDefaultFileShare() + { + return FileShare.None; + } + + /// + protected override void ValidateOpenOptions(FileMode mode, FileAccess access, FileShare share) + { + // Writable files allow all options, so no validation needed + // Override the base class validation that restricts write access + } +} + diff --git a/afs/nio/test/NebulaStore.Afs.Nio.Tests.csproj b/afs/nio/test/NebulaStore.Afs.Nio.Tests.csproj new file mode 100644 index 0000000..7798c9a --- /dev/null +++ b/afs/nio/test/NebulaStore.Afs.Nio.Tests.csproj @@ -0,0 +1,29 @@ + + + + net9.0 + enable + enable + false + true + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + diff --git a/afs/nio/test/NioConnectorTests.cs b/afs/nio/test/NioConnectorTests.cs new file mode 100644 index 0000000..2c7f8a2 --- /dev/null +++ b/afs/nio/test/NioConnectorTests.cs @@ -0,0 +1,250 @@ +using System; +using System.IO; +using System.Linq; +using System.Text; +using Xunit; +using NebulaStore.Afs.Nio; +using NebulaStore.Afs.Blobstore; + +namespace NebulaStore.Afs.Nio.Tests; + +public class NioConnectorTests : IDisposable +{ + private readonly string _testDirectory; + + public NioConnectorTests() + { + _testDirectory = Path.Combine(Path.GetTempPath(), $"nio-connector-test-{Guid.NewGuid()}"); + Directory.CreateDirectory(_testDirectory); + } + + public void Dispose() + { + if (Directory.Exists(_testDirectory)) + { + Directory.Delete(_testDirectory, recursive: true); + } + } + + [Fact] + public void New_CreatesConnectorWithBaseDirectory() + { + // Arrange & Act + using var connector = NioConnector.New(_testDirectory); + + // Assert + Assert.NotNull(connector); + } + + [Fact] + public void New_ThrowsOnNullBaseDirectory() + { + // Arrange, Act & Assert + Assert.Throws(() => NioConnector.New(null!)); + } + + [Fact] + public void CreateDirectory_CreatesDirectory() + { + // Arrange + using var connector = NioConnector.New(_testDirectory); + var path = BlobStorePath.New("container", "subfolder"); + + // Act + connector.CreateDirectory(path); + + // Assert + Assert.True(connector.DirectoryExists(path)); + } + + [Fact] + public void CreateFile_CreatesEmptyFile() + { + // Arrange + using var connector = NioConnector.New(_testDirectory); + var path = BlobStorePath.New("container", "test.txt"); + + // Act + connector.CreateFile(path); + + // Assert + Assert.True(connector.FileExists(path)); + Assert.Equal(0, connector.GetFileSize(path)); + } + + [Fact] + public void WriteData_WritesDataToFile() + { + // Arrange + using var connector = NioConnector.New(_testDirectory); + var path = BlobStorePath.New("container", "test.txt"); + var testData = Encoding.UTF8.GetBytes("Hello, NIO Connector!"); + + // Act + connector.WriteData(path, testData); + + // Assert + Assert.True(connector.FileExists(path)); + Assert.Equal(testData.Length, connector.GetFileSize(path)); + } + + [Fact] + public void ReadData_ReadsDataFromFile() + { + // Arrange + using var connector = NioConnector.New(_testDirectory); + var path = BlobStorePath.New("container", "test.txt"); + var testData = Encoding.UTF8.GetBytes("Hello, NIO Connector!"); + connector.WriteData(path, testData); + + // Act + var readData = connector.ReadData(path, 0, -1); + + // Assert + Assert.Equal(testData, readData); + } + + [Fact] + public void ReadData_ReadsPartialData() + { + // Arrange + using var connector = NioConnector.New(_testDirectory); + var path = BlobStorePath.New("container", "test.txt"); + var testData = Encoding.UTF8.GetBytes("Hello, NIO Connector!"); + connector.WriteData(path, testData); + + // Act + var readData = connector.ReadData(path, 0, 5); + + // Assert + Assert.Equal(5, readData.Length); + Assert.Equal("Hello", Encoding.UTF8.GetString(readData)); + } + + [Fact] + public void DeleteFile_DeletesExistingFile() + { + // Arrange + using var connector = NioConnector.New(_testDirectory); + var path = BlobStorePath.New("container", "test.txt"); + connector.CreateFile(path); + + // Act + var deleted = connector.DeleteFile(path); + + // Assert + Assert.True(deleted); + Assert.False(connector.FileExists(path)); + } + + [Fact] + public void DeleteFile_ReturnsFalseForNonExistentFile() + { + // Arrange + using var connector = NioConnector.New(_testDirectory); + var path = BlobStorePath.New("container", "nonexistent.txt"); + + // Act + var deleted = connector.DeleteFile(path); + + // Assert + Assert.False(deleted); + } + + [Fact] + public void ListFiles_ReturnsFileNames() + { + // Arrange + using var connector = NioConnector.New(_testDirectory); + var dirPath = BlobStorePath.New("container"); + connector.CreateFile(BlobStorePath.New("container", "file1.txt")); + connector.CreateFile(BlobStorePath.New("container", "file2.txt")); + + // Act + var files = connector.ListFiles(dirPath).ToList(); + + // Assert + Assert.Equal(2, files.Count); + Assert.Contains("file1.txt", files); + Assert.Contains("file2.txt", files); + } + + [Fact] + public void ListDirectories_ReturnsDirectoryNames() + { + // Arrange + using var connector = NioConnector.New(_testDirectory); + var dirPath = BlobStorePath.New("container"); + connector.CreateDirectory(BlobStorePath.New("container", "dir1")); + connector.CreateDirectory(BlobStorePath.New("container", "dir2")); + + // Act + var directories = connector.ListDirectories(dirPath).ToList(); + + // Assert + Assert.Equal(2, directories.Count); + Assert.Contains("dir1", directories); + Assert.Contains("dir2", directories); + } + + [Fact] + public void MoveFile_MovesFileToNewLocation() + { + // Arrange + using var connector = NioConnector.New(_testDirectory); + var sourcePath = BlobStorePath.New("container", "source.txt"); + var targetPath = BlobStorePath.New("container", "target.txt"); + var testData = Encoding.UTF8.GetBytes("Test data"); + connector.WriteData(sourcePath, testData); + + // Act + connector.MoveFile(sourcePath, targetPath); + + // Assert + Assert.False(connector.FileExists(sourcePath)); + Assert.True(connector.FileExists(targetPath)); + var readData = connector.ReadData(targetPath, 0, -1); + Assert.Equal(testData, readData); + } + + [Fact] + public void CopyFile_CopiesFileToNewLocation() + { + // Arrange + using var connector = NioConnector.New(_testDirectory); + var sourcePath = BlobStorePath.New("container", "source.txt"); + var targetPath = BlobStorePath.New("container", "target.txt"); + var testData = Encoding.UTF8.GetBytes("Test data"); + connector.WriteData(sourcePath, testData); + + // Act + connector.CopyFile(sourcePath, targetPath); + + // Assert + Assert.True(connector.FileExists(sourcePath)); + Assert.True(connector.FileExists(targetPath)); + var sourceData = connector.ReadData(sourcePath, 0, -1); + var targetData = connector.ReadData(targetPath, 0, -1); + Assert.Equal(sourceData, targetData); + } + + [Fact] + public void WriteData_WithMultipleChunks_WritesAllChunks() + { + // Arrange + using var connector = NioConnector.New(_testDirectory); + var path = BlobStorePath.New("container", "test.txt"); + var chunk1 = Encoding.UTF8.GetBytes("Hello, "); + var chunk2 = Encoding.UTF8.GetBytes("NIO "); + var chunk3 = Encoding.UTF8.GetBytes("Connector!"); + + // Act + connector.WriteData(path, new[] { chunk1, chunk2, chunk3 }); + + // Assert + var readData = connector.ReadData(path, 0, -1); + var content = Encoding.UTF8.GetString(readData); + Assert.Equal("Hello, NIO Connector!", content); + } +} + diff --git a/afs/nio/test/NioFileSystemTests.cs b/afs/nio/test/NioFileSystemTests.cs new file mode 100644 index 0000000..8f6ebee --- /dev/null +++ b/afs/nio/test/NioFileSystemTests.cs @@ -0,0 +1,165 @@ +using System; +using System.IO; +using System.Text; +using Xunit; +using NebulaStore.Afs.Nio; +using NebulaStore.Afs.Blobstore; + +namespace NebulaStore.Afs.Nio.Tests; + +public class NioFileSystemTests : IDisposable +{ + private readonly string _testDirectory; + + public NioFileSystemTests() + { + _testDirectory = Path.Combine(Path.GetTempPath(), $"nio-test-{Guid.NewGuid()}"); + Directory.CreateDirectory(_testDirectory); + } + + public void Dispose() + { + if (Directory.Exists(_testDirectory)) + { + Directory.Delete(_testDirectory, recursive: true); + } + } + + [Fact] + public void New_CreatesFileSystemWithDefaultProtocol() + { + // Arrange & Act + using var fileSystem = NioFileSystem.New(); + + // Assert + Assert.NotNull(fileSystem); + Assert.Equal(NioFileSystem.DefaultFileProtocol, fileSystem.DefaultProtocol); + Assert.NotNull(fileSystem.IoHandler); + } + + [Fact] + public void New_CreatesFileSystemWithCustomProtocol() + { + // Arrange + var customProtocol = "custom:///"; + + // Act + using var fileSystem = NioFileSystem.New(customProtocol); + + // Assert + Assert.Equal(customProtocol, fileSystem.DefaultProtocol); + } + + [Fact] + public void ResolvePath_HandlesHomeDirectory() + { + // Arrange + using var fileSystem = NioFileSystem.New(); + var homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + + // Act + var elements = fileSystem.ResolvePath("~/Documents/test"); + + // Assert + Assert.NotEmpty(elements); + Assert.Contains("Documents", elements); + Assert.Contains("test", elements); + } + + [Fact] + public void ResolvePath_HandlesRegularPath() + { + // Arrange + using var fileSystem = NioFileSystem.New(); + + // Act + var elements = fileSystem.ResolvePath("/var/data/files"); + + // Assert + Assert.Contains("var", elements); + Assert.Contains("data", elements); + Assert.Contains("files", elements); + } + + [Fact] + public void WrapForReading_CreatesReadableFile() + { + // Arrange + using var fileSystem = NioFileSystem.New(); + var path = BlobStorePath.New("container", "test.txt"); + + // Act + using var readableFile = fileSystem.WrapForReading(path); + + // Assert + Assert.NotNull(readableFile); + Assert.Equal(path, readableFile.BlobPath); + } + + [Fact] + public void WrapForWriting_CreatesWritableFile() + { + // Arrange + using var fileSystem = NioFileSystem.New(); + var path = BlobStorePath.New("container", "test.txt"); + + // Act + using var writableFile = fileSystem.WrapForWriting(path); + + // Assert + Assert.NotNull(writableFile); + Assert.Equal(path, writableFile.BlobPath); + } + + [Fact] + public void ConvertToWriting_ConvertsReadableToWritable() + { + // Arrange + using var fileSystem = NioFileSystem.New(); + var path = BlobStorePath.New("container", "test.txt"); + using var readableFile = fileSystem.WrapForReading(path); + + // Act + using var writableFile = fileSystem.ConvertToWriting(readableFile); + + // Assert + Assert.NotNull(writableFile); + Assert.Equal(path, writableFile.BlobPath); + } + + [Fact] + public void ConvertToReading_ConvertsWritableToReadable() + { + // Arrange + using var fileSystem = NioFileSystem.New(); + var path = BlobStorePath.New("container", "test.txt"); + using var writableFile = fileSystem.WrapForWriting(path); + + // Act + using var readableFile = fileSystem.ConvertToReading(writableFile); + + // Assert + Assert.NotNull(readableFile); + Assert.Equal(path, readableFile.BlobPath); + } + + [Fact] + public void WriteAndReadBytes_WorksCorrectly() + { + // Arrange + using var connector = NioConnector.New(_testDirectory); + var path = BlobStorePath.New("container", "test.txt"); + var testData = Encoding.UTF8.GetBytes("Hello, NIO!"); + + // Act - Write + connector.WriteData(path, testData); + + // Act - Read + var readData = connector.ReadData(path, 0, -1); + + // Assert + Assert.Equal(testData, readData); + Assert.True(connector.FileExists(path)); + } +} +