rusty_cat/download_trait.rs
1use crate::{InnerErrorCode, MeowError, TransferTask};
2use reqwest::header::HeaderMap;
3
4/// Header merge context for download HEAD request.
5pub struct DownloadHeadCtx<'a> {
6 /// Immutable task snapshot.
7 pub task: &'a TransferTask,
8 /// Mutable base headers cloned from task.
9 pub base: &'a mut HeaderMap,
10}
11
12/// Header merge context for download range GET request.
13pub struct DownloadRangeGetCtx<'a> {
14 /// Immutable task snapshot.
15 pub task: &'a TransferTask,
16 /// Fully formatted `Range` header value, for example `bytes=0-1048575`.
17 pub range_value: &'a str,
18 /// Mutable base headers cloned from task.
19 pub base: &'a mut HeaderMap,
20}
21
22/// Custom breakpoint download protocol.
23///
24/// Implementors control HEAD/range-GET URL and header semantics, and parse
25/// remote total size from HEAD response headers. Executor handles HTTP sending,
26/// response validation, file writes, retries, progress, pause/resume, and state.
27///
28/// # Typical call flow
29///
30/// 1. Prepare stage: executor sends HEAD after `head_url` and
31/// `merge_head_headers`.
32/// 2. Chunk stage: executor sends range GET after `merge_range_get_headers`.
33///
34/// # Executor integration contract
35///
36/// - Default implementation uses task-level `range_accept` as `Accept` header.
37/// - `range_value` is generated by executor and should usually be preserved.
38/// - `total_size_from_head` failure terminates prepare stage.
39///
40/// # Examples
41///
42/// ```no_run
43/// use rusty_cat::api::{
44/// BreakpointDownload, DownloadHeadCtx, DownloadRangeGetCtx, MeowError, StandardRangeDownload,
45/// };
46///
47/// #[derive(Default)]
48/// struct MyDownloadProtocol;
49///
50/// impl BreakpointDownload for MyDownloadProtocol {
51/// fn merge_head_headers(&self, _ctx: DownloadHeadCtx<'_>) -> Result<(), MeowError> {
52/// Ok(())
53/// }
54///
55/// fn merge_range_get_headers(&self, ctx: DownloadRangeGetCtx<'_>) -> Result<(), MeowError> {
56/// // Reuse default behavior or customize as needed.
57/// StandardRangeDownload.merge_range_get_headers(ctx)
58/// }
59/// }
60/// ```
61pub trait BreakpointDownload: Send + Sync {
62 /// Returns known remote total size and skips the HEAD prepare request when
63 /// present.
64 ///
65 /// This is useful for presigned URL downloads where a GET URL cannot be
66 /// reused as HEAD, or where the application server already returned object
67 /// metadata together with the presigned range URL.
68 fn total_size_hint(&self, _task: &TransferTask) -> Option<u64> {
69 None
70 }
71
72 /// Returns full URL for HEAD request.
73 ///
74 /// Default implementation returns [`TransferTask::url`].
75 ///
76 /// # Panics
77 ///
78 /// Implementations should avoid panicking and prefer returning recoverable
79 /// errors from later merge/parse methods.
80 ///
81 /// # Examples
82 ///
83 /// ```no_run
84 /// use rusty_cat::api::{BreakpointDownload, StandardRangeDownload, TransferTask};
85 ///
86 /// fn head_url_for(task: &TransferTask) -> String {
87 /// BreakpointDownload::head_url(&StandardRangeDownload, task)
88 /// }
89 /// ```
90 fn head_url(&self, task: &TransferTask) -> String {
91 task.url().to_string()
92 }
93
94 /// Returns full URL for range GET requests.
95 ///
96 /// Default implementation returns [`TransferTask::url`]. Presigned
97 /// protocols can override this when HEAD and GET use different URLs.
98 fn range_url(&self, task: &TransferTask) -> String {
99 task.url().to_string()
100 }
101
102 /// Merges protocol-specific headers before sending HEAD request.
103 ///
104 /// Default implementation is no-op.
105 ///
106 /// # Errors
107 ///
108 /// Return [`MeowError`] when required HEAD headers cannot be generated
109 /// (for example, signing failure or invalid header values).
110 ///
111 /// # Examples
112 ///
113 /// ```no_run
114 /// use rusty_cat::api::DownloadHeadCtx;
115 ///
116 /// fn inspect_head_ctx(ctx: &DownloadHeadCtx<'_>) {
117 /// let _ = ctx.task.file_name();
118 /// let _ = ctx.base.len();
119 /// }
120 /// ```
121 fn merge_head_headers(&self, _ctx: DownloadHeadCtx<'_>) -> Result<(), MeowError> {
122 Ok(())
123 }
124
125 /// Merges protocol-specific headers before range GET request.
126 ///
127 /// Default implementation sets:
128 /// - `Range: <range_value>`
129 /// - `Accept: <task.range_accept or application/octet-stream>`
130 ///
131 /// # Errors
132 ///
133 /// Return [`MeowError`] when protocol-specific range headers cannot be
134 /// generated.
135 ///
136 /// # Examples
137 ///
138 /// ```no_run
139 /// use rusty_cat::api::DownloadRangeGetCtx;
140 ///
141 /// fn inspect_range_ctx(ctx: &DownloadRangeGetCtx<'_>) {
142 /// let _ = (ctx.range_value, ctx.task.url());
143 /// }
144 /// ```
145 fn merge_range_get_headers(&self, ctx: DownloadRangeGetCtx<'_>) -> Result<(), MeowError> {
146 let _ = self;
147 crate::http_breakpoint::insert_header(ctx.base, "Range", ctx.range_value);
148 let accept = ctx
149 .task
150 .breakpoint_download_http()
151 .map(|c| c.range_accept.as_str())
152 .unwrap_or(crate::http_breakpoint::DEFAULT_RANGE_ACCEPT);
153 crate::http_breakpoint::insert_header(ctx.base, "Accept", accept);
154 Ok(())
155 }
156
157 /// Parses total resource size from successful HEAD response headers.
158 ///
159 /// Default implementation requires valid `Content-Length > 0`.
160 ///
161 /// # Errors
162 ///
163 /// Returns `MissingOrInvalidContentLengthFromHead` when total size cannot
164 /// be parsed from response headers.
165 ///
166 /// # Examples
167 ///
168 /// ```no_run
169 /// use reqwest::header::{HeaderMap, HeaderValue, CONTENT_LENGTH};
170 /// use rusty_cat::api::{BreakpointDownload, StandardRangeDownload};
171 ///
172 /// let mut headers = HeaderMap::new();
173 /// headers.insert(CONTENT_LENGTH, HeaderValue::from_static("1024"));
174 /// let total = StandardRangeDownload.total_size_from_head(&headers)?;
175 /// assert_eq!(total, 1024);
176 /// # Ok::<(), rusty_cat::api::MeowError>(())
177 /// ```
178 fn total_size_from_head(&self, headers: &HeaderMap) -> Result<u64, MeowError> {
179 headers
180 .get(reqwest::header::CONTENT_LENGTH)
181 .and_then(|v| v.to_str().ok())
182 .and_then(|s| s.parse::<u64>().ok())
183 .filter(|&n| n > 0)
184 .ok_or_else(|| {
185 MeowError::from_code_str(
186 InnerErrorCode::MissingOrInvalidContentLengthFromHead,
187 "missing or invalid content-length from HEAD",
188 )
189 })
190 }
191}