226 lines
5.8 KiB
Go
226 lines
5.8 KiB
Go
package service
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"mime/multipart"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// UploadService 文件上传服务接口
|
|
type UploadService interface {
|
|
// 上传音频文件
|
|
UploadAudio(file *multipart.FileHeader) (*UploadResult, error)
|
|
// 上传图片文件
|
|
UploadImage(file *multipart.FileHeader) (*UploadResult, error)
|
|
// 删除文件
|
|
DeleteFile(filePath string) error
|
|
// 获取文件URL
|
|
GetFileURL(filePath string) string
|
|
}
|
|
|
|
type uploadService struct {
|
|
uploadDir string
|
|
baseURL string
|
|
}
|
|
|
|
// UploadResult 上传结果
|
|
type UploadResult struct {
|
|
FileName string `json:"file_name"`
|
|
FilePath string `json:"file_path"`
|
|
FileURL string `json:"file_url"`
|
|
FileSize int64 `json:"file_size"`
|
|
ContentType string `json:"content_type"`
|
|
}
|
|
|
|
// NewUploadService 创建文件上传服务实例
|
|
func NewUploadService(uploadDir, baseURL string) UploadService {
|
|
// 确保上传目录存在
|
|
os.MkdirAll(uploadDir, 0755)
|
|
os.MkdirAll(filepath.Join(uploadDir, "audio"), 0755)
|
|
os.MkdirAll(filepath.Join(uploadDir, "images"), 0755)
|
|
|
|
return &uploadService{
|
|
uploadDir: uploadDir,
|
|
baseURL: baseURL,
|
|
}
|
|
}
|
|
|
|
// 允许的文件类型
|
|
var (
|
|
allowedAudioTypes = map[string]bool{
|
|
"audio/mpeg": true, // mp3
|
|
"audio/wav": true, // wav
|
|
"audio/mp4": true, // m4a
|
|
"audio/webm": true, // webm
|
|
"audio/ogg": true, // ogg
|
|
}
|
|
|
|
allowedImageTypes = map[string]bool{
|
|
"image/jpeg": true, // jpg, jpeg
|
|
"image/png": true, // png
|
|
"image/gif": true, // gif
|
|
"image/webp": true, // webp
|
|
}
|
|
|
|
// 文件大小限制(字节)
|
|
maxAudioSize = 50 * 1024 * 1024 // 50MB
|
|
maxImageSize = 10 * 1024 * 1024 // 10MB
|
|
)
|
|
|
|
// UploadAudio 上传音频文件
|
|
func (s *uploadService) UploadAudio(file *multipart.FileHeader) (*UploadResult, error) {
|
|
// 检查文件大小
|
|
if file.Size > int64(maxAudioSize) {
|
|
return nil, fmt.Errorf("audio file too large, max size is %d MB", maxAudioSize/(1024*1024))
|
|
}
|
|
|
|
// 检查文件类型
|
|
contentType := file.Header.Get("Content-Type")
|
|
if !allowedAudioTypes[contentType] {
|
|
return nil, fmt.Errorf("unsupported audio format: %s", contentType)
|
|
}
|
|
|
|
// 生成唯一文件名
|
|
ext := filepath.Ext(file.Filename)
|
|
fileName := fmt.Sprintf("%s_%d%s", uuid.New().String(), time.Now().Unix(), ext)
|
|
relativePath := filepath.Join("audio", fileName)
|
|
fullPath := filepath.Join(s.uploadDir, relativePath)
|
|
|
|
// 保存文件
|
|
if err := s.saveFile(file, fullPath); err != nil {
|
|
return nil, fmt.Errorf("failed to save audio file: %w", err)
|
|
}
|
|
|
|
return &UploadResult{
|
|
FileName: fileName,
|
|
FilePath: relativePath,
|
|
FileURL: s.GetFileURL(relativePath),
|
|
FileSize: file.Size,
|
|
ContentType: contentType,
|
|
}, nil
|
|
}
|
|
|
|
// UploadImage 上传图片文件
|
|
func (s *uploadService) UploadImage(file *multipart.FileHeader) (*UploadResult, error) {
|
|
// 检查文件大小
|
|
if file.Size > int64(maxImageSize) {
|
|
return nil, fmt.Errorf("image file too large, max size is %d MB", maxImageSize/(1024*1024))
|
|
}
|
|
|
|
// 检查文件类型
|
|
contentType := file.Header.Get("Content-Type")
|
|
if !allowedImageTypes[contentType] {
|
|
return nil, fmt.Errorf("unsupported image format: %s", contentType)
|
|
}
|
|
|
|
// 生成唯一文件名
|
|
ext := filepath.Ext(file.Filename)
|
|
fileName := fmt.Sprintf("%s_%d%s", uuid.New().String(), time.Now().Unix(), ext)
|
|
relativePath := filepath.Join("images", fileName)
|
|
fullPath := filepath.Join(s.uploadDir, relativePath)
|
|
|
|
// 保存文件
|
|
if err := s.saveFile(file, fullPath); err != nil {
|
|
return nil, fmt.Errorf("failed to save image file: %w", err)
|
|
}
|
|
|
|
return &UploadResult{
|
|
FileName: fileName,
|
|
FilePath: relativePath,
|
|
FileURL: s.GetFileURL(relativePath),
|
|
FileSize: file.Size,
|
|
ContentType: contentType,
|
|
}, nil
|
|
}
|
|
|
|
// saveFile 保存文件到磁盘
|
|
func (s *uploadService) saveFile(fileHeader *multipart.FileHeader, destPath string) error {
|
|
// 确保目录存在
|
|
dir := filepath.Dir(destPath)
|
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
|
return fmt.Errorf("failed to create directory: %w", err)
|
|
}
|
|
|
|
// 打开上传的文件
|
|
src, err := fileHeader.Open()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to open uploaded file: %w", err)
|
|
}
|
|
defer src.Close()
|
|
|
|
// 创建目标文件
|
|
dst, err := os.Create(destPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create destination file: %w", err)
|
|
}
|
|
defer dst.Close()
|
|
|
|
// 复制文件内容
|
|
_, err = io.Copy(dst, src)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to copy file content: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// DeleteFile 删除文件
|
|
func (s *uploadService) DeleteFile(filePath string) error {
|
|
// 安全检查:确保文件路径在上传目录内
|
|
if !strings.HasPrefix(filePath, s.uploadDir) {
|
|
fullPath := filepath.Join(s.uploadDir, filePath)
|
|
filePath = fullPath
|
|
}
|
|
|
|
// 检查文件是否存在
|
|
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
|
return fmt.Errorf("file does not exist: %s", filePath)
|
|
}
|
|
|
|
// 删除文件
|
|
if err := os.Remove(filePath); err != nil {
|
|
return fmt.Errorf("failed to delete file: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetFileURL 获取文件URL
|
|
func (s *uploadService) GetFileURL(filePath string) string {
|
|
// 清理路径分隔符
|
|
cleanPath := strings.ReplaceAll(filePath, "\\", "/")
|
|
return fmt.Sprintf("%s/uploads/%s", s.baseURL, cleanPath)
|
|
}
|
|
|
|
// ValidateFileType 验证文件类型
|
|
func ValidateFileType(filename string, allowedTypes map[string]bool) bool {
|
|
ext := strings.ToLower(filepath.Ext(filename))
|
|
switch ext {
|
|
case ".mp3":
|
|
return allowedTypes["audio/mpeg"]
|
|
case ".wav":
|
|
return allowedTypes["audio/wav"]
|
|
case ".m4a":
|
|
return allowedTypes["audio/mp4"]
|
|
case ".webm":
|
|
return allowedTypes["audio/webm"]
|
|
case ".ogg":
|
|
return allowedTypes["audio/ogg"]
|
|
case ".jpg", ".jpeg":
|
|
return allowedTypes["image/jpeg"]
|
|
case ".png":
|
|
return allowedTypes["image/png"]
|
|
case ".gif":
|
|
return allowedTypes["image/gif"]
|
|
case ".webp":
|
|
return allowedTypes["image/webp"]
|
|
default:
|
|
return false
|
|
}
|
|
} |