feat: add direct s3 upload flow
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
awsconfig "github.com/aws/aws-sdk-go-v2/config"
|
||||
"github.com/aws/aws-sdk-go-v2/credentials"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3/types"
|
||||
"github.com/perfect-panel/server/internal/config"
|
||||
)
|
||||
|
||||
type PresignUploadResult struct {
|
||||
URL string
|
||||
Method string
|
||||
ExpiresAt int64
|
||||
}
|
||||
|
||||
type ObjectMeta struct {
|
||||
ETag string
|
||||
ContentType string
|
||||
ContentLength int64
|
||||
}
|
||||
|
||||
type S3Store struct {
|
||||
cfg config.S3Config
|
||||
client *s3.Client
|
||||
presign *s3.PresignClient
|
||||
}
|
||||
|
||||
func NewS3Store(ctx context.Context, cfg config.S3Config) (*S3Store, error) {
|
||||
loaders := make([]func(*awsconfig.LoadOptions) error, 0, 3)
|
||||
if cfg.Region != "" {
|
||||
loaders = append(loaders, awsconfig.WithRegion(cfg.Region))
|
||||
}
|
||||
if cfg.AccessKey != "" || cfg.SecretKey != "" || cfg.SessionToken != "" {
|
||||
loaders = append(loaders, awsconfig.WithCredentialsProvider(
|
||||
credentials.NewStaticCredentialsProvider(cfg.AccessKey, cfg.SecretKey, cfg.SessionToken),
|
||||
))
|
||||
}
|
||||
|
||||
awsCfg, err := awsconfig.LoadDefaultConfig(ctx, loaders...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client := s3.NewFromConfig(awsCfg, func(o *s3.Options) {
|
||||
o.UsePathStyle = cfg.UsePathStyle
|
||||
if cfg.Endpoint != "" {
|
||||
o.BaseEndpoint = aws.String(cfg.Endpoint)
|
||||
}
|
||||
})
|
||||
|
||||
return &S3Store{
|
||||
cfg: cfg,
|
||||
client: client,
|
||||
presign: s3.NewPresignClient(client),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *S3Store) PresignPutObject(ctx context.Context, objectKey string, contentType string, expires time.Duration) (*PresignUploadResult, error) {
|
||||
req, err := s.presign.PresignPutObject(ctx, &s3.PutObjectInput{
|
||||
Bucket: aws.String(s.cfg.Bucket),
|
||||
Key: aws.String(objectKey),
|
||||
ContentType: aws.String(contentType),
|
||||
}, s3.WithPresignExpires(expires))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &PresignUploadResult{
|
||||
URL: req.URL,
|
||||
Method: req.Method,
|
||||
ExpiresAt: time.Now().Add(expires).Unix(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *S3Store) HeadObject(ctx context.Context, objectKey string) (*ObjectMeta, error) {
|
||||
resp, err := s.client.HeadObject(ctx, &s3.HeadObjectInput{
|
||||
Bucket: aws.String(s.cfg.Bucket),
|
||||
Key: aws.String(objectKey),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ObjectMeta{
|
||||
ETag: strings.Trim(aws.ToString(resp.ETag), "\""),
|
||||
ContentType: aws.ToString(resp.ContentType),
|
||||
ContentLength: aws.ToInt64(resp.ContentLength),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *S3Store) BuildObjectURL(objectKey string) string {
|
||||
if s.cfg.PublicBaseURL != "" {
|
||||
return strings.TrimRight(s.cfg.PublicBaseURL, "/") + "/" + strings.TrimLeft(objectKey, "/")
|
||||
}
|
||||
|
||||
if s.cfg.Endpoint != "" {
|
||||
base := strings.TrimRight(s.cfg.Endpoint, "/")
|
||||
return fmt.Sprintf("%s/%s/%s", base, url.PathEscape(s.cfg.Bucket), strings.TrimLeft(objectKey, "/"))
|
||||
}
|
||||
|
||||
return fmt.Sprintf("https://%s.s3.%s.amazonaws.com/%s", s.cfg.Bucket, s.cfg.Region, strings.TrimLeft(objectKey, "/"))
|
||||
}
|
||||
|
||||
func (s *S3Store) PutJSONTagging(ctx context.Context, objectKey string, values map[string]string) error {
|
||||
tagging := make([]types.Tag, 0, len(values))
|
||||
for k, v := range values {
|
||||
key := k
|
||||
value := v
|
||||
tagging = append(tagging, types.Tag{Key: &key, Value: &value})
|
||||
}
|
||||
|
||||
_, err := s.client.PutObjectTagging(ctx, &s3.PutObjectTaggingInput{
|
||||
Bucket: aws.String(s.cfg.Bucket),
|
||||
Key: aws.String(objectKey),
|
||||
Tagging: &types.Tagging{
|
||||
TagSet: tagging,
|
||||
},
|
||||
})
|
||||
return err
|
||||
}
|
||||
Reference in New Issue
Block a user