mirror of
https://github.com/knadh/listmonk.git
synced 2025-01-22 06:08:09 +08:00
942eb7c3d8
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.
116 lines
3.3 KiB
Go
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)
|
|
}
|