98 lines
3.0 KiB
Go
98 lines
3.0 KiB
Go
// Package s3 adapts a caller-owned MinIO client to S3-compatible object storage.
|
|
package s3
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"git.yuntuo.hk/k2safe/go-omni/storage"
|
|
"github.com/minio/minio-go/v7"
|
|
"io"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type Store struct {
|
|
client *minio.Client
|
|
bucket string
|
|
maxBytes int64
|
|
}
|
|
|
|
func New(client *minio.Client, bucket string, maxBytes int64) (*Store, error) {
|
|
if client == nil || bucket == "" || maxBytes < 0 || maxBytes > 5<<40 {
|
|
return nil, errors.New("storage/s3: client, bucket and nonnegative limit required")
|
|
}
|
|
if maxBytes == 0 {
|
|
maxBytes = 32 << 20
|
|
}
|
|
return &Store{client: client, bucket: bucket, maxBytes: maxBytes}, nil
|
|
}
|
|
func (s *Store) Put(ctx context.Context, key string, reader io.Reader, size int64, contentType string) (storage.Info, error) {
|
|
if err := storage.Check(ctx, key); err != nil {
|
|
return storage.Info{}, err
|
|
}
|
|
if reader == nil || size < 0 || len(contentType) > 256 || strings.ContainsAny(contentType, "\r\n") {
|
|
return storage.Info{}, errors.New("storage/s3: invalid upload")
|
|
}
|
|
if size > s.maxBytes {
|
|
return storage.Info{}, storage.ErrTooLarge
|
|
}
|
|
checked, err := (storage.UploadPolicy{MaxBytes: s.maxBytes}).PrepareUpload(ctx, reader, size, contentType)
|
|
if err != nil {
|
|
return storage.Info{}, err
|
|
}
|
|
result, err := s.client.PutObject(ctx, s.bucket, key, checked, size, minio.PutObjectOptions{ContentType: contentType})
|
|
if err != nil {
|
|
return storage.Info{}, err
|
|
}
|
|
return storage.Info{Key: key, Size: result.Size, ContentType: contentType, ModifiedAt: time.Now().UTC()}, nil
|
|
}
|
|
func (s *Store) Get(ctx context.Context, key string) (storage.Object, error) {
|
|
if err := storage.Check(ctx, key); err != nil {
|
|
return storage.Object{}, err
|
|
}
|
|
object, err := s.client.GetObject(ctx, s.bucket, key, minio.GetObjectOptions{})
|
|
if err != nil {
|
|
return storage.Object{}, mapError(err)
|
|
}
|
|
stat, err := object.Stat()
|
|
if err != nil {
|
|
object.Close()
|
|
return storage.Object{}, mapError(err)
|
|
}
|
|
if stat.Size > s.maxBytes {
|
|
object.Close()
|
|
return storage.Object{}, storage.ErrTooLarge
|
|
}
|
|
return storage.Object{Info: storage.Info{Key: key, Size: stat.Size, ContentType: stat.ContentType, ModifiedAt: stat.LastModified}, Body: object}, nil
|
|
}
|
|
func (s *Store) Delete(ctx context.Context, key string) error {
|
|
if err := storage.Check(ctx, key); err != nil {
|
|
return err
|
|
}
|
|
err := s.client.RemoveObject(ctx, s.bucket, key, minio.RemoveObjectOptions{})
|
|
if errors.Is(mapError(err), storage.ErrNotFound) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
func (s *Store) PresignGet(ctx context.Context, key string, ttl time.Duration) (*url.URL, error) {
|
|
if err := storage.Check(ctx, key); err != nil {
|
|
return nil, err
|
|
}
|
|
if ttl < time.Second || ttl > time.Hour {
|
|
return nil, errors.New("storage/s3: TTL must be 1s..1h")
|
|
}
|
|
return s.client.PresignedGetObject(ctx, s.bucket, key, ttl, url.Values{"response-content-disposition": {"attachment"}})
|
|
}
|
|
func mapError(err error) error {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
switch minio.ToErrorResponse(err).Code {
|
|
case "NoSuchKey", "NoSuchObject":
|
|
return storage.ErrNotFound
|
|
}
|
|
return err
|
|
}
|