KitStack.Storage is a modular .NET library for file storage: one IFileStorageManager abstraction, provider
packages for the local filesystem, Amazon S3, Azure Blob Storage, SFTP and FTP, an in-memory fake for tests,
an EF Core metadata store, and ASP.NET Core integration. Applications only reference the SDKs they use.
| Package | Contents |
|---|---|
KitStack.Abstractions |
IFileStorageManager, IFileEntry, StoredFileRef, IFileEntryStore, upload policy, encryption at rest, the shared provider pipeline and AddKitStackStorage |
KitStack.Storage.Local |
Local filesystem provider |
KitStack.Storage.S3 |
Amazon S3 / S3-compatible provider (presigned URLs, per-variant buckets, SSE) |
KitStack.Storage.Azure |
Azure Blob provider (SAS URLs, access tiers) |
KitStack.Storage.Sftp |
SFTP provider (SSH.NET, password or key auth, host-key pinning) |
KitStack.Storage.Ftp |
FTP / FTPS provider (FluentFTP) |
KitStack.Storage.EntityFrameworkCore |
EF Core IFileEntryStore, StoredFileRef mapping, link/unlink interceptor |
KitStack.Storage.Mongo |
MongoDB IFileEntryStore |
KitStack.Fakes |
In-memory provider for tests |
KitStack.AspNetCore |
Authorised file-download endpoint (no provider dependencies) |
builder.Services.AddKitStackStorage(builder.Configuration) // Storage section: policy, encryption
.AddLocal(); // or AddS3(), AddAzureBlob(), AddSftp(), AddFtp(), AddFake()
app.MapKitStackStorageFiles("/files"); // KitStack.AspNetCore, requires authorization// Write: from a form or from any stream (background jobs, imports).
var entry = await storage.CreateAsync<Invoice>(formFile, "Billing");
var report = await storage.CreateAsync<Invoice>(pdfStream, "report.pdf", "application/pdf", "Billing");
// Read, check, delete, link: the same for every provider.
await using var stream = await storage.OpenReadAsync(entry);
return File(stream, entry.ContentType);
bool exists = await storage.ExistsAsync(entry);
Uri? url = await storage.GetUrlAsync(entry, TimeSpan.FromMinutes(10)); // presigned / SAS / PublicBaseUrl
var updated = await storage.ReplaceAsync<Invoice>(entry, newStream, "v2.pdf", null, "Billing");
await storage.OverwriteAsync(updated, restoredStream); // same location, e.g. restoring a backup
await storage.DeleteAsync(updated); // also deletes image variantsEvery stored entry carries a ContentType resolved from the file's bytes (the client's claim is recorded but
not trusted) and a SHA-256 Checksum computed while the file is written.
Choosing the provider from configuration:
var storage = builder.Services.AddKitStackStorage(builder.Configuration);
if (storage.IsProvider("S3")) storage.AddS3();
else if (storage.IsProvider("Azure")) storage.AddAzureBlob();
else storage.AddLocal();{
"Storage": {
"Provider": "Local",
"MasterEncryptionKey": "<base64 32-byte key from a secret store>",
"Upload": {
"MaxBytes": 26214400,
"AllowedContentTypes": [ "image/*", "application/pdf" ],
"AllowedExtensions": [ ".jpg", ".png", ".pdf" ]
},
"Local": { "Path": "Files", "PublicBaseUrl": "https://app.example.com/files" },
"S3": { "MainTarget": { "BucketName": "files", "Region": "eu-west-1" } },
"Azure": { "ConnectionString": "...", "ContainerName": "files" },
"Sftp": { "Host": "sftp.example.com", "Username": "app", "PrivateKeyPath": "/run/secrets/key", "HostKeyFingerprint": "SHA256:..." },
"Ftp": { "Host": "ftp.example.com", "UseSsl": true }
}
}- Encryption at rest: when
MasterEncryptionKeyis set, every new file (and image variant) is encrypted with AES-256-GCM using a per-file key derived with HKDF. Reads decrypt transparently;GetUrlAsyncreturnsnullfor encrypted files because a direct URL would serve ciphertext. Move old keys toPreviousEncryptionKeysto rotate. Generate a key withopenssl rand -base64 32and keep it in a secret store, never inappsettings.json. - Upload policy is enforced before any provider is called; violations throw
StorageValidationException.
KitStack.Storage.EntityFrameworkCore records uploads, links files to entities through StoredFileRef owned
properties, releases links when references are replaced or owners are deleted, and can sweep orphaned files.
See its README.
public class LegalDocument
{
public Guid Id { get; set; }
public StoredFileRef? File { get; set; } // FileEntryId, Location, ContentType, Size, Checksum, ...
}See CHANGELOG.md for the breaking changes (the Sftp → Ftp rename, AddKitStackStorage
moving to KitStack.Abstractions, anonymous static files becoming opt-in, and encryption now being real).
dotnet build
dotnet testImageSharp 4 validates a Six Labors license key in Release builds. Set the SIXLABORS_LICENSE_KEY
environment variable (CI reads it from the repository secret of the same name). Debug builds only warn.
Apache-2.0, see LICENSE.