filesystem.go 3.3 KB

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