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/// - `DownloadRangeGetCtx::base` may already contain an executor-owned
39///   `If-Match` copied from the prepared strong ETag. Implementations must
40///   preserve that header exactly. Signing protocols must calculate
41///   authorization only after all protocol-specific headers have been merged.
42/// - `total_size_from_head` failure terminates prepare stage.
43///
44/// # Examples
45///
46/// ```no_run
47/// use rusty_cat::api::{
48///     BreakpointDownload, DownloadHeadCtx, DownloadRangeGetCtx, MeowError, StandardRangeDownload,
49/// };
50///
51/// #[derive(Default)]
52/// struct MyDownloadProtocol;
53///
54/// impl BreakpointDownload for MyDownloadProtocol {
55///     fn merge_head_headers(&self, _ctx: DownloadHeadCtx<'_>) -> Result<(), MeowError> {
56///         Ok(())
57///     }
58///
59///     fn merge_range_get_headers(&self, ctx: DownloadRangeGetCtx<'_>) -> Result<(), MeowError> {
60///         // Reuse default behavior or customize as needed.
61///         StandardRangeDownload.merge_range_get_headers(ctx)
62///     }
63/// }
64/// ```
65pub trait BreakpointDownload: Send + Sync {
66    /// Returns stable, protocol-specific bytes that bind a persisted download
67    /// checkpoint to the effective range-request representation.
68    ///
69    /// The returned bytes are combined with the canonical range URL and the
70    /// strong ETag, then persisted only as a domain-separated SHA-256 digest.
71    /// They are never logged or written to the sidecar verbatim. Implementors
72    /// should include every invariant header or principal/tenant selector that
73    /// can change response bytes, while excluding per-part values such as
74    /// `Range` and short-lived signature timestamps.
75    ///
76    /// The conservative default is `None`, which disables cross-process
77    /// checkpoint reuse for a custom protocol. The current transfer still
78    /// validates every range response against the strong ETag. Return `Some`
79    /// only when the context is complete and stable across credential refresh.
80    ///
81    /// # Errors
82    ///
83    /// Return [`MeowError`] when stable identity context cannot be constructed.
84    /// Return `Ok(None)` when the protocol fundamentally cannot provide one.
85    fn resume_identity(&self, _task: &TransferTask) -> Result<Option<Vec<u8>>, MeowError> {
86        Ok(None)
87    }
88
89    /// Returns known remote total size and skips the HEAD prepare request when
90    /// present.
91    ///
92    /// This is useful for presigned URL downloads where a GET URL cannot be
93    /// reused as HEAD, or where the application server already returned object
94    /// metadata together with the presigned range URL.
95    fn total_size_hint(&self, _task: &TransferTask) -> Option<u64> {
96        None
97    }
98
99    /// Whether this download protocol is safe to fetch out of order, so the
100    /// executor may run up to `max_parts_in_flight` range GETs of one file
101    /// concurrently and write them at absolute offsets.
102    ///
103    /// Default `false` keeps every protocol strictly serial. Plain HTTP Range
104    /// (RFC 7233) is order-agnostic, so [`crate::api::StandardRangeDownload`]
105    /// overrides this to `true`. A custom protocol should return `true` only if
106    /// each `range_url`/`merge_range_get_headers` result is independent of any
107    /// other chunk's completion.
108    fn supports_parallel_parts(&self) -> bool {
109        false
110    }
111
112    /// Returns full URL for HEAD request.
113    ///
114    /// Default implementation returns [`TransferTask::url`].
115    ///
116    /// # Panics
117    ///
118    /// Implementations should avoid panicking and prefer returning recoverable
119    /// errors from later merge/parse methods.
120    ///
121    /// # Examples
122    ///
123    /// ```no_run
124    /// use rusty_cat::api::{BreakpointDownload, StandardRangeDownload, TransferTask};
125    ///
126    /// fn head_url_for(task: &TransferTask) -> String {
127    ///     BreakpointDownload::head_url(&StandardRangeDownload, task)
128    /// }
129    /// ```
130    fn head_url(&self, task: &TransferTask) -> String {
131        task.url().to_string()
132    }
133
134    /// Returns full URL for range GET requests.
135    ///
136    /// Default implementation returns [`TransferTask::url`]. Presigned
137    /// protocols can override this when HEAD and GET use different URLs.
138    fn range_url(&self, task: &TransferTask) -> String {
139        task.url().to_string()
140    }
141
142    /// Merges protocol-specific headers before sending HEAD request.
143    ///
144    /// Default implementation is no-op.
145    ///
146    /// # Errors
147    ///
148    /// Return [`MeowError`] when required HEAD headers cannot be generated
149    /// (for example, signing failure or invalid header values).
150    ///
151    /// # Examples
152    ///
153    /// ```no_run
154    /// use rusty_cat::api::DownloadHeadCtx;
155    ///
156    /// fn inspect_head_ctx(ctx: &DownloadHeadCtx<'_>) {
157    ///     let _ = ctx.task.file_name();
158    ///     let _ = ctx.base.len();
159    /// }
160    /// ```
161    fn merge_head_headers(&self, _ctx: DownloadHeadCtx<'_>) -> Result<(), MeowError> {
162        Ok(())
163    }
164
165    /// Merges protocol-specific headers before range GET request.
166    ///
167    /// The executor may pre-populate `ctx.base` with an `If-Match` header that
168    /// binds every range to the remote generation observed during preparation.
169    /// Implementations must not remove, replace, or append another value to that
170    /// header. The executor validates this contract before network I/O, allowing
171    /// authentication implementations to sign the final conditional headers.
172    ///
173    /// Default implementation sets:
174    /// - `Range: <range_value>`
175    /// - `Accept: <task.range_accept or application/octet-stream>`
176    ///
177    /// # Errors
178    ///
179    /// Return [`MeowError`] when protocol-specific range headers cannot be
180    /// generated.
181    ///
182    /// # Examples
183    ///
184    /// ```no_run
185    /// use rusty_cat::api::DownloadRangeGetCtx;
186    ///
187    /// fn inspect_range_ctx(ctx: &DownloadRangeGetCtx<'_>) {
188    ///     let _ = (ctx.range_value, ctx.task.url());
189    /// }
190    /// ```
191    fn merge_range_get_headers(&self, ctx: DownloadRangeGetCtx<'_>) -> Result<(), MeowError> {
192        let _ = self;
193        crate::http_breakpoint::insert_header(ctx.base, "Range", ctx.range_value);
194        let accept = ctx
195            .task
196            .breakpoint_download_http()
197            .map(|c| c.range_accept.as_str())
198            .unwrap_or(crate::http_breakpoint::DEFAULT_RANGE_ACCEPT);
199        crate::http_breakpoint::insert_header(ctx.base, "Accept", accept);
200        Ok(())
201    }
202
203    /// Parses total resource size from successful HEAD response headers.
204    ///
205    /// Default implementation requires valid `Content-Length > 0`.
206    ///
207    /// # Errors
208    ///
209    /// Returns `MissingOrInvalidContentLengthFromHead` when total size cannot
210    /// be parsed from response headers.
211    ///
212    /// # Examples
213    ///
214    /// ```no_run
215    /// use reqwest::header::{HeaderMap, HeaderValue, CONTENT_LENGTH};
216    /// use rusty_cat::api::{BreakpointDownload, StandardRangeDownload};
217    ///
218    /// let mut headers = HeaderMap::new();
219    /// headers.insert(CONTENT_LENGTH, HeaderValue::from_static("1024"));
220    /// let total = StandardRangeDownload.total_size_from_head(&headers)?;
221    /// assert_eq!(total, 1024);
222    /// # Ok::<(), rusty_cat::api::MeowError>(())
223    /// ```
224    fn total_size_from_head(&self, headers: &HeaderMap) -> Result<u64, MeowError> {
225        headers
226            .get(reqwest::header::CONTENT_LENGTH)
227            .and_then(|v| v.to_str().ok())
228            .and_then(|s| s.parse::<u64>().ok())
229            .filter(|&n| n > 0)
230            .ok_or_else(|| {
231                MeowError::from_code_str(
232                    InnerErrorCode::MissingOrInvalidContentLengthFromHead,
233                    "missing or invalid content-length from HEAD",
234                )
235            })
236    }
237}