rusty_cat/
http_breakpoint.rs1use 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 pub completed_file_id: Option<String>,
13 pub next_byte: Option<u64>,
15}
16
17pub trait BreakpointUpload: Send + Sync {
19 fn prepare_multipart(&self, task: &TransferTask) -> multipart::Form;
21
22 fn chunk_multipart(
24 &self,
25 task: &TransferTask,
26 chunk: &[u8],
27 offset: u64,
28 ) -> Result<multipart::Form, MeowError>;
29
30 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#[derive(Debug, Clone, PartialEq, Eq)]
130pub struct BreakpointDownloadHttpConfig {
131 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
145pub trait BreakpointDownload: Send + Sync {
147 fn head_url(&self, task: &TransferTask) -> String {
149 task.url().to_string()
150 }
151
152 fn merge_head_headers(&self, _task: &TransferTask, _base: &mut HeaderMap) {}
154
155 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 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#[derive(Debug, Clone, Default)]
199pub struct StandardRangeDownload;
200
201impl BreakpointDownload for StandardRangeDownload {}