diff --git a/server/api/admin/common/upload.go b/server/api/admin/common/upload.go index 85f48a5..0c03c8f 100644 --- a/server/api/admin/common/upload.go +++ b/server/api/admin/common/upload.go @@ -6,8 +6,9 @@ package common import ( - "github.com/gogf/gf/v2/frame/g" "hotgo/internal/model/input/sysin" + + "github.com/gogf/gf/v2/frame/g" ) // UploadFileReq 上传文件 @@ -17,6 +18,14 @@ type UploadFileReq struct { type UploadFileRes *sysin.AttachmentListModel +type UploadVideoReq struct { + g.Meta `path:"/upload/video" tags:"附件" method:"post" summary:"上传视频"` +} + +type UploadVideoRes struct { + Path string `json:"path"` +} + // CheckMultipartReq 检查文件分片 type CheckMultipartReq struct { g.Meta `path:"/upload/checkMultipart" tags:"附件" method:"post" summary:"检查文件分片"` diff --git a/server/internal/controller/admin/common/upload.go b/server/internal/controller/admin/common/upload.go index 383b5cb..f8c8022 100644 --- a/server/internal/controller/admin/common/upload.go +++ b/server/internal/controller/admin/common/upload.go @@ -7,12 +7,17 @@ package common import ( "context" - "github.com/gogf/gf/v2/errors/gerror" - "github.com/gogf/gf/v2/frame/g" "hotgo/api/admin/common" "hotgo/internal/library/storager" "hotgo/internal/service" "hotgo/utility/validate" + "os" + "os/exec" + "path/filepath" + + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/frame/g" + "github.com/google/uuid" ) var Upload = new(cUpload) @@ -36,6 +41,70 @@ func (c *cUpload) UploadFile(ctx context.Context, _ *common.UploadFileReq) (res return service.CommonUpload().UploadFile(ctx, uploadType, file) } +// UploadVideo 上传视频 +func (c *cUpload) UploadVideo(ctx context.Context, _ *common.UploadVideoReq) (res common.UploadVideoRes, err error) { + r := g.RequestFromCtx(ctx) + uploadType := r.Header.Get("uploadType") + if uploadType != "default" && !validate.InSlice(storager.KindSlice, uploadType) { + err = gerror.New("上传类型是无效的") + return + } + + file := r.GetUploadFile("file") + if file == nil { + err = gerror.New("没有找到上传的文件") + return + } + + // 1. 保存上传视频到本地临时目录 + tmpDir := "./tmp/video" + _, _ = file.Save(tmpDir) + tmpFile := tmpDir + "/" + file.Filename + + // 2. 用ffmpeg切片为m3u8和ts文件 + + m3u8UUID := uuid.New().String() + + hlsDir := "./tmp/hls/" + m3u8UUID + _ = os.MkdirAll(hlsDir, 0755) + + defer func() { + os.RemoveAll(hlsDir) + }() + + m3u8File := m3u8UUID + ".m3u8" + m3u8Path := hlsDir + "/" + m3u8File + ffmpegCmd := exec.Command("ffmpeg", "-i", tmpFile, "-c:v", "libx264", "-hls_time", "10", "-hls_playlist_type", "vod", m3u8Path) + err = ffmpegCmd.Run() + if err != nil { + err = gerror.Wrap(err, "ffmpeg切片失败") + return + } + + // 3. 遍历切片目录,上传所有m3u8和ts文件到minio + minioDrive := new(storager.MinioDrive) + var m3u8MinioPath string + files, _ := filepath.Glob(hlsDir + "/*") + for _, f := range files { + fileInfo, errStat := os.Stat(f) + if errStat == nil && !fileInfo.IsDir() { + minioPath, err2 := minioDrive.UploadFromPath(ctx, f, filepath.Base(f)) + if err2 != nil { + err = err2 + return + } + if filepath.Ext(f) == ".m3u8" { + m3u8MinioPath = minioPath + } + } + } + + // 4. 返回m3u8文件的minio路径 + return common.UploadVideoRes{ + Path: m3u8MinioPath, + }, nil +} + // CheckMultipart 检查文件分片 func (c *cUpload) CheckMultipart(ctx context.Context, req *common.CheckMultipartReq) (res *common.CheckMultipartRes, err error) { data, err := service.CommonUpload().CheckMultipart(ctx, &req.CheckMultipartInp) diff --git a/server/internal/library/storager/upload_minio.go b/server/internal/library/storager/upload_minio.go index b05b88f..f1a4325 100644 --- a/server/internal/library/storager/upload_minio.go +++ b/server/internal/library/storager/upload_minio.go @@ -7,14 +7,18 @@ package storager import ( "context" + "fmt" + "mime" + "os" + "path/filepath" + "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/net/ghttp" "github.com/gogf/gf/v2/os/gfile" + "github.com/gogf/gf/v2/os/gtime" "github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7/pkg/credentials" "github.com/minio/minio-go/v7/pkg/s3utils" - "mime" - "path/filepath" ) // MinioDrive minio对象存储驱动 @@ -63,6 +67,59 @@ func (d *MinioDrive) Upload(ctx context.Context, file *ghttp.UploadFile) (fullPa return } +// UploadFromPath 从本地文件上传 +func (d *MinioDrive) UploadFromPath(ctx context.Context, path string, filename string) (fullPath string, err error) { + if config.MinioPath == "" { + err = gerror.New("minio存储驱动必须配置存储路径!") + return + } + + client, err := minio.New(config.MinioEndpoint, &minio.Options{ + Creds: credentials.NewStaticV4(config.MinioAccessKey, config.MinioSecretKey, ""), + Secure: config.MinioUseSSL == 1, + }) + if err != nil { + return "", err + } + + if err = s3utils.CheckValidBucketName(config.MinioBucket); err != nil { + return + } + + fullPath = config.MinioPath + gtime.Date() + "/" + filename + if err = s3utils.CheckValidObjectName(fullPath); err != nil { + return + } + + fileInfo, err := os.Stat(path) + if err != nil { + return "", err + } + if fileInfo.IsDir() { + return "", fmt.Errorf("上传路径是目录: %s", path) + } + file, err := os.Open(path) + if err != nil { + return "", err + } + defer file.Close() + + stat, err := file.Stat() + if err != nil { + return "", err + } + + opts := minio.PutObjectOptions{ + ContentType: mime.TypeByExtension(filepath.Ext(path)), + } + if opts.ContentType == "" { + opts.ContentType = "application/octet-stream" + } + + _, err = client.PutObject(ctx, config.MinioBucket, fullPath, file, stat.Size(), opts) + return +} + // CreateMultipart 创建分片事件 func (d *MinioDrive) CreateMultipart(ctx context.Context, in *CheckMultipartParams) (res *MultipartProgress, err error) { err = gerror.New("当前驱动暂不支持分片上传!")