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": "删除成功"}) }