No description
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Kristoffer Opsahl ea5f5b6096
All checks were successful
test / test (push) Successful in 40s
ci: run on debian-latest (#2)
ci-images consolidated its per-stack images into a single debian-ci image
with every toolchain pre-installed; debian-latest is its label. The old
debian-trixie-<stack> labels are deprecated aliases.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-18 20:27:57 +02:00
.forgejo/workflows ci: run on debian-latest (#2) 2026-09-18 20:27:57 +02:00
testdata/sigv4_vectors test: port AWS SigV4 GET-only test vectors 2026-05-11 10:11:13 +02:00
.gitignore chore: initialize s3lite module 2026-05-11 10:07:10 +02:00
client.go docs: update README for current API surface 2026-09-13 00:59:53 +02:00
client_test.go feat: add PutObject and DeleteObject 2026-09-13 00:52:58 +02:00
errors.go feat: error types — ResponseError and ErrNotFound 2026-05-11 10:31:11 +02:00
errors_test.go feat: error types — ResponseError and ErrNotFound 2026-05-11 10:31:11 +02:00
go.mod chore: initialize s3lite module 2026-05-11 10:07:10 +02:00
LICENSE chore: initialize s3lite module 2026-05-11 10:07:10 +02:00
live_test.go feat: add PutObject and DeleteObject 2026-09-13 00:52:58 +02:00
presign.go feat: add PutObject and DeleteObject 2026-09-13 00:52:58 +02:00
presign_test.go feat: PresignGetObject — query-string SigV4 for a GET with no Authorization header 2026-09-04 19:36:43 +02:00
README.md docs: update README for current API surface 2026-09-13 00:59:53 +02:00
signer.go feat: add PutObject and DeleteObject 2026-09-13 00:52:58 +02:00
signer_test.go feat: PresignGetObject — query-string SigV4 for a GET with no Authorization header 2026-09-04 19:36:43 +02:00
validate_test.go test: cover validateBucket + validateKey directly 2026-05-11 12:31:04 +02:00

s3lite

Minimal, stdlib-only Go client for S3-compatible object storage.

go.mod license: MIT

Why this exists

The official AWS SDK works fine against non-AWS S3-compatible endpoints (MinIO, Hetzner Object Storage, R2, Backblaze, Spaces) but it drags in an AWS-flavoured configuration model and a non-trivial dependency tree. s3lite is the inverse: one package, no dependencies, a handful of operations. Suitable when you need basic object get/put/delete/list and nothing else.

Install

import "git.kristofferopsahl.com/kristofferopsahl/s3lite"
go get git.kristofferopsahl.com/kristofferopsahl/s3lite@latest

Quickstart

client, err := s3lite.New(s3lite.Config{
    Endpoint:  "https://hel1.your-objectstorage.com",
    Region:    "hel1",
    AccessKey: os.Getenv("S3_ACCESS_KEY"),
    SecretKey: os.Getenv("S3_SECRET_KEY"),
})
if err != nil { panic(err) }

body, hdr, err := client.GetObject(ctx, "my-bucket", "path/to/object.jpg")
if errors.Is(err, s3lite.ErrNotFound) {
    // 404 handling
}
if err != nil { panic(err) }
defer body.Close()

io.Copy(os.Stdout, body)

Supported endpoints

Tested against:

  • Hetzner Object Storage

By construction (path-style + SigV4) it should work against any S3-compatible service that supports path-style addressing — MinIO, Backblaze B2 (S3 API), Cloudflare R2 in path mode, DigitalOcean Spaces, Wasabi. Open an issue if it doesn't.

Scope

  • GetObject(ctx, bucket, key) returns the response body (caller closes) and headers. ErrNotFound for HTTP 404; other non-2xx responses return a *ResponseError.
  • PutObject(ctx, bucket, key, body io.Reader, opts ...PutOption) uploads body. It signs X-Amz-Content-Sha256 as the literal UNSIGNED-PAYLOAD rather than hashing the payload up front, so body streams straight through instead of being buffered in memory first — this requires an HTTPS endpoint, since TLS (not SigV4) is what protects the bytes in transit. WithContentType(ct) and WithACL(acl) set the corresponding headers, both included in the signed-headers set.
  • DeleteObject(ctx, bucket, key) removes an object. S3 answers 204 whether or not the key existed; some S3-compatible backends answer 200 or 404 instead — all three are treated as success, since an idempotent erasure re-run must not fail on a key it already removed.
  • ListObjects(ctx, bucket, prefix, continuationToken) returns one page (≤ 1000 keys) of a bucket's keys under prefix, in lexical order, and the continuation token for the next page ("" once there is no next page).
  • PresignGetObject(bucket, key, signAt, ttl) mints a query-string-signed GET URL (no Authorization header, so it works from a browser or an <img> tag) that is byte-identical for the same signAt+ttl — callers that need a stable, cacheable URL pass a fixed signAt rather than time.Now().

Not yet supported (open an issue if you need any of these): CopyObject, HeadObject, multipart uploads, virtual-hosted-style addressing, retries, server-side encryption.

SigV4

The library implements AWS Signature Version 4 in signer.go. It is exercised against AWS's published GET-only test vectors (in testdata/sigv4_vectors/). Path-style addressing only. The signer accepts any service name (the AWS-supplied vectors verify against the literal service string); Client always passes s3 in the credential scope. The region comes from Config.Region.

Tests

go test ./...                                  # unit tests; stdlib + httptest only
S3LITE_TEST_*=... go test -tags=live ./...     # opt-in real-endpoint test

License

MIT. See LICENSE.