package minio import ( "context" "fmt" "io" "net/http" "net/url" "time" miniogo "github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7/pkg/credentials" "code.nochebuena.dev/go/health" "code.nochebuena.dev/go/launcher" "code.nochebuena.dev/go/logz" ) // Client is the MinIO operation interface for the most common bucket operations. // Inject it into repositories; use Native() on Component for SDK operations not covered here. type Client interface { // PutObject uploads an object to the given bucket and key. PutObject(ctx context.Context, bucket, key string, reader io.Reader, size int64, opts miniogo.PutObjectOptions) (miniogo.UploadInfo, error) // RemoveObject deletes an object from the given bucket and key. RemoveObject(ctx context.Context, bucket, key string, opts miniogo.RemoveObjectOptions) error // GetObject downloads an object and returns it as a readable stream. // The caller must close the returned object after reading. GetObject(ctx context.Context, bucket, key string, opts miniogo.GetObjectOptions) (*miniogo.Object, error) // PresignedGetObject returns a pre-signed URL for downloading an object. PresignedGetObject(ctx context.Context, bucket, key string, expires time.Duration, reqParams url.Values) (*url.URL, error) // HandleError maps a minio-go error to a typed xerrors value. HandleError(err error) error } // Component bundles launcher lifecycle, health check, and MinIO client operations. type Component interface { launcher.Component health.Checkable Client // Native returns the underlying minio-go SDK client for operations not covered by Client. Native() *miniogo.Client } // Config holds MinIO connection settings. The struct is embeddable in application // config structs and populated from environment variables. type Config struct { Endpoint string `env:"MINIO_ENDPOINT,required"` AccessKey string `env:"MINIO_ACCESS_KEY,required"` SecretKey string `env:"MINIO_SECRET_KEY,required"` UseSSL bool `env:"MINIO_USE_SSL" envDefault:"false"` Bucket string `env:"MINIO_BUCKET,required"` // Region defaults to "us-east-1" — the region MinIO uses by default on self-hosted // deployments. Setting a non-empty region bypasses per-request region detection. Region string `env:"MINIO_REGION" envDefault:"us-east-1"` // Transport is used for testing only. Nil uses the minio-go default transport. Transport http.RoundTripper `env:"-"` } var _ Component = (*minioComponent)(nil) type minioComponent struct { cfg Config logger logz.Logger mc *miniogo.Client } // New returns a MinIO Component. Call lc.Append(mc) to manage its lifecycle. func New(logger logz.Logger, cfg Config) Component { return &minioComponent{cfg: cfg, logger: logger} } func (c *minioComponent) OnInit() error { opts := &miniogo.Options{ Creds: credentials.NewStaticV4(c.cfg.AccessKey, c.cfg.SecretKey, ""), Secure: c.cfg.UseSSL, Region: c.cfg.Region, Transport: c.cfg.Transport, } mc, err := miniogo.New(c.cfg.Endpoint, opts) if err != nil { return fmt.Errorf("minio: create client: %w", err) } c.mc = mc return nil } func (c *minioComponent) OnStart() error { if c.mc == nil { return fmt.Errorf("minio: client not initialized") } exists, err := c.mc.BucketExists(context.Background(), c.cfg.Bucket) if err != nil { return fmt.Errorf("minio: check bucket %q: %w", c.cfg.Bucket, err) } if !exists { if err := c.mc.MakeBucket(context.Background(), c.cfg.Bucket, miniogo.MakeBucketOptions{}); err != nil { return fmt.Errorf("minio: create bucket %q: %w", c.cfg.Bucket, err) } c.logger.Info("minio: bucket created", "bucket", c.cfg.Bucket) } c.logger.Info("minio: connected", "bucket", c.cfg.Bucket) return nil } func (c *minioComponent) OnStop() error { c.mc = nil return nil } // GetObject downloads an object as a readable stream. The caller must close the returned object. func (c *minioComponent) GetObject(ctx context.Context, bucket, key string, opts miniogo.GetObjectOptions) (*miniogo.Object, error) { obj, err := c.mc.GetObject(ctx, bucket, key, opts) if err != nil { return nil, HandleError(err) } return obj, nil } // PutObject uploads an object and maps any minio-go error to a typed xerrors value. func (c *minioComponent) PutObject(ctx context.Context, bucket, key string, reader io.Reader, size int64, opts miniogo.PutObjectOptions) (miniogo.UploadInfo, error) { info, err := c.mc.PutObject(ctx, bucket, key, reader, size, opts) if err != nil { return miniogo.UploadInfo{}, HandleError(err) } return info, nil } // RemoveObject deletes an object and maps any minio-go error to a typed xerrors value. func (c *minioComponent) RemoveObject(ctx context.Context, bucket, key string, opts miniogo.RemoveObjectOptions) error { return HandleError(c.mc.RemoveObject(ctx, bucket, key, opts)) } // PresignedGetObject returns a pre-signed download URL and maps any minio-go error to a typed xerrors value. func (c *minioComponent) PresignedGetObject(ctx context.Context, bucket, key string, expires time.Duration, reqParams url.Values) (*url.URL, error) { u, err := c.mc.PresignedGetObject(ctx, bucket, key, expires, reqParams) if err != nil { return nil, HandleError(err) } return u, nil } // HandleError maps a minio-go error to a typed xerrors value. func (c *minioComponent) HandleError(err error) error { return HandleError(err) } // Native returns the underlying minio-go SDK client for operations not covered by Client. func (c *minioComponent) Native() *miniogo.Client { return c.mc } // HealthCheck verifies MinIO is reachable by checking that the configured bucket exists. func (c *minioComponent) HealthCheck(ctx context.Context) error { if c.mc == nil { return fmt.Errorf("minio: client not initialized") } _, err := c.mc.BucketExists(ctx, c.cfg.Bucket) if err != nil { return fmt.Errorf("minio: health check: %w", err) } return nil } // Name returns the component name used by the health check aggregator. func (c *minioComponent) Name() string { return "minio" } // Priority returns LevelCritical — MinIO is the primary store for object data; // an outage means uploads and retrievals fail completely. func (c *minioComponent) Priority() health.Level { return health.LevelCritical }