package api import ( "easyaudioencode/internal/core/audioencode" "fmt" "git.lnton.com/lnton/pkg/reason" "github.com/gin-gonic/gin" "github.com/google/uuid" "io" "mime/multipart" "net/http" "os" "path/filepath" "strings" ) // 配置常量 const ( // 最大上传文件大小 100MB maxUploadSize = 100 * 1024 * 1024 // 文件保存目录 uploadDir = "./uploads" sourceDir = "source" encodeDir = "encode" ) var ( // 允许的文件扩展名 allowedExts = map[string]bool{ ".mp3": true, ".wav": true, } // 允许的 MIME 类型 allowedMimeTypes = map[string]bool{ "audio/mpeg": true, // MP3 "audio/wav": true, // WAV "audio/x-wav": true, // WAV 另一种常见 MIME "audio/x-pn-wav": true, } ) // uploadAudioHandler 处理音频文件上传 func (a AudioEncodeAPI) uploadAudioHandler(c *gin.Context, _ *struct{}) (any, error) { if err := os.MkdirAll(filepath.Join(uploadDir, sourceDir), 0755); err != nil { return nil, reason.ErrServer.SetMsg(fmt.Sprintf("创建上传目录失败: %v", err.Error())) } // 限制请求体大小 c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, maxUploadSize) if err := c.Request.ParseMultipartForm(maxUploadSize); err != nil { return nil, reason.ErrServer.SetMsg(fmt.Sprintf("文件过大(最大 %dMB)或解析失败: %v", maxUploadSize/1024/1024, err)) } // 获取上传的文件 file, fileHeader, err := c.Request.FormFile("audio") if err != nil { return nil, reason.ErrServer.SetMsg(fmt.Sprintf("获取文件失败: %v", err)) } defer file.Close() // 1. 校验文件扩展名 fileExt := strings.ToLower(filepath.Ext(fileHeader.Filename)) sourceFileName := strings.TrimSuffix(fileHeader.Filename, fileExt) if !allowedExts[fileExt] { return nil, reason.ErrServer.SetMsg(fmt.Sprintf("不支持的文件类型,仅允许 MP3/WAV,当前扩展名: %s", fileExt)) } // 2. 校验文件 MIME 类型 //mimeType, err := getFileMimeType(file) //if err != nil { // return nil, reason.ErrServer.SetMsg(fmt.Sprintf("获取文件 MIME 类型失败: %v", err)) //} //if !allowedMimeTypes[mimeType] { // return nil, reason.ErrServer.SetMsg(fmt.Sprintf("文件类型校验失败,实际 MIME: %s,仅允许 MP3/WAV", mimeType)) //} uuidStr := uuid.New().String() // 生成唯一文件名(避免覆盖) fileName := fmt.Sprintf("%s%s", uuidStr, fileExt) filePath := filepath.Join(uploadDir, sourceDir, fileName) // 创建目标文件 dstFile, err := os.Create(filePath) if err != nil { return nil, reason.ErrServer.SetMsg(fmt.Sprintf("创建文件失败: %v", err)) } defer dstFile.Close() // 复制文件内容 if _, err := io.Copy(dstFile, file); err != nil { return nil, reason.ErrServer.SetMsg(fmt.Sprintf("保存文件失败: %v", err.Error())) } in := &audioencode.AddAudioEncodeInput{ Name: sourceFileName, FileName: fileName, Size: fileHeader.Size, SourceUrl: filePath, Mode: fileExt, } _, err = a.core.AddAudioEncode(c.Request.Context(), in) if err != nil { return nil, reason.ErrServer.SetMsg(fmt.Sprintf(`add audioencode err [%s]`, err.Error())) } return gin.H{"data": "上传成功!", "filePath": filePath, "filename": fileName, "size": fileHeader.Size}, err } // getFileMimeType 获取文件的真实 MIME 类型(通过读取文件前几个字节) func getFileMimeType(file multipart.File) (string, error) { // 读取文件前 512 字节用于检测 MIME 类型 buffer := make([]byte, 512) n, err := file.Read(buffer) if err != nil && err != io.EOF { return "", err } // 重置文件指针到开头 if _, err := file.Seek(0, io.SeekStart); err != nil { return "", err } // 检测 MIME 类型 return http.DetectContentType(buffer[:n]), nil }