Files
minio/CLAUDE.md
Rene Nochebuena 3d12d18200 feat(minio): initial stable release v1.0.0 — Client interface, xerrors, GetObject, doc examples
Add Client interface with PutObject, GetObject, RemoveObject, PresignedGetObject, and
HandleError; Component now embeds Client with Native() as escape hatch for operations
not covered by the interface. Add xerrors dependency: HandleError maps minio-go error
codes to portable typed codes (NoSuchKey → ErrNotFound, AccessDenied →
ErrPermissionDenied, BucketAlreadyExists → ErrAlreadyExists, etc.). OnStop sets
c.mc = nil for lifecycle consistency. doc.go updated with launcher wiring, upload,
presigned URL, GetObject, and HandleError usage examples. API committed as stable.

What's included:
- Config with Endpoint, AccessKey, SecretKey, UseSSL, Bucket, Region (env-driven, embeddable)
- Transport field in Config for test injection (env:"-", nil uses minio-go default)
- Client interface: PutObject, GetObject, RemoveObject, PresignedGetObject, HandleError
- Component interface: launcher.Component + health.Checkable + Client + Native()
- New(logger, cfg) constructor for lifecycle registration via lc.Append
- Automatic bucket creation on OnStart if bucket does not exist
- Health check via BucketExists at LevelCritical priority
- HandleError: maps minio-go ErrorResponse codes to xerrors typed errors
- 21 unit tests using mock HTTP transport; no live server required
2026-05-19 21:42:30 -06:00

4.5 KiB

minio

MinIO (S3-compatible) client with launcher lifecycle and health check integration.

Purpose

Manages the lifecycle of a minio-go SDK client: constructs it from config, verifies bucket existence at startup (creating the bucket if absent), exposes the native SDK client to consumers, and provides a health check via BucketExists. Does not wrap SDK operations — callers use the full minio-go API directly via Native().

Tier & Dependencies

Tier 3 (infrastructure) — depends on:

  • code.nochebuena.dev/go/health (Tier 2)
  • code.nochebuena.dev/go/launcher (Tier 2)
  • code.nochebuena.dev/go/logz (Tier 1)
  • github.com/minio/minio-go/v7 (native MinIO client)

Does not depend on xerrors — errors from the minio-go client are wrapped with fmt.Errorf and returned as-is.

Key Design Decisions

  • Native client exposure: Native() *miniogo.Client returns the raw minio-go SDK client. No wrapper, no operation subset. Callers use PutObject, RemoveObject, PresignedGetObject, and all other SDK methods directly.

  • Single bucket per component: Config.Bucket is used only for health checks and the startup init hook. Callers needing multiple buckets instantiate multiple Component values with different configs. Object key namespacing (e.g. products/{id}/{uuid}.ext) handles logical separation within a single bucket — no multi-bucket API is needed.

  • Bucket auto-creation: If Config.Bucket does not exist when OnStart runs, the component creates it automatically and logs the event. Failure to create the bucket is a hard startup error — the application will not start.

  • Region defaults to us-east-1: Self-hosted MinIO uses this region by default. Setting a non-empty region in Options.Region bypasses per-request region detection in minio-go, which avoids an extra GET /<bucket>?location HTTP call on every bucket operation.

  • Stateless client — OnStop is a no-op: minio-go holds no persistent connections; there is nothing to close on shutdown.

  • Health priority is Critical: Priority() returns health.LevelCritical. Without MinIO, object uploads and retrievals fail completely — there is no graceful degradation path.

  • Transport field for testing: Config.Transport http.RoundTripper (tagged env:"-") lets tests inject a mock transport without a live server. Setting it to nil in production uses the minio-go default transport.

Patterns

Lifecycle registration:

mc := minio.New(logger, cfg)
lc.Append(mc)
r.Get("/health", health.NewHandler(logger, mc))

Uploading an object:

_, err := provider.Native().PutObject(ctx, cfg.Bucket, key, reader, size,
    miniogo.PutObjectOptions{ContentType: "image/jpeg"})

Generating a presigned URL:

url, err := provider.Native().PresignedGetObject(ctx, cfg.Bucket, key, time.Hour, nil)

Injecting into a repository:

type imageRepo struct {
    provider minio.Provider
    bucket   string
}

func (r *imageRepo) Upload(ctx context.Context, key string, rd io.Reader, size int64) error {
    _, err := r.provider.Native().PutObject(ctx, r.bucket, key,
        rd, size, miniogo.PutObjectOptions{})
    return err
}

What to Avoid

  • Do not wrap minio-go operations in this module. Keep PutObject, RemoveObject, etc. in the caller (repository layer). This module is a lifecycle building block, not a domain repository.
  • Do not define interfaces that subset minio-go methods here. If a consumer needs a minimal testable interface, define it in the consumer package.
  • Do not call Native() before OnInit has run — it returns nil.
  • Do not add a Region auto-detection request in tests — use Config.Region = "us-east-1" to bypass the GET /<bucket>?location lookup.

Testing Notes

  • Unit tests (minio_test.go) use an injected mockTransport via Config.Transport. No live MinIO server is required.
  • Config.Region = "us-east-1" must be set in test configs — it prevents minio-go from making an extra region detection request that the mock transport would not handle.
  • TestComponent_OnStart_BucketError and TestComponent_HealthCheck_Unreachable take ~4 seconds each due to minio-go's built-in retry backoff on errors. This is expected and correct behavior.
  • compliance_test.go (package minio_test) asserts New(...) satisfies Component at compile time.
  • Integration tests requiring a live MinIO instance belong in a higher-tier test suite (e.g. the consuming application's integration test suite).