listmonk/internal/media/providers/s3/s3.go
Kailash Nadh 942eb7c3d8 Add settings UI and "hot reload" support to the app.
This is a major breaking change that moves away from having the
entire app configuration in external TOML files to settings being
in the database with a UI to update them dynamically.

The app loads all config into memory (app settings, SMTP conf)
on boot. "Hot" replacing them is complex and it's a fair tradeoff
to instead just restart the application as it is practically
instant.

A new `settings` table stores arbitrary string keys with a JSONB
value field which happens to support arbitrary types. After every
settings update, the app gracefully releases all resources
(HTTP server, DB pool, SMTP pool etc.) and restarts itself,
occupying the same PID. If there are any running campaigns, the
auto-restart doesn't happen and the user is prompted to invoke
it manually with a one-click button once all running campaigns
have been paused.
2020-07-21 00:23:57 +05:30

116 lines
3.3 KiB
Go

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