- Features
- Installation
- Quick Start
- Core Concepts
- Providers
- Advanced Usage
- API Reference
- Inspiration
- Contributing
- License
- Akka.Streams-First API —
Source<ReadOnlyMemory<byte>>/Sink<ReadOnlyMemory<byte>>with full backpressure - Immutable Data Model — sealed records for all types (
BlobItem,BlobReadResult,BlobWriteResult) - Multiple Providers — Local filesystem, in-memory, and virtual mount-based routing
- Composable Pipelines — plug Akka.Streams operators (throttle, buffer, map) directly into storage pipelines
- Read Metadata — blob metadata returned as materialized value from
Read, no extra call needed - Virtual Storage — mount multiple providers under path prefixes, route transparently
dotnet add package NaschStorageRequirements: .NET 10.0 or higher
using Akka.Actor;
using Akka.Streams;
using Akka.Streams.Dsl;
using NaschStorage.Local;
using var system = ActorSystem.Create("my-app");
var materializer = system.Materializer();
var store = new LocalBlobStore("/path/to/storage");
// Write
var data = "hello world"u8.ToArray();
var writeResult = await Source.Single(new ReadOnlyMemory<byte>(data))
.RunWith(store.Write("greeting.txt"), materializer);
Console.WriteLine($"Wrote {writeResult.BytesWritten} bytes");
// Read
var (readResult, chunks) = await store.Read("greeting.txt")
.ToMaterialized(Sink.Seq<ReadOnlyMemory<byte>>(), Keep.Both)
.Run(materializer);
var metadata = await readResult;
Console.WriteLine($"Read {metadata.Size} bytes from {metadata.Path}");using NaschStorage.InMemory;
var store = new InMemoryBlobStore();
await Source.Single(new ReadOnlyMemory<byte>("cached data"u8.ToArray()))
.RunWith(store.Write("cache/key.bin"), materializer);using NaschStorage.Virtual;
var store = new VirtualStorageBuilder()
.Mount("/local", new LocalBlobStore("/data"))
.Mount("/cache", new InMemoryBlobStore())
.Build();
// Routes to LocalBlobStore
await Source.Single(new ReadOnlyMemory<byte>(data))
.RunWith(store.Write("/local/documents/report.pdf"), materializer);
// Routes to InMemoryBlobStore
await Source.Single(new ReadOnlyMemory<byte>(data))
.RunWith(store.Write("/cache/temp.bin"), materializer);
// List across all mounts
var allBlobs = await store.List()
.RunWith(Sink.Seq<BlobItem>(), materializer);NaschStorage uses Akka.Streams for all data transfer, providing automatic backpressure:
// Read returns a Source — compose with any Akka.Streams operator
var totalBytes = await store.Read("large-file.bin")
.Select(chunk => chunk.Length)
.RunWith(Sink.Aggregate<int, long>(0L, (sum, len) => sum + len), materializer);// Check existence
var exists = await store.ExistsAsync(["file1.txt", "file2.txt"]);
// List with filtering
var items = await store.List(new ListOptions
{
Prefix = "documents/",
Recursive = true,
MaxResults = 100,
})
.RunWith(Sink.Seq<BlobItem>(), materializer);
// Delete
await store.DeleteAsync(["old-file.txt", "temp/"]);
// Get metadata without reading content
var blobs = await store.GetBlobsAsync(["report.pdf"]);
Console.WriteLine($"Size: {blobs.First().Size}, Modified: {blobs.First().ModifiedOn}");
// Update metadata
await store.SetBlobsAsync([new BlobItem
{
Path = "report.pdf",
ContentType = "application/pdf",
Properties = new Dictionary<string, string> { ["author"] = "Jane" },
}]);// Write initial data
await Source.Single(new ReadOnlyMemory<byte>("line 1\n"u8.ToArray()))
.RunWith(store.Write("log.txt"), materializer);
// Append more data
await Source.Single(new ReadOnlyMemory<byte>("line 2\n"u8.ToArray()))
.RunWith(store.Write("log.txt", append: true), materializer);| Package | Providers | Description |
|---|---|---|
NaschStorage |
LocalBlobStore |
Filesystem-backed, maps paths to files relative to a root directory |
NaschStorage |
InMemoryBlobStore |
In-memory store using ConcurrentDictionary, ideal for tests and caching |
NaschStorage |
VirtualBlobStore |
Composite store routing by path prefix to mounted sub-stores |
| Package | Provider |
|---|---|
NaschStorage.AWS |
Amazon S3 (+ MinIO, Wasabi, DigitalOcean Spaces) |
NaschStorage.Azure.Blobs |
Azure Blob Storage |
NaschStorage.Azure.Files |
Azure File Shares |
NaschStorage.GCP |
Google Cloud Storage |
NaschStorage.FTP |
FTP/FTPS |
NaschStorage.SFTP |
SFTP |
// Process a file in chunks without loading it all into memory
await store.Read("huge-file.bin")
.Select(chunk =>
{
// Process each chunk (e.g., compute hash, transform, compress)
return chunk;
})
.RunWith(anotherStore.Write("processed.bin"), materializer);var local = new LocalBlobStore("/data");
var cache = new InMemoryBlobStore();
// Stream directly from local to in-memory — no intermediate buffering
await local.Read("source.dat")
.RunWith(cache.Write("cached-copy.dat"), materializer);// Throttle write speed using Akka.Streams operators
await Source.From(largeChunks)
.Throttle(10, TimeSpan.FromSeconds(1), 1, ThrottleMode.Shaping)
.RunWith(store.Write("rate-limited.bin"), materializer);| Method | Return Type | Description |
|---|---|---|
Read(path) |
Source<ReadOnlyMemory<byte>, Task<BlobReadResult>> |
Stream blob content with metadata |
Write(path, append) |
Sink<ReadOnlyMemory<byte>, Task<BlobWriteResult>> |
Stream data into a blob |
List(options) |
Source<BlobItem, NotUsed> |
Stream blob listing with backpressure |
DeleteAsync(paths) |
Task |
Delete blobs by path |
ExistsAsync(paths) |
Task<IReadOnlyCollection<bool>> |
Check blob existence |
GetBlobsAsync(paths) |
Task<IReadOnlyCollection<BlobItem>> |
Get blob metadata |
SetBlobsAsync(blobs) |
Task |
Update blob metadata |
| Type | Description |
|---|---|
BlobItem |
Blob metadata — path, kind, size, timestamps, content type, ETag, custom properties |
BlobReadResult |
Read metadata — path, size, content type, ETag, modified date, properties |
BlobWriteResult |
Write result — path, bytes written, ETag |
ListOptions |
List filter — prefix, recursive, max results |
BlobKind |
Enum: File, Folder |
NaschStorage is inspired by FluentStorage, a polycloud storage abstraction for .NET.
While FluentStorage provides a solid foundation, NaschStorage takes a different approach:
- Akka.Streams instead of .NET
Stream— full backpressure and composable pipelines ReadOnlyMemory<byte>instead ofbyte[]— zero-copy where possible- Immutable records instead of mutable classes — thread-safe by design
- Virtual storage — built-in mount-based routing across providers
- Metadata on read —
BlobReadResultreturned as materialized value, no extra API call - Honest capabilities — no
NotSupportedExceptionat runtime; capabilities are provider-specific interfaces
Contributions are welcome! This library grows with the community's needs.
- Fork the repository
- Create a feature branch:
git checkout -b feature/amazing-feature - Write tests for your changes
- Ensure all tests pass:
dotnet test - Submit a Pull Request
- Follow existing code style and conventions
- Include unit tests for new features
- Update documentation for API changes
- Keep PRs focused and atomic
- Write meaningful commit messages (conventional commits)
git clone https://github.com/st0o0/NaschStorage.git
cd NaschStorage/src
dotnet restore NaschStorage.slnx
dotnet build NaschStorage.slnx
dotnet test --solution *.slnxThis project is licensed under the MIT License - see the LICENSE file for details.
Built with Akka.Streams for high-performance .NET storage