增加图片上传管理

This commit is contained in:
2026-07-28 11:23:21 +08:00
parent 8d0b75e484
commit 786f11a2ca
7 changed files with 545 additions and 4 deletions
+217
View File
@@ -0,0 +1,217 @@
package handlers
import (
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"sort"
"strings"
"time"
"github.com/gin-gonic/gin"
)
const (
// uploadDir 上传文件保存目录
uploadDir = "./static/uploads"
// uploadURLPrefix 上传文件对外访问的 URL 前缀
uploadURLPrefix = "/static/uploads/"
// maxUploadSize 单个文件最大大小(5MB)
maxUploadSize = 5 << 20
)
// allowedImageExts 允许上传的图片扩展名
var allowedImageExts = map[string]bool{
".jpg": true,
".jpeg": true,
".png": true,
".gif": true,
".webp": true,
}
// allowedImageMIMEs 允许上传的图片真实 MIME 类型
var allowedImageMIMEs = map[string]bool{
"image/jpeg": true,
"image/png": true,
"image/gif": true,
"image/webp": true,
}
// UploadedFile 上传文件信息
type UploadedFile struct {
Name string `json:"name"`
URL string `json:"url"`
Size int64 `json:"size"`
ModTime string `json:"mod_time"`
}
// sanitizeBaseName 清理文件名(去除扩展名和不安全字符)
func sanitizeBaseName(filename string) string {
base := strings.TrimSuffix(filepath.Base(filename), filepath.Ext(filename))
base = strings.ReplaceAll(base, " ", "-")
var b strings.Builder
for _, r := range base {
if r == '-' || r == '_' ||
(r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') {
b.WriteRune(r)
}
}
res := b.String()
if res == "" {
res = "image"
}
if len(res) > 40 {
res = res[:40]
}
return res
}
// UploadImage 上传图片(校验格式与大小)
func UploadImage(c *gin.Context) {
file, err := c.FormFile("file")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "请选择要上传的文件"})
return
}
// 校验文件大小
if file.Size > maxUploadSize {
c.JSON(http.StatusBadRequest, gin.H{
"error": fmt.Sprintf("文件大小超过限制(最大 %d MB)", maxUploadSize>>20),
})
return
}
// 校验扩展名
ext := strings.ToLower(filepath.Ext(file.Filename))
if !allowedImageExts[ext] {
c.JSON(http.StatusBadRequest, gin.H{"error": "不支持的图片格式,仅支持 jpg/jpeg/png/gif/webp"})
return
}
// 打开文件并检测真实 MIME 类型(防止伪造扩展名)
src, err := file.Open()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "读取文件失败"})
return
}
defer src.Close()
head := make([]byte, 512)
n, _ := src.Read(head)
contentType := http.DetectContentType(head[:n])
if !allowedImageMIMEs[contentType] {
c.JSON(http.StatusBadRequest, gin.H{"error": "文件内容不是有效的图片"})
return
}
// 重置读取位置,准备写入
if _, err := src.Seek(0, io.SeekStart); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "读取文件失败"})
return
}
// 确保上传目录存在
if err := os.MkdirAll(uploadDir, 0o755); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建上传目录失败"})
return
}
// 生成唯一文件名:时间戳_原始名.扩展名
filename := fmt.Sprintf("%d_%s%s", time.Now().UnixNano(), sanitizeBaseName(file.Filename), ext)
dstPath := filepath.Join(uploadDir, filename)
dst, err := os.Create(dstPath)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存文件失败"})
return
}
defer dst.Close()
if _, err := io.Copy(dst, src); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存文件失败"})
return
}
c.JSON(http.StatusOK, gin.H{
"url": uploadURLPrefix + filename,
"name": filename,
"size": file.Size,
})
}
// ListUploads 列出已上传的图片
func ListUploads(c *gin.Context) {
if err := os.MkdirAll(uploadDir, 0o755); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "读取上传目录失败"})
return
}
entries, err := os.ReadDir(uploadDir)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "读取上传目录失败"})
return
}
files := make([]UploadedFile, 0, len(entries))
for _, e := range entries {
if e.IsDir() {
continue
}
ext := strings.ToLower(filepath.Ext(e.Name()))
if !allowedImageExts[ext] {
continue
}
info, err := e.Info()
if err != nil {
continue
}
files = append(files, UploadedFile{
Name: e.Name(),
URL: uploadURLPrefix + e.Name(),
Size: info.Size(),
ModTime: info.ModTime().Format("2006-01-02 15:04:05"),
})
}
// 按修改时间倒序(最新的在前)
sort.Slice(files, func(i, j int) bool {
return files[i].ModTime > files[j].ModTime
})
c.JSON(http.StatusOK, gin.H{"data": files})
}
// DeleteUpload 删除已上传的图片
func DeleteUpload(c *gin.Context) {
name := c.Param("name")
// 防止路径穿越
if name == "" || strings.Contains(name, "..") || strings.ContainsAny(name, `/\`) {
c.JSON(http.StatusBadRequest, gin.H{"error": "非法的文件名"})
return
}
ext := strings.ToLower(filepath.Ext(name))
if !allowedImageExts[ext] {
c.JSON(http.StatusBadRequest, gin.H{"error": "非法的文件类型"})
return
}
path := filepath.Join(uploadDir, name)
if _, err := os.Stat(path); err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "文件不存在"})
return
}
if err := os.Remove(path); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "删除文件失败"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "删除成功"})
}