Files
soul-yongping/soul-api/internal/handler/upload.go

132 lines
4.1 KiB
Go
Raw Normal View History

package handler
import (
"fmt"
"math/rand"
"net/http"
"os"
"path/filepath"
"strings"
"time"
2026-03-14 23:27:22 +08:00
"soul-api/internal/config"
2026-03-17 14:02:09 +08:00
"soul-api/internal/oss"
2026-03-14 23:27:22 +08:00
"github.com/gin-gonic/gin"
)
const maxUploadBytes = 5 * 1024 * 1024 // 5MB
var allowedTypes = map[string]bool{"image/jpeg": true, "image/png": true, "image/gif": true, "image/webp": true}
// UploadPost POST /api/upload 上传图片(表单 file
2026-03-17 14:02:09 +08:00
// 若管理端已配置 OSS优先上传到 OSSOSS 失败或未配置时回退本地磁盘(容灾)
func UploadPost(c *gin.Context) {
file, err := c.FormFile("file")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "请选择要上传的文件"})
return
}
if file.Size > maxUploadBytes {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "文件大小不能超过5MB"})
return
}
ct := file.Header.Get("Content-Type")
if !allowedTypes[ct] && !strings.HasPrefix(ct, "image/") {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "仅支持图片格式"})
return
}
ext := filepath.Ext(file.Filename)
if ext == "" {
ext = ".jpg"
}
folder := c.PostForm("folder")
if folder == "" {
folder = "avatars"
}
2026-03-17 14:02:09 +08:00
name := fmt.Sprintf("%d_%s%s", time.Now().UnixNano(), randomStrUpload(6), ext)
objectKey := filepath.ToSlash(filepath.Join("uploads", folder, name))
// 优先尝试 OSS已配置时
if oss.IsEnabled() {
f, err := file.Open()
if err == nil {
url, uploadErr := oss.Upload(objectKey, f)
_ = f.Close()
if uploadErr == nil && url != "" {
c.JSON(http.StatusOK, gin.H{"success": true, "url": url, "data": gin.H{"url": url, "fileName": name, "size": file.Size, "type": ct}})
return
}
// OSS 失败,回退本地(容灾)
}
}
// 本地磁盘存储OSS 未配置或失败时)
2026-03-14 23:27:22 +08:00
uploadDir := config.Get().UploadDir
if uploadDir == "" {
uploadDir = "uploads"
}
dir := filepath.Join(uploadDir, folder)
_ = os.MkdirAll(dir, 0755)
dst := filepath.Join(dir, name)
if err := c.SaveUploadedFile(file, dst); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": "保存失败"})
return
}
2026-03-14 23:27:22 +08:00
relPath := "/uploads/" + filepath.ToSlash(filepath.Join(folder, name))
fullURL := relPath
if cfg := config.Get(); cfg != nil && cfg.BaseURL != "" {
fullURL = cfg.BaseURLJoin(relPath)
}
c.JSON(http.StatusOK, gin.H{"success": true, "url": fullURL, "data": gin.H{"url": fullURL, "fileName": name, "size": file.Size, "type": ct}})
}
func randomStrUpload(n int) string {
const letters = "abcdefghijklmnopqrstuvwxyz0123456789"
b := make([]byte, n)
for i := range b {
b[i] = letters[rand.Intn(len(letters))]
}
return string(b)
}
// UploadDelete DELETE /api/upload
2026-03-17 14:02:09 +08:00
// path 支持:/uploads/xxx本地或 https://bucket.oss-xxx.aliyuncs.com/uploads/xxxOSS
func UploadDelete(c *gin.Context) {
path := c.Query("path")
if path == "" {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "请指定 path"})
return
}
2026-03-17 14:02:09 +08:00
// OSS 公网 URL从 OSS 删除
if oss.IsOSSURL(path) {
objectKey := oss.ParseObjectKeyFromURL(path)
if objectKey != "" {
if err := oss.Delete(objectKey); err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "OSS 删除失败"})
return
}
c.JSON(http.StatusOK, gin.H{"success": true, "message": "删除成功"})
return
}
}
// 本地路径:支持 /uploads/xxx、uploads/xxx 或含 /uploads/ 的完整 URL
if idx := strings.Index(path, "/uploads/"); idx >= 0 {
path = path[idx+1:] // 从 uploads/ 开始
}
2026-03-14 23:27:22 +08:00
rel := strings.TrimPrefix(path, "/uploads/")
rel = strings.TrimPrefix(rel, "uploads/")
2026-03-17 14:02:09 +08:00
if rel == "" || strings.Contains(rel, "..") {
c.JSON(http.StatusForbidden, gin.H{"success": false, "error": "无权限删除此文件"})
return
}
2026-03-14 23:27:22 +08:00
uploadDir := config.Get().UploadDir
if uploadDir == "" {
uploadDir = "uploads"
}
fullPath := filepath.Join(uploadDir, filepath.FromSlash(rel))
if err := os.Remove(fullPath); err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "文件不存在或删除失败"})
return
}
c.JSON(http.StatusOK, gin.H{"success": true, "message": "删除成功"})
}