2018-10-25 21:51:47 +08:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2020-03-08 03:05:34 +08:00
|
|
|
"bytes"
|
|
|
|
"mime/multipart"
|
2018-10-25 21:51:47 +08:00
|
|
|
"net/http"
|
2021-05-23 22:47:42 +08:00
|
|
|
"path/filepath"
|
2018-10-25 21:51:47 +08:00
|
|
|
"strconv"
|
2023-03-25 14:16:05 +08:00
|
|
|
"strings"
|
2018-10-25 21:51:47 +08:00
|
|
|
|
2020-03-08 03:05:34 +08:00
|
|
|
"github.com/disintegration/imaging"
|
2022-10-03 01:34:51 +08:00
|
|
|
"github.com/knadh/listmonk/models"
|
2021-12-09 23:21:07 +08:00
|
|
|
"github.com/labstack/echo/v4"
|
2018-10-25 21:51:47 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
2019-10-25 13:41:47 +08:00
|
|
|
thumbPrefix = "thumb_"
|
2023-05-18 19:25:59 +08:00
|
|
|
thumbnailSize = 250
|
2018-10-25 21:51:47 +08:00
|
|
|
)
|
|
|
|
|
2021-05-23 22:47:42 +08:00
|
|
|
var (
|
2023-05-18 19:25:59 +08:00
|
|
|
vectorExts = []string{"svg"}
|
|
|
|
imageExts = []string{"gif", "png", "jpg", "jpeg"}
|
2021-05-23 22:47:42 +08:00
|
|
|
)
|
2020-03-08 01:30:55 +08:00
|
|
|
|
2018-10-25 21:51:47 +08:00
|
|
|
// handleUploadMedia handles media file uploads.
|
|
|
|
func handleUploadMedia(c echo.Context) error {
|
|
|
|
var (
|
|
|
|
app = c.Get("app").(*App)
|
|
|
|
cleanUp = false
|
|
|
|
)
|
2019-10-15 20:21:32 +08:00
|
|
|
file, err := c.FormFile("file")
|
|
|
|
if err != nil {
|
|
|
|
return echo.NewHTTPError(http.StatusBadRequest,
|
2021-01-23 21:24:33 +08:00
|
|
|
app.i18n.Ts("media.invalidFile", "error", err.Error()))
|
2019-10-15 20:21:32 +08:00
|
|
|
}
|
2020-03-08 01:30:55 +08:00
|
|
|
|
2019-10-15 20:21:32 +08:00
|
|
|
// Read file contents in memory
|
|
|
|
src, err := file.Open()
|
|
|
|
if err != nil {
|
2020-12-19 18:55:52 +08:00
|
|
|
return echo.NewHTTPError(http.StatusInternalServerError,
|
2021-01-23 21:24:33 +08:00
|
|
|
app.i18n.Ts("media.errorReadingFile", "error", err.Error()))
|
2019-10-15 20:21:32 +08:00
|
|
|
}
|
|
|
|
defer src.Close()
|
2020-03-08 01:30:55 +08:00
|
|
|
|
2023-05-18 19:25:59 +08:00
|
|
|
var (
|
|
|
|
// Naive check for content type and extension.
|
|
|
|
ext = strings.TrimPrefix(strings.ToLower(filepath.Ext(file.Filename)), ".")
|
|
|
|
contentType = file.Header.Get("Content-Type")
|
|
|
|
)
|
2024-08-05 22:38:33 +08:00
|
|
|
if !isASCII(file.Filename) {
|
|
|
|
return echo.NewHTTPError(http.StatusUnprocessableEntity,
|
|
|
|
app.i18n.Ts("media.invalidFileName", "name", file.Filename))
|
|
|
|
}
|
2023-05-18 19:25:59 +08:00
|
|
|
|
|
|
|
// Validate file extension.
|
|
|
|
if !inArray("*", app.constants.MediaUpload.Extensions) {
|
|
|
|
if ok := inArray(ext, app.constants.MediaUpload.Extensions); !ok {
|
|
|
|
return echo.NewHTTPError(http.StatusBadRequest,
|
|
|
|
app.i18n.Ts("media.unsupportedFileType", "type", ext))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-08-08 18:12:29 +08:00
|
|
|
// Sanitize filename.
|
2023-05-18 19:25:59 +08:00
|
|
|
fName := makeFilename(file.Filename)
|
2024-08-08 18:12:29 +08:00
|
|
|
|
|
|
|
// Add a random suffix to the filename to ensure uniqueness.
|
|
|
|
suffix, _ := generateRandomString(6)
|
|
|
|
fName = appendSuffixToFilename(fName, suffix)
|
|
|
|
|
|
|
|
// Upload the file.
|
2023-05-18 19:25:59 +08:00
|
|
|
fName, err = app.media.Put(fName, contentType, src)
|
2018-10-25 21:51:47 +08:00
|
|
|
if err != nil {
|
2020-03-08 02:33:22 +08:00
|
|
|
app.log.Printf("error uploading file: %v", err)
|
2018-10-25 21:51:47 +08:00
|
|
|
return echo.NewHTTPError(http.StatusInternalServerError,
|
2021-01-23 21:24:33 +08:00
|
|
|
app.i18n.Ts("media.errorUploading", "error", err.Error()))
|
2018-10-25 21:51:47 +08:00
|
|
|
}
|
|
|
|
|
2023-05-18 19:25:59 +08:00
|
|
|
var (
|
|
|
|
thumbfName = ""
|
|
|
|
width = 0
|
|
|
|
height = 0
|
|
|
|
)
|
2018-10-25 21:51:47 +08:00
|
|
|
defer func() {
|
|
|
|
// If any of the subroutines in this function fail,
|
|
|
|
// the uploaded image should be removed.
|
|
|
|
if cleanUp {
|
2020-03-08 02:33:22 +08:00
|
|
|
app.media.Delete(fName)
|
2023-05-18 19:25:59 +08:00
|
|
|
|
|
|
|
if thumbfName != "" {
|
|
|
|
app.media.Delete(thumbfName)
|
|
|
|
}
|
2018-10-25 21:51:47 +08:00
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
2023-03-19 16:28:41 +08:00
|
|
|
// Create thumbnail from file for non-vector formats.
|
2023-05-18 19:25:59 +08:00
|
|
|
isImage := inArray(ext, imageExts)
|
|
|
|
if isImage {
|
2023-03-19 16:28:41 +08:00
|
|
|
thumbFile, w, h, err := processImage(file)
|
|
|
|
if err != nil {
|
|
|
|
cleanUp = true
|
|
|
|
app.log.Printf("error resizing image: %v", err)
|
|
|
|
return echo.NewHTTPError(http.StatusInternalServerError,
|
|
|
|
app.i18n.Ts("media.errorResizing", "error", err.Error()))
|
|
|
|
}
|
|
|
|
width = w
|
|
|
|
height = h
|
2020-03-07 23:07:48 +08:00
|
|
|
|
2023-03-19 16:28:41 +08:00
|
|
|
// Upload thumbnail.
|
2023-05-18 19:25:59 +08:00
|
|
|
tf, err := app.media.Put(thumbPrefix+fName, contentType, thumbFile)
|
2023-03-19 16:28:41 +08:00
|
|
|
if err != nil {
|
|
|
|
cleanUp = true
|
|
|
|
app.log.Printf("error saving thumbnail: %v", err)
|
|
|
|
return echo.NewHTTPError(http.StatusInternalServerError,
|
|
|
|
app.i18n.Ts("media.errorSavingThumbnail", "error", err.Error()))
|
|
|
|
}
|
|
|
|
thumbfName = tf
|
2018-10-25 21:51:47 +08:00
|
|
|
}
|
2023-05-18 19:25:59 +08:00
|
|
|
if inArray(ext, vectorExts) {
|
|
|
|
thumbfName = fName
|
|
|
|
}
|
2020-03-07 23:07:48 +08:00
|
|
|
|
2018-10-25 21:51:47 +08:00
|
|
|
// Write to the DB.
|
2023-05-18 19:25:59 +08:00
|
|
|
meta := models.JSON{}
|
|
|
|
if isImage {
|
|
|
|
meta = models.JSON{
|
|
|
|
"width": width,
|
|
|
|
"height": height,
|
|
|
|
}
|
2022-10-03 01:34:51 +08:00
|
|
|
}
|
2023-05-18 19:25:59 +08:00
|
|
|
m, err := app.core.InsertMedia(fName, thumbfName, contentType, meta, app.constants.MediaUpload.Provider, app.media)
|
2022-07-13 22:18:09 +08:00
|
|
|
if err != nil {
|
2018-10-25 21:51:47 +08:00
|
|
|
cleanUp = true
|
2022-04-03 23:24:40 +08:00
|
|
|
return err
|
2018-10-25 21:51:47 +08:00
|
|
|
}
|
2022-07-13 22:18:09 +08:00
|
|
|
return c.JSON(http.StatusOK, okResp{m})
|
2018-10-25 21:51:47 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// handleGetMedia handles retrieval of uploaded media.
|
|
|
|
func handleGetMedia(c echo.Context) error {
|
|
|
|
var (
|
2022-04-03 23:24:40 +08:00
|
|
|
app = c.Get("app").(*App)
|
2023-05-21 17:49:12 +08:00
|
|
|
pg = app.paginator.NewFromURL(c.Request().URL.Query())
|
|
|
|
query = c.FormValue("query")
|
2022-04-03 23:24:40 +08:00
|
|
|
id, _ = strconv.Atoi(c.Param("id"))
|
2018-10-25 21:51:47 +08:00
|
|
|
)
|
|
|
|
|
2022-04-03 23:24:40 +08:00
|
|
|
// Fetch one list.
|
|
|
|
if id > 0 {
|
|
|
|
out, err := app.core.GetMedia(id, "", app.media)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, okResp{out})
|
2018-10-25 21:51:47 +08:00
|
|
|
}
|
|
|
|
|
2023-05-21 17:49:12 +08:00
|
|
|
res, total, err := app.core.QueryMedia(app.constants.MediaUpload.Provider, app.media, query, pg.Offset, pg.Limit)
|
2022-04-03 23:24:40 +08:00
|
|
|
if err != nil {
|
|
|
|
return err
|
2018-10-25 21:51:47 +08:00
|
|
|
}
|
|
|
|
|
2023-05-21 17:49:12 +08:00
|
|
|
out := models.PageResults{
|
|
|
|
Results: res,
|
|
|
|
Total: total,
|
|
|
|
Page: pg.Page,
|
|
|
|
PerPage: pg.PerPage,
|
|
|
|
}
|
|
|
|
|
2018-10-25 21:51:47 +08:00
|
|
|
return c.JSON(http.StatusOK, okResp{out})
|
|
|
|
}
|
|
|
|
|
|
|
|
// deleteMedia handles deletion of uploaded media.
|
|
|
|
func handleDeleteMedia(c echo.Context) error {
|
|
|
|
var (
|
|
|
|
app = c.Get("app").(*App)
|
|
|
|
id, _ = strconv.Atoi(c.Param("id"))
|
|
|
|
)
|
|
|
|
|
|
|
|
if id < 1 {
|
2020-12-19 18:55:52 +08:00
|
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
|
2018-10-25 21:51:47 +08:00
|
|
|
}
|
|
|
|
|
2022-04-03 23:24:40 +08:00
|
|
|
fname, err := app.core.DeleteMedia(id)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
2018-10-25 21:51:47 +08:00
|
|
|
}
|
2019-10-15 20:21:32 +08:00
|
|
|
|
2022-04-03 23:24:40 +08:00
|
|
|
app.media.Delete(fname)
|
|
|
|
app.media.Delete(thumbPrefix + fname)
|
|
|
|
|
2018-10-25 21:51:47 +08:00
|
|
|
return c.JSON(http.StatusOK, okResp{true})
|
|
|
|
}
|
2020-03-08 03:05:34 +08:00
|
|
|
|
2022-10-03 01:34:51 +08:00
|
|
|
// processImage reads the image file and returns thumbnail bytes and
|
|
|
|
// the original image's width, and height.
|
|
|
|
func processImage(file *multipart.FileHeader) (*bytes.Reader, int, int, error) {
|
2020-03-08 03:05:34 +08:00
|
|
|
src, err := file.Open()
|
|
|
|
if err != nil {
|
2022-10-03 01:34:51 +08:00
|
|
|
return nil, 0, 0, err
|
2020-03-08 03:05:34 +08:00
|
|
|
}
|
|
|
|
defer src.Close()
|
|
|
|
|
|
|
|
img, err := imaging.Decode(src)
|
|
|
|
if err != nil {
|
2022-10-03 01:34:51 +08:00
|
|
|
return nil, 0, 0, err
|
2020-03-08 03:05:34 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// Encode the image into a byte slice as PNG.
|
|
|
|
var (
|
|
|
|
thumb = imaging.Resize(img, thumbnailSize, 0, imaging.Lanczos)
|
|
|
|
out bytes.Buffer
|
|
|
|
)
|
|
|
|
if err := imaging.Encode(&out, thumb, imaging.PNG); err != nil {
|
2022-10-03 01:34:51 +08:00
|
|
|
return nil, 0, 0, err
|
2020-03-08 03:05:34 +08:00
|
|
|
}
|
2022-10-03 01:34:51 +08:00
|
|
|
|
|
|
|
b := img.Bounds().Max
|
|
|
|
return bytes.NewReader(out.Bytes()), b.X, b.Y, nil
|
2020-03-08 03:05:34 +08:00
|
|
|
}
|