// Package storage provides S3-compatible object storage using RustFS/MinIO package storage import ( "context" "errors" "io" "log" "os" "sync" "time" "github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7/pkg/credentials" ) var ( // Client is the S3 client for storage operations Client *minio.Client // BucketName is the default bucket for storing objects BucketName string once sync.Once ) // Init initializes the S3 storage client from environment variables // Required environment variables: // - RUSTFS_ENDPOINT: S3 endpoint URL (e.g., "localhost:9000") // - RUSTFS_ACCESS_KEY: Access key ID // - RUSTFS_SECRET_KEY: Secret access key // - RUSTFS_BUCKET: Bucket name (optional, defaults to "rsmon") // - RUSTFS_REGION: Region (optional, defaults to "us-east-1") // - RUSTFS_USE_SSL: Use SSL (optional, defaults to "false") func Init() error { var initErr error once.Do(func() { endpoint := os.Getenv("RUSTFS_ENDPOINT") if endpoint == "" { initErr = errors.New("RUSTFS_ENDPOINT not set") return } accessKey := os.Getenv("RUSTFS_ACCESS_KEY") if accessKey == "" { initErr = errors.New("RUSTFS_ACCESS_KEY not set") return } secretKey := os.Getenv("RUSTFS_SECRET_KEY") if secretKey == "" { initErr = errors.New("RUSTFS_SECRET_KEY not set") return } bucket := os.Getenv("RUSTFS_BUCKET") if bucket == "" { bucket = "rsmon" } BucketName = bucket region := os.Getenv("RUSTFS_REGION") if region == "" { region = "us-east-1" } useSSL := os.Getenv("RUSTFS_USE_SSL") == "true" // Initialize minio client client, err := minio.New(endpoint, &minio.Options{ Creds: credentials.NewStaticV4(accessKey, secretKey, ""), Secure: useSSL, Region: region, }) if err != nil { initErr = err return } Client = client log.Printf("[storage] Initialized S3 client: endpoint=%s, bucket=%s, ssl=%v", endpoint, bucket, useSSL) //nolint:lll // accepted security trade-off // Create bucket if it doesn't exist ctx := context.Background() exists, err := client.BucketExists(ctx, bucket) if err != nil { initErr = err return } if !exists { err = client.MakeBucket(ctx, bucket, minio.MakeBucketOptions{Region: region}) if err != nil { log.Printf("[storage] Warning: failed to create bucket: %v", err) } else { log.Printf("[storage] Created bucket: %s", bucket) } } }) return initErr } // IsAvailable returns true if the storage client is initialized func IsAvailable() bool { return Client != nil } // PutObject stores an object in S3 func PutObject(ctx context.Context, objectName string, reader io.Reader, size int64, opts minio.PutObjectOptions) (info minio.UploadInfo, err error) { //nolint:lll,gocritic // hugeParam: accepted for interface compatibility if Client == nil { return minio.UploadInfo{}, errors.New("storage client not initialized") } return Client.PutObject(ctx, BucketName, objectName, reader, size, opts) } // GetObject retrieves an object from S3 func GetObject(ctx context.Context, objectName string, opts minio.GetObjectOptions) (*minio.Object, error) { //nolint:gocritic,lll // hugeParam: accepted for interface compatibility if Client == nil { return nil, errors.New("storage client not initialized") } return Client.GetObject(ctx, BucketName, objectName, opts) } // RemoveObject removes an object from S3 func RemoveObject(ctx context.Context, objectName string, opts minio.RemoveObjectOptions) error { //nolint:gocritic,lll // hugeParam: accepted for interface compatibility if Client == nil { return errors.New("storage client not initialized") } return Client.RemoveObject(ctx, BucketName, objectName, opts) } // PresignedGetObject generates a presigned URL for getting an object func PresignedGetObject(ctx context.Context, objectName string, expires time.Duration) (string, error) { if Client == nil { return "", errors.New("storage client not initialized") } presignedURL, err := Client.PresignedGetObject(ctx, BucketName, objectName, expires, nil) if err != nil { return "", err } return presignedURL.String(), nil }