package storage import ( "context" "io" "net/url" "github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7/pkg/credentials" ) // Config holds the S3-compatible (MinIO) connection settings. type Config struct { Endpoint string AccessKeyID string SecretAccessKey string Bucket string Region string UseSSL bool MaxFileBytes int64 } // Client is the thin object-store adapter. The domain layer imports this // package instead of minio-go directly, keeping the S3 client at the platform // boundary like cache and database. type Client struct { mc *minio.Client bucket string region string maxBytes int64 } func NewClient(cfg Config) (*Client, error) { // minio-go 的 Endpoint 参数必须是裸 host[:port],不接受带 scheme 的完整 // URL(否则报 "Endpoint url cannot have fully qualified paths.")。这里剥离 // scheme,并由 scheme 推导 Secure(https→true),二者都比 S3_USE_SSL 优先。 secure := cfg.UseSSL host := cfg.Endpoint if parsed, err := url.Parse(cfg.Endpoint); err == nil && parsed.Scheme != "" { secure = parsed.Scheme == "https" host = parsed.Host } mc, err := minio.New(host, &minio.Options{ Creds: credentials.NewStaticV4(cfg.AccessKeyID, cfg.SecretAccessKey, ""), Secure: secure, Region: cfg.Region, }) if err != nil { return nil, err } mc.SetAppInfo("ai-gateway", "0.10.0") return &Client{mc: mc, bucket: cfg.Bucket, region: cfg.Region, maxBytes: cfg.MaxFileBytes}, nil } // EnsureBucket creates the configured bucket if it does not exist. Safe to call // repeatedly; it is a no-op once the bucket exists. func (c *Client) EnsureBucket(ctx context.Context) error { exists, err := c.mc.BucketExists(ctx, c.bucket) if err != nil { return err } if exists { return nil } return c.mc.MakeBucket(ctx, c.bucket, minio.MakeBucketOptions{Region: c.region}) } func (c *Client) MaxBytes() int64 { return c.maxBytes } // PutObject streams a reader to the object store. A size of -1 lets the client // use chunked multipart upload so files are not buffered in memory. func (c *Client) PutObject(ctx context.Context, key string, r io.Reader, size int64, contentType string) (int64, error) { info, err := c.mc.PutObject(ctx, c.bucket, key, r, size, minio.PutObjectOptions{ContentType: contentType}) if err != nil { return 0, err } return info.Size, nil } // OpenObject returns a streaming reader plus the object size. func (c *Client) OpenObject(ctx context.Context, key string) (io.ReadCloser, int64, error) { obj, err := c.mc.GetObject(ctx, c.bucket, key, minio.GetObjectOptions{}) if err != nil { return nil, 0, err } stat, err := obj.Stat() if err != nil { _ = obj.Close() return nil, 0, err } return obj, stat.Size, nil } func (c *Client) DeleteObject(ctx context.Context, key string) error { return c.mc.RemoveObject(ctx, c.bucket, key, minio.RemoveObjectOptions{}) }