@@ -0,0 +1,78 @@
|
||||
package filesystem
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"source.offmarket.win/aleagle/eaglecast/internal/media"
|
||||
)
|
||||
|
||||
// Opts represents filesystem params
|
||||
type Opts struct {
|
||||
UploadPath string `koanf:"upload_path"`
|
||||
UploadURI string `koanf:"upload_uri"`
|
||||
RootURL string `koanf:"root_url"`
|
||||
}
|
||||
|
||||
// Client implements `media.Store`
|
||||
type Client struct {
|
||||
opts Opts
|
||||
}
|
||||
|
||||
// New initialises store for Filesystem provider.
|
||||
func New(opts Opts) (media.Store, error) {
|
||||
return &Client{
|
||||
opts: opts,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Put accepts the filename, the content type and file object itself and stores the file in disk.
|
||||
func (c *Client) Put(filename string, cType string, src io.ReadSeeker) (string, error) {
|
||||
// Get the directory path
|
||||
dir := getDir(c.opts.UploadPath)
|
||||
|
||||
// Read the file contents.
|
||||
out, err := os.OpenFile(filepath.Join(dir, filename), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0664)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
// Copy it to the target location.
|
||||
if _, err := io.Copy(out, src); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return filename, nil
|
||||
}
|
||||
|
||||
// GetURL accepts a filename and retrieves the full path from disk.
|
||||
func (c *Client) GetURL(name string) string {
|
||||
return fmt.Sprintf("%s%s/%s", c.opts.RootURL, c.opts.UploadURI, name)
|
||||
}
|
||||
|
||||
// GetBlob accepts a URL, reads the file, and returns the blob.
|
||||
func (c *Client) GetBlob(url string) ([]byte, error) {
|
||||
b, err := os.ReadFile(filepath.Join(getDir(c.opts.UploadPath), filepath.Base(url)))
|
||||
return b, err
|
||||
}
|
||||
|
||||
// Delete accepts a filename and removes it from disk.
|
||||
func (c *Client) Delete(file string) error {
|
||||
dir := getDir(c.opts.UploadPath)
|
||||
|
||||
err := os.Remove(filepath.Join(dir, file))
|
||||
return err
|
||||
}
|
||||
|
||||
// getDir returns the current working directory path if no directory is specified,
|
||||
// else returns the directory path specified itself.
|
||||
func getDir(dir string) string {
|
||||
if dir == "" {
|
||||
dir, _ = os.Getwd()
|
||||
}
|
||||
|
||||
return dir
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
package s3
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"source.offmarket.win/aleagle/eaglecast/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"`
|
||||
RootURL string `koanf:"root_url"`
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// Download the file from S3.
|
||||
file, err := c.s3.FileDownload(simples3.DownloadInput{
|
||||
Bucket: c.opts.Bucket,
|
||||
ObjectKey: c.makeBucketPath(filepath.Base(uurl)),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Read it into a byte blob.
|
||||
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 != "" {
|
||||
prefix := c.opts.PublicURL
|
||||
if strings.HasPrefix(prefix, "/") {
|
||||
prefix = c.opts.RootURL + prefix
|
||||
}
|
||||
return prefix + "/" + c.makeBucketPath(name)
|
||||
}
|
||||
|
||||
return c.opts.URL + "/" + c.opts.Bucket + "/" + c.makeBucketPath(name)
|
||||
}
|
||||
Reference in New Issue
Block a user