/
upload.go
239 lines (205 loc) · 6.39 KB
/
upload.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
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 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))
}