listmonk/internal/media/providers/s3/s3.go

117 lines
3.3 KiB
Go
Raw Permalink Normal View History

package s3
import (
"errors"
"fmt"
"io"
"strings"
"time"
"github.com/knadh/listmonk/internal/media"
"github.com/rhnvrm/simples3"
)
const amznS3PublicURL = "https://%s.s3.%s.amazonaws.com%s"
// Opts represents AWS S3 specific params
type Opts struct {
AccessKey string `koanf:"aws_access_key_id"`
SecretKey string `koanf:"aws_secret_access_key"`
Region string `koanf:"aws_default_region"`
Bucket string `koanf:"bucket"`
BucketPath string `koanf:"bucket_path"`
BucketURL string `koanf:"bucket_url"`
BucketType string `koanf:"bucket_type"`
Expiry time.Duration `koanf:"expiry"`
}
// Client implements `media.Store` for S3 provider
type Client struct {
s3 *simples3.S3
opts Opts
}
// NewS3Store initialises store for S3 provider. It takes in the AWS configuration
// and sets up the `simples3` client to interact with AWS APIs for all bucket operations.
func NewS3Store(opts Opts) (media.Store, error) {
var s3svc *simples3.S3
var err error
if opts.Region == "" {
return nil, errors.New("Invalid AWS Region specified. Please check `upload.s3` config")
}
// Use Access Key/Secret Key if specified in config.
if opts.AccessKey != "" && opts.SecretKey != "" {
s3svc = simples3.New(opts.Region, opts.AccessKey, opts.SecretKey)
} else {
// fallback to IAM role if no access key/secret key is provided.
s3svc, err = simples3.NewUsingIAM(opts.Region)
if err != nil {
return nil, err
}
}
return &Client{
s3: s3svc,
opts: opts,
}, nil
}
// Put takes in the filename, the content type and file object itself and uploads to S3.
func (c *Client) Put(name string, cType string, file io.ReadSeeker) (string, error) {
// Upload input parameters
upParams := simples3.UploadInput{
Bucket: c.opts.Bucket,
ContentType: cType,
FileName: name,
Body: file,
// Paths inside the bucket should not start with /.
ObjectKey: strings.TrimPrefix(makeBucketPath(c.opts.BucketPath, name), "/"),
}
// Perform an upload.
if _, err := c.s3.FileUpload(upParams); err != nil {
return "", err
}
return name, nil
}
// Get accepts the filename of the object stored and retrieves from S3.
func (c *Client) Get(name string) string {
// Generate a private S3 pre-signed URL if it's a private bucket.
if c.opts.BucketType == "private" {
url := c.s3.GeneratePresignedURL(simples3.PresignedInput{
Bucket: c.opts.Bucket,
ObjectKey: makeBucketPath(c.opts.BucketPath, name),
Method: "GET",
Timestamp: time.Now(),
ExpirySeconds: int(c.opts.Expiry.Seconds()),
})
return url
}
// Generate a public S3 URL if it's a public bucket.
url := ""
if c.opts.BucketURL != "" {
url = c.opts.BucketURL + makeBucketPath(c.opts.BucketPath, name)
} else {
url = fmt.Sprintf(amznS3PublicURL, c.opts.Bucket, c.opts.Region,
makeBucketPath(c.opts.BucketPath, name))
}
return url
}
// Delete accepts the filename of the object and deletes from S3.
func (c *Client) Delete(name string) error {
err := c.s3.FileDelete(simples3.DeleteInput{
Bucket: c.opts.Bucket,
ObjectKey: strings.TrimPrefix(makeBucketPath(c.opts.BucketPath, name), "/"),
})
return err
}
func makeBucketPath(bucketPath string, name string) string {
if bucketPath == "/" {
return "/" + name
}
return fmt.Sprintf("%s/%s", bucketPath, name)
}