Co-authored-by: Rene Nochebuena Guerrero <rene@nochebuena.dev>
einherjar/storage-s3
A vault is only as free as the key that opens it. Carry a key every lock understands.
code.nochebuena.dev/einherjar/storage-s3 is the object storage component of the Einherjar framework. It speaks the S3 API through the AWS SDK for Go v2, so the server behind it is a deployment choice — AWS, Cloudflare R2, Ceph, SeaweedFS, Garage, or anything else that answers S3. It wraps *s3.Client behind a lifecycle-aware Component with four common operations — upload, download, delete, and presigned downloads. For anything beyond that scope, Native() returns the client itself.
Usage
Setup
import storages3 "code.nochebuena.dev/einherjar/storage-s3"
s := storages3.New(logger, cfg) // cfg from env, or storages3.DefaultConfig() plus the required fields
lc.Append(s) // OnInit builds the client; OnStart checks the bucket and creates it if absent
// s is observability.Checkable (HeadBucket, LevelCritical):
srv.Get("/health", health.NewHandler(logger, s).ServeHTTP)
Operations
Every Provider method takes the SDK's own input type. Leave Bucket unset to use the configured bucket; set it to reach another one. The input is copied, never modified.
import (
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/s3"
)
_, err := s.PutObject(ctx, &s3.PutObjectInput{
Key: aws.String("uploads/photo.jpg"),
Body: bytes.NewReader(data),
ContentType: aws.String("image/jpeg"),
})
out, err := s.GetObject(ctx, &s3.GetObjectInput{Key: aws.String("uploads/photo.jpg")})
if err != nil {
return err // already an xerrors value
}
defer out.Body.Close()
_, err = s.DeleteObject(ctx, &s3.DeleteObjectInput{Key: aws.String("uploads/photo.jpg")})
Over plain HTTP the upload is signed over its payload, so Body must be an io.ReadSeeker (bytes.Reader, strings.Reader, *os.File). For a stream, use TLS or the SDK's upload manager through Native().
Presigned download (time-limited access without credentials)
req, err := s.PresignGetObject(ctx, &s3.GetObjectInput{Key: aws.String("uploads/photo.jpg")}, 15*time.Minute)
// req.URL is the string to hand out
Native escape hatch
For multipart uploads, listings, bucket policies, or any operation not in Provider, use the client:
native := s.Native() // *s3.Client
_, err := native.GetBucketPolicy(ctx, &s3.GetBucketPolicyInput{Bucket: aws.String(bucket)})
err = storages3.HandleError(err)
Error handling
if err := s.HandleError(someErr); err != nil {
// NoSuchKey / NoSuchBucket / a bare 404 → ErrNotFound
// AccessDenied / InvalidAccessKeyId / SignatureDoesNotMatch → ErrPermissionDenied
// BucketAlreadyExists / BucketAlreadyOwnedByYou → ErrAlreadyExists
// context.Canceled → ErrCancelled
// context.DeadlineExceeded → ErrDeadlineExceeded
// anything else → ErrInternal
}
HandleError is also available as a package-level function: storages3.HandleError(err).
Environment variables
| Variable | Required | Default | Description |
|---|---|---|---|
EINHERJAR_S3_ENDPOINT |
No | — | Base URL with scheme, e.g. http://seaweedfs:8333. Empty resolves AWS for the region |
EINHERJAR_S3_REGION |
No | us-east-1 |
Signing region |
EINHERJAR_S3_ACCESS_KEY_ID |
Yes | — | Access key ID |
EINHERJAR_S3_SECRET_ACCESS_KEY |
Yes | — | Secret access key |
EINHERJAR_S3_BUCKET |
Yes | — | Default bucket: checked and created at startup, probed by the health check |
EINHERJAR_S3_USE_PATH_STYLE |
No | true |
<endpoint>/<bucket> addressing. Self-hosted servers need it; set false for AWS |
The access key pair is the only credential source. The component never reads the AWS environment variables, shared config files or instance metadata.
Dependency graph
contracts (zero dependencies)
↑
core
↑
storage-s3 (contracts, core, aws-sdk-go-v2/service/s3)
↑
your app
Verification
cd storage-s3/
go build ./...
go vet ./...
go test ./...
gofmt -l .
The artifact survives the battle that created it. Store it well. Someone will need it after you are gone.