// 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 }