listmonk/internal/media/providers/s3/s3.go
guangwu 4577868567
chore: remove refs to deprecated io/ioutil (#1593)
Signed-off-by: guoguangwu <guoguangwu@magic-shield.com>
2023-11-16 13:57:00 +05:30

162 lines
4.1 KiB
Go

package s3
import (
"fmt"
"io"
"net/url"
"path/filepath"
"strings"
"time"
"github.com/knadh/listmonk/internal/media"
"github.com/rhnvrm/simples3"
)
// Opt represents AWS S3 specific params
type Opt struct {
URL string `koanf:"url"`
PublicURL string `koanf:"public_url"`
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"`
BucketType string `koanf:"bucket_type"`
Expiry time.Duration `koanf:"expiry"`
}
// Client implements `media.Store` for S3 provider
type Client struct {
s3 *simples3.S3
opts Opt
}
// 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(opt Opt) (media.Store, error) {
var cl *simples3.S3
if opt.URL == "" {
opt.URL = fmt.Sprintf("https://s3.%s.amazonaws.com", opt.Region)
}
opt.URL = strings.TrimRight(opt.URL, "/")
// Default (and max S3 expiry) is 7 days.
if opt.Expiry.Seconds() < 1 {
opt.Expiry = time.Duration(167) * time.Hour
}
if opt.AccessKey == "" && opt.SecretKey == "" {
// fallback to IAM role if no access key/secret key is provided.
cl, _ = simples3.NewUsingIAM(opt.Region)
}
if cl == nil {
cl = simples3.New(opt.Region, opt.AccessKey, opt.SecretKey)
}
cl.SetEndpoint(opt.URL)
return &Client{
s3: cl,
opts: opt,
}, 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
p := simples3.UploadInput{
Bucket: c.opts.Bucket,
ContentType: cType,
FileName: name,
Body: file,
// Paths inside the bucket should not start with /.
ObjectKey: c.makeBucketPath(name),
}
if c.opts.BucketType == "public" {
p.ACL = "public-read"
}
// Upload.
if _, err := c.s3.FilePut(p); err != nil {
return "", err
}
return name, nil
}
// Get accepts the filename of the object stored and retrieves from S3.
func (c *Client) GetURL(name string) string {
// Generate a private S3 pre-signed URL if it's a private bucket, and there
// is no public URL provided.
if c.opts.BucketType == "private" && c.opts.PublicURL == "" {
u := c.s3.GeneratePresignedURL(simples3.PresignedInput{
Bucket: c.opts.Bucket,
ObjectKey: c.makeBucketPath(name),
Method: "GET",
Timestamp: time.Now(),
ExpirySeconds: int(c.opts.Expiry.Seconds()),
})
return u
}
// Generate a public S3 URL if it's a public bucket or a public URL is
// provided.
return c.makeFileURL(name)
}
// GetBlob reads a file from S3 and returns the raw bytes.
func (c *Client) GetBlob(uurl string) ([]byte, error) {
if p, err := url.Parse(uurl); err != nil {
uurl = filepath.Base(uurl)
} else {
uurl = filepath.Base(p.Path)
}
file, err := c.s3.FileDownload(simples3.DownloadInput{
Bucket: c.opts.Bucket,
ObjectKey: c.makeBucketPath(filepath.Base(uurl)),
})
if err != nil {
return nil, err
}
b, err := io.ReadAll(file)
if err != nil {
return nil, err
}
defer file.Close()
return b, nil
}
// 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: c.makeBucketPath(name),
})
return err
}
// makeBucketPath returns the file path inside the bucket. The path should not
// start with a /.
func (c *Client) makeBucketPath(name string) string {
// If the path is root (/), return the filename without the preceding slash.
p := strings.TrimPrefix(strings.TrimSuffix(c.opts.BucketPath, "/"), "/")
if p == "" {
return name
}
// whatever/bucket/path/filename.jpg: No preceding slash.
return p + "/" + name
}
func (c *Client) makeFileURL(name string) string {
if c.opts.PublicURL != "" {
return c.opts.PublicURL + "/" + c.makeBucketPath(name)
}
return c.opts.URL + "/" + c.opts.Bucket + "/" + c.makeBucketPath(name)
}