第22章:素材下载与 URL 重写
本章分析 internal/pkg/utils/download.go 与 internal/pkg/utils/urlrewrite:文件下载器的实现要点(流式落盘、错误包装)、Host 精确映射的改写器(让公网 URL 走内网直连),以及 Rewriter 如何从配置构造并注入下载链路。素材无论进 ASR 还是进草稿流水线,第一步都是「把它下下来」,本章是所有流水线的公共第一步。
流程
下载链路在 main.go 装配:rewriter := cfg.Download.URLRewriter() 从下载配置构造 Rewriter → downloader := service.NewFileDownloader(logger, rewriter) → 注入 ASR 音频预处理器与草稿 Generator。执行时:调用方给出 URL 与目标路径 → 下载器先 rewriter.Rewrite(url) 尝试改写 Host → 用改写后(或原样)的 URL 发起 HTTP GET → 流式拷贝到本地文件 → 返回实际落盘路径。
URL 重写的动机是混合网络拓扑:素材的公网 URL(如对象存储的签名地址)对公网用户可访问,但服务器自己下载时走公网既慢又费流量。download.url_rewriter.exact 配置一张 Host 映射表(example.com → internal.example.com),下载器出站前把 Host 精确替换,path 与 query 原样保留——签名 URL 的 query 参数是跟 path 绑定的,任何多余改动都会让签名失效,这正是「只动 Host」规则的技术根源。
实现
urlrewrite.Rewriter 的实现特点:
- 精确匹配而非通配/正则:
exact map[string]string,只替换完全相等的 hostname。配置面小、行为可预测,不会出现「*.cdn.com 误伤业务域名」的事故。 - hostname 与 port 分离处理:
hostnameOnly用net.SplitHostPort剥离端口取 hostname 查表,命中后replaceHostname把端口拼回去——cdn.example.com:8443 → cdn-internal.example.com:8443,端口语义保留。 - normalizeHost 的宽容解析:配置里写
https://a.com/、a.com:80都能归一化成 hostname(剥协议前缀、剥路径),配置容错强。 - 三处防御性返回:无规则、URL 解析失败、Host 为空时都原样返回(
return raw, false)——改写器失败永远不阻断下载,退化为公网直连,可用性优先。 Empty()判定:调用方可以探测「是否配置了改写」,日志里可区分「走了内网/走了公网」。
FileDownloader(service/live_material_download.go 同族实现)的要点:
- HTTP 响应流直接
io.Copy到目标文件,不读进内存——直播回放动辄数 GB,全量缓冲会 OOM; - 错误包装带上下文(URL、目标路径),排障时第一现场完整;
- 下载前确保父目录存在(staging 目录由调用方创建,下载器做兜底)。
配置侧:DownloadConfig(config/download.go)持有一组 from → to 映射并暴露 URLRewriter() 构造方法,download_test.go 覆盖了协议前缀、端口、无匹配、非法 URL 等分支——一个 89 行的包配了完整测试,因为它站在所有数据入口的关键路径上。
📌 设计决策
- Host 精确映射而非透明代理:无需额外进程、无需 TLS 信任链变更;代价是每换一个源站要加一条配置。对源站数量有限(自家对象存储 + 少数直播平台)的场景是最优解。
- 改写失败静默降级:重写器是优化器而非守门员,任何异常都不应让下载失败。
- 只改写出站下载,不改写入站 API:入站请求的 Host 处理交给 nginx(第 50 章),职责分界清晰。
代码示例
Rewriter 的核心两跳(查表 + 端口保留):
// internal/pkg/utils/urlrewrite/rewrite.go(节选)
func (r *Rewriter) Rewrite(raw string) (string, bool) {
if r == nil || r.Empty() {
return raw, false
}
u, err := url.Parse(raw)
if err != nil || u.Host == "" {
return raw, false // 解析失败原样返回,不阻断下载
}
hostname := hostnameOnly(u.Host) // 剥端口取 hostname
mapped, ok := r.exact[hostname] // 精确查表
if !ok {
return raw, false
}
u.Host = replaceHostname(u.Host, mapped) // 端口拼回
return u.String(), true
}
配置到下载器的装配(main.go):
// cmd/webserver/main.go(节选)
rewriter := cfg.Download.URLRewriter() // exact: {公网Host: 内网Host}
downloader := service.NewFileDownloader(logger, rewriter)
// downloader 随后注入:
// ASR 音频预处理器(抽音频前先拉源视频)
// draft.Generator(prepare 流水线下载 source.mp4)
小结
- 下载链路 = Rewriter(Host 精确映射,失败静默)+ FileDownloader(流式落盘)。
- 只动 Host 不动 path/query 是签名 URL 兼容的硬约束。
- 改写器是优化器:公网直连是永远的兜底路径。
思考题
- 若源站使用 IP 直连更优(无域名),Host 映射方案需要怎么扩展?
- 下载器如何补「断点续传」与「哈希校验」两个能力?各自的失败语义如何设计?
项目信息
- GitHub仓库:Chyona/live-mixer