feat: add s3 storage provider integration
This commit is contained in:
@@ -5,4 +5,11 @@ type Config struct {
|
||||
LocalPath string // for local
|
||||
Secret string // for signing
|
||||
BaseURL string // public url prefix
|
||||
// S3-compatible config
|
||||
AccessKey string
|
||||
SecretKey string
|
||||
Region string
|
||||
Bucket string
|
||||
Endpoint string
|
||||
PathStyle bool
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
@@ -9,8 +10,11 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio-go/v7"
|
||||
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||
"go.ipao.vip/atom/container"
|
||||
"go.ipao.vip/atom/opt"
|
||||
)
|
||||
@@ -33,16 +37,23 @@ func Provide(opts ...opt.Option) error {
|
||||
return err
|
||||
}
|
||||
return container.Container.Provide(func() (*Storage, error) {
|
||||
return &Storage{Config: &config}, nil
|
||||
store := &Storage{Config: &config}
|
||||
if store.storageType() == "s3" {
|
||||
if _, err := store.s3ClientForUse(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return store, nil
|
||||
}, o.DiOptions()...)
|
||||
}
|
||||
|
||||
type Storage struct {
|
||||
Config *Config
|
||||
Config *Config
|
||||
s3Client *minio.Client
|
||||
}
|
||||
|
||||
func (s *Storage) Delete(key string) error {
|
||||
if s.Config.Type == "local" {
|
||||
if s.storageType() == "local" {
|
||||
localPath := s.Config.LocalPath
|
||||
if localPath == "" {
|
||||
localPath = "./storage"
|
||||
@@ -50,15 +61,41 @@ func (s *Storage) Delete(key string) error {
|
||||
path := filepath.Join(localPath, key)
|
||||
return os.Remove(path)
|
||||
}
|
||||
// TODO: S3 implementation
|
||||
return nil
|
||||
client, err := s.s3ClientForUse()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return client.RemoveObject(context.Background(), s.Config.Bucket, key, minio.RemoveObjectOptions{})
|
||||
}
|
||||
|
||||
func (s *Storage) SignURL(method, key string, expires time.Duration) (string, error) {
|
||||
if s.storageType() == "s3" {
|
||||
client, err := s.s3ClientForUse()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
switch strings.ToUpper(method) {
|
||||
case "GET":
|
||||
u, err := client.PresignedGetObject(context.Background(), s.Config.Bucket, key, expires, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return u.String(), nil
|
||||
case "PUT":
|
||||
u, err := client.PresignedPutObject(context.Background(), s.Config.Bucket, key, expires)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return u.String(), nil
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported method")
|
||||
}
|
||||
}
|
||||
|
||||
exp := time.Now().Add(expires).Unix()
|
||||
sign := s.signature(method, key, exp)
|
||||
|
||||
baseURL := s.Config.BaseURL
|
||||
baseURL := strings.TrimRight(s.Config.BaseURL, "/")
|
||||
// Ensure BaseURL doesn't end with slash if we add one
|
||||
// Simplified: assume standard /v1/storage prefix in BaseURL or append it
|
||||
// We'll append /<key>
|
||||
@@ -77,6 +114,9 @@ func (s *Storage) SignURL(method, key string, expires time.Duration) (string, er
|
||||
}
|
||||
|
||||
func (s *Storage) Verify(method, key, expStr, sign string) error {
|
||||
if s.storageType() == "s3" {
|
||||
return fmt.Errorf("s3 storage does not use signed local urls")
|
||||
}
|
||||
exp, err := strconv.ParseInt(expStr, 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid expiry")
|
||||
@@ -98,3 +138,105 @@ func (s *Storage) signature(method, key string, exp int64) string {
|
||||
h.Write([]byte(str))
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
func (s *Storage) PutObject(ctx context.Context, key, filePath, contentType string) error {
|
||||
if s.storageType() == "local" {
|
||||
localPath := s.Config.LocalPath
|
||||
if localPath == "" {
|
||||
localPath = "./storage"
|
||||
}
|
||||
dstPath := filepath.Join(localPath, key)
|
||||
if err := os.MkdirAll(filepath.Dir(dstPath), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(filePath, dstPath)
|
||||
}
|
||||
|
||||
client, err := s.s3ClientForUse()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
opts := minio.PutObjectOptions{}
|
||||
if contentType != "" {
|
||||
opts.ContentType = contentType
|
||||
}
|
||||
_, err = client.FPutObject(ctx, s.Config.Bucket, key, filePath, opts)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Storage) Provider() string {
|
||||
if s.storageType() == "s3" {
|
||||
return "s3"
|
||||
}
|
||||
return "local"
|
||||
}
|
||||
|
||||
func (s *Storage) Bucket() string {
|
||||
if s.storageType() == "s3" && s.Config.Bucket != "" {
|
||||
return s.Config.Bucket
|
||||
}
|
||||
return "default"
|
||||
}
|
||||
|
||||
func (s *Storage) storageType() string {
|
||||
if s.Config == nil {
|
||||
return "local"
|
||||
}
|
||||
typ := strings.TrimSpace(strings.ToLower(s.Config.Type))
|
||||
if typ == "" {
|
||||
return "local"
|
||||
}
|
||||
return typ
|
||||
}
|
||||
|
||||
func (s *Storage) s3ClientForUse() (*minio.Client, error) {
|
||||
if s.s3Client != nil {
|
||||
return s.s3Client, nil
|
||||
}
|
||||
if strings.TrimSpace(s.Config.Endpoint) == "" {
|
||||
return nil, fmt.Errorf("storage endpoint is required")
|
||||
}
|
||||
if strings.TrimSpace(s.Config.AccessKey) == "" || strings.TrimSpace(s.Config.SecretKey) == "" {
|
||||
return nil, fmt.Errorf("storage access key or secret key is required")
|
||||
}
|
||||
if strings.TrimSpace(s.Config.Bucket) == "" {
|
||||
return nil, fmt.Errorf("storage bucket is required")
|
||||
}
|
||||
|
||||
endpoint, secure, err := parseEndpoint(s.Config.Endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
opts := &minio.Options{
|
||||
Creds: credentials.NewStaticV4(s.Config.AccessKey, s.Config.SecretKey, ""),
|
||||
Secure: secure,
|
||||
Region: s.Config.Region,
|
||||
}
|
||||
if s.Config.PathStyle {
|
||||
opts.BucketLookup = minio.BucketLookupPath
|
||||
} else {
|
||||
opts.BucketLookup = minio.BucketLookupDNS
|
||||
}
|
||||
|
||||
client, err := minio.New(endpoint, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.s3Client = client
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func parseEndpoint(endpoint string) (string, bool, error) {
|
||||
if strings.HasPrefix(endpoint, "http://") || strings.HasPrefix(endpoint, "https://") {
|
||||
u, err := url.Parse(endpoint)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
if u.Host == "" {
|
||||
return "", false, fmt.Errorf("invalid endpoint")
|
||||
}
|
||||
return u.Host, u.Scheme == "https", nil
|
||||
}
|
||||
return endpoint, false, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user