Skip to main content

rusty_cat/
http_breakpoint.rs

1//! 断点上传/下载的 HTTP 形态由业务决定:通过 [`BreakpointUpload`] / [`BreakpointDownload`]
2//! 从外部构造 `multipart::Form`、合并 Header,便于作为第三方库接入不同后端。
3
4use crate::error::{InnerErrorCode, MeowError};
5use crate::transfer_task::TransferTask;
6use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
7use reqwest::multipart;
8
9#[derive(Debug, Clone, Default)]
10pub struct UploadResumeInfo {
11    /// 若已上传完成,服务端返回的文件 ID。
12    pub completed_file_id: Option<String>,
13    /// 建议下一字节偏移(`nextByte`)。
14    pub next_byte: Option<u64>,
15}
16
17/// 自定义断点上传:准备阶段表单、分块表单及响应解析。
18pub trait BreakpointUpload: Send + Sync {
19    /// 断点探测(无文件分块)使用的 multipart,例如仅含 fileMd5、fileName、totalSize 等。
20    fn prepare_multipart(&self, task: &TransferTask) -> multipart::Form;
21
22    /// 实际上传分块使用的 multipart。
23    fn chunk_multipart(
24        &self,
25        task: &TransferTask,
26        chunk: &[u8],
27        offset: u64,
28    ) -> Result<multipart::Form, MeowError>;
29
30    /// 解析上传接口响应体(通常为 JSON)。
31    fn parse_upload_response(&self, body: &str) -> Result<UploadResumeInfo, MeowError>;
32}
33
34#[derive(Debug, Clone)]
35pub struct DefaultStyleUpload {
36    pub category: String,
37}
38
39impl Default for DefaultStyleUpload {
40    fn default() -> Self {
41        Self {
42            category: String::new(),
43        }
44    }
45}
46
47const KEY_FILE_MD5: &str = "fileMd5";
48const KEY_FILE_NAME: &str = "fileName";
49const KEY_CATEGORY: &str = "category";
50const KEY_TOTAL_SIZE: &str = "totalSize";
51const KEY_OFFSET: &str = "offset";
52const KEY_PART_SIZE: &str = "partSize";
53const KEY_FILE: &str = "file";
54const KEY_UPLOAD_CHUNK_DATA: &str = "upload_chunk_data";
55
56#[derive(serde::Deserialize)]
57struct DefaultUploadResp {
58    #[serde(rename = "fileId")]
59    file_id: Option<String>,
60    #[serde(rename = "nextByte")]
61    next_byte: Option<i64>,
62}
63
64impl BreakpointUpload for DefaultStyleUpload {
65    fn prepare_multipart(&self, task: &TransferTask) -> multipart::Form {
66        multipart::Form::new()
67            .text(KEY_FILE_MD5, task.file_sign().to_string())
68            .text(KEY_FILE_NAME, task.file_name().to_string())
69            .text(KEY_CATEGORY, self.category.clone())
70            .text(KEY_TOTAL_SIZE, task.total_size().to_string())
71    }
72
73    fn chunk_multipart(
74        &self,
75        task: &TransferTask,
76        chunk: &[u8],
77        offset: u64,
78    ) -> Result<multipart::Form, MeowError> {
79        let part = multipart::Part::bytes(chunk.to_vec())
80            .file_name(KEY_UPLOAD_CHUNK_DATA)
81            .mime_str("application/octet-stream")
82            .map_err(|e| MeowError::from_code(InnerErrorCode::HttpError, e.to_string()))?;
83
84        Ok(multipart::Form::new()
85            .part(KEY_FILE, part)
86            .text(KEY_FILE_MD5, task.file_sign().to_string())
87            .text(KEY_FILE_NAME, task.file_name().to_string())
88            .text(KEY_CATEGORY, self.category.clone())
89            .text(KEY_OFFSET, offset.to_string())
90            .text(KEY_PART_SIZE, chunk.len().to_string())
91            .text(KEY_TOTAL_SIZE, task.total_size().to_string()))
92    }
93
94    fn parse_upload_response(&self, body: &str) -> Result<UploadResumeInfo, MeowError> {
95        if body.trim().is_empty() {
96            crate::meow_flow_log!(
97                "upload_protocol",
98                "empty upload response body, fallback default"
99            );
100            return Ok(UploadResumeInfo::default());
101        }
102        let v: DefaultUploadResp = serde_json::from_str(body).map_err(|e| {
103            crate::meow_flow_log!(
104                "upload_protocol",
105                "upload response parse failed: body_len={} err={}",
106                body.len(),
107                e
108            );
109            MeowError::from_code(
110                InnerErrorCode::ResponseParseError,
111                format!("upload response json: {e}, body: {body}"),
112            )
113        })?;
114        crate::meow_flow_log!(
115            "upload_protocol",
116            "upload response parsed: file_id_present={} next_byte={:?}",
117            v.file_id.is_some(),
118            v.next_byte
119        );
120        Ok(UploadResumeInfo {
121            completed_file_id: v.file_id,
122            next_byte: v.next_byte.map(|n| if n < 0 { 0u64 } else { n as u64 }),
123        })
124    }
125}
126
127/// 断点下载中与 HTTP 请求相关的可配置项(Range GET 的 Accept、HEAD 行为等),通常放在
128/// [`TransferTask`] 上由调用方传入;未设置时由 [`crate::meow_config::MeowConfig`] 在入队时补齐。
129#[derive(Debug, Clone, PartialEq, Eq)]
130pub struct BreakpointDownloadHttpConfig {
131    /// 分片 GET(带 `Range`)使用的 `Accept` 头。
132    pub range_accept: String,
133}
134
135impl Default for BreakpointDownloadHttpConfig {
136    fn default() -> Self {
137        Self {
138            range_accept: DEFAULT_RANGE_ACCEPT.to_string(),
139        }
140    }
141}
142
143const DEFAULT_RANGE_ACCEPT: &str = "application/octet-stream";
144
145/// 自定义断点下载:HEAD 与带 Range 的 GET 如何拼 URL / Header、如何从 HEAD 取总长度。
146pub trait BreakpointDownload: Send + Sync {
147    /// HEAD 请求 URL,默认与任务 URL 相同。
148    fn head_url(&self, task: &TransferTask) -> String {
149        task.url().to_string()
150    }
151
152    /// 将 HEAD 专用头并入 `base`(`base` 已含 `TransferTask` 上的头)。
153    fn merge_head_headers(&self, _task: &TransferTask, _base: &mut HeaderMap) {}
154
155    /// 将分片 GET 专用头并入 `base`;`range_value` 形如 `bytes=0-1048575`。
156    /// `Accept` 取自 [`TransferTask`] 上的断点下载 HTTP 配置(未设置时用 [`BreakpointDownloadHttpConfig`] 默认值)。
157    fn merge_range_get_headers(
158        &self,
159        task: &TransferTask,
160        range_value: &str,
161        base: &mut HeaderMap,
162    ) {
163        let _ = self;
164        insert_header(base, "Range", range_value);
165        let accept = task
166            .breakpoint_download_http()
167            .map(|c| c.range_accept.as_str())
168            .unwrap_or(DEFAULT_RANGE_ACCEPT);
169        insert_header(base, "Accept", accept);
170    }
171
172    /// 从 HEAD 响应头解析资源总字节数。
173    fn total_size_from_head(&self, headers: &HeaderMap) -> Result<u64, MeowError> {
174        headers
175            .get(reqwest::header::CONTENT_LENGTH)
176            .and_then(|v| v.to_str().ok())
177            .and_then(|s| s.parse::<u64>().ok())
178            .filter(|&n| n > 0)
179            .ok_or_else(|| {
180                MeowError::from_code_str(
181                    InnerErrorCode::MissingOrInvalidContentLengthFromHead,
182                    "missing or invalid content-length from HEAD",
183                )
184            })
185    }
186}
187
188fn insert_header(map: &mut HeaderMap, name: &str, value: &str) {
189    if let (Ok(n), Ok(v)) = (
190        HeaderName::from_bytes(name.as_bytes()),
191        HeaderValue::from_str(value),
192    ) {
193        map.insert(n, v);
194    }
195}
196
197/// 默认 Range 下载:仅设置 Range / Accept,总长度取自 `Content-Length`。
198#[derive(Debug, Clone, Default)]
199pub struct StandardRangeDownload;
200
201impl BreakpointDownload for StandardRangeDownload {}