Skip to main content

rusty_cat/
download_trait.rs

1use reqwest::header::HeaderMap;
2use crate::{InnerErrorCode, MeowError, TransferTask};
3
4/// 下载 HEAD 阶段请求头合并上下文。
5pub struct DownloadHeadCtx<'a> {
6    pub task: &'a TransferTask,
7    pub base: &'a mut HeaderMap,
8}
9
10/// 下载 Range GET 阶段请求头合并上下文。
11pub struct DownloadRangeGetCtx<'a> {
12    pub task: &'a TransferTask,
13    pub range_value: &'a str,
14    pub base: &'a mut HeaderMap,
15}
16
17/// 自定义断点下载协议。
18///
19/// 实现方负责:HEAD 与分片 GET 的 URL/请求头语义,以及从 HEAD 响应中解析远端资源总长度。
20/// 执行器负责:发起 HTTP、校验 `206 Partial Content` 与 `Content-Range`、写本地文件、并发/重试/进度/暂停恢复。
21///
22/// 典型调用顺序:
23///
24/// 1. 准备阶段:克隆 [`TransferTask`] 上的请求头,用 [`BreakpointDownload::head_url`] 得到 HEAD URL,
25///    再经 [`BreakpointDownload::merge_head_headers`] 合并 HEAD 专用头后发送 `HEAD`;
26///    成功后用 [`BreakpointDownload::total_size_from_head`] 从响应头得到远端总字节数,并与本地已落盘长度对齐续传起点。
27/// 2. 分片阶段:对每个分片克隆任务头,调用 [`BreakpointDownload::merge_range_get_headers`] 写入 `Range`(及实现约定的其它头),
28///    对 [`TransferTask::url`] 发起 `GET`;执行器要求响应状态为 `206`,且带合法 `Content-Range`。
29///
30/// # 与执行器协作的约定
31///
32/// - [`TransferTask`] 上的 [`crate::http_breakpoint::BreakpointDownloadHttpConfig`](由任务或 [`crate::meow_config::MeowConfig`] 提供)
33///   中的 `range_accept` 会被默认的 [`BreakpointDownload::merge_range_get_headers`] 用作 Range GET 的 `Accept`;
34///   自定义实现若覆盖该方法,应自行决定如何体现该配置或协议等价物。
35/// - `Range` 请求头值由执行器按当前分片生成,格式为 HTTP Range 单元字符串,例如 `bytes=0-1048575` 或 `bytes=0-`(空总长度时的退化形式);
36///   实现**不应**随意改写起止含义,除非协议明确要求(此时须在文档中说明并与服务端一致)。
37/// - [`BreakpointDownload::total_size_from_head`] 失败时,准备阶段失败,任务进入错误路径(可能重试,取决于上层配置)。
38pub trait BreakpointDownload: Send + Sync {
39    /// 返回本次 **HEAD** 请求应使用的完整 URL。
40    ///
41    /// 用于在准备阶段探测远端资源长度。默认实现返回 [`TransferTask::url`],适用于「HEAD 与 GET 同址」的常见对象存储或静态资源;
42    /// 若协议要求 HEAD 落在单独端点(例如带不同 path 或 query),应覆盖本方法。
43    ///
44    /// # 参数
45    ///
46    /// - `self`:协议实现(如 [`crate::http_breakpoint::StandardRangeDownload`] 或自定义类型)。
47    /// - `task`:当前下载任务快照,含业务 URL、方法、头、本地路径等;实现通常至少读取 `task.url()`,也可读取其它 getter 拼出 HEAD 地址。
48    ///
49    /// # 返回值
50    ///
51    /// - 合法 URL 字符串,将被执行器直接传给 HTTP 客户端的 `HEAD` 请求。
52    fn head_url(&self, task: &TransferTask) -> String {
53        task.url().to_string()
54    }
55
56    /// 在发送 **HEAD** 之前,将本协议需要的专用请求头合并进 `base`。
57    ///
58    /// `base` 在执行器侧已由 [`TransferTask`] 的头部克隆而来;本方法应**向 `base` 插入或更新**键值,而不是替换整张表,
59    /// 以便保留调用方在任务上配置的通用头(鉴权、`User-Agent` 等)。默认实现不修改任何头。
60    ///
61    /// # 参数
62    ///
63    /// - `self`:协议实现。
64    /// - `task`:当前任务;若 HEAD 需要与 GET 不同的业务参数(如版本号、桶名),可从此处读取并写入 `base`。
65    /// - `base`:即将用于 `HEAD` 请求的 [`HeaderMap`],可变引用;实现应只追加/覆盖与本协议相关的项。
66    ///
67    /// # 返回值
68    ///
69    /// - `Ok(())`:头部合并成功;
70    /// - `Err`:准备 HEAD 头失败(例如签名失败/非法头值),执行器将终止准备阶段并返回该错误。
71    fn merge_head_headers(&self, _ctx: DownloadHeadCtx<'_>) -> Result<(), MeowError> {
72        Ok(())
73    }
74
75    /// 在发送 **带 Range 的分片 GET** 之前,将本协议需要的专用请求头合并进 `base`。
76    ///
77    /// 默认实现会写入:
78    ///
79    /// - `Range`:值为参数 `range_value`(执行器生成的单元范围字符串);
80    /// - `Accept`:优先取任务所带断点下载 HTTP 配置中的 `range_accept`(入队时由
81    ///   [`crate::meow_config::MeowConfig`] 或 builder 写入的 [`crate::http_breakpoint::BreakpointDownloadHttpConfig`]),
82    ///   若未设置则与标准实现一致,为 `application/octet-stream`。
83    ///
84    /// 覆盖本方法时,若仍希望支持调用方配置的 Accept,请读取同一套
85    /// [`crate::http_breakpoint::BreakpointDownloadHttpConfig::range_accept`] 语义(与默认实现保持一致)。
86    ///
87    /// # 参数
88    ///
89    /// - `self`:协议实现;默认实现中未使用 `self`,仅为保持 trait 对象统一签名。
90    /// - `task`:当前任务;用于解析断点下载 HTTP 配置及实现自定义头逻辑时读取 URL/头等元数据。
91    /// - `range_value`:执行器构造的 [`Range`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range) 字段**完整取值**,
92    ///   形如 `bytes=<first>-<last>` 或 `bytes=<first>-`(例如 `bytes=0-1048575`);实现应将其原样写入(或通过网关翻译为协议等价头),
93    ///   除非文档明确说明与标准 Range 语义不同。
94    /// - `base`:即将用于本次 `GET` 的 [`HeaderMap`],由任务头克隆而来;在此追加 `Range`、`Accept` 等分片下载专用头。
95    ///
96    /// # 返回值
97    ///
98    /// - `Ok(())`:头部合并成功;
99    /// - `Err`:构造分片请求头失败(例如非法 Range/签名失败),执行器将终止当前分片并进入错误处理。
100    fn merge_range_get_headers(&self, ctx: DownloadRangeGetCtx<'_>) -> Result<(), MeowError> {
101        let _ = self;
102        crate::http_breakpoint::insert_header(ctx.base, "Range", ctx.range_value);
103        let accept = ctx
104            .task
105            .breakpoint_download_http()
106            .map(|c| c.range_accept.as_str())
107            .unwrap_or(crate::http_breakpoint::DEFAULT_RANGE_ACCEPT);
108        crate::http_breakpoint::insert_header(ctx.base, "Accept", accept);
109        Ok(())
110    }
111
112    /// 从 **HEAD** 成功响应的头字段中解析远端资源的总字节数。
113    ///
114    /// 默认实现要求存在合法且大于 0 的 `Content-Length`,否则返回
115    /// [`InnerErrorCode::MissingOrInvalidContentLengthFromHead`] 对应的 [`MeowError`]。
116    /// 对象存储或 CDN 若对 HEAD 返回其它长度表示方式(例如自定义头、`x-*-size`),应覆盖本方法并返回与 GET 分片一致的总大小。
117    ///
118    /// # 参数
119    ///
120    /// - `self`:协议实现。
121    /// - `headers`:**HEAD** 响应头(而非 GET);实现应只从中读取长度相关字段,避免依赖响应体(HEAD 通常无 body)。
122    ///
123    /// # 返回值
124    ///
125    /// - `Ok(n)`:`n` 为资源总字节数,且应满足 `n > 0`(默认实现会拒绝 0);执行器用它与本地文件长度比较以决定续传起点或已完成。
126    /// - `Err`:无法确定有效总长度;执行器将中止准备阶段并按错误处理。
127    fn total_size_from_head(&self, headers: &HeaderMap) -> Result<u64, MeowError> {
128        headers
129            .get(reqwest::header::CONTENT_LENGTH)
130            .and_then(|v| v.to_str().ok())
131            .and_then(|s| s.parse::<u64>().ok())
132            .filter(|&n| n > 0)
133            .ok_or_else(|| {
134                MeowError::from_code_str(
135                    InnerErrorCode::MissingOrInvalidContentLengthFromHead,
136                    "missing or invalid content-length from HEAD",
137                )
138            })
139    }
140}