Skip to main content

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    /// Whether this download protocol is safe to fetch out of order, so the
73    /// executor may run up to `max_parts_in_flight` range GETs of one file
74    /// concurrently and write them at absolute offsets.
75    ///
76    /// Default `false` keeps every protocol strictly serial. Plain HTTP Range
77    /// (RFC 7233) is order-agnostic, so [`crate::api::StandardRangeDownload`]
78    /// overrides this to `true`. A custom protocol should return `true` only if
79    /// each `range_url`/`merge_range_get_headers` result is independent of any
80    /// other chunk's completion.
81    fn supports_parallel_parts(&self) -> bool {
82        false
83    }
84
85    /// Returns full URL for HEAD request.
86    ///
87    /// Default implementation returns [`TransferTask::url`].
88    ///
89    /// # Panics
90    ///
91    /// Implementations should avoid panicking and prefer returning recoverable
92    /// errors from later merge/parse methods.
93    ///
94    /// # Examples
95    ///
96    /// ```no_run
97    /// use rusty_cat::api::{BreakpointDownload, StandardRangeDownload, TransferTask};
98    ///
99    /// fn head_url_for(task: &TransferTask) -> String {
100    ///     BreakpointDownload::head_url(&StandardRangeDownload, task)
101    /// }
102    /// ```
103    fn head_url(&self, task: &TransferTask) -> String {
104        task.url().to_string()
105    }
106
107    /// Returns full URL for range GET requests.
108    ///
109    /// Default implementation returns [`TransferTask::url`]. Presigned
110    /// protocols can override this when HEAD and GET use different URLs.
111    fn range_url(&self, task: &TransferTask) -> String {
112        task.url().to_string()
113    }
114
115    /// Merges protocol-specific headers before sending HEAD request.
116    ///
117    /// Default implementation is no-op.
118    ///
119    /// # Errors
120    ///
121    /// Return [`MeowError`] when required HEAD headers cannot be generated
122    /// (for example, signing failure or invalid header values).
123    ///
124    /// # Examples
125    ///
126    /// ```no_run
127    /// use rusty_cat::api::DownloadHeadCtx;
128    ///
129    /// fn inspect_head_ctx(ctx: &DownloadHeadCtx<'_>) {
130    ///     let _ = ctx.task.file_name();
131    ///     let _ = ctx.base.len();
132    /// }
133    /// ```
134    fn merge_head_headers(&self, _ctx: DownloadHeadCtx<'_>) -> Result<(), MeowError> {
135        Ok(())
136    }
137
138    /// Merges protocol-specific headers before range GET request.
139    ///
140    /// Default implementation sets:
141    /// - `Range: <range_value>`
142    /// - `Accept: <task.range_accept or application/octet-stream>`
143    ///
144    /// # Errors
145    ///
146    /// Return [`MeowError`] when protocol-specific range headers cannot be
147    /// generated.
148    ///
149    /// # Examples
150    ///
151    /// ```no_run
152    /// use rusty_cat::api::DownloadRangeGetCtx;
153    ///
154    /// fn inspect_range_ctx(ctx: &DownloadRangeGetCtx<'_>) {
155    ///     let _ = (ctx.range_value, ctx.task.url());
156    /// }
157    /// ```
158    fn merge_range_get_headers(&self, ctx: DownloadRangeGetCtx<'_>) -> Result<(), MeowError> {
159        let _ = self;
160        crate::http_breakpoint::insert_header(ctx.base, "Range", ctx.range_value);
161        let accept = ctx
162            .task
163            .breakpoint_download_http()
164            .map(|c| c.range_accept.as_str())
165            .unwrap_or(crate::http_breakpoint::DEFAULT_RANGE_ACCEPT);
166        crate::http_breakpoint::insert_header(ctx.base, "Accept", accept);
167        Ok(())
168    }
169
170    /// Parses total resource size from successful HEAD response headers.
171    ///
172    /// Default implementation requires valid `Content-Length > 0`.
173    ///
174    /// # Errors
175    ///
176    /// Returns `MissingOrInvalidContentLengthFromHead` when total size cannot
177    /// be parsed from response headers.
178    ///
179    /// # Examples
180    ///
181    /// ```no_run
182    /// use reqwest::header::{HeaderMap, HeaderValue, CONTENT_LENGTH};
183    /// use rusty_cat::api::{BreakpointDownload, StandardRangeDownload};
184    ///
185    /// let mut headers = HeaderMap::new();
186    /// headers.insert(CONTENT_LENGTH, HeaderValue::from_static("1024"));
187    /// let total = StandardRangeDownload.total_size_from_head(&headers)?;
188    /// assert_eq!(total, 1024);
189    /// # Ok::<(), rusty_cat::api::MeowError>(())
190    /// ```
191    fn total_size_from_head(&self, headers: &HeaderMap) -> Result<u64, MeowError> {
192        headers
193            .get(reqwest::header::CONTENT_LENGTH)
194            .and_then(|v| v.to_str().ok())
195            .and_then(|s| s.parse::<u64>().ok())
196            .filter(|&n| n > 0)
197            .ok_or_else(|| {
198                MeowError::from_code_str(
199                    InnerErrorCode::MissingOrInvalidContentLengthFromHead,
200                    "missing or invalid content-length from HEAD",
201                )
202            })
203    }
204}