Files
storage-minio/README.md
Rene Nochebuena 1a34b84ee9 feat(storage-minio): initial implementation — MinIO/S3 object storage with lifecycle (v1.0.0)
Introduces code.nochebuena.dev/einherjar/storage-minio — the object storage
starter for the Einherjar framework. Absorbs the minio package from micro-lib,
replacing fmt.Errorf wrapping with core/xerrors.

Interfaces (CT-6: one TypeSpec per file):
- Provider — PutObject, RemoveObject, GetObject, PresignedGetObject, HandleError
- Component — lifecycle.Component + observability.Checkable + Provider + Native()

Implementation:
- New(logger, cfg) Component — client not created until OnInit
- OnInit: minio.New with credentials and transport; bucket existence check
- OnStart: BucketExists PING; logs "minio: connected"
- OnStop: logs "minio: closing client" (minio-go is stateless; no explicit close)
- HealthCheck: BucketExists check; Priority LevelCritical
- Native() *miniogo.Client — escape hatch for operations not in Provider
- HandleError: maps minio-go errors to xerrors (NotFound, AlreadyExists, Internal)

Config (EINHERJAR_MINIO_* env vars):
  Endpoint(required), AccessKey(required), SecretKey(required),
  Bucket(required), UseSSL(false), Region(us-east-1)

- Component interface embeds observability.Identifiable; identifiable.go implements
  ModulePath and ModuleVersion via runtime/debug.ReadBuildInfo() — prints in launcher banner
2026-05-29 16:03:52 +00:00

3.4 KiB

einherjar/storage-minio

version license go health

The shield does not care who forged it. It holds what it is given and gives it back unchanged.

code.nochebuena.dev/einherjar/storage-minio is the MinIO/S3 object storage component of the Einherjar framework. It wraps minio-go/v7 behind a lifecycle-aware Component with four common operations — upload, download, delete, and presigned URLs. For anything beyond that scope, Native() returns the raw *miniogo.Client.


Usage

Setup

import storageminio "code.nochebuena.dev/einherjar/storage-minio"

s := storageminio.New(logger, storageminio.DefaultConfig())
launcher.Append(s)   // OnInit connects; OnStop is a no-op (stateless client)
health.Register(s)   // BucketExists check; LevelCritical

Uploading

import miniogo "github.com/minio/minio-go/v7"

info, err := s.PutObject(ctx, "my-bucket", "uploads/photo.jpg", reader, size, miniogo.PutObjectOptions{
    ContentType: "image/jpeg",
})

Downloading

obj, err := s.GetObject(ctx, "my-bucket", "uploads/photo.jpg", miniogo.GetObjectOptions{})
if err != nil {
    return s.HandleError(err)
}
defer obj.Close()

Presigned URL (time-limited public access)

url, err := s.PresignedGetObject(ctx, "my-bucket", "uploads/photo.jpg", 15*time.Minute, nil)
// url is a *url.URL — call url.String() to get the string form

Deleting

err := s.RemoveObject(ctx, "my-bucket", "uploads/photo.jpg", miniogo.RemoveObjectOptions{})

Native escape hatch

For multipart uploads, bucket management, or any operation not in Provider, use the raw client:

native := s.Native() // *miniogo.Client

Callers that use Native() must import github.com/minio/minio-go/v7 directly.

Error handling

if err := s.HandleError(someErr); err != nil {
    // minio-go error responses mapped to xerrors:
    // NoSuchKey / NoSuchBucket → ErrNotFound
    // AccessDenied             → ErrPermissionDenied
    // context.Canceled         → ErrCancelled
    // context.DeadlineExceeded → ErrDeadlineExceeded
}

HandleError is also available as a package-level function: storageminio.HandleError(err).


Environment variables

Variable Required Default Description
EINHERJAR_MINIO_ENDPOINT Yes MinIO/S3 endpoint (host:port or domain)
EINHERJAR_MINIO_ACCESS_KEY Yes Access key ID
EINHERJAR_MINIO_SECRET_KEY Yes Secret access key
EINHERJAR_MINIO_BUCKET Yes Default bucket for health check
EINHERJAR_MINIO_USE_SSL No false Use TLS
EINHERJAR_MINIO_REGION No us-east-1 Bucket region

Dependency graph

contracts  (zero dependencies)
    ↑
  core
    ↑
storage-minio  (contracts, core, minio-go/v7)
    ↑
  your app

Verification

cd storage-minio/
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.