Files
plumber/internal/blob/spaces.go
T

110 lines
3.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package blob
import (
"context"
"fmt"
"io"
"os"
"strings"
"github.com/aws/aws-sdk-go-v2/aws"
"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"
)
// Uploader stores public avatar objects.
type Uploader interface {
Enabled() bool
Upload(ctx context.Context, obj Object) (publicURL string, err error)
}
// Object is a file to upload to object storage.
type Object struct {
Key string
Body io.Reader
ContentType string
Size int64
}
// Disabled is a no-op uploader used when Spaces is not configured.
type Disabled struct{}
// SpacesConfig holds DigitalOcean Spaces settings.
type SpacesConfig struct {
Key string
Secret string
Region string
Bucket string
Endpoint string // e.g. https://nyc3.digitaloceanspaces.com
CDNBase string // optional public base URL without trailing slash
}
type spaces struct {
client *s3.Client
cfg SpacesConfig
}
func (Disabled) Enabled() bool { return false }
func (Disabled) Upload(context.Context, Object) (string, error) {
return "", fmt.Errorf("avatar uploads are not configured")
}
// FromEnv builds an Uploader from SPACES_* environment variables.
func FromEnv() Uploader {
return NewSpaces(SpacesConfig{
Key: os.Getenv("SPACES_KEY"),
Secret: os.Getenv("SPACES_SECRET"),
Region: os.Getenv("SPACES_REGION"),
Bucket: os.Getenv("SPACES_BUCKET"),
Endpoint: os.Getenv("SPACES_ENDPOINT"),
CDNBase: os.Getenv("SPACES_CDN_BASE"),
})
}
// NewSpaces returns an Uploader when required env is present; otherwise Disabled.
func NewSpaces(cfg SpacesConfig) Uploader {
cfg.Key = strings.TrimSpace(cfg.Key)
cfg.Secret = strings.TrimSpace(cfg.Secret)
cfg.Region = strings.TrimSpace(cfg.Region)
cfg.Bucket = strings.TrimSpace(cfg.Bucket)
cfg.Endpoint = strings.TrimSpace(cfg.Endpoint)
cfg.CDNBase = strings.TrimRight(strings.TrimSpace(cfg.CDNBase), "/")
if cfg.Key == "" || cfg.Secret == "" || cfg.Region == "" || cfg.Bucket == "" || cfg.Endpoint == "" {
return Disabled{}
}
client := s3.New(s3.Options{
Region: cfg.Region,
Credentials: credentials.NewStaticCredentialsProvider(cfg.Key, cfg.Secret, ""),
BaseEndpoint: aws.String(cfg.Endpoint),
})
return &spaces{client: client, cfg: cfg}
}
func (s *spaces) Enabled() bool { return true }
func (s *spaces) Upload(ctx context.Context, obj Object) (string, error) {
key := strings.TrimPrefix(obj.Key, "/")
input := &s3.PutObjectInput{
Bucket: aws.String(s.cfg.Bucket),
Key: aws.String(key),
Body: obj.Body,
ContentType: aws.String(obj.ContentType),
ACL: types.ObjectCannedACLPublicRead,
}
if obj.Size > 0 {
input.ContentLength = aws.Int64(obj.Size)
}
if _, err := s.client.PutObject(ctx, input); err != nil {
return "", err
}
if s.cfg.CDNBase != "" {
return s.cfg.CDNBase + "/" + key, nil
}
// Virtual-hostedstyle Spaces URL.
host := strings.TrimPrefix(s.cfg.Endpoint, "https://")
host = strings.TrimPrefix(host, "http://")
return fmt.Sprintf("https://%s.%s/%s", s.cfg.Bucket, host, key), nil
}