182 lines
4.7 KiB
Go
182 lines
4.7 KiB
Go
package service
|
||
|
||
import (
|
||
"fmt"
|
||
"io"
|
||
"os"
|
||
"path/filepath"
|
||
"sort"
|
||
"strings"
|
||
"time"
|
||
|
||
"gotest/internal/config"
|
||
"gotest/internal/storage"
|
||
)
|
||
|
||
// UploadService 文件上传服务
|
||
type UploadService struct {
|
||
storage storage.Storage
|
||
cfg *config.Config
|
||
}
|
||
|
||
// NewUploadService 创建上传服务实例
|
||
func NewUploadService(s storage.Storage, cfg *config.Config) *UploadService {
|
||
return &UploadService{storage: s, cfg: cfg}
|
||
}
|
||
|
||
// allowedModules 允许的模块列表
|
||
var allowedModules = map[string]string{
|
||
"enterprise": "enterprise",
|
||
"shop": "shop",
|
||
"food": "food",
|
||
"game": "game",
|
||
"app": "app",
|
||
"music": "music",
|
||
"common": "common",
|
||
}
|
||
|
||
// Upload 上传文件,返回访问 URL
|
||
// module 指定上传到的模块(enterprise/shop/food/game/app/music/common)
|
||
// filename 原始文件名,contentType MIME 类型,fileSize 文件大小,reader 文件内容流
|
||
func (s *UploadService) Upload(module string, filename string, contentType string, fileSize int64, reader io.Reader) (string, error) {
|
||
// 校验文件大小
|
||
if fileSize > s.cfg.Upload.MaxSize {
|
||
return "", fmt.Errorf("文件大小超过限制(最大 %d 字节)", s.cfg.Upload.MaxSize)
|
||
}
|
||
|
||
// 校验文件类型
|
||
allowed := false
|
||
for _, t := range s.cfg.Upload.AllowedTypes {
|
||
if t == contentType {
|
||
allowed = true
|
||
break
|
||
}
|
||
}
|
||
if !allowed {
|
||
return "", fmt.Errorf("不支持的文件类型: %s", contentType)
|
||
}
|
||
|
||
// 校验并规范化模块名
|
||
mod := allowedModules[module]
|
||
if mod == "" {
|
||
mod = "common"
|
||
}
|
||
|
||
// 生成存储 key:模块名/日期/时间戳文件名
|
||
ext := filepath.Ext(filename)
|
||
key := fmt.Sprintf("%s/%s%s", mod, time.Now().Format("2006/01/02/150405.000000"), ext)
|
||
|
||
url, err := s.storage.Upload(key, reader, contentType)
|
||
if err != nil {
|
||
return "", fmt.Errorf("文件上传失败: %w", err)
|
||
}
|
||
|
||
return url, nil
|
||
}
|
||
|
||
// FileInfo 文件信息
|
||
type FileInfo struct {
|
||
Name string `json:"name"`
|
||
URL string `json:"url"`
|
||
Size int64 `json:"size"`
|
||
ModTime string `json:"mod_time"`
|
||
IsDir bool `json:"is_dir"`
|
||
}
|
||
|
||
// ListFiles 列出指定模块下的所有文件(递归遍历子目录)
|
||
func (s *UploadService) ListFiles(module string) ([]FileInfo, error) {
|
||
mod := allowedModules[module]
|
||
if mod == "" {
|
||
mod = "common"
|
||
}
|
||
|
||
dirPath := filepath.Join(s.cfg.Upload.Path, mod)
|
||
var result []FileInfo
|
||
|
||
// 递归遍历
|
||
err := filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error {
|
||
if err != nil {
|
||
return nil // 跳过错误
|
||
}
|
||
if info.IsDir() {
|
||
return nil // 跳过目录
|
||
}
|
||
// 计算相对路径作为 key
|
||
relPath, err := filepath.Rel(s.cfg.Upload.Path, path)
|
||
if err != nil {
|
||
return nil
|
||
}
|
||
relPath = filepath.ToSlash(relPath) // 统一用 /
|
||
url := s.storage.GetURL(relPath)
|
||
result = append(result, FileInfo{
|
||
Name: info.Name(),
|
||
URL: url,
|
||
Size: info.Size(),
|
||
ModTime: info.ModTime().Format("2006-01-02 15:04:05"),
|
||
IsDir: false,
|
||
})
|
||
return nil
|
||
})
|
||
if err != nil {
|
||
if os.IsNotExist(err) {
|
||
return []FileInfo{}, nil
|
||
}
|
||
return nil, fmt.Errorf("读取目录失败: %w", err)
|
||
}
|
||
|
||
// 按修改时间倒序
|
||
sort.Slice(result, func(i, j int) bool {
|
||
return result[i].ModTime > result[j].ModTime
|
||
})
|
||
|
||
return result, nil
|
||
}
|
||
|
||
// DeleteFile 删除指定模块下的文件
|
||
// filename 可以是文件名或完整相对路径(如 2026/07/10/xxx.png)
|
||
func (s *UploadService) DeleteFile(module, filename string) error {
|
||
mod := allowedModules[module]
|
||
if mod == "" {
|
||
mod = "common"
|
||
}
|
||
|
||
// 防止路径穿越:去掉模块名前缀(如果有的话)和 ..
|
||
cleanName := strings.ReplaceAll(filename, "..", "")
|
||
cleanName = strings.TrimPrefix(cleanName, mod+"/")
|
||
cleanName = strings.TrimPrefix(cleanName, mod)
|
||
|
||
key := filepath.Join(mod, cleanName)
|
||
key = filepath.ToSlash(key)
|
||
|
||
return s.storage.Delete(key)
|
||
}
|
||
|
||
// GetModules 返回所有支持的模块
|
||
func (s *UploadService) GetModules() []map[string]string {
|
||
return []map[string]string{
|
||
{"key": "enterprise", "label": "企业官网"},
|
||
{"key": "shop", "label": "跨境电商"},
|
||
{"key": "food", "label": "外卖平台"},
|
||
{"key": "game", "label": "游戏平台"},
|
||
{"key": "app", "label": "应用商店"},
|
||
{"key": "music", "label": "音乐平台"},
|
||
{"key": "common", "label": "公共资源"},
|
||
}
|
||
}
|
||
|
||
// EnsureUploadDir 确保上传目录和各模块子目录存在
|
||
func (s *UploadService) EnsureUploadDir() {
|
||
basePath := s.cfg.Upload.Path
|
||
for mod := range allowedModules {
|
||
os.MkdirAll(filepath.Join(basePath, mod), 0755)
|
||
}
|
||
}
|
||
|
||
// sanitizeFilename 清理文件名中的危险字符
|
||
func sanitizeFilename(name string) string {
|
||
name = strings.ReplaceAll(name, "..", "")
|
||
name = strings.ReplaceAll(name, "/", "_")
|
||
name = strings.ReplaceAll(name, "\\", "_")
|
||
return name
|
||
}
|