![]() |
VOOZH | about |
dotnet add package S3Server --version 7.2.0
NuGet\Install-Package S3Server -Version 7.2.0
<PackageReference Include="S3Server" Version="7.2.0" />
<PackageVersion Include="S3Server" Version="7.2.0" />Directory.Packages.props
<PackageReference Include="S3Server" />Project file
paket add S3Server --version 7.2.0
#r "nuget: S3Server, 7.2.0"
#:package S3Server@7.2.0
#addin nuget:?package=S3Server&version=7.2.0Install as a Cake Addin
#tool nuget:?package=S3Server&version=7.2.0Install as a Cake Tool
S3Server is a lightweight, high-performance .NET library that provides a server-side interface for building Amazon S3-compatible storage services. It parses incoming S3 HTTP requests and routes them to your callback implementations, allowing you to focus on storage logic rather than protocol details.
S3Server is a protocol adapter that handles the complexity of the Amazon S3 REST API, allowing you to build S3-compatible storage servers without dealing with HTTP parsing, XML serialization, signature validation, or AWS-specific request routing.
What S3Server does:
What S3Server does NOT do:
Want a complete S3-compatible storage server built using S3Server? Check out Less3.
✅ Complete S3 API Coverage
✅ URL Style Support
http://host:port/bucket/key (default)http://bucket.domain/key (configurable)✅ Security & Validation
✅ Developer Friendly
dotnet add package S3Server
using S3ServerLibrary;
using S3ServerLibrary.S3Objects;
namespace S3ServerLibrary
{
using System;
using System.Threading.Tasks;
using WatsonWebserver.Core;
// Configure server settings
S3ServerSettings settings = new S3ServerSettings();
settings.Webserver = new WebserverSettings("localhost", 8000, false);
settings.Logger = Console.WriteLine;
// Create and configure server
S3Server server = new S3Server(settings);
// Wire up callbacks
server.Service.ListBuckets = async (ctx) =>
{
ListAllMyBucketsResult result = new ListAllMyBucketsResult();
result.Owner = new Owner("admin", "Administrator");
result.Buckets = new Buckets(new List<Bucket>
{
new Bucket("my-bucket", DateTime.UtcNow)
});
return result;
};
server.Bucket.Exists = async (ctx) =>
{
// Check if bucket exists in your storage
return true;
};
server.Object.Write = async (ctx) =>
{
// Save object data from ctx.Request.Data stream
Console.WriteLine($"Writing object: {ctx.Request.Bucket}/{ctx.Request.Key}");
Console.WriteLine($"Content length: {ctx.Request.ContentLength}");
// Implement your storage logic here
};
server.Object.Read = async (ctx) =>
{
// Retrieve object from your storage
byte[] data = System.Text.Encoding.UTF8.GetBytes("Hello, S3!");
return new S3Object(
ctx.Request.Key,
"version-1",
true,
DateTime.UtcNow,
"etag-123",
data.Length,
new Owner("admin", "Administrator"),
data,
"text/plain",
StorageClassEnum.STANDARD
);
};
// Start server
server.Start();
Console.WriteLine("S3 Server listening on http://localhost:8000");
}
S3ServerSettings settings = new S3ServerSettings
{
// Required: Webserver configuration
Webserver = new WebserverSettings("localhost", 8000, false),
// Optional: Logger for diagnostic output
Logger = (msg) => Console.WriteLine(msg),
// Optional: Enable specific logging categories
Logging = new LoggingSettings
{
HttpRequests = true,
S3Requests = true,
SignatureV4Validation = false
},
// Optional: Operation limits
OperationLimits = new OperationLimitsSettings
{
MaxPutObjectSize = 5368709120 // 5GB default
},
// Optional: Enable AWS Signature V4 validation
EnableSignatures = false,
// Optional: Enable legacy AWS Signature V2 validation when signatures are enabled
EnableSignatureV2 = false,
// Note: UseTcpServer is deprecated in v7.0; Watson now uses TCP natively
UseTcpServer = false
};
S3Server provides hooks to intercept requests at different stages:
// Pre-request handler (auth, logging, validation)
// Return true to terminate request, false to continue routing
settings.PreRequestHandler = async (ctx) =>
{
// Check authentication
if (!IsAuthenticated(ctx))
{
ctx.Response.StatusCode = 403;
await ctx.Response.Send(ErrorCode.AccessDenied);
return true; // Terminate
}
// Add custom metadata for downstream callbacks
ctx.Metadata = new { UserId = "user123" };
return false; // Continue to callback routing
};
// Default request handler (called when no callback matches)
settings.DefaultRequestHandler = async (ctx) =>
{
Console.WriteLine($"Unhandled request: {ctx.Request.RequestType}");
await ctx.Response.Send(ErrorCode.InvalidRequest);
};
// Post-request handler (logging, metrics)
settings.PostRequestHandler = async (ctx) =>
{
Console.WriteLine($"Completed: {ctx.Request.RequestType} - {ctx.Response.StatusCode}");
// Log metrics, update statistics, etc.
};
Enable AWS signature validation for authenticated requests. Signature V4 is enabled by EnableSignatures; legacy Signature V2 remains disabled unless EnableSignatureV2 is also set.
settings.EnableSignatures = true;
settings.EnableSignatureV2 = false; // Set true only for legacy S3-compatible clients
settings.Logging.SignatureV4Validation = true; // Optional debug logging
// Implement callback to retrieve secret key for access key
server.Service.GetSecretKey = (ctx) =>
{
string accessKey = ctx.Request.AccessKey;
// Look up and return the secret key for this access key
return "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY";
// Or throw exception if access key is invalid
// throw new S3Exception(new Error(ErrorCode.InvalidAccessKeyId));
};
When EnableSignatureV2 is true, S3Server validates legacy V2 header signatures such as Authorization: AWS AWSAccessKeyId:Signature and V2 signed URLs containing AWSAccessKeyId, Expires, and Signature. Expired V2 signed URLs are rejected. V2 support is intended for compatibility with older S3 clients and S3-compatible storage ecosystems; AWS has deprecated Signature V2, so leave it disabled unless you need that legacy behavior.
Support bucket names in hostnames (http://bucket.s3.local/key instead of http://s3.local/bucket/key):
// 1. Use wildcard listener (requires admin privileges on Windows)
settings.Webserver.Hostname = "*"; // or "+" or "0.0.0.0"
// 2. Implement base domain finder
server.Service.FindMatchingBaseDomain = (hostname) =>
{
// Input: "mybucket.s3.local.gd"
// Output: "s3.local.gd" (the base domain)
if (hostname.EndsWith(".s3.local.gd"))
return "s3.local.gd";
if (hostname.EndsWith(".s3.example.com"))
return "s3.example.com";
// No match found - will be treated as path-style
throw new KeyNotFoundException($"No base domain for {hostname}");
};
DNS Configuration:
hosts file to resolve bucket subdomains*.local.gd and *.fbi.com resolve to localhostmybucket.s3.local.gd → 127.0.0.1server.Bucket.ReadAcl = async (ctx) =>
{
AccessControlList acl = new AccessControlList(
new List<Grant>
{
new Grant(
new Grantee("admin", "Administrator", null, "CanonicalUser", "admin@example.com"),
PermissionEnum.FullControl
)
}
);
return new AccessControlPolicy(
new Owner("admin", "Administrator"),
acl
);
};
server.Bucket.Delete = async (ctx) =>
{
string bucketName = ctx.Request.Bucket;
// Delete bucket from your storage
DeleteBucketFromStorage(bucketName);
// Return normally - S3Server sends 204 No Content
return;
};
server.Bucket.Exists = async (ctx) =>
{
if (!BucketExistsInStorage(ctx.Request.Bucket))
{
throw new S3Exception(new Error(ErrorCode.NoSuchBucket));
}
return true;
};
// Set metadata in PreRequestHandler
settings.PreRequestHandler = async (ctx) =>
{
ctx.Metadata = new { TenantId = GetTenantFromAuth(ctx) };
return false;
};
// Access metadata in callbacks
server.Object.Write = async (ctx) =>
{
dynamic metadata = ctx.Metadata;
string tenantId = metadata.TenantId;
// Use tenant context for multi-tenant storage
SaveObject(tenantId, ctx.Request.Bucket, ctx.Request.Key, ctx.Request.Data);
};
| Callback | Description | Method | URL | Response Type |
|---|---|---|---|---|
Service.ListBuckets |
List all buckets | GET | / |
ListAllMyBucketsResult |
Service.ServiceExists |
Check service and return region | HEAD | / |
string (region) |
Service.FindMatchingBaseDomain |
Find base domain for virtual hosting | N/A | N/A | string (base domain) |
Service.GetSecretKey |
Get secret key for access key (auth) | N/A | N/A | string (secret key) |
| Callback | Description | Method | URL | Response Type |
|---|---|---|---|---|
Bucket.Write |
Create a bucket | PUT | /[bucket] |
void |
Bucket.Read |
List objects in bucket | GET | /[bucket] |
ListBucketResult |
Bucket.Exists |
Check if bucket exists | HEAD | /[bucket] |
bool |
Bucket.Delete |
Delete a bucket | DELETE | /[bucket] |
void |
Bucket.ReadAcl |
Read bucket ACL | GET | /[bucket]?acl |
AccessControlPolicy |
Bucket.WriteAcl |
Write bucket ACL | PUT | /[bucket]?acl |
void |
Bucket.DeleteAcl |
Delete bucket ACL | DELETE | /[bucket]?acl |
void |
Bucket.ReadLocation |
Get bucket region | GET | /[bucket]?location |
LocationConstraint |
Bucket.ReadLogging |
Get logging config | GET | /[bucket]?logging |
BucketLoggingStatus |
Bucket.WriteLogging |
Set logging config | PUT | /[bucket]?logging |
void |
Bucket.ReadTagging |
Get bucket tags | GET | /[bucket]?tagging |
Tagging |
Bucket.WriteTagging |
Set bucket tags | PUT | /[bucket]?tagging |
void |
Bucket.DeleteTagging |
Delete bucket tags | DELETE | /[bucket]?tagging |
void |
Bucket.ReadVersioning |
Get versioning config | GET | /[bucket]?versioning |
VersioningConfiguration |
Bucket.WriteVersioning |
Set versioning config | PUT | /[bucket]?versioning |
void |
Bucket.ReadVersions |
List object versions | GET | /[bucket]?versions |
ListVersionsResult |
Bucket.ReadWebsite |
Get website config | GET | /[bucket]?website |
WebsiteConfiguration |
Bucket.WriteWebsite |
Set website config | PUT | /[bucket]?website |
void |
Bucket.DeleteWebsite |
Delete website config | DELETE | /[bucket]?website |
void |
Bucket.ReadMultipartUploads |
List multipart uploads | GET | /[bucket]?uploads |
ListMultipartUploadsResult |
| Callback | Description | Method | URL | Response Type |
|---|---|---|---|---|
Object.Write |
Upload object | PUT | /[bucket]/[key] |
void |
Object.Read |
Download object | GET | /[bucket]/[key] |
S3Object |
Object.Exists |
Check if object exists | HEAD | /[bucket]/[key] |
ObjectMetadata |
Object.Delete |
Delete object | DELETE | /[bucket]/[key] |
void |
Object.ReadRange |
Download byte range | GET | /[bucket]/[key]* |
S3Object |
Object.ReadAcl |
Get object ACL | GET | /[bucket]/[key]?acl |
AccessControlPolicy |
Object.WriteAcl |
Set object ACL | PUT | /[bucket]/[key]?acl |
void |
Object.DeleteAcl |
Delete object ACL | DELETE | /[bucket]/[key]?acl |
void |
Object.ReadTagging |
Get object tags | GET | /[bucket]/[key]?tagging |
Tagging |
Object.WriteTagging |
Set object tags | PUT | /[bucket]/[key]?tagging |
void |
Object.DeleteTagging |
Delete object tags | DELETE | /[bucket]/[key]?tagging |
void |
Object.ReadLegalHold |
Get legal hold status | GET | /[bucket]/[key]?legal-hold |
LegalHold |
Object.WriteLegalHold |
Set legal hold status | PUT | /[bucket]/[key]?legal-hold |
void |
Object.ReadRetention |
Get retention status | GET | /[bucket]/[key]?retention |
Retention |
Object.WriteRetention |
Set retention status | PUT | /[bucket]/[key]?retention |
void |
Object.DeleteMultiple |
Delete multiple objects | POST | /[bucket]?delete |
DeleteResult |
Object.Restore |
Restore archived object | POST | /[bucket]/[key]?restore |
RestoreObjectResult |
Object.SelectContent |
S3 Select query | POST | /[bucket]/[key]?select&select-type=2 |
void |
* ReadRange is triggered when Range header is present
For archived objects, set ObjectMetadata.RestoreStatus and S3Object.RestoreStatus to have S3Server emit the x-amz-restore response header on HEAD and GET.
| Callback | Description | Method | URL | Response Type |
|---|---|---|---|---|
Object.CreateMultipartUpload |
Initiate multipart upload | POST | /[bucket]/[key]?uploads |
InitiateMultipartUploadResult |
Object.UploadPart |
Upload a part | PUT | /[bucket]/[key]?partNumber=N&uploadId=ID |
void |
Object.ReadParts |
List uploaded parts | GET | /[bucket]/[key]?uploadId=ID |
ListPartsResult |
Object.CompleteMultipartUpload |
Complete upload | POST | /[bucket]/[key]?uploadId=ID |
CompleteMultipartUploadResult |
Object.AbortMultipartUpload |
Abort upload | DELETE | /[bucket]/[key]?uploadId=ID |
void |
The S3Context object is passed to all callbacks:
public class S3Context
{
// Parsed S3 request details
public S3Request Request { get; }
// Response builder
public S3Response Response { get; }
// Underlying HTTP context (WatsonWebserver)
public HttpContextBase Http { get; }
// User-defined metadata (set in PreRequestHandler)
public object Metadata { get; set; }
// Timestamp information
public Timestamp Timestamp { get; }
}
Key properties available in S3Context.Request:
// Request identification
string RequestId // Unique request ID
string TraceId // Trace ID for debugging
// Request type and style
S3RequestType RequestType // Enum: ServiceExists, BucketWrite, ObjectRead, etc.
S3RequestStyle RequestStyle // PathStyle or VirtualHostedStyle
// S3 resource identifiers
string Bucket // Bucket name
string Key // Object key
string VersionId // Version ID (if versioning enabled)
// Authentication
string AccessKey // AWS access key
string Signature // Request signature
S3SignatureVersion SignatureVersion // Version2, Version4, or Unknown
// Content details
long ContentLength // Request body size
string ContentType // Content type
Stream Data // Request body stream
string DataAsString // Request body as string (fully reads stream)
byte[] DataAsBytes // Request body as bytes (fully reads stream)
// Range requests
long? RangeStart // Start byte for range request
long? RangeEnd // End byte for range request
// Listing parameters
int MaxKeys // Maximum keys to return (default 1000)
string Prefix // Object key prefix filter
string Delimiter // Delimiter for grouping
string Marker // Pagination marker
string ContinuationToken // Continuation token for v2 listing
// Multipart upload
string UploadId // Multipart upload ID
int PartNumber // Part number for multipart upload
int MaxParts // Maximum parts to return
// Permissions
S3PermissionType PermissionsRequired // Permission needed for this operation
// Helper methods
bool HeaderExists(string key)
bool QuerystringExists(string key)
string RetrieveHeaderValue(string key)
string RetrieveQueryValue(string key)
Task<Chunk> ReadChunk() // Read chunk for chunked transfer encoding
Methods for sending responses:
// Send empty response
await ctx.Response.Send();
// Send string response
await ctx.Response.Send("response data");
// Send byte array response
await ctx.Response.Send(bytes);
// Send stream response
await ctx.Response.Send(contentLength, stream);
// Send error response
await ctx.Response.Send(ErrorCode.NoSuchBucket);
await ctx.Response.Send(new Error(ErrorCode.AccessDenied));
// Chunked transfer encoding
await ctx.Response.SendChunk(chunkData, isFinal);
// Set response properties before sending
ctx.Response.StatusCode = 200;
ctx.Response.ContentType = "application/json";
ctx.Response.ContentLength = data.Length;
ctx.Response.Headers.Add("X-Custom-Header", "value");
S3Server provides comprehensive error handling with S3-compliant error codes:
server.Object.Read = async (ctx) =>
{
if (!ObjectExists(ctx.Request.Key))
{
throw new S3Exception(new Error(ErrorCode.NoSuchKey));
}
if (!HasPermission(ctx, ctx.Request.Key))
{
throw new S3Exception(new Error(ErrorCode.AccessDenied));
}
// ... return object
};
Common error codes:
ErrorCode.NoSuchBucket - 404ErrorCode.NoSuchKey - 404ErrorCode.AccessDenied - 403ErrorCode.BucketAlreadyExists - 409ErrorCode.BucketNotEmpty - 409ErrorCode.EntityTooLarge - 400ErrorCode.InvalidBucketName - 400ErrorCode.InternalError - 500ErrorCode.SignatureDoesNotMatch - 403See S3Objects/ErrorCode.cs for the complete list of 60+ error codes.
Use the AWS SDK to connect to your S3Server instance:
using Amazon;
using Amazon.Runtime;
using Amazon.S3;
using Amazon.S3.Model;
BasicAWSCredentials cred = new BasicAWSCredentials("access-key", "secret-key");
AmazonS3Config config = new AmazonS3Config
{
ServiceURL = "http://localhost:8000/",
// Use path-style URLs (bucket in path, not hostname)
ForcePathStyle = true,
// Or use virtual-hosted-style URLs
// ForcePathStyle = false,
UseHttp = true,
// Optional: Set region
AuthenticationRegion = "us-west-1"
};
IAmazonS3 client = new AmazonS3Client(cred, config);
// Use client
await client.PutBucketAsync("my-bucket");
await client.PutObjectAsync(new PutObjectRequest
{
BucketName = "my-bucket",
Key = "test.txt",
ContentBody = "Hello, S3!"
});
Handle chunked uploads (e.g., AWS CLI streaming uploads):
server.Object.Write = async (ctx) =>
{
if (ctx.Request.Chunked)
{
List<byte[]> chunks = new List<byte[]>();
while (true)
{
Chunk chunk = await ctx.Request.ReadChunk();
if (chunk.Length > 0)
{
chunks.Add(chunk.Data);
}
if (chunk.IsFinal)
break;
}
byte[] completeData = CombineChunks(chunks);
SaveObject(ctx.Request.Bucket, ctx.Request.Key, completeData);
}
else
{
// Non-chunked upload
SaveObject(ctx.Request.Bucket, ctx.Request.Key, ctx.Request.DataAsBytes);
}
};
// 1. Initiate
server.Object.CreateMultipartUpload = async (ctx) =>
{
string uploadId = Guid.NewGuid().ToString();
// Store upload metadata
StoreUploadMetadata(ctx.Request.Bucket, ctx.Request.Key, uploadId);
return new InitiateMultipartUploadResult(
ctx.Request.Bucket,
ctx.Request.Key,
uploadId
);
};
// 2. Upload parts
server.Object.UploadPart = async (ctx) =>
{
string uploadId = ctx.Request.UploadId;
int partNumber = ctx.Request.PartNumber;
// Store part data
StorePart(uploadId, partNumber, ctx.Request.DataAsBytes);
// Set ETag header for part
string etag = CalculateETag(ctx.Request.DataAsBytes);
ctx.Response.Headers.Add("ETag", etag);
};
// 3. Complete
server.Object.CompleteMultipartUpload = async (ctx, request) =>
{
// Combine parts in order
byte[] finalData = CombineParts(ctx.Request.UploadId, request.Parts);
// Save final object
SaveObject(ctx.Request.Bucket, ctx.Request.Key, finalData);
// Clean up parts
CleanupUpload(ctx.Request.UploadId);
return new CompleteMultipartUploadResult
{
Location = $"http://localhost:8000/{ctx.Request.Bucket}/{ctx.Request.Key}",
Bucket = ctx.Request.Bucket,
Key = ctx.Request.Key,
ETag = CalculateETag(finalData)
};
};
server.Object.Restore = async (ctx, request) =>
{
// Start restore work in your backend, or extend an existing restore window.
bool alreadyRestored = TryExtendRestoreWindow(
ctx.Request.Bucket,
ctx.Request.Key,
request.Days.Value,
request.EffectiveTier);
return new RestoreObjectResult
{
AlreadyRestored = alreadyRestored
};
};
server.Object.Exists = async (ctx) =>
{
return new ObjectMetadata(ctx.Request.Key, DateTime.UtcNow, "etag", 123, new Owner("admin", "Administrator"), StorageClassEnum.GLACIER)
{
ContentType = "application/octet-stream",
RestoreStatus = new RestoreStatus
{
OngoingRequest = false,
ExpiryDate = DateTime.UtcNow.AddDays(7)
}
};
};
server.Object.Read = async (ctx) =>
{
if (!IsRestored(ctx.Request.Bucket, ctx.Request.Key))
throw new S3Exception(new Error(ErrorCode.InvalidObjectState));
return new S3Object(ctx.Request.Key, "1", true, DateTime.UtcNow, "etag", 5, new Owner("admin", "Administrator"), "hello", "text/plain", StorageClassEnum.GLACIER)
{
RestoreStatus = new RestoreStatus
{
OngoingRequest = false,
ExpiryDate = DateTime.UtcNow.AddDays(7)
}
};
};
S3Server routes POST ?restore, returns 202 Accepted for a newly initiated restore, 200 OK when your callback reports an already-restored object, and surfaces restore state through the x-amz-restore header on HEAD and GET.
Control maximum upload sizes:
settings.OperationLimits = new OperationLimitsSettings
{
// Maximum size for single PutObject (default 5GB)
MaxPutObjectSize = 5368709120
};
When exceeded, S3Server automatically returns EntityTooLarge error.
Note: Multipart upload parts are not subject to this limit individually.
The following S3 operations are not exposed through callbacks (may be added in future releases):
Bucket operations:
Object operations:
Comprehensive examples are available in the repository:
Test.Server: Complete server implementation with all callbacksTest.Client: S3 client examples using AWS SDKTest.RequestStyle: Manual path-style vs virtual-hosted-style URL example harnessTest.SignatureValidation: Manual AWS Signature V4 and V2 validation example harnessTest.Automated: Touchstone console runner over the shared suitesTest.Shared: Touchstone descriptor source of truth for automated, xUnit, and NUnit tests, including parser/routing, compatibility, adversarial, fuzz, lifecycle, and signature coverageTest.Xunit: xUnit test project using Touchstone.XunitAdapterTest.Nunit: NUnit test project using Touchstone.NunitAdapterRun the test server (requires admin on Windows for wildcard listeners):
dotnet run --project src/Test.Server/Test.Server.csproj
Run the shared automated suite through each supported runner:
dotnet run --project src/Test.Automated/Test.Automated.csproj --framework net8.0
dotnet run --project src/Test.Automated/Test.Automated.csproj --framework net10.0
dotnet test src/Test.Xunit/Test.Xunit.csproj
dotnet test src/Test.Nunit/Test.Nunit.csproj
The xUnit and NUnit adapter projects execute these socket-bound integration descriptors serially to avoid local listener port races.
Collect coverage for the library with the shared runsettings file:
dotnet test src/Test.Xunit/Test.Xunit.csproj --settings coverage.runsettings --collect:"XPlat Code Coverage"
dotnet test src/Test.Nunit/Test.Nunit.csproj --settings coverage.runsettings --collect:"XPlat Code Coverage"
# Build solution
dotnet build src/S3Server.sln
# Build specific configuration
dotnet build src/S3Server.sln -c Release
# Pack NuGet package
dotnet pack src/S3Server/S3Server.csproj -c Release
Have a feature request or found an issue? Please file an issue on GitHub!
Refer to for version history and release notes.
S3ServerSettings.EnableSignatureV2Authorization: AWS ... headers and V2 signed URLs with AWSAccessKeyId, Expires, and SignatureS3Response.Send(Error)coverage.runsettings for XPlat Code Coverage collectionAWSSignatureGenerator to 1.1.0Test.NunitObject.Restore callback support for POST ?restoreS3Object and ObjectMetadata now emit x-amz-restore when RestoreStatus is supplied*, +, 0.0.0.0)STREAMING-AWS4-HMAC-SHA256-PAYLOAD-TRAILER)MaxPutObjectSize check uses X-Amz-Decoded-Content-Length when aws-chunked encoding is in useUseTcpServer setting (Watson 7.0 uses TCP natively)new byte[0] to Array.Empty<byte>()ObjectWrite (e.g. PutObject), returns EntityTooLarge if exceededUseTcpServer setting: when true, uses WatsonWebserver.Lite (TCP-based) instead of WatsonWebserver (http.sys-based)MIT License - see LICENSE file for details
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 net5.0 was computed. net5.0-windows net5.0-windows was computed. net6.0 net6.0 was computed. net6.0-android net6.0-android was computed. net6.0-ios net6.0-ios was computed. net6.0-maccatalyst net6.0-maccatalyst was computed. net6.0-macos net6.0-macos was computed. net6.0-tvos net6.0-tvos was computed. net6.0-windows net6.0-windows was computed. net7.0 net7.0 was computed. net7.0-android net7.0-android was computed. net7.0-ios net7.0-ios was computed. net7.0-maccatalyst net7.0-maccatalyst was computed. net7.0-macos net7.0-macos was computed. net7.0-tvos net7.0-tvos was computed. net7.0-windows net7.0-windows was computed. net8.0 net8.0 is compatible. net8.0-android net8.0-android was computed. net8.0-browser net8.0-browser was computed. net8.0-ios net8.0-ios was computed. net8.0-maccatalyst net8.0-maccatalyst was computed. net8.0-macos net8.0-macos was computed. net8.0-tvos net8.0-tvos was computed. net8.0-windows net8.0-windows was computed. net9.0 net9.0 was computed. net9.0-android net9.0-android was computed. net9.0-browser net9.0-browser was computed. net9.0-ios net9.0-ios was computed. net9.0-maccatalyst net9.0-maccatalyst was computed. net9.0-macos net9.0-macos was computed. net9.0-tvos net9.0-tvos was computed. net9.0-windows net9.0-windows was computed. net10.0 net10.0 is compatible. net10.0-android net10.0-android was computed. net10.0-browser net10.0-browser was computed. net10.0-ios net10.0-ios was computed. net10.0-maccatalyst net10.0-maccatalyst was computed. net10.0-macos net10.0-macos was computed. net10.0-tvos net10.0-tvos was computed. net10.0-windows net10.0-windows was computed. |
| .NET Core | netcoreapp3.0 netcoreapp3.0 was computed. netcoreapp3.1 netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.1 netstandard2.1 is compatible. |
| MonoAndroid | monoandroid monoandroid was computed. |
| MonoMac | monomac monomac was computed. |
| MonoTouch | monotouch monotouch was computed. |
| Tizen | tizen60 tizen60 was computed. |
| Xamarin.iOS | xamarinios xamarinios was computed. |
| Xamarin.Mac | xamarinmac xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos xamarinwatchos was computed. |
Showing the top 2 NuGet packages that depend on S3Server:
| Package | Downloads |
|---|---|
|
Less3
<3 Less3 is S3-compatible object storage that you can run on your laptop, server, or anywhere you like. |
|
|
View.Models
Database models, services, and supporting classes for for View AI. |
Showing the top 1 popular GitHub repositories that depend on S3Server:
| Repository | Stars |
|---|---|
|
jchristn/Less3
Less3 is an S3-compatible object storage server that runs on your laptop, servers, just about anywhere!
|
| Version | Downloads | Last Updated |
|---|---|---|
| 7.2.0 | 103 | 6/11/2026 |
| 7.1.0 | 289 | 5/10/2026 |
| 7.0.7 | 722 | 4/6/2026 |
| 7.0.6 | 110 | 4/6/2026 |
| 7.0.5 | 120 | 4/3/2026 |
| 7.0.4 | 156 | 4/2/2026 |
| 7.0.3 | 112 | 4/2/2026 |
| 7.0.2 | 113 | 4/2/2026 |
| 7.0.1 | 109 | 4/2/2026 |
| 6.0.36 | 501 | 3/22/2026 |
| 6.0.35 | 472 | 2/16/2026 |
| 6.0.34 | 119 | 2/6/2026 |
| 6.0.33 | 120 | 2/6/2026 |
| 6.0.31 | 124 | 2/6/2026 |
| 6.0.30 | 192 | 1/9/2026 |
| 6.0.29 | 2,925 | 10/14/2025 |
| 6.0.28 | 229 | 10/13/2025 |
| 6.0.27 | 977 | 9/7/2025 |
| 6.0.26 | 247 | 9/7/2025 |
| 6.0.25 | 234 | 9/7/2025 |
Added opt-in legacy AWS Signature V2 validation for header signatures and signed URLs, canonical AWS Signature V2 fixtures, virtual-hosted-style request coverage, fixed-time V2 signature comparison, and broader serialized Touchstone test coverage across console, xUnit, and NUnit runners.