Files
backend/internal/storage/s3_signer.go
Andriy Oblivantsev dda20f82e6
CI / test (push) Successful in 3s
Serve asset downloads via backend instead of redirecting to storage.
The download endpoint now streams object bytes from storage on the same API URL so clients never get redirected to MinIO/internal hosts, while preserving public/private access checks.

Made-with: Cursor
2026-03-02 22:14:12 +00:00

82 lines
1.9 KiB
Go

package storage
import (
"context"
"errors"
"io"
"time"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
)
type S3Config struct {
Endpoint string
Region string
Bucket string
AccessKey string
SecretKey string
UseTLS bool
PathStyle bool
}
type S3Signer struct {
client *minio.Client
bucket string
}
func NewS3Signer(cfg S3Config) (*S3Signer, error) {
if cfg.Endpoint == "" || cfg.Bucket == "" {
return nil, errors.New("s3 endpoint and bucket are required")
}
client, err := minio.New(cfg.Endpoint, &minio.Options{
Creds: credentials.NewStaticV4(cfg.AccessKey, cfg.SecretKey, ""),
Secure: cfg.UseTLS,
Region: cfg.Region,
BucketLookup: bucketLookup(cfg.PathStyle),
})
if err != nil {
return nil, err
}
return &S3Signer{client: client, bucket: cfg.Bucket}, nil
}
func bucketLookup(pathStyle bool) minio.BucketLookupType {
if pathStyle {
return minio.BucketLookupPath
}
return minio.BucketLookupAuto
}
func (s *S3Signer) SignedGetObjectURL(ctx context.Context, objectKey string, expiry time.Duration) (string, error) {
u, err := s.client.PresignedGetObject(ctx, s.bucket, objectKey, expiry, nil)
if err != nil {
return "", err
}
return u.String(), nil
}
func (s *S3Signer) PutObject(ctx context.Context, objectKey, contentType string, body io.Reader, size int64) error {
_, err := s.client.PutObject(ctx, s.bucket, objectKey, body, size, minio.PutObjectOptions{
ContentType: contentType,
})
return err
}
func (s *S3Signer) GetObject(ctx context.Context, objectKey string) (io.ReadCloser, string, int64, error) {
obj, err := s.client.GetObject(ctx, s.bucket, objectKey, minio.GetObjectOptions{})
if err != nil {
return nil, "", 0, err
}
info, err := obj.Stat()
if err != nil {
_ = obj.Close()
return nil, "", 0, err
}
contentType := info.ContentType
if contentType == "" {
contentType = "application/octet-stream"
}
return obj, contentType, info.Size, nil
}