Files
qwqcode cf7725edba refactor(go): bump the module version and support go install (#979)
BREAKING CHANGE: The original Go module name was `github.com/ArtalkJS/Artalk`, which has now been modified to `github.com/artalkjs/artalk/v2` (all lowercase). This change may affect the CI pipeline or other areas if you are compiling the Artalk server and have specified the fixed Go module name. Regarding the original `-ldflags -X github.com/ArtalkJS/Artalk/internal/config.Version` and `CommitHash` definitions, please remove them, as they are now obsolete. The `Version` has been hardcoded into the Go code, and the `CommitHash` is now read from the Go build info via runtime/debug’s vcs.revision value. This change only affects the server developers and does not impact the frontend UI client.
2024-09-18 21:43:43 +08:00

240 lines
6.4 KiB
Go

package handler
import (
"fmt"
"io"
"net/http"
"os"
"os/exec"
"path"
"strings"
"syscall"
"time"
"github.com/artalkjs/artalk/v2/internal/config"
"github.com/artalkjs/artalk/v2/internal/core"
"github.com/artalkjs/artalk/v2/internal/i18n"
"github.com/artalkjs/artalk/v2/internal/log"
"github.com/artalkjs/artalk/v2/internal/utils"
"github.com/artalkjs/artalk/v2/server/common"
"github.com/gofiber/fiber/v2"
)
type ParamsUpload struct {
}
type ResponseUpload struct {
FileType string `json:"file_type" enum:"image"`
FileName string `json:"file_name"`
PublicURL string `json:"public_url"`
}
// @Id Upload
// @Summary Upload
// @Description Upload file from this endpoint
// @Tags Upload
// @Param file formData file true "Upload file"
// @Security ApiKeyAuth
// @Accept mpfd
// @Produce json
// @Success 200 {object} ResponseUpload
// @Failure 400 {object} Map{msg=string}
// @Failure 403 {object} Map{msg=string}
// @Failure 500 {object} Map{msg=string}
// @Router /upload [post]
func Upload(app *core.App, router fiber.Router) {
router.Post("/upload", common.LimiterGuard(app, func(c *fiber.Ctx) error {
// 功能开关 (管理员始终开启)
if !app.Conf().ImgUpload.Enabled && !common.CheckIsAdminReq(app, c) {
return common.RespError(c, 403, i18n.T("Image upload forbidden"), common.Map{
"img_upload_enabled": false,
})
}
// 传入参数解析
var p ParamsUpload
if isOK, resp := common.ParamsDecode(c, &p); !isOK {
return resp
}
// 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, 400, 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, 500, "File read failed")
}
// 打开文件
src, err := file.Open()
if err != nil {
log.Error(err)
return common.RespError(c, 500, "File open failed")
}
defer src.Close()
// 读取文件
buf, err := io.ReadAll(src)
if err != nil {
log.Error(err)
return common.RespError(c, 500, "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, 400, i18n.T("Image exceeds {{file_size}} limit", Map{
"file_size": fmt.Sprintf("%dMB", app.Conf().ImgUpload.MaxSize),
}))
}
}
// 文件格式判断
// The http.DetectContentType function reads the first 512 bytes of a file
// and uses these bytes (aka the magic number) to determine the file's content type.
// @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, 400, 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, 500, "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, 500, "File creation failed")
}
defer dst.Close()
// 写入图片文件
if _, err = dst.Write(buf); err != nil {
log.Error(err)
return common.RespError(c, 500, "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, 500, 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, ResponseUpload{
FileType: "image",
FileName: filename,
PublicURL: imgURL,
})
}))
}
// Call UpGit to upload images
func execUpgitUpload(execCommand string, filename string) string {
LogTag := "[IMG_UPLOAD] [upgit] "
// Separate the command and arguments
cmdStrSplitted := strings.Split(execCommand, " ")
// For security reasons, Artalk no longer allows you to specify the executable file path of UpGit.
// execApp := cmdStrSplitted[0]
execApp := "upgit"
execArgs := cmdStrSplitted[1:]
execArgs = append(execArgs, filename)
// Execute the command
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))
}