mirror of
https://github.com/knadh/listmonk.git
synced 2024-11-15 20:15:29 +08:00
4577868567
Signed-off-by: guoguangwu <guoguangwu@magic-shield.com>
162 lines
4.1 KiB
Go
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)
|
|
}
|