Files

253 lines
6.7 KiB
Go

package handler
import (
"fmt"
"io"
"net/http"
"os"
"os/exec"
"path"
"strings"
"syscall"
"time"
"github.com/ArtalkJS/Artalk/internal/config"
"github.com/ArtalkJS/Artalk/internal/core"
"github.com/ArtalkJS/Artalk/internal/i18n"
"github.com/ArtalkJS/Artalk/internal/log"
"github.com/ArtalkJS/Artalk/internal/utils"
"github.com/ArtalkJS/Artalk/server/common"
"github.com/gofiber/fiber/v2"
)
type ParamsImgUpload struct {
Name string `form:"name" validate:"required"`
Email string `form:"email" validate:"required"`
PageKey string `form:"page_key" validate:"required"`
PageTitle string `form:"page_title"`
SiteName string
SiteID uint
SiteAll bool
}
type ResponseImgUpload struct {
ImgFile string `json:"img_file"`
ImgURL string `json:"img_url"`
}
// @Summary Image Upload
// @Description Upload image from this endpoint
// @Tags Upload
// @Param file formData file true "upload file in preparation for import"
// @Param name formData string true "the username"
// @Param email formData string true "the user email"
// @Param page_key formData string true "the page key"
// @Param page_title formData string false "the page title"
// @Param site_name formData string false "the site name of your content scope"
// @Security ApiKeyAuth
// @Success 200 {object} common.JSONResult{data=ResponseImgUpload}
// @Router /img-upload [post]
func ImgUpload(app *core.App, router fiber.Router) {
router.Post("/img-upload", func(c *fiber.Ctx) error {
// 功能开关 (管理员始终开启)
if !app.Conf().ImgUpload.Enabled && !common.CheckIsAdminReq(app, c) {
return common.RespError(c, i18n.T("Image upload forbidden"), common.Map{
"img_upload_enabled": false,
})
}
// 传入参数解析
var p ParamsImgUpload
if isOK, resp := common.ParamsDecode(c, &p); !isOK {
return resp
}
if !utils.ValidateEmail(p.Email) {
return common.RespError(c, i18n.T("Invalid {{name}}", Map{"name": i18n.T("Email")}))
}
// use site
common.UseSite(c, &p.SiteName, &p.SiteID, &p.SiteAll)
// find page
// page := entity.FindPage(p.PageKey, p.PageTitle)
// ip := c.RealIP()
// ua := c.Request().UserAgent()
// 图片大小限制 (Based on content length)
if app.Conf().ImgUpload.MaxSize != 0 {
if int64(c.Request().Header.ContentLength()) > app.Conf().ImgUpload.MaxSize*1024*1024 {
return common.RespError(c, i18n.T("Image exceeds {{file_size}} limit", Map{
"file_size": fmt.Sprintf("%dMB", app.Conf().ImgUpload.MaxSize),
}))
}
}
// 获取 Form
file, err := c.FormFile("file")
if err != nil {
log.Error(err)
return common.RespError(c, "File read failed")
}
// 打开文件
src, err := file.Open()
if err != nil {
log.Error(err)
return common.RespError(c, "File open failed")
}
defer src.Close()
// 读取文件
buf, err := io.ReadAll(src)
if err != nil {
log.Error(err)
return common.RespError(c, "File read failed")
}
// 大小限制 (Based on content read)
if app.Conf().ImgUpload.MaxSize != 0 {
if int64(len(buf)) > app.Conf().ImgUpload.MaxSize*1024*1024 {
return common.RespError(c, i18n.T("Image exceeds {{file_size}} limit", Map{
"file_size": fmt.Sprintf("%dMB", app.Conf().ImgUpload.MaxSize),
}))
}
}
// 文件格式判断
// @link https://mimesniff.spec.whatwg.org/
// @link https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Basics_of_HTTP/MIME_types
fileMine := http.DetectContentType(buf)
allowMines := []string{
"image/jpeg", "image/png", "image/gif", "image/webp", "image/bmp",
// "image/svg+xml",
}
if !utils.ContainsStr(allowMines, fileMine) {
return common.RespError(c, i18n.T("Unsupported formats"))
}
// 图片文件名
mineToExts := map[string]string{
"image/jpeg": ".jpg",
"image/png": ".png",
"image/gif": ".gif",
"image/webp": ".webp",
"image/bmp": ".bmp",
// "image/svg+xml": ".svg",
}
t := time.Now()
filename := t.Format("20060102-150405.000") + mineToExts[fileMine]
// 创建图片目标文件
if err := utils.EnsureDir(app.Conf().ImgUpload.Path); err != nil {
log.Error(err)
return common.RespError(c, "Folder creation failed")
}
fileFullPath := strings.TrimSuffix(app.Conf().ImgUpload.Path, "/") + "/" + filename
dst, err := os.Create(fileFullPath)
if err != nil {
log.Error(err)
return common.RespError(c, "File creation failed")
}
defer dst.Close()
// 写入图片文件
if _, err = dst.Write(buf); err != nil {
log.Error(err)
return common.RespError(c, "File write failed")
}
// 生成外部可访问链接
baseURL := app.Conf().ImgUpload.PublicPath
if baseURL == "" {
baseURL = config.IMG_UPLOAD_PUBLIC_PATH
}
var imgURL string
if utils.ValidateURL(baseURL) {
// full url
imgURL = strings.TrimSuffix(baseURL, "/") + "/" + filename
} else {
// relative path
imgURL = path.Join(baseURL, filename)
}
// 使用 upgit
if app.Conf().ImgUpload.Upgit.Enabled {
upgitURL := execUpgitUpload(app.Conf().ImgUpload.Upgit.Exec, fileFullPath)
if upgitURL == "" || !utils.ValidateURL(upgitURL) {
// 上传失败,删除源图片文件
var err = os.Remove(fileFullPath)
if err != nil {
log.Error(err)
}
log.Error("[IMG_UPLOAD] [upgit] upgit output: ", upgitURL)
return common.RespError(c, i18n.T("Upload image via {{method}} failed", Map{"method": "upgit"}))
}
// 上传成功,删除本地文件
if app.Conf().ImgUpload.Upgit.DelLocal {
var err = os.Remove(fileFullPath)
if err != nil {
log.Error(err)
}
}
// 使用从 upgit 获取的图片 URL
imgURL = upgitURL
}
// 响应数据
return common.RespData(c, ResponseImgUpload{
ImgFile: filename,
ImgURL: imgURL,
})
})
}
// 调用 upgit 上传图片获得 URL
func execUpgitUpload(execCommand string, filename string) string {
LogTag := "[IMG_UPLOAD] [upgit] "
// 处理参数
cmdStrSplitted := strings.Split(execCommand, " ")
execApp := cmdStrSplitted[0]
execArgs := []string{}
for i, arg := range cmdStrSplitted {
if i > 0 {
execArgs = append(execArgs, arg)
}
}
execArgs = append(execArgs, filename)
// 执行命令
cmd := exec.Command(execApp, execArgs...)
stdout, _ := cmd.StdoutPipe()
if err := cmd.Start(); err != nil {
log.Error(LogTag, "cmd.Start: ", err)
return ""
}
result, _ := io.ReadAll(stdout)
if err := cmd.Wait(); err != nil {
if exiterr, ok := err.(*exec.ExitError); ok {
if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
log.Error(LogTag, "Exit Status: ", status.ExitStatus())
}
} else {
log.Error(LogTag, "cmd.Wait: ", err)
}
return ""
}
return strings.TrimSpace(string(result))
}