feat: 产品视频改后台上传,promotion-media 公开访问;gitignore 大文件;保留原文件名上传
Made-with: Cursor
This commit is contained in:
@@ -153,7 +153,61 @@ func listSubDirs(c *gin.Context, siteID, currentPath string) []string {
|
||||
return names
|
||||
}
|
||||
|
||||
// UploadSiteAsset 上传功能模块/文件;form 可选:folder(当前目录相对路径)、downloadable(true/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(当前目录相对路径)、downloadable(true/false)、preserve_filename(true 时保持原名并覆盖同路径旧文件,用于首页推广视频固定 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
|
||||
|
||||
@@ -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())
|
||||
|
||||
// CORS(ALLOWED_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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user