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