Files
web/server/handlers/module_upload.go

247 lines
7.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package handlers
import (
"context"
"net/http"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
"time"
"go.mongodb.org/mongo-driver/v2/bson"
"go.mongodb.org/mongo-driver/v2/mongo/options"
"yh_web/server/config"
"yh_web/server/models"
"github.com/gin-gonic/gin"
)
const uploadDir = "uploads"
// pathPrefix 站点下相对路径前缀,用于多级目录
func pathPrefix(siteID string) string {
return "sites/" + siteID + "/"
}
// ListSiteAssets 站点功能模块/上传文件列表query path 为当前目录相对路径(空为根)
func ListSiteAssets(c *gin.Context) {
siteID := c.Param("site_id")
if siteID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "请提供 site_id"})
return
}
path := c.Query("path")
prefix := pathPrefix(siteID)
if path != "" {
prefix = prefix + path
if prefix[len(prefix)-1] != '/' {
prefix += "/"
}
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
coll := config.GetDB(config.DBName).Collection("site_assets")
// 仅当前目录下直接文件file_path 为 prefix + 不含 / 的文件名)
filter := bson.M{"site_id": siteID, "file_path": bson.M{"$regex": "^" + regexp.QuoteMeta(prefix) + "[^/]+$"}}
opts := options.Find().SetSort(bson.D{{Key: "created_at", Value: -1}})
cursor, err := coll.Find(ctx, filter, opts)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
defer cursor.Close(ctx)
var list []models.SiteAsset
if err = cursor.All(ctx, &list); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
total, _ := coll.CountDocuments(ctx, filter)
// 子目录列表:从 file_path 中提取当前 path 下的一级子目录名
subDirs := listSubDirs(c, siteID, path)
c.JSON(http.StatusOK, gin.H{"list": list, "total": total, "sub_dirs": subDirs})
}
func listSubDirs(c *gin.Context, siteID, currentPath string) []string {
prefix := pathPrefix(siteID)
if currentPath != "" {
prefix = prefix + currentPath
if prefix[len(prefix)-1] != '/' {
prefix += "/"
}
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
coll := config.GetDB(config.DBName).Collection("site_assets")
cursor, err := coll.Find(ctx, bson.M{"site_id": siteID, "file_path": bson.M{"$regex": "^" + regexp.QuoteMeta(prefix)}})
if err != nil {
return nil
}
defer cursor.Close(ctx)
var docs []struct {
FilePath string `bson:"file_path"`
}
_ = cursor.All(ctx, &docs)
seen := make(map[string]bool)
for _, d := range docs {
rel := strings.TrimPrefix(d.FilePath, prefix)
if rel == "" || rel == d.FilePath {
continue
}
parts := strings.SplitN(rel, "/", 2)
if len(parts) > 0 && parts[0] != "" {
seen[parts[0]] = true
}
}
// 再扫描物理目录
baseDir := filepath.Join(uploadDir, filepath.FromSlash(prefix))
entries, _ := os.ReadDir(baseDir)
for _, e := range entries {
if e.IsDir() {
seen[e.Name()] = true
}
}
names := make([]string, 0, len(seen))
for n := range seen {
names = append(names, n)
}
sort.Strings(names)
return names
}
// UploadSiteAsset 上传功能模块/文件form 可选folder当前目录相对路径、downloadabletrue/false
func UploadSiteAsset(c *gin.Context) {
siteID := c.Param("site_id")
if siteID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "请提供 site_id"})
return
}
file, err := c.FormFile("file")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "请选择要上传的文件"})
return
}
folder := c.PostForm("folder")
downloadable := c.PostForm("downloadable") == "true" || c.PostForm("downloadable") == "1"
baseDir := filepath.Join(uploadDir, "sites", siteID, filepath.Clean(folder))
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(uploadDir, filepath.FromSlash(relPath))
if err := c.SaveUploadedFile(file, destPath); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存文件失败"})
return
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
doc := models.SiteAsset{
SiteID: siteID,
Name: file.Filename,
FilePath: relPath,
Size: file.Size,
ContentType: file.Header.Get("Content-Type"),
Downloadable: downloadable,
CreatedAt: time.Now().Format(time.RFC3339),
}
res, err := config.GetDB(config.DBName).Collection("site_assets").InsertOne(ctx, bson.M{
"site_id": doc.SiteID,
"name": doc.Name,
"file_path": doc.FilePath,
"size": doc.Size,
"content_type": doc.ContentType,
"downloadable": doc.Downloadable,
"created_at": doc.CreatedAt,
})
if err != nil {
os.Remove(destPath)
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"id": res.InsertedID, "file_path": doc.FilePath, "message": "上传成功"})
}
// DeleteSiteAsset 删除站点资源
func DeleteSiteAsset(c *gin.Context) {
siteID := c.Param("site_id")
idStr := c.Param("asset_id")
if siteID == "" || idStr == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})
return
}
oid, err := bson.ObjectIDFromHex(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的ID"})
return
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
coll := config.GetDB(config.DBName).Collection("site_assets")
var asset models.SiteAsset
err = coll.FindOne(ctx, bson.M{"_id": oid, "site_id": siteID}).Decode(&asset)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "资源不存在"})
return
}
fullPath := filepath.Join(uploadDir, filepath.FromSlash(asset.FilePath))
os.Remove(fullPath)
_, err = coll.DeleteOne(ctx, bson.M{"_id": oid})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "删除成功"})
}
// CreateSiteFolderInput 创建目录
type CreateSiteFolderInput struct {
Path string `json:"path" binding:"required"`
}
// CreateSiteFolder 在站点下创建多级目录
func CreateSiteFolder(c *gin.Context) {
siteID := c.Param("site_id")
if siteID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "请提供 site_id"})
return
}
var input CreateSiteFolderInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "请填写目录路径"})
return
}
clean := filepath.Clean(input.Path)
if clean == "." || clean == ".." || strings.HasPrefix(clean, "..") {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的目录路径"})
return
}
baseDir := filepath.Join(uploadDir, "sites", siteID, clean)
if err := os.MkdirAll(baseDir, 0755); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建目录失败"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "创建成功", "path": filepath.ToSlash(clean)})
}