Files
starter/internal/handler/file_handler.go

262 lines
7.0 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 handler
import (
"fmt"
"path/filepath"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/limitcool/starter/configs"
"github.com/limitcool/starter/internal/api/response"
"github.com/limitcool/starter/internal/filestore"
"github.com/limitcool/starter/internal/model"
"github.com/limitcool/starter/internal/pkg/errorx"
"github.com/limitcool/starter/internal/pkg/logger"
"github.com/spf13/cast"
"gorm.io/gorm"
)
// FileHandler 文件处理器
type FileHandler struct {
db *gorm.DB
config *configs.Config
storage *filestore.Storage
}
// NewFileHandler 创建文件处理器
func NewFileHandler(db *gorm.DB, config *configs.Config, storage *filestore.Storage) *FileHandler {
handler := &FileHandler{
db: db,
config: config,
storage: storage,
}
logger.Info("FileHandler initialized")
return handler
}
// UploadFile 上传文件
func (h *FileHandler) UploadFile(ctx *gin.Context) {
// 获取请求上下文
reqCtx := ctx.Request.Context()
// 从上下文中获取用户ID
userID, exists := ctx.Get("user_id")
if !exists {
logger.WarnContext(reqCtx, "UploadFile 未找到用户ID")
response.Error(ctx, errorx.ErrUserNoLogin)
return
}
// 转换用户ID
id := cast.ToInt64(userID)
// 获取用户类型
userTypeValue, exists := ctx.Get("user_type")
if !exists {
logger.WarnContext(reqCtx, "UploadFile 未找到用户类型")
response.Error(ctx, errorx.ErrUserNoLogin)
return
}
// 获取用户类型(简化为数字)
userType := cast.ToUint8(userTypeValue)
// 获取文件
fileHeader, err := ctx.FormFile("file")
if err != nil {
logger.WarnContext(reqCtx, "UploadFile 获取文件失败",
"error", err,
"user_id", id)
response.Error(ctx, errorx.ErrInvalidParams.WithError(err))
return
}
// 获取文件类型
fileType := ctx.DefaultPostForm("type", model.FileTypeOther)
// 获取是否公开
isPublic := ctx.DefaultPostForm("is_public", "false") == "true"
// 获取文件基本信息
originalName := fileHeader.Filename
ext := strings.ToLower(filepath.Ext(originalName))
mimeType := filestore.GetMimeType(originalName)
size := fileHeader.Size
// 验证文件类型
if !isAllowedFileType(ext, fileType) {
logger.WarnContext(reqCtx, "UploadFile 文件类型不允许",
"user_id", id,
"file_type", fileType,
"ext", ext)
response.Error(ctx, errorx.ErrInvalidParams.WithMsg("文件类型不允许"))
return
}
// 验证文件大小
if !isAllowedFileSize(size, fileType) {
logger.WarnContext(reqCtx, "UploadFile 文件大小超出限制",
"user_id", id,
"file_type", fileType,
"size", size)
response.Error(ctx, errorx.ErrInvalidParams.WithMsg("文件大小超出限制"))
return
}
// 打开上传的文件
file, err := fileHeader.Open()
if err != nil {
logger.ErrorContext(reqCtx, "UploadFile 打开文件失败",
"error", err,
"user_id", id)
response.Error(ctx, errorx.ErrInvalidParams.WithError(err))
return
}
defer file.Close()
// 生成文件名和存储路径
fileName := generateFileName(originalName)
storagePath := getStoragePath(fileType, fileName)
// 上传文件到存储
err = h.storage.Put(reqCtx, storagePath, file)
if err != nil {
logger.ErrorContext(reqCtx, "UploadFile 上传文件到存储失败",
"error", err,
"user_id", id,
"storage_path", storagePath)
response.Error(ctx, errorx.WrapError(err, fmt.Sprintf("上传文件到存储失败: %s", storagePath)))
return
}
// 获取文件访问URL
fileURL, err := h.storage.GetURL(reqCtx, storagePath)
if err != nil {
logger.ErrorContext(reqCtx, "UploadFile 获取文件访问URL失败",
"error", err,
"user_id", id,
"storage_path", storagePath)
response.Error(ctx, errorx.WrapError(err, fmt.Sprintf("获取文件访问URL失败: %s", storagePath)))
return
}
// 创建文件仓库
fileRepo := model.NewFileRepo(h.db)
// 记录到数据库
fileModel := &model.File{
Name: fileName,
OriginalName: originalName,
Path: storagePath,
URL: fileURL,
Type: fileType,
Usage: model.FileUsageGeneral, // 默认用途为通用
Size: size,
MimeType: mimeType,
Extension: ext,
StorageType: string(h.storage.Config.Type),
UploadedBy: id,
UploadedByType: uint8(userType),
UploadedAt: time.Now(),
Status: 1, // 状态正常
IsPublic: isPublic, // 设置是否公开
}
if err := fileRepo.Create(reqCtx, fileModel); err != nil {
logger.ErrorContext(reqCtx, "UploadFile 保存文件记录失败",
"error", err,
"user_id", id)
response.Error(ctx, err)
return
}
logger.InfoContext(reqCtx, "UploadFile 上传文件成功",
"user_id", id,
"file_id", fileModel.ID,
"file_name", fileModel.Name)
response.Success(ctx, fileModel)
}
// 生成文件名
func generateFileName(originalName string) string {
ext := filepath.Ext(originalName)
name := fmt.Sprintf("%d_%s%s", time.Now().UnixNano(), randString(8), ext)
return name
}
// 随机字符串
func randString(n int) string {
const letters = "abcdefghijklmnopqrstuvwxyz0123456789"
b := make([]byte, n)
for i := range b {
b[i] = letters[int(time.Now().UnixNano()%int64(len(letters)))]
time.Sleep(1 * time.Nanosecond) // 确保随机性
}
return string(b)
}
// 获取存储路径
func getStoragePath(fileType, fileName string) string {
var baseDir string
switch fileType {
case model.FileTypeImage:
baseDir = "images"
case model.FileTypeDocument:
baseDir = "documents"
case model.FileTypeVideo:
baseDir = "videos"
case model.FileTypeAudio:
baseDir = "audios"
default:
baseDir = "others"
}
// 添加日期子目录
dateDir := time.Now().Format("2006/01/02")
// 使用path包而不是filepath包确保始终使用/作为分隔符
path := filepath.Join(baseDir, dateDir, fileName)
// 确保Windows上也使用正斜杠
return strings.ReplaceAll(path, "\\", "/")
}
// 检查文件类型是否允许
func isAllowedFileType(ext string, fileType string) bool {
ext = strings.ToLower(ext)
switch fileType {
case model.FileTypeImage:
return ext == ".jpg" || ext == ".jpeg" || ext == ".png" || ext == ".gif" || ext == ".webp" || ext == ".svg"
case model.FileTypeDocument:
return ext == ".pdf" || ext == ".doc" || ext == ".docx" || ext == ".xls" || ext == ".xlsx" || ext == ".txt"
case model.FileTypeVideo:
return ext == ".mp4" || ext == ".avi" || ext == ".mov" || ext == ".wmv" || ext == ".flv"
case model.FileTypeAudio:
return ext == ".mp3" || ext == ".wav" || ext == ".ogg" || ext == ".flac"
default:
return true
}
}
// 检查文件大小是否允许
func isAllowedFileSize(size int64, fileType string) bool {
const (
MB = 1024 * 1024
)
switch fileType {
case model.FileTypeImage:
return size <= 10*MB // 图片最大10MB
case model.FileTypeDocument:
return size <= 50*MB // 文档最大50MB
case model.FileTypeVideo:
return size <= 500*MB // 视频最大500MB
case model.FileTypeAudio:
return size <= 100*MB // 音频最大100MB
default:
return size <= 50*MB // 其他类型最大50MB
}
}