增加图片上传管理
This commit is contained in:
@@ -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": "删除成功"})
|
||||
}
|
||||
Reference in New Issue
Block a user