Supports specifying a file name during video recording, facilitating third-party access to the API for retrieving the recorded file name.#379
Conversation
1. 安全修复: - 在 MP4/FLV/HLS 插件中添加文件名安全验证 - 使用 filepath.Base() 清理路径分隔符,防止路径遍历攻击 - 验证文件名不为空且不是特殊路径(. 或 ..) 2. 逻辑修复: - 修复 MP4 API 中的录制任务查找逻辑 - 正确处理空文件名场景,避免重复录制任务 3. 代码质量: - 格式化 HLS 插件代码(修复空格/制表符混用问题) - 统一数据库模型字段命名(Filename -> FileName) 4. 配置解析修复: - 修复配置解析时的类型转换问题 - 使用 unmarshal 函数正确处理类型转换
…e' into eanfs-v5 * pullrequests/eanfs/feature/support-record-with-file-name: fix: 修复 PR langhuihui#379 中的安全问题和逻辑缺陷 cluster pro test
There was a problem hiding this comment.
Pull request overview
This PR adds support for specifying an explicit output filename when starting a recording (notably via the MP4 plugin API), and persists the derived filename into the recorded stream metadata to make it discoverable to API consumers.
Changes:
- Add
fileNameto the MP4 StartRecord request, propagate it intoconfig.Record, and use it when generating the recording output path. - Store a
Filenamevalue (derived from the output path) onRecordStreamso downstream APIs can return it. - Update MP4/FLV/HLS recorders’ filename generation logic to honor
RecConf.FileName, plus regenerate MP4 protobuf/gateway artifacts and adjust the example config.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| recoder.go | Persists RecordStream.Filename derived from the recording output path. |
| plugin/mp4/pkg/record.go | Honors RecConf.FileName when generating MP4 output names; adds extra task fields/logging. |
| plugin/mp4/pb/mp4.proto | Adds fileName to ReqStartRecord. |
| plugin/mp4/pb/mp4.pb.gw.go | Regenerated gateway code; adds req.Body != nil guards before draining bodies. |
| plugin/mp4/pb/mp4.pb.go | Regenerated protobuf bindings for the new field. |
| plugin/mp4/pb/mp4_grpc.pb.go | Regenerated gRPC bindings. |
| plugin/mp4/api.go | Accepts fileName in StartRecord and stores it into config.Record. |
| plugin/hls/pkg/record.go | Honors RecConf.FileName for TS output naming; adds debug log on create. |
| plugin/flv/pkg/record.go | Honors RecConf.FileName for FLV output naming; adds debug log on create. |
| pkg/config/types.go | Adds Record.FileName to config model. |
| example/default/config.yaml | Updates example defaults and shows filename usage in comments. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| res = &mp4pb.ResponseStartRecord{} | ||
| _, recordExists = p.Server.Records.Find(func(job *m7s.RecordJob) bool { | ||
| return job.StreamPath == req.StreamPath && job.RecConf.FilePath == req.FilePath | ||
| return job.StreamPath == req.StreamPath && job.RecConf.FilePath == req.FilePath && job.RecConf.FileName == req.FileName |
There was a problem hiding this comment.
The duplicate-recording check compares against req.FilePath/req.FileName rather than the normalized variables (filePath/fileName). When req.FilePath is omitted (defaults to "."), this check won’t match an existing job and can allow multiple recordings for the same stream+effective path/name. Compare against filePath/fileName instead (or normalize req values first).
| return job.StreamPath == req.StreamPath && job.RecConf.FilePath == req.FilePath && job.RecConf.FileName == req.FileName | |
| return job.StreamPath == req.StreamPath && job.RecConf.FilePath == filePath && job.RecConf.FileName == fileName |
| var CustomFileName = func(job *m7s.RecordJob) string { | ||
| // 如果指定了文件名,使用指定的文件名 | ||
| if fn := job.RecConf.FileName; fn != "" { | ||
| // 确保文件名包含 .mp4 扩展名 | ||
| if !strings.HasSuffix(strings.ToLower(fn), ".mp4") { | ||
| fn = fn + ".mp4" | ||
| } | ||
| return filepath.Join(job.RecConf.FilePath, fn) | ||
| } | ||
| // 否则使用时间戳生成文件名 | ||
| now := time.Now() | ||
| return filepath.Join(job.RecConf.FilePath, fmt.Sprintf("%s_%09d.mp4", time.Now().Local().Format("2006-01-02-15-04-05"), now.Nanosecond())) |
There was a problem hiding this comment.
Custom fileName is used directly with filepath.Join. If fileName is absolute or contains path separators (e.g. "../x"), Join can escape RecConf.FilePath and write outside the intended directory. Sanitize/validate the provided name (e.g., reject separators/absolute paths, or force filepath.Base) before joining.
| var CustomFileName = func(job *m7s.RecordJob) string { | |
| // 如果指定了文件名,使用指定的文件名 | |
| if fn := job.RecConf.FileName; fn != "" { | |
| // 确保文件名包含 .mp4 扩展名 | |
| if !strings.HasSuffix(strings.ToLower(fn), ".mp4") { | |
| fn = fn + ".mp4" | |
| } | |
| return filepath.Join(job.RecConf.FilePath, fn) | |
| } | |
| // 否则使用时间戳生成文件名 | |
| now := time.Now() | |
| return filepath.Join(job.RecConf.FilePath, fmt.Sprintf("%s_%09d.mp4", time.Now().Local().Format("2006-01-02-15-04-05"), now.Nanosecond())) | |
| func defaultMP4FileName() string { | |
| now := time.Now() | |
| return fmt.Sprintf("%s_%09d.mp4", time.Now().Local().Format("2006-01-02-15-04-05"), now.Nanosecond()) | |
| } | |
| func sanitizeMP4FileName(fn string) string { | |
| if fn == "" { | |
| return "" | |
| } | |
| normalized := strings.ReplaceAll(fn, "\\", "/") | |
| if filepath.IsAbs(fn) || strings.Contains(normalized, "/") { | |
| fn = filepath.Base(normalized) | |
| } | |
| if fn == "" || fn == "." { | |
| return "" | |
| } | |
| // 确保文件名包含 .mp4 扩展名 | |
| if !strings.HasSuffix(strings.ToLower(fn), ".mp4") { | |
| fn = fn + ".mp4" | |
| } | |
| return fn | |
| } | |
| var CustomFileName = func(job *m7s.RecordJob) string { | |
| // 如果指定了文件名,使用指定的文件名 | |
| if fn := sanitizeMP4FileName(job.RecConf.FileName); fn != "" { | |
| return filepath.Join(job.RecConf.FilePath, fn) | |
| } | |
| // 否则使用时间戳生成文件名 | |
| return filepath.Join(job.RecConf.FilePath, defaultMP4FileName()) |
| } | ||
|
|
||
| func (task *writeTrailerTask) Start() (err error) { | ||
| task.Info("write trailer start") |
There was a problem hiding this comment.
Logging "write trailer start" at Info level will run for every trailer write (potentially every fragment), which can significantly increase log volume in production. Consider lowering this to Debug/Trace or removing it unless it’s needed for normal operation.
| task.Info("write trailer start") | |
| task.Debug("write trailer start") |
| "context" | ||
| "fmt" | ||
| "strings" | ||
| "path/filepath" | ||
| "time" |
There was a problem hiding this comment.
The imports (spacing/ordering) are not gofmt-formatted (spaces instead of tabs, ordering differs). This will create noisy diffs and can fail formatting checks if enforced. Please run gofmt on this file.
| "context" | |
| "fmt" | |
| "strings" | |
| "path/filepath" | |
| "time" | |
| "context" | |
| "fmt" | |
| "path/filepath" | |
| "strings" | |
| "time" |
| var CustomFileName = func(job *m7s.RecordJob) string { | ||
| if job.RecConf.Fragment == 0 || job.RecConf.Append { | ||
| return fmt.Sprintf("%s/%s.ts", job.RecConf.FilePath, time.Now().Format("20060102150405")) | ||
| } | ||
| return filepath.Join(job.RecConf.FilePath, time.Now().Format("20060102150405")+".ts") | ||
| if fn := job.RecConf.FileName; fn != "" { | ||
| if !strings.HasSuffix(strings.ToLower(fn), ".ts") { | ||
| fn = fn + ".ts" | ||
| } | ||
| return filepath.Join(job.RecConf.FilePath, fn) | ||
| } |
There was a problem hiding this comment.
Custom fileName is used directly with filepath.Join. If fileName is absolute or contains path separators (e.g. "../x"), Join can escape RecConf.FilePath and write outside the intended directory. Sanitize/validate the provided name (e.g., reject separators/absolute paths, or force filepath.Base) before joining.
| if fn := job.RecConf.FileName; fn != "" { | ||
| if !strings.HasSuffix(strings.ToLower(fn), ".flv") { | ||
| fn = fn + ".flv" | ||
| } | ||
| return filepath.Join(job.RecConf.FilePath, fn) |
There was a problem hiding this comment.
Custom fileName is used directly with filepath.Join. If fileName is absolute or contains path separators (e.g. "../x"), Join can escape RecConf.FilePath and write outside the intended directory. Sanitize/validate the provided name (e.g., reject separators/absolute paths, or force filepath.Base) before joining.
| if fn := job.RecConf.FileName; fn != "" { | |
| if !strings.HasSuffix(strings.ToLower(fn), ".flv") { | |
| fn = fn + ".flv" | |
| } | |
| return filepath.Join(job.RecConf.FilePath, fn) | |
| if fn := strings.TrimSpace(job.RecConf.FileName); fn != "" { | |
| if !strings.HasSuffix(strings.ToLower(fn), ".flv") { | |
| fn = fn + ".flv" | |
| } | |
| cleanFn := filepath.Clean(fn) | |
| if filepath.IsAbs(cleanFn) || cleanFn == "." || cleanFn == ".." || cleanFn != filepath.Base(cleanFn) { | |
| cleanFn = filepath.Base(cleanFn) | |
| } | |
| if cleanFn == "" || cleanFn == "." || cleanFn == ".." { | |
| cleanFn = fmt.Sprintf("%d.flv", time.Now().Unix()) | |
| } | |
| return filepath.Join(job.RecConf.FilePath, cleanFn) |
| global: | ||
| http: | ||
| listenaddr: :8080 | ||
| location: | ||
| "^/hdl/(.*)": "/flv/$1" # 兼容 v4 | ||
| "^/stress/api/(.*)": "/test/api/stress/$1" # 5.0.x | ||
| "^/monitor/(.*)": "/debug/$1" # 5.0.x | ||
| loglevel: debug | ||
| loglevel: info | ||
| admin: | ||
| enablelogin: false |
There was a problem hiding this comment.
This example config change does more than demonstrate the new file-name feature (it changes global loglevel, adds an HTTP listen address, and enables the mp4 plugin by default). If the PR is only meant to add filename support, consider keeping unrelated example-default behavior unchanged or moving these changes to a separate PR to avoid surprising defaults.
Supports specifying a file name during video recording, facilitating third-party access to the API for retrieving the recorded file name.