feat: 产品视频改后台上传,promotion-media 公开访问;gitignore 大文件;保留原文件名上传

Made-with: Cursor
This commit is contained in:
whm
2026-03-20 17:10:56 +08:00
parent 5067fb6f76
commit 654b683067
20 changed files with 225 additions and 47 deletions

View File

@@ -153,7 +153,61 @@ func listSubDirs(c *gin.Context, siteID, currentPath string) []string {
return names
}
// UploadSiteAsset 上传功能模块/文件form 可选folder当前目录相对路径、downloadabletrue/false
func promotionMimeType(ext string) string {
switch strings.ToLower(ext) {
case ".mov":
return "video/quicktime"
case ".mp4":
return "video/mp4"
case ".webm":
return "video/webm"
case ".jpg", ".jpeg":
return "image/jpeg"
case ".png":
return "image/png"
case ".gif":
return "image/gif"
default:
return ""
}
}
// ServePromotionMedia 公开读取站点 uploads/sites/{site_id}/promotion/ 下文件(与后台上传到 promotion/… 目录对应)
func ServePromotionMedia(c *gin.Context) {
siteID := c.Param("site_id")
raw := strings.TrimPrefix(c.Param("filepath"), "/")
if siteID == "" || raw == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})
return
}
rel := filepath.ToSlash(filepath.Clean(raw))
if rel == "." || strings.HasPrefix(rel, "../") || strings.Contains(rel, "/../") {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效路径"})
return
}
baseDir := filepath.Join(getUploadDir(), "sites", siteID, "promotion")
fullPath := filepath.Join(baseDir, filepath.FromSlash(rel))
relBack, err := filepath.Rel(baseDir, fullPath)
if err != nil || strings.HasPrefix(relBack, "..") {
c.JSON(http.StatusForbidden, gin.H{"error": "禁止访问"})
return
}
fi, err := os.Stat(fullPath)
if err != nil || fi.IsDir() {
c.JSON(http.StatusNotFound, gin.H{"error": "文件不存在"})
return
}
ext := filepath.Ext(fullPath)
ct := promotionMimeType(ext)
if ct == "" {
ct = "application/octet-stream"
}
c.Header("Content-Type", ct)
c.Header("Cache-Control", "public, max-age=86400")
c.File(fullPath)
}
// UploadSiteAsset 上传功能模块/文件form 可选folder当前目录相对路径、downloadabletrue/false、preserve_filenametrue 时保持原名并覆盖同路径旧文件,用于首页推广视频固定 URL
func UploadSiteAsset(c *gin.Context) {
siteID := c.Param("site_id")
if siteID == "" {
@@ -167,22 +221,59 @@ func UploadSiteAsset(c *gin.Context) {
return
}
folder := c.PostForm("folder")
folder := strings.TrimSpace(c.PostForm("folder"))
downloadable := c.PostForm("downloadable") == "true" || c.PostForm("downloadable") == "1"
baseDir := filepath.Join(getUploadDir(), "sites", siteID, filepath.Clean(folder))
preserve := c.PostForm("preserve_filename") == "true" || c.PostForm("preserve_filename") == "1"
name := file.Filename
ext := filepath.Ext(name)
nameNoExt := strings.TrimSuffix(name, ext)
var saveName string
if preserve {
saveName = filepath.Base(name)
if saveName == "." || saveName == string(filepath.Separator) || saveName == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的文件名"})
return
}
} else {
if len(ext) == 0 {
saveName = nameNoExt + "_" + time.Now().Format("20060102150405")
} else {
saveName = nameNoExt + "_" + time.Now().Format("20060102150405") + ext
}
}
folderClean := ""
if folder != "" {
folderClean = filepath.ToSlash(filepath.Clean(folder))
if strings.HasPrefix(folderClean, "../") || strings.Contains(folderClean, "/../") {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的目录路径"})
return
}
}
var relPath string
if folderClean != "" {
relPath = filepath.ToSlash(filepath.Join("sites", siteID, folderClean, saveName))
} else {
relPath = filepath.ToSlash(filepath.Join("sites", siteID, saveName))
}
destPath := filepath.Join(getUploadDir(), filepath.FromSlash(relPath))
if preserve {
ctxDel, cancelDel := context.WithTimeout(context.Background(), 8*time.Second)
defer cancelDel()
coll := config.GetDB(config.DBName).Collection("site_assets")
_, _ = coll.DeleteMany(ctxDel, bson.M{"site_id": siteID, "file_path": relPath})
_ = os.Remove(destPath)
}
baseDir := filepath.Dir(destPath)
if err := os.MkdirAll(baseDir, 0755); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建目录失败"})
return
}
name := file.Filename
ext := filepath.Ext(name)
nameNoExt := name[:len(name)-len(ext)]
saveName := nameNoExt + "_" + time.Now().Format("20060102150405") + ext
relPath := filepath.Join("sites", siteID, filepath.Clean(folder), saveName)
relPath = filepath.ToSlash(relPath)
destPath := filepath.Join(getUploadDir(), filepath.FromSlash(relPath))
if err := c.SaveUploadedFile(file, destPath); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存文件失败"})
return

View File

@@ -79,7 +79,7 @@ func main() {
}
r := gin.Default()
r.MaxMultipartMemory = 200 << 20 // 200MB Nginx client_max_body_size 一致,避免上传 413
r.MaxMultipartMemory = 32 << 20 // 大单片先落临时文件;整体体积受 Nginx client_max_body_size 限制
r.Use(middleware.ErrorLogger())
// CORSALLOWED_ORIGINS 为空则允许所有来源;否则仅允许配置的域名)
@@ -215,6 +215,8 @@ func main() {
web.GET("/info", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "web api"})
})
// 推广/产品视频:站点 uploads 下 promotion 目录(无需鉴权,与后台上传到 promotion/… 对应)
web.GET("/sites/:site_id/promotion-media/*filepath", handlers.ServePromotionMedia)
// 可下载资源公开下载(首页等链接指向此路径)
web.GET("/sites/:site_id/assets/:asset_id/download", handlers.DownloadSiteAsset)
}