package objectstorage import ( "bytes" "context" "fmt" "strings" "time" "img_infinite_canvas/internal/domain/design" "github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7/pkg/credentials" ) type S3Options struct { Driver string Bucket string Endpoint string Region string AccessKey string SecretKey string PublicBase string UseSSL bool ExpiresIn int64 } type S3Storage struct { driver string bucket string publicBase string expiresIn int64 client *minio.Client } func NewS3Storage(ctx context.Context, opts S3Options) (*S3Storage, error) { client, err := minio.New(opts.Endpoint, &minio.Options{ Creds: credentials.NewStaticV4(opts.AccessKey, opts.SecretKey, ""), Secure: opts.UseSSL, Region: opts.Region, }) if err != nil { return nil, err } exists, err := client.BucketExists(ctx, opts.Bucket) if err != nil { return nil, err } if !exists { if err := client.MakeBucket(ctx, opts.Bucket, minio.MakeBucketOptions{Region: opts.Region}); err != nil { return nil, err } } if strings.EqualFold(opts.Driver, "minio") && strings.TrimSpace(opts.PublicBase) != "" { if err := setMinioPublicReadPolicy(ctx, client, opts.Bucket); err != nil { return nil, err } } if opts.ExpiresIn <= 0 { opts.ExpiresIn = 900 } return &S3Storage{ driver: opts.Driver, bucket: opts.Bucket, publicBase: strings.TrimRight(opts.PublicBase, "/"), expiresIn: opts.ExpiresIn, client: client, }, nil } func setMinioPublicReadPolicy(ctx context.Context, client *minio.Client, bucket string) error { policy := fmt.Sprintf(`{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": {"AWS": ["*"]}, "Action": ["s3:GetObject"], "Resource": ["arn:aws:s3:::%s/*"] } ] }`, bucket) return client.SetBucketPolicy(ctx, bucket, policy) } func (s *S3Storage) CreateUpload(ctx context.Context, req design.AssetUploadRequest) (design.AssetUpload, error) { objectKey := objectKey(req.FileName) headers := map[string]string{"Content-Type": req.ContentType} uploadURL, err := s.client.PresignedPutObject(ctx, s.bucket, objectKey, time.Duration(s.expiresIn)*time.Second) if err != nil { return design.AssetUpload{}, err } publicURL := uploadURL.String() if s.publicBase != "" { publicURL = publicObjectURL(s.publicBase, objectKey) } return design.AssetUpload{ Driver: s.driver, Bucket: s.bucket, ObjectKey: objectKey, UploadURL: uploadURL.String(), PublicURL: publicURL, Headers: headers, ExpiresIn: s.expiresIn, }, nil } func (s *S3Storage) PutObject(ctx context.Context, req design.AssetObjectRequest) (design.AssetObject, error) { objectKey := objectKey(req.FileName) if _, err := s.client.PutObject(ctx, s.bucket, objectKey, bytes.NewReader(req.Data), int64(len(req.Data)), minio.PutObjectOptions{ ContentType: req.ContentType, }); err != nil { return design.AssetObject{}, err } publicURL := publicObjectURL(s.publicBase, objectKey) return design.AssetObject{ Driver: s.driver, Bucket: s.bucket, ObjectKey: objectKey, PublicURL: publicURL, }, nil } func (s *S3Storage) DeleteObject(ctx context.Context, publicURL string) error { objectKey, ok := objectKeyFromPublicURL(s.publicBase, publicURL) if !ok { return nil } return s.client.RemoveObject(ctx, s.bucket, objectKey, minio.RemoveObjectOptions{}) }