package fileStorage import ( "backend/pkg/permission/domain/token" "context" "fmt" "io" "mime/multipart" "path/filepath" "strings" "time" "backend/internal/svc" "backend/internal/types" "github.com/google/uuid" "github.com/zeromicro/go-zero/core/logx" ) type UploadVideoLogic struct { logx.Logger ctx context.Context svcCtx *svc.ServiceContext } func NewUploadVideoLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UploadVideoLogic { return &UploadVideoLogic{ Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx, } } func (l *UploadVideoLogic) UploadVideo(req *types.UploadVideoReq, file multipart.File, header *multipart.FileHeader) (resp *types.UploadResp, err error) { // 驗證文件大小(100MB 限制) const maxVideoSize = 100 << 20 // 100MB if header.Size > maxVideoSize { return nil, fmt.Errorf("video size exceeds 100MB limit") } // 讀取文件內容 videoData, err := io.ReadAll(file) if err != nil { return nil, fmt.Errorf("failed to read file: %w", err) } // 驗證實際文件大小 if int64(len(videoData)) > maxVideoSize { return nil, fmt.Errorf("video size exceeds 100MB limit") } // 檢測視頻 MIME 類型 mimeType := l.detectVideoType(videoData, header) if mimeType == "" { // 從文件名推斷 ext := strings.ToLower(filepath.Ext(header.Filename)) mimeType = l.getMimeTypeFromExtension(ext) if mimeType == "" { return nil, fmt.Errorf("unsupported video format") } } // 生成唯一文件名 fileExt := filepath.Ext(header.Filename) if fileExt == "" { fileExt = l.getExtensionFromMimeType(mimeType) } fileName := fmt.Sprintf("%s%s", uuid.New().String(), fileExt) objectPath := fmt.Sprintf("videos/%s/%d/%s", token.UID(l.ctx), time.Now().Year(), fileName) // 上傳到 S3 fileStorageUC := l.svcCtx.FileStorageUC if err := fileStorageUC.UploadFromData(l.ctx, videoData, objectPath, mimeType); err != nil { return nil, fmt.Errorf("failed to upload video: %w", err) } // 獲取公開 URL fileUrl := fileStorageUC.GetPublicURL(l.ctx, objectPath) return &types.UploadResp{ FileUrl: fileUrl, FileSize: int64(len(videoData)), MimeType: mimeType, }, nil } // detectVideoType 檢測視頻類型(通過 magic bytes) func (l *UploadVideoLogic) detectVideoType(data []byte, header *multipart.FileHeader) string { if len(data) < 12 { return "" } // 檢查常見視頻格式的 magic bytes // MP4: ftyp box 通常在開頭 if len(data) >= 12 { // MP4/MOV: 通常以 ftyp 開頭 if string(data[4:8]) == "ftyp" { if strings.Contains(string(data[8:12]), "mp4") || strings.Contains(string(data[8:12]), "isom") { return "video/mp4" } if strings.Contains(string(data[8:12]), "qt") { return "video/quicktime" } } } // AVI: RIFF...AVI if len(data) >= 12 && string(data[0:4]) == "RIFF" && string(data[8:12]) == "AVI " { return "video/x-msvideo" } // WebM: webm if len(data) >= 4 && string(data[0:4]) == "\x1a\x45\xdf\xa3" { return "video/webm" } // 從 Content-Type header 獲取 if header != nil && len(header.Header) > 0 { contentType := header.Header.Get("Content-Type") if contentType != "" && strings.HasPrefix(contentType, "video/") { return contentType } } return "" } // getMimeTypeFromExtension 根據文件擴展名獲取 MIME 類型 func (l *UploadVideoLogic) getMimeTypeFromExtension(ext string) string { ext = strings.ToLower(ext) switch ext { case ".mp4": return "video/mp4" case ".mov": return "video/quicktime" case ".avi": return "video/x-msvideo" case ".webm": return "video/webm" case ".mkv": return "video/x-matroska" case ".flv": return "video/x-flv" case ".wmv": return "video/x-ms-wmv" default: return "" } } // getExtensionFromMimeType 根據 MIME 類型獲取文件擴展名 func (l *UploadVideoLogic) getExtensionFromMimeType(mimeType string) string { switch mimeType { case "video/mp4": return ".mp4" case "video/quicktime": return ".mov" case "video/x-msvideo": return ".avi" case "video/webm": return ".webm" case "video/x-matroska": return ".mkv" case "video/x-flv": return ".flv" case "video/x-ms-wmv": return ".wmv" default: return ".mp4" } }