filesystem.go 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. package filesystem
  2. import (
  3. "crypto/rand"
  4. "fmt"
  5. "io"
  6. "os"
  7. "path/filepath"
  8. "regexp"
  9. "strconv"
  10. "strings"
  11. "github.com/knadh/listmonk/internal/media"
  12. )
  13. const tmpFilePrefix = "listmonk"
  14. // Opts represents filesystem params
  15. type Opts struct {
  16. UploadPath string `koanf:"upload_path"`
  17. UploadURI string `koanf:"upload_uri"`
  18. RootURL string `koanf:"root_url"`
  19. }
  20. // Client implements `media.Store`
  21. type Client struct {
  22. opts Opts
  23. }
  24. // This matches filenames, sans extensions, of the format
  25. // filename_(number). The number is incremented in case
  26. // new file uploads conflict with existing filenames
  27. // on the filesystem.
  28. var fnameRegexp = regexp.MustCompile(`(.+?)_([0-9]+)$`)
  29. // NewDiskStore initialises store for Filesystem provider.
  30. func NewDiskStore(opts Opts) (media.Store, error) {
  31. return &Client{
  32. opts: opts,
  33. }, nil
  34. }
  35. // Put accepts the filename, the content type and file object itself and stores the file in disk.
  36. func (c *Client) Put(filename string, cType string, src io.ReadSeeker) (string, error) {
  37. var out *os.File
  38. // There's no explicit name. Use the one posted in the HTTP request.
  39. if filename == "" {
  40. filename = strings.TrimSpace(filename)
  41. if filename == "" {
  42. filename, _ = generateRandomString(10)
  43. }
  44. }
  45. // Get the directory path
  46. dir := getDir(c.opts.UploadPath)
  47. filename = assertUniqueFilename(dir, filename)
  48. o, err := os.OpenFile(filepath.Join(dir, filename), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0664)
  49. if err != nil {
  50. return "", err
  51. }
  52. out = o
  53. defer out.Close()
  54. if _, err := io.Copy(out, src); err != nil {
  55. return "", err
  56. }
  57. return filename, nil
  58. }
  59. // Get accepts a filename and retrieves the full path from disk.
  60. func (c *Client) Get(name string) string {
  61. return fmt.Sprintf("%s%s/%s", c.opts.RootURL, c.opts.UploadURI, name)
  62. }
  63. // Delete accepts a filename and removes it from disk.
  64. func (c *Client) Delete(file string) error {
  65. dir := getDir(c.opts.UploadPath)
  66. err := os.Remove(filepath.Join(dir, file))
  67. if err != nil {
  68. return err
  69. }
  70. return nil
  71. }
  72. // assertUniqueFilename takes a file path and check if it exists on the disk. If it doesn't,
  73. // it returns the same name and if it does, it adds a small random hash to the filename
  74. // and returns that.
  75. func assertUniqueFilename(dir, fileName string) string {
  76. var (
  77. ext = filepath.Ext(fileName)
  78. base = fileName[0 : len(fileName)-len(ext)]
  79. num = 0
  80. )
  81. for {
  82. // There's no name conflict.
  83. if _, err := os.Stat(filepath.Join(dir, fileName)); os.IsNotExist(err) {
  84. return fileName
  85. }
  86. // Does the name match the _(num) syntax?
  87. r := fnameRegexp.FindAllStringSubmatch(fileName, -1)
  88. if len(r) == 1 && len(r[0]) == 3 {
  89. num, _ = strconv.Atoi(r[0][2])
  90. }
  91. num++
  92. fileName = fmt.Sprintf("%s_%d%s", base, num, ext)
  93. }
  94. }
  95. // generateRandomString generates a cryptographically random, alphanumeric string of length n.
  96. func generateRandomString(n int) (string, error) {
  97. const dictionary = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
  98. var bytes = make([]byte, n)
  99. if _, err := rand.Read(bytes); err != nil {
  100. return "", err
  101. }
  102. for k, v := range bytes {
  103. bytes[k] = dictionary[v%byte(len(dictionary))]
  104. }
  105. return string(bytes), nil
  106. }
  107. // getDir returns the current working directory path if no directory is specified,
  108. // else returns the directory path specified itself.
  109. func getDir(dir string) string {
  110. if dir == "" {
  111. dir, _ = os.Getwd()
  112. }
  113. return dir
  114. }