2019-10-15 14:21:32 +02:00
|
|
|
package filesystem
|
|
|
|
|
|
|
|
import (
|
|
|
|
"crypto/rand"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"os"
|
|
|
|
"path/filepath"
|
|
|
|
"regexp"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
|
2020-03-07 19:47:54 +01:00
|
|
|
"github.com/knadh/listmonk/internal/media"
|
2019-10-15 14:21:32 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
const tmpFilePrefix = "listmonk"
|
|
|
|
|
|
|
|
// Opts represents filesystem params
|
|
|
|
type Opts struct {
|
|
|
|
UploadPath string `koanf:"upload_path"`
|
|
|
|
UploadURI string `koanf:"upload_uri"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// Client implements `media.Store`
|
|
|
|
type Client struct {
|
|
|
|
opts Opts
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewDiskStore initialises store for Filesystem provider.
|
|
|
|
func NewDiskStore(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 (e *Client) Put(filename string, cType string, src io.ReadSeeker) (string, error) {
|
|
|
|
var out *os.File
|
|
|
|
// There's no explicit name. Use the one posted in the HTTP request.
|
|
|
|
if filename == "" {
|
|
|
|
filename = strings.TrimSpace(filename)
|
|
|
|
if filename == "" {
|
|
|
|
filename, _ = generateRandomString(10)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Get the directory path
|
|
|
|
dir := getDir(e.opts.UploadPath)
|
|
|
|
filename = assertUniqueFilename(dir, filename)
|
|
|
|
o, err := os.OpenFile(filepath.Join(dir, filename), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0664)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
out = o
|
|
|
|
defer out.Close()
|
|
|
|
|
|
|
|
if _, err := io.Copy(out, src); err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
return filename, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get accepts a filename and retrieves the full path from disk.
|
|
|
|
func (e *Client) Get(name string) string {
|
|
|
|
return fmt.Sprintf("%s/%s", e.opts.UploadURI, name)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Delete accepts a filename and removes it from disk.
|
|
|
|
func (e *Client) Delete(file string) error {
|
|
|
|
dir := getDir(e.opts.UploadPath)
|
|
|
|
err := os.Remove(filepath.Join(dir, file))
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// This matches filenames, sans extensions, of the format
|
|
|
|
// filename_(number). The number is incremented in case
|
|
|
|
// new file uploads conflict with existing filenames
|
|
|
|
// on the filesystem.
|
|
|
|
var fnameRegexp = regexp.MustCompile(`(.+?)_([0-9]+)$`)
|
|
|
|
|
|
|
|
// assertUniqueFilename takes a file path and check if it exists on the disk. If it doesn't,
|
|
|
|
// it returns the same name and if it does, it adds a small random hash to the filename
|
|
|
|
// and returns that.
|
|
|
|
func assertUniqueFilename(dir, fileName string) string {
|
|
|
|
var (
|
|
|
|
ext = filepath.Ext(fileName)
|
|
|
|
base = fileName[0 : len(fileName)-len(ext)]
|
|
|
|
num = 0
|
|
|
|
)
|
|
|
|
|
|
|
|
for {
|
|
|
|
// There's no name conflict.
|
|
|
|
if _, err := os.Stat(filepath.Join(dir, fileName)); os.IsNotExist(err) {
|
|
|
|
return fileName
|
|
|
|
}
|
|
|
|
|
|
|
|
// Does the name match the _(num) syntax?
|
|
|
|
r := fnameRegexp.FindAllStringSubmatch(fileName, -1)
|
|
|
|
if len(r) == 1 && len(r[0]) == 3 {
|
|
|
|
num, _ = strconv.Atoi(r[0][2])
|
|
|
|
}
|
|
|
|
num++
|
|
|
|
|
|
|
|
fileName = fmt.Sprintf("%s_%d%s", base, num, ext)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// generateRandomString generates a cryptographically random, alphanumeric string of length n.
|
|
|
|
func generateRandomString(n int) (string, error) {
|
|
|
|
const dictionary = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
|
|
|
|
|
|
|
var bytes = make([]byte, n)
|
|
|
|
if _, err := rand.Read(bytes); err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
for k, v := range bytes {
|
|
|
|
bytes[k] = dictionary[v%byte(len(dictionary))]
|
|
|
|
}
|
|
|
|
|
|
|
|
return string(bytes), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|