Skip to main content

Local Filesystem Provider

Vali-Blob.Local provides LocalStorageProvider, implementing IStorageProvider, IResumableUploadProvider, and IPresignedUrlProvider backed by the local filesystem. It requires no external services and is the fastest path to getting started with Vali-Blob.


Installation

dotnet add package Vali-Blob.Core
dotnet add package Vali-Blob.Local

LocalStorageOptions Reference

OptionTypeRequiredDescription
BasePathstringYesAbsolute path to the root directory where files are stored.
CreateIfNotExistsboolNoCreate BasePath (and parent directories) on startup if missing. Default: false.
PublicBaseUrlstring?NoBase URL prefix for URL generation. If null, returns file:// URIs.

DI Registration

using Vali-Blob.Core;
using Vali-Blob.Local;

var storagePath = Path.Combine(builder.Environment.ContentRootPath, "storage");

builder.Services
.AddVali-Blob(o => o.DefaultProvider = "local")
.AddProvider<LocalStorageProvider>("local", opts =>
{
opts.BasePath = storagePath;
opts.CreateIfNotExists = true;
opts.PublicBaseUrl = builder.Environment.IsDevelopment()
? "http://localhost:5000/files"
: null;
})
.WithPipeline(p => p
.UseValidation(v =>
{
v.MaxFileSizeBytes = 50_000_000;
v.AllowedExtensions = [".jpg", ".png", ".pdf", ".mp4"];
})
.UseContentTypeDetection()
.UseConflictResolution(ConflictResolution.ReplaceExisting)
);

Directory Structure

When a file is uploaded to "uploads/images/avatar.jpg", the provider creates:

storage/                          ← BasePath
uploads/
images/
avatar.jpg ← the uploaded file
avatar.jpg.meta.json ← metadata sidecar file

Subdirectories are created automatically. The provider never writes files outside BasePath.


Metadata Sidecar Files

Every uploaded file gets a companion .meta.json sidecar file in the same directory. This file persists metadata that the filesystem cannot natively store — content type, custom user metadata, upload timestamp, and ETag:

{
"contentType": "image/jpeg",
"contentLength": 102400,
"eTag": "d41d8cd98f00b204e9800998ecf8427e",
"uploadedAt": "2026-03-18T10:30:00Z",
"customMetadata": {
"x-user-id": "42",
"x-original-name": "profile-photo.jpg",
"x-vali-compressed": "gzip",
"x-vali-original-size": "310000"
}
}

GetMetadataAsync reads from the sidecar without touching the binary file. DeleteAsync removes both the main file and its .meta.json sidecar atomically.


Resumable Upload Storage

In-progress resumable uploads are stored in a hidden .resumable/ subdirectory under BasePath:

storage/
.resumable/
abc123-upload-id/
0.chunk ← bytes 0–5,242,879
5242880.chunk ← bytes 5,242,880–10,485,759
10485760.chunk ← ...
uploads/
large-video.mp4 ← assembled after CompleteResumableUploadAsync

Each chunk file is named by its byte offset. On CompleteResumableUploadAsync, all chunks are concatenated in offset order into the final file, and the .resumable/{uploadId}/ directory is deleted.


URL Generation

With PublicBaseUrl

opts.PublicBaseUrl = "http://localhost:5000/files";

A file at "uploads/images/avatar.jpg" gets the URL:

http://localhost:5000/files/uploads/images/avatar.jpg

Suitable when you serve the storage directory via app.UseStaticFiles() or a reverse proxy.

Without PublicBaseUrl

The provider returns a file:// URI pointing to the absolute disk path:

file:///home/app/storage/uploads/images/avatar.jpg

Useful for server-side processing where public accessibility is not needed.


Serving Files over HTTP

Map the storage directory as a static file endpoint:

// Program.cs
var storagePath = Path.Combine(builder.Environment.ContentRootPath, "storage");

app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(storagePath),
RequestPath = "/files"
});

Files become accessible at:

GET http://localhost:5000/files/uploads/images/avatar.jpg

Hiding Sidecar Files

To prevent .meta.json sidecar files from being served publicly, add middleware before UseStaticFiles:

app.Use(async (ctx, next) =>
{
if (ctx.Request.Path.Value?.EndsWith(".meta.json",
StringComparison.OrdinalIgnoreCase) == true)
{
ctx.Response.StatusCode = StatusCodes.Status404NotFound;
return;
}
await next();
});

app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(storagePath),
RequestPath = "/files"
});

Presigned URLs (Token-Based)

LocalStorageProvider implements IPresignedUrlProvider using short-lived signed tokens. Tokens are signed with a server-side HMAC key and verified by a Vali-Blob download endpoint registered in your application:

// Register the token endpoint — handles GET /valiblob/download/{token}
app.MapVali-Blob();

// Generate a presigned download URL
var provider = factory.Create("local");

if (provider is IPresignedUrlProvider presigned)
{
var downloadUrl = await presigned.GetPresignedDownloadUrlAsync(
"private/salary-report.pdf",
expiresIn: TimeSpan.FromHours(1));

// → http://localhost:5000/valiblob/download/eyJ...signedtoken
return Results.Ok(new { url = downloadUrl.Value });
}

The /valiblob/download/{token} endpoint validates the token signature and expiry, then streams the file. No cloud infrastructure is required.


Concurrent Access

The local provider handles concurrent writes safely:

  • Write operations open FileStream with FileShare.None during the write — preventing partial reads by other threads.
  • Read operations use FileShare.Read and can run concurrently.
  • Resumable upload chunks are written to separate per-chunk files, so concurrent chunk uploads from different HTTP requests do not conflict.

For heavy concurrent workloads or multi-server deployments, switch to a cloud provider (AWS S3, Azure Blob, GCS).


Complete Setup Example

// Program.cs
using Vali-Blob.Core;
using Vali-Blob.Local;

var storagePath = Path.Combine(builder.Environment.ContentRootPath, "storage");

builder.Services
.AddVali-Blob(o => o.DefaultProvider = "local")
.AddProvider<LocalStorageProvider>("local", opts =>
{
opts.BasePath = storagePath;
opts.CreateIfNotExists = true;
opts.PublicBaseUrl = "http://localhost:5000/files";
})
.WithPipeline(p => p
.UseValidation(v =>
{
v.MaxFileSizeBytes = 100_000_000; // 100 MB
v.AllowedExtensions = [".jpg", ".jpeg", ".png", ".pdf"];
v.MinFileSizeBytes = 1;
})
.UseContentTypeDetection()
.UseConflictResolution(ConflictResolution.ReplaceExisting)
);

var app = builder.Build();

// Hide sidecar files
app.Use(async (ctx, next) =>
{
if (ctx.Request.Path.Value?.EndsWith(".meta.json",
StringComparison.OrdinalIgnoreCase) == true)
{
ctx.Response.StatusCode = 404;
return;
}
await next();
});

// Serve storage directory at /files
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(storagePath),
RequestPath = "/files"
});

// Register presigned URL token endpoint
app.MapVali-Blob();

app.MapControllers();
app.Run();

When to Use the Local Provider

ScenarioSuitable
Local developmentYes — zero configuration, instant startup
Integration testsYes — real file I/O, clean up with Directory.Delete
Single-server production applicationsYes — when the web server and storage are on the same machine
Edge or on-premise deploymentsYes — IoT devices, embedded systems, air-gapped environments
Multi-instance deployments (multiple web servers)No — files are not shared between instances
Serverless or ephemeral computeNo — files are lost when containers are recycled
Large-scale production (redundancy, CDN)No — use a cloud provider

Supported Operations

OperationSupportedNotes
UploadAsyncYes
DownloadAsyncYesIncluding byte range
DeleteAsyncYesRemoves file and .meta.json sidecar
DeleteFolderAsyncYesDirectory.Delete recursive
ExistsAsyncYesFile.Exists
CopyAsyncYesFile.Copy + sidecar copy
GetMetadataAsyncYesVia .meta.json sidecar
SetMetadataAsyncYesOverwrites .meta.json
ListFilesAsyncYesDirectory.EnumerateFiles with prefix
ListFoldersAsyncYesDirectory.EnumerateDirectories
GetUrlAsyncYesPublicBaseUrl-based or file:// URI
StartResumableUploadAsyncYesCreates .resumable/{uploadId}/ directory
UploadChunkAsyncYesWrites {offset}.chunk file
CompleteResumableUploadAsyncYesConcatenates chunks into final file
AbortResumableUploadAsyncYesDeletes .resumable/{uploadId}/ directory
GetPresignedUploadUrlAsyncYesHMAC-signed token
GetPresignedDownloadUrlAsyncYesHMAC-signed token