CosmoS3 2.3.5
dotnet add package CosmoS3 --version 2.3.5
NuGet\Install-Package CosmoS3 -Version 2.3.5
<PackageReference Include="CosmoS3" Version="2.3.5" />
<PackageVersion Include="CosmoS3" Version="2.3.5" />
<PackageReference Include="CosmoS3" />
paket add CosmoS3 --version 2.3.5
#r "nuget: CosmoS3, 2.3.5"
#:package CosmoS3@2.3.5
#addin nuget:?package=CosmoS3&version=2.3.5
#tool nuget:?package=CosmoS3&version=2.3.5
CosmoS3
CosmoS3 is an Amazon S3–compatible object-storage middleware library for CosmoApiServer. It implements core S3 operations with a pluggable metadata store — SQL Server, PostgreSQL, MySQL, SQLite, the embedded CosmoKv SQL engine, or kvdirect (a no-SQL ordered-KV index for single-node deployments) — and the local disk (or a pluggable storage driver) for object data. Full-object GETs stream disk→socket through a kernel zero-copy (sendfile) path.
It can be consumed three ways: as a standalone S3 endpoint (CosmoS3.Host), as middleware inside an existing CosmoApiServer app, or embedded in-process via the IS3Repository data layer.
On a Linux NVMe server, v2.3.0 beats native-config MinIO on every operation measured — GET 2–3× (up to ~9.85 GiB/s), LIST 2.1×, HEAD 1.47× — see Performance.
Table of Contents
- Architecture
- Security & Correctness
- Performance
- Recent Updates
- Quick Start
- Configuration
- Database Schema
- S3 Feature Compatibility
- Static Website Hosting
- Presigned URLs
- Multipart Upload
- Using with AWS CLI
- Testing
- Project Structure
Architecture
┌─────────────────────────────────────────────────────┐
│ CosmoApiServer │
│ (System.IO.Pipelines transport, HTTP/1.1·2·3; │
│ kernel sendfile zero-copy GET on plaintext HTTP/1) │
└──────────────────────┬───────────────────────────────┘
│ IMiddleware
▼
┌──────────────────────────────────────────────────────┐
│ S3Middleware │
│ • S3Context.CreateAsync — async, non-blocking parse │
│ • Authenticates (SigV4 / SigV2 / presigned) │
│ • Verifies the SigV4 signature (ValidateSignatures) │
│ • Routes to ServiceHandler / BucketHandler / │
│ ObjectHandler / Admin + internal handlers │
└────────────────┬───────────────────────┬──────────────┘
│ │
┌───────────▼─────────┐ ┌───────────▼────────────┐
│ DataAccess │ │ Storage Driver │
│ → IS3Repository: │ │ (DiskStorageDriver: │
│ • S3Repository │ │ atomic write + move, │
│ (5 SQL backends) │ │ ArrayPool, SubStream, │
│ • KvDirectRepo │ │ zero-copy GET path) │
│ (ordered KV) │ │ │
└──────────────────────┘ └─────────────────────────┘
Key types:
| Type | Role |
|---|---|
S3Middleware |
Entry point; implements IMiddleware. Builds the request via S3Context.CreateAsync (fully async — no sync-over-async body buffering) and enforces SigV4 when ValidateSignatures is on |
S3Request / S3Response / S3Context |
Async request parse, S3-formatted response writer, combined handler context |
AuthManager |
Resolves the credential/user and computes the authorization decision (SigV2 / SigV4 / presigned) |
SignatureV4Verifier / Helpers/SignatureV4 |
Recomputes the AWS SigV4 canonical request and compares signatures in constant time |
BucketManager |
Lock-free in-memory bucket registry (ConcurrentDictionary); lazily populated from the DB on a miss |
ConfigManager |
User / credential / bucket / object lookup (DB or seeded no-DB mode) |
DataAccess → IS3Repository |
Static facade over the metadata layer. Two implementations behind one contract: S3Repository (parameterized SQL via CosmoSQLClient, portable across all five SQL backends) and KvDirectRepository (metadata directly on the CosmoKv ordered-KV engine — no SQL) |
KvDirectRepository |
DatabaseType=kvdirect — all metadata in one byte-ordered KV keyspace; latest-version lookup is a single seek and listing is a native cursor. Single-node embedded backend |
DiskStorageDriver |
Streaming I/O using ArrayPool<byte> and SubStream; writes to a temp sibling then File.Move for atomic, durable publish. Full-object GETs use the framework's kernel sendfile path |
Security & Correctness
The design choices below are deliberate; each closes a concrete failure mode. They are enforced by the unit + integration test suite (see Testing).
| Area | Behavior | Why |
|---|---|---|
| SigV4 verification | When ValidateSignatures is true (default) the server recomputes the canonical request and compares signatures with CryptographicOperations.FixedTimeEquals, plus a ±15-minute clock-skew check (RequestTimeTooSkewed). Validated against AWS's official get-vanilla test vector. |
Authentication that parses a signature but never checks it is no authentication. Constant-time comparison avoids leaking the secret via timing. |
| No default admin key | AdminApiKey defaults to ""; an empty configured key never matches any presented key (SecurityHelper.ConfiguredKeyMatches). |
A shipped default master key (the old "cosmos3admin") is a backdoor. Operators must set one explicitly. |
| Real principal on writes | Object writes are credited to the authenticated user. Anonymous / public-write requests fall back to the configurable AnonymousOwnerGuid; if that is unset the write is rejected with AccessDenied. |
The former 0.0.0.0:0 sentinel owner resolved to no user, which made GET ?acl return 500. Owners must reference a real principal. |
| Bucket-name validation | SecurityHelper.IsValidBucketName rejects path-traversal and out-of-spec names before they reach the filesystem. |
Bucket names become directory names; ../ must never escape the storage root. |
| Atomic, durable objects | The disk driver writes to a unique temp sibling, flush(true), then File.Move to publish; a short read against the declared length throws instead of publishing a truncated object. |
A crash mid-write must never leave a half-written object visible. |
| Versioning write race | (bucketguid, objectkey, version) is a UNIQUE index; concurrent writers that collide on a version retry with the next number. |
Two simultaneous PUTs to the same key must not silently produce duplicate or lost versions. |
| Conditional requests | If-Match / If-None-Match / If-[Un]Modified-Since are honored on GET/HEAD (304 / 412) and PUT (If-None-Match: * overwrite guard, If-Match optimistic concurrency) per RFC 7232. |
Lets clients do safe caching and lost-update-free updates. |
| Bounded-memory listing | ListObjects(V2) streams rows and stops at the page boundary, then hydrates only that page's keys — it never loads a whole bucket into memory. |
A bucket with millions of keys must not OOM the server on a single ls. |
| Single-pass multipart | CompleteMultipartUpload streams the part files straight into the destination blob (ConcatReadStream) instead of concatenating to a temp file and re-copying. |
Avoids writing every byte twice and the extra temp-space high-water mark on large objects. |
Performance
CosmoS3 is optimized for high-throughput, low-latency workloads. With the kvdirect embedded backend and
a kernel zero-copy GET path, v2.3.0 beats native-config MinIO on every operation on a Linux NVMe server —
decisively on reads (GET up to ~9.85 GiB/s) and listing.
Benchmark: CosmoS3 vs. MinIO — Linux NVMe
CosmoS3 v2.3.0 (kvdirect backend) vs MinIO on a 12-core x86_64 NVMe server (the deployment
target). Both Dockerized with host networking on a single local disk; warp S3 benchmark, 1 MiB / 4 MiB
objects, 15 s, zero errors either side. MinIO runs single-drive (no erasure coding — its fastest
config, the hardest baseline). Sequential runs.
| Operation | Concurrency | CosmoS3 v2.3.0 | MinIO | CosmoS3 vs MinIO |
|---|---|---|---|---|
| PUT 1 MiB | 16 | 333 obj/s | 281 | +19% |
| GET 1 MiB | 8 | 6,109 obj/s (6.1 GiB/s) | 2,510 | +143% (2.4×) |
| GET 1 MiB | 16 | 5,825 obj/s | 2,863 | +103% (2.0×) |
| GET 4 MiB | 16 | 9,850 MiB/s (2,463 obj/s) | 3,328 MiB/s | +196% (3.0×) |
| HEAD | 16 | 26,350 obj/s | 17,919 | +47% |
| LIST | 16 | 126,245 obj/s | 59,511 | +112% (2.1×) |
CosmoS3 wins every operation measured. GET is the standout — the kernel sendfile path streams
full-object bodies disk→socket with no user-space copy, sustaining up to 9.85 GiB/s. PUT is CPU-bound
and was measured on a shared host, so treat its margin as parity-to-favourable; the GET/LIST/HEAD wins
(sendfile + ordered-KV, not CPU-bound the same way) are large and robust.
Key Optimizations:
- Zero-copy GET (kernel sendfile): full-file plaintext GETs go disk→socket via
sendfile(2)/TransmitFile— no user-space copy or read loop; objects ≤ 256 KiB serve from an in-memory file cache. (Ranges / TLS / HTTP-2/3 fall back to buffered streaming.) kvdirectordered-KV metadata: latest-version lookup is a single seek and listing is a native byte-ordered cursor (no SQL parse/plan), driving the LIST 2.1× and HEAD 1.47× wins. See the DatabaseSettings section.- Metadata fast-path cache: opt-in TTL cache (
MetadataCacheSeconds) over credentials / bucket-ACL / object metadata so an authenticated GET/HEAD can hit zero metadata round-trips. - Zero-Allocation I/O:
ArrayPool<byte>across all read/write paths to eliminate transient allocations and GC pressure. - Lock-Free Concurrency:
ConcurrentDictionarybucket registry for non-blocking lookups under high concurrency.
Recent Updates
- Zero-copy GET (v2.3.0): full-file plaintext GETs stream disk→socket via kernel
sendfile— GET up to ~9.85 GiB/s, ~2–3× native MinIO on Linux NVMe. (Requires CosmoApiServer.Core ≥ 3.8.0.) kvdirectembedded backend (v2.2.0): anIS3Repositorydirectly on the CosmoKv ordered-KV engine — no SQL — for single-node deployments; listing 2.1× and HEAD 1.47× vs MinIO.- SigV4 signature verification: requests are now cryptographically verified (constant-time, ±15-min skew), validated against the official AWS test vector — not just parsed.
- Conditional requests (RFC 7232):
If-Match/If-None-Match/If-[Un]Modified-Sinceon GET/HEAD/PUT (304 / 412, overwrite guard, optimistic concurrency). - Object versioning: per-key version rows with a UNIQUE
(bucket, key, version)index and collision-retry assignment;GET/PUT ?versioning,ListObjectVersions, andx-amz-version-id. - Bounded-memory listing:
ListObjects(V2)streams and pages at the database level instead of loading the whole bucket. - Single-pass multipart assembly: parts stream directly into the destination blob — no temp-file double-write.
- Hardened defaults: no default admin key, anonymous writes off unless an owner is configured, path-traversal-safe bucket names.
- Five SQL backends: SQL Server, PostgreSQL, MySQL, SQLite, and the embedded CosmoKv engine, behind one backend-portable
IS3Repository. - Fully async request path:
S3Context.CreateAsyncremoved the sync-over-async body buffering (and the thread-pool workaround it required). - Static website hosting,
aws-chunkedstreaming decode, and a benchmarking suite for comparison against any S3-compatible backend.
Quick Start
1. Run a sample host
A ready-to-run host is included at samples/CosmoS3Host/, with backend-specific variants and demos alongside it:
cd samples/CosmoS3Host # default
dotnet run
| Sample | Purpose |
|---|---|
CosmoS3Host |
Default standalone S3 endpoint |
CosmoS3Host.SqlServer / .Postgres / .MySQL / .SQLite |
Same host wired to each SQL backend |
InternalApiDemo |
Using the lightweight internal REST API (InternalApiKey) |
CosmoBroker.AuthDemo |
Authentication / credential wiring |
2. Wire CosmoS3 into your own CosmoApiServer app
using CosmoS3;
using CosmoS3.Settings;
var settings = new SettingsBase
{
RegionString = "us-east-1",
ValidateSignatures = false, // set true in production
Storage = new StorageSettings
{
StorageType = CosmoS3.Storage.StorageDriverType.Disk,
DiskDirectory = "./data/objects"
},
Database = new DatabaseSettings
{
Hostname = "localhost",
Port = 1433,
DatabaseName = "MyDatabase",
Username = "sa",
Password = "your-password"
},
// Optional: enable CORS for browser-based S3 clients
Cors = new CorsSettings { Enabled = true },
// Optional: enable HTTPS
// CertificatePath = "./certs/server.pfx",
// CertificatePassword = "changeme",
// Optional: enable HTTP/2 cleartext (h2c)
// EnableHttp2 = true,
};
// CosmoS3Application.Create() wires TLS, HTTP/2, CORS, logging, and S3Middleware.
var app = CosmoS3Application.Create(settings, port: 8100);
app.Run();
Or wire manually for full control over the middleware order:
using CosmoApiServer.Core.Hosting;
using CosmoS3;
using CosmoS3.Settings;
var app = CosmoWebApplicationBuilder.Create()
.ListenOn(8100)
.UseHttps("./certs/server.pfx", "changeme") // optional TLS
.UseHttp3() // optional HTTP/3 over QUIC
.UseHttp2() // optional h2c
.UseCors() // optional CORS
.UseLogging()
.UseMiddleware(new S3Middleware(settings))
.Build();
app.Run();
Configuration
SettingsBase
| Property | Type | Default | Description |
|---|---|---|---|
ValidateSignatures |
bool |
true |
Verify AWS SigV4/V2 on every request. When on, signatures are recomputed and checked (not just parsed). Disable for local dev only. |
BaseDomain |
string? |
null |
Set to enable virtual-hosted–style URLs (e.g. "localhost"). Leave null for path-style. |
RegionString |
string |
"us-west-1" |
AWS region identifier returned in responses. |
HeaderApiKey |
string |
"x-api-key" |
HTTP header name for admin API authentication. |
AdminApiKey |
string |
"" |
Secret value expected in HeaderApiKey for admin endpoints. Empty by default — an empty key never matches; set one to enable the admin API. |
InternalApiKey |
string? |
null |
When set, enables the lightweight internal REST API at /internal/ for requests carrying Authorization: Bearer {InternalApiKey}. Null disables it. |
AnonymousOwnerGuid |
string |
"" |
Principal credited as owner when an anonymous / public-write request creates an object. Empty rejects anonymous writes (AccessDenied); set it to a real user GUID to allow them. |
MetadataCacheSeconds |
double |
0 |
Opt-in TTL (seconds) for the in-process metadata fast-path (credentials, bucket ACL, object metadata, latest-version). 0 disables it. Keep small (1–5 s): a revoked credential can still authenticate for up to the TTL on a node that didn't process the revocation. |
Database |
DatabaseSettings |
(required) | Metadata-store connection (any of the six backends). |
Storage |
StorageSettings |
(required) | Object storage configuration. |
Logging |
LoggingSettings |
default | Log level callbacks. |
Debug |
DebugSettings |
default | Enable extra debug output. |
Users / Credentials / Buckets |
List<T> |
empty | Seed in-memory data for no-database mode (testing). |
CertificatePath |
string? |
null |
Path to PFX file for HTTPS. When set, TLS is automatically applied. |
CertificatePassword |
string? |
null |
Password for the PFX certificate. |
EnableHttp2 |
bool |
false |
Enable h2c (HTTP/2 cleartext) support. |
EnableHttp3 |
bool |
false |
Enable HTTP/3 over QUIC on the TLS listener. Requires CertificatePath. |
Cors |
CorsSettings |
disabled | CORS configuration for browser-based S3 clients. |
DatabaseSettings
CosmoS3 selects the backend from DatabaseType; the schema is created/updated at startup via DatabaseFactory.EnsureSchemaAsync.
| Property | Default | Description |
|---|---|---|
DatabaseType |
"mssql" |
One of mssql, postgres, mysql, sqlite, cosmokv, kvdirect. |
ConnectionString |
null |
Full connection string. When set, used verbatim (required for sqlite/cosmokv/kvdirect, e.g. Data Source=...). |
Hostname / Port / Username / Password / DatabaseName / Instance |
— | Convenience fields for the server-based engines; used to build a connection string when ConnectionString is not given. |
For SQL Server the built connection string is:
server=HOSTNAME,PORT;database=DBNAME;user id=USER;password=PASS;TrustServerCertificate=true;
SQLite and the embedded CosmoKv engine need no server — point ConnectionString at a file/data-source path.
kvdirect — embedded ordered-KV metadata (no SQL)
kvdirect stores all metadata directly on the CosmoKv ordered-KV engine — the same LSM store the
cosmokv backend uses, but without the SQL parser/planner/connection-pool on top. Object keys are
byte-ordered (<bucket> 0x00 <key> 0x00 <invVersion>) so the latest version is a single seek and a
bucket listing is a native cursor; there is no schema (the default user/credential/bucket/owner-ACL are
seeded automatically on first open). Point ConnectionString at a directory:
"Database": { "DatabaseType": "kvdirect", "ConnectionString": "Data Source=./cosmos3-kvdirect" }
It is single-process / single-node by design (one embedded store, no shared DB) — the MinIO-style
"just run the binary" deployment. Use a SQL backend (postgres/mssql/mysql) when multiple nodes must
share one metadata store. End-to-end it runs ~6.3× faster across PUT/GET/HEAD/DELETE than the same
engine via SQL; see benchmarks/results/kvdirect-spike-SUMMARY.md.
StorageSettings
| Property | Default | Description |
|---|---|---|
StorageType |
Disk |
Disk is the only currently supported driver |
DiskDirectory |
"./disk/" |
Root directory for object files (no trailing slash) |
TempDirectory |
"./temp/" |
Scratch directory for multipart upload assembly |
Database Schema
CosmoS3 supports multiple database engines including SQL Server, PostgreSQL, MySQL, and SQLite. The schema is automatically created or updated at startup via DatabaseFactory.EnsureSchemaAsync.
The
kvdirectbackend has no SQL schema — the tables below map to a single byte-ordered key namespace on the CosmoKv engine (object rows ato\0<bucket>\0<key>\0<invVersion>, with secondary indexes for name / access-key / object-GUID lookups), and the default user/credential/bucket/owner-ACL are seeded on first open. Seekvdirect.
Key Tables
| Table | Purpose |
|---|---|
s3_users |
S3 user accounts |
s3_credentials |
Access key / secret key pairs linked to users |
s3_buckets |
Bucket metadata (name, owner, region, storage config) |
s3_objects |
Object metadata (key, size, ETag, version, blob reference) |
s3_objecttags |
Per-object tags |
s3_buckettags |
Per-bucket tags |
s3_uploads |
Active multipart upload sessions |
s3_uploadparts |
Uploaded parts for active sessions |
Key Indexes for Performance & Correctness
The schema includes composite indexes that stay fast with millions of objects and also enforce integrity:
ux_s3_objects_bucket_key_version: UNIQUE(bucketguid, objectkey, version DESC)— serves the common "get latest version" lookup and makes the versioning write race impossible at the storage layer (collisions retry with the next version). On CosmoKv theDESCqualifier is dropped (engine limitation) but the uniqueness constraint is identical.idx_s3_objects_guid:(guid)— blob/version resolution by object GUID.idx_s3_credentials_accesskey:(accesskey)— rapid authentication.idx_s3_buckets_name:(name)— fast bucket resolution.
All database operations go through the static DataAccess facade over the backend-portable IS3Repository. The SQL backends use S3Repository (parameterized SQL — no stored procedures, so the same code runs unchanged on every engine); kvdirect uses KvDirectRepository, which implements the identical contract directly on ordered-KV primitives. The versioning write race is prevented on both: a UNIQUE (bucket, key, version) index on SQL, and optimistic-commit conflict retry on kvdirect.
S3 Feature Compatibility
Service-Level Operations
| Operation | AWS CLI command | Status |
|---|---|---|
| List Buckets | aws s3 ls |
✅ |
Bucket Operations
| Operation | AWS CLI command | Status |
|---|---|---|
| Create Bucket | aws s3 mb s3://bucket |
✅ |
| Delete Bucket | aws s3 rb s3://bucket |
✅ |
| List Objects (v1 & v2) | aws s3 ls s3://bucket/ |
✅ |
| Get Bucket ACL | aws s3api get-bucket-acl |
✅ |
| Put Bucket ACL | aws s3api put-bucket-acl |
✅ |
| Get Bucket Tags | aws s3api get-bucket-tagging |
✅ |
| Put Bucket Tags | aws s3api put-bucket-tagging |
✅ |
| Delete Bucket Tags | aws s3api delete-bucket-tagging |
✅ |
| Get/Put/Delete Bucket Website | aws s3api *-bucket-website |
✅ |
| Get Bucket Location | aws s3api get-bucket-location |
✅ |
| Get Bucket Versioning | aws s3api get-bucket-versioning |
✅ |
Object Operations
| Operation | AWS CLI command | Status |
|---|---|---|
| Put Object | aws s3 cp local.txt s3://bucket/key |
✅ |
| Get Object | aws s3 cp s3://bucket/key local.txt |
✅ |
| Head Object | aws s3api head-object |
✅ |
| Delete Object | aws s3 rm s3://bucket/key |
✅ |
| Delete Objects (batch) | aws s3 sync --delete |
✅ |
| Copy Object | aws s3 cp s3://src s3://dst |
✅ |
| Get Object ACL | aws s3api get-object-acl |
✅ |
| Put Object ACL | aws s3api put-object-acl |
✅ |
| Get Object Tags | aws s3api get-object-tagging |
✅ |
| Put Object Tags | aws s3api put-object-tagging |
✅ |
| Delete Object Tags | aws s3api delete-object-tagging |
✅ |
| Presigned GET/PUT URLs | SDK GetPreSignedURL |
✅ |
| Multipart Upload | aws s3 cp (large files) |
✅ |
| List Multipart Uploads | aws s3api list-multipart-uploads |
✅ |
| Abort Multipart Upload | aws s3api abort-multipart-upload |
✅ |
| Conditional GET/HEAD/PUT | If-Match / If-None-Match / If-[Un]Modified-Since |
✅ |
| List Object Versions | aws s3api list-object-versions |
✅ |
Notes on Compatibility
- Signature versions: Both SigV4 and SigV2 are supported for authentication and presigned URLs. With
ValidateSignaturesenabled (default), SigV4 signatures are cryptographically verified, not merely parsed. - Conditional requests: GET/HEAD return
304 Not Modified/412 Precondition Failed; PUT supportsIf-None-Match: *(don't overwrite) andIf-Match(optimistic concurrency) per RFC 7232. aws-chunkedtransfer encoding: Automatically decoded via the async body path; works withaws s3 cpfor any file size.- Versioning: Supported. Toggle with
PUT ?versioning; when enabled, writes create new version rows, deletes write delete markers, and responses carryx-amz-version-id. List historic versions withlist-object-versions. - Anonymous writes: Off by default — set
AnonymousOwnerGuidto a real user GUID to allow public-write buckets. - Bucket policies / lifecycle / replication: Not implemented. (CORS is configured server-side via
CorsSettings, not per-bucket.)
Static Website Hosting
A bucket can be configured to serve static files over plain HTTP (no AWS credentials required).
Configure a bucket for website hosting
# Create bucket
aws --endpoint-url http://localhost:8100 s3 mb s3://my-site
# Upload content
aws --endpoint-url http://localhost:8100 s3 cp index.html s3://my-site/index.html --content-type text/html
aws --endpoint-url http://localhost:8100 s3 cp error.html s3://my-site/error.html --content-type text/html
# Enable website hosting
aws --endpoint-url http://localhost:8100 s3 website s3://my-site \
--index-document index.html \
--error-document error.html
Browse the site
# Bucket root returns index.html
curl http://localhost:8100/my-site/
# Unknown path returns error.html with 404
curl http://localhost:8100/my-site/missing.html
Redirect all requests
aws --endpoint-url http://localhost:8100 s3api put-bucket-website \
--bucket my-site \
--website-configuration '{
"RedirectAllRequestsTo": { "HostName": "example.com", "Protocol": "https" }
}'
Routing rules
aws --endpoint-url http://localhost:8100 s3api put-bucket-website \
--bucket my-site \
--website-configuration '{
"IndexDocument": { "Suffix": "index.html" },
"ErrorDocument": { "Key": "error.html" },
"RoutingRules": [
{
"Condition": { "KeyPrefixEquals": "old/" },
"Redirect": { "ReplaceKeyPrefixWith": "new/" }
}
]
}'
How it works:
- Website configuration is stored as
website.xmlat<DiskDirectory>/<bucketName>/website.xml. - Requests to a website-enabled bucket without AWS authentication headers are served as static files.
- If the request path ends with
/, the index document is served. - If the object is not found, the error document is returned with HTTP 404.
- Redirect rules are evaluated before object lookup.
Presigned URLs
Presigned URLs grant time-limited access to an S3 object without requiring the caller to have AWS credentials.
Generate a presigned URL (C# SDK)
var request = new GetPreSignedUrlRequest
{
BucketName = "my-bucket",
Key = "my-object.txt",
Expires = DateTime.UtcNow.AddMinutes(15),
Verb = HttpVerb.GET
};
string url = s3Client.GetPreSignedURL(request);
Use the presigned URL
# Download with curl (no AWS credentials needed)
curl "<presigned-url>" -o downloaded.txt
# Upload with a presigned PUT URL
curl -X PUT "<presigned-put-url>" --data-binary @file.txt
Signature version behavior:
AWSSDK generates SigV2 presigned URLs for custom (non-AWS) endpoints. CosmoS3 validates both:
| Version | Query params |
|---|---|
| SigV2 | AWSAccessKeyId, Signature, Expires (Unix timestamp) |
| SigV4 | X-Amz-Credential, X-Amz-Signature, X-Amz-Expires |
Expired presigned URLs return HTTP 403 ExpiredToken.
Multipart Upload
Multipart upload allows large files to be uploaded in parts and assembled server-side.
Via AWS CLI (automatic for files > 8 MB by default)
# CosmoS3 handles chunked uploads transparently
aws --endpoint-url http://localhost:8100 \
s3 cp large-file.bin s3://my-bucket/large-file.bin \
--expected-size 1073741824 # hint for 1 GB file
Via SDK (manual)
// 1. Initiate upload
var initResponse = await s3.InitiateMultipartUploadAsync(new InitiateMultipartUploadRequest
{
BucketName = "my-bucket",
Key = "my-object"
});
string uploadId = initResponse.UploadId;
// 2. Upload parts (minimum 5 MB each, except the last)
var uploadPartResponse = await s3.UploadPartAsync(new UploadPartRequest
{
BucketName = "my-bucket",
Key = "my-object",
UploadId = uploadId,
PartNumber = 1,
InputStream = partStream,
PartSize = partStream.Length
});
// 3. Complete the upload
await s3.CompleteMultipartUploadAsync(new CompleteMultipartUploadRequest
{
BucketName = "my-bucket",
Key = "my-object",
UploadId = uploadId,
PartETags = new List<PartETag> { new PartETag(1, uploadPartResponse.ETag) }
});
Internals:
- Each uploaded part is written to a part file under
TempDirectoryand hashed on arrival. CompleteMultipartUploadstreams the ordered part files directly into the destination blob viaConcatReadStream(single pass — no concatenate-to-temp-then-recopy), then deletes the parts. The object length is the sum of the recorded part lengths.- Active sessions and their parts are tracked in
s3_uploads/s3_uploadpartsand can be listed or aborted.
Using with AWS CLI
Configure the AWS CLI for local use
aws configure
# AWS Access Key ID: default
# AWS Secret Access Key: default
# Default region name: us-east-1
# Default output format: json
Common commands
ENDPOINT=http://localhost:8100
# List buckets
aws --endpoint-url $ENDPOINT s3 ls
# Create bucket
aws --endpoint-url $ENDPOINT s3 mb s3://my-bucket
# Upload file
aws --endpoint-url $ENDPOINT s3 cp file.txt s3://my-bucket/
# Download file
aws --endpoint-url $ENDPOINT s3 cp s3://my-bucket/file.txt ./
# List objects
aws --endpoint-url $ENDPOINT s3 ls s3://my-bucket/
# Sync directory
aws --endpoint-url $ENDPOINT s3 sync ./local-dir/ s3://my-bucket/prefix/
# Delete object
aws --endpoint-url $ENDPOINT s3 rm s3://my-bucket/file.txt
# Delete bucket (must be empty)
aws --endpoint-url $ENDPOINT s3 rb s3://my-bucket
# Tag a bucket
aws --endpoint-url $ENDPOINT s3api put-bucket-tagging \
--bucket my-bucket \
--tagging '{"TagSet":[{"Key":"env","Value":"dev"}]}'
# Get bucket tags
aws --endpoint-url $ENDPOINT s3api get-bucket-tagging --bucket my-bucket
# Get bucket website config
aws --endpoint-url $ENDPOINT s3api get-bucket-website --bucket my-bucket
# Presigned URL (60 seconds)
aws --endpoint-url $ENDPOINT s3 presign s3://my-bucket/file.txt --expires-in 60
Testing
The suite (tests/CosmoS3.Tests/) uses xUnit + AWSSDK.S3 and needs no external database or running server — it boots the real server in-process against an embedded CosmoKv DB.
Tests are split into two xUnit traits that must run as separate dotnet test invocations, because the server's data layer is a process-global singleton (DataAccess._repo), so only one server instance can exist per process:
# Server-free unit tests (signature math, helpers, conditional-header logic, ...)
dotnet test tests/CosmoS3.Tests --filter "Category=Unit"
# Integration tests — boot the in-process server, drive it through the AWS SDK
dotnet test tests/CosmoS3.Tests --filter "Category=Integration"
CI runs the two lanes as separate steps (.github/workflows/ci.yml). Current status: 62 unit + 12 integration, all passing.
Fixtures
CosmoServerFixture(integration) boots the server withCosmoWebApplication.RunAsyncon an ephemeral port over a temp CosmoKv database, and hands tests a configuredAmazonS3Client(NewS3Client()). Shared via the[Collection("CosmoServer")]xUnit collection.S3Fixtureis the legacy fixture that targets an externally running endpoint, retained for the original end-to-end tests.
Representative coverage
| Area | Tests |
|---|---|
SigV4 signing math (official AWS get-vanilla vector) + live accept/reject |
SignatureV4Tests, SignatureV4IntegrationTests |
| Conditional headers (304 / 412, RFC 7232 precedence) | ConditionalHeadersTests, ConditionalHeadersIntegrationTests |
| Listing: pagination, delimiter common-prefixes, delete-marker hiding | ListingPagingIntegrationTests |
| Multipart ordered round-trip | MultipartIntegrationTests |
| ACL owner resolution after authenticated PUT (M2) | OwnerAclIntegrationTests |
| Security helpers, data integrity, streaming safety, bucket cache | SecurityHelperTests, DataIntegrityTests, StreamingSafetyTests, BucketCacheTests |
| Bucket / object / multipart / presigned / website end-to-end | BucketTests, ObjectTests, MultipartTests, PresignedUrlTests, WebsiteTests |
Project Structure
src/CosmoS3/
├── S3Middleware.cs # IMiddleware entry point; async parse, SigV4 verify, routing
├── S3Request.cs # HTTP → S3 request parsing (CreateAsync, aws-chunked decode)
├── S3Response.cs # S3-formatted response writer
├── S3Context.cs # Combined request + response context (CreateAsync factory)
├── S3Exception.cs # Typed S3 error thrown by handlers
├── SignatureV4Verifier.cs # Recompute + constant-time compare of the SigV4 signature
├── DataAccess.cs # Static facade over IS3Repository
├── IS3Repository.cs # Backend-portable data-layer contract
├── S3Repository.cs # Parameterized-SQL implementation (all 5 SQL backends)
├── KvDirectRepository.cs # Embedded ordered-KV implementation (no SQL — DatabaseType=kvdirect)
├── DatabaseFactory.cs # Backend selection + schema bootstrap
├── CosmoS3Application.cs # Turn-key host builder (TLS / HTTP2·3 / CORS / middleware)
├── Settings/ # SettingsBase, Database/Storage/Logging/Debug/Cors settings
├── Helpers/
│ ├── SignatureV4.cs # SigV4 canonical-request + signing-key primitives
│ ├── SecurityHelper.cs # FixedTimeEquals, ConfiguredKeyMatches, IsValidBucketName
│ ├── ConditionalHeaders.cs# RFC 7232 If-Match / If-None-Match / If-[Un]Modified-Since
│ ├── ConcatReadStream.cs # Single-pass multipart assembly stream
│ ├── AclConverter.cs # ACL ⇆ policy conversion (grantees validated vs DB)
│ ├── RequestValidator.cs # Shared handler precondition checks
│ └── HashHelper.cs # Single-pass MD5/SHA hashing
├── Classes/
│ ├── AuthManager.cs # Credential/user resolution + authorization decision
│ ├── BucketManager.cs # Lock-free ConcurrentDictionary bucket registry
│ ├── BucketClient.cs # Per-bucket storage driver accessor
│ ├── ConfigManager.cs # User / credential / bucket / object lookup
│ └── CleanupManager.cs # Background task: expire stale temp files
├── Api/S3/
│ ├── ApiHandler.cs # Top-level dispatcher (service/bucket/object)
│ ├── ServiceHandler.cs # ListBuckets
│ ├── BucketHandler.cs # Bucket operations (incl. versioning, website, ACL, tags)
│ ├── ObjectHandler.cs # Object operations + multipart + conditional PUT
│ └── ApiHelper.cs # Shared XML response helpers
├── Storage/
│ ├── StorageDriverBase.cs
│ └── DiskStorageDriver.cs # Atomic temp-then-move writes; ArrayPool + SubStream I/O
├── Schema/ # Per-backend DDL: mssql · postgres · mysql · sqlite · cosmokv
├── S3Objects/ # XML DTOs (request/response bodies)
└── Logging/
└── S3Logger.cs # Console/callback-based logger
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- CosmoApiServer.Core (>= 3.8.1)
- CosmoNova (>= 6.43.2)
- CosmoSQLClient.Core (>= 6.3.1)
- CosmoSQLClient.CosmoKv (>= 6.2.7)
- CosmoSQLClient.MsSql (>= 6.3.1)
- CosmoSQLClient.MySql (>= 6.3.1)
- CosmoSQLClient.Postgres (>= 6.3.1)
- CosmoSQLClient.Sqlite (>= 6.3.1)
- Microsoft.Extensions.Caching.Memory (>= 10.0.9)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 2.3.5 | 0 | 6/16/2026 |
| 2.3.4 | 0 | 6/16/2026 |
| 2.3.3 | 0 | 6/16/2026 |
| 2.3.2 | 0 | 6/16/2026 |
| 2.3.1 | 52 | 6/14/2026 |
| 2.3.0 | 55 | 6/14/2026 |
| 2.2.0 | 54 | 6/14/2026 |
| 2.1.0 | 48 | 6/14/2026 |
| 2.0.1 | 50 | 6/14/2026 |
| 2.0.0 | 47 | 6/13/2026 |
| 1.9.25 | 95 | 5/24/2026 |
| 1.9.24 | 95 | 5/24/2026 |
| 1.9.23 | 102 | 5/23/2026 |
| 1.9.22 | 101 | 5/18/2026 |
| 1.9.21 | 123 | 5/18/2026 |
| 1.9.20 | 95 | 5/10/2026 |
| 1.9.19 | 102 | 5/10/2026 |
| 1.9.18 | 105 | 5/10/2026 |
| 1.9.17 | 92 | 5/3/2026 |
| 1.9.16 | 105 | 4/26/2026 |