Skip to main content

rusty_cat/
download_trait.rs

1use reqwest::header::HeaderMap;
2use crate::{InnerErrorCode, MeowError, TransferTask};
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::{BreakpointDownload, DownloadHeadCtx, DownloadRangeGetCtx, MeowError};
44///
45/// #[derive(Default)]
46/// struct MyDownloadProtocol;
47///
48/// impl BreakpointDownload for MyDownloadProtocol {
49///     fn merge_head_headers(&self, _ctx: DownloadHeadCtx<'_>) -> Result<(), MeowError> {
50///         Ok(())
51///     }
52///
53///     fn merge_range_get_headers(&self, ctx: DownloadRangeGetCtx<'_>) -> Result<(), MeowError> {
54///         // Reuse default behavior or customize as needed.
55///         BreakpointDownload::merge_range_get_headers(self, ctx)
56///     }
57/// }
58/// ```
59pub trait BreakpointDownload: Send + Sync {
60    /// Returns full URL for HEAD request.
61    ///
62    /// Default implementation returns [`TransferTask::url`].
63    ///
64    /// # Panics
65    ///
66    /// Implementations should avoid panicking and prefer returning recoverable
67    /// errors from later merge/parse methods.
68    ///
69    /// # Examples
70    ///
71    /// ```no_run
72    /// use rusty_cat::api::{BreakpointDownload, StandardRangeDownload, TransferTask};
73    ///
74    /// fn head_url_for(task: &TransferTask) -> String {
75    ///     StandardRangeDownload.head_url(task)
76    /// }
77    /// ```
78    fn head_url(&self, task: &TransferTask) -> String {
79        task.url().to_string()
80    }
81
82    /// Merges protocol-specific headers before sending HEAD request.
83    ///
84    /// Default implementation is no-op.
85    ///
86    /// # Errors
87    ///
88    /// Return [`MeowError`] when required HEAD headers cannot be generated
89    /// (for example, signing failure or invalid header values).
90    ///
91    /// # Examples
92    ///
93    /// ```no_run
94    /// use rusty_cat::api::DownloadHeadCtx;
95    ///
96    /// fn inspect_head_ctx(ctx: &DownloadHeadCtx<'_>) {
97    ///     let _ = (ctx.task.file_name(), ctx.base);
98    /// }
99    /// ```
100    fn merge_head_headers(&self, _ctx: DownloadHeadCtx<'_>) -> Result<(), MeowError> {
101        Ok(())
102    }
103
104    /// Merges protocol-specific headers before range GET request.
105    ///
106    /// Default implementation sets:
107    /// - `Range: <range_value>`
108    /// - `Accept: <task.range_accept or application/octet-stream>`
109    ///
110    /// # Errors
111    ///
112    /// Return [`MeowError`] when protocol-specific range headers cannot be
113    /// generated.
114    ///
115    /// # Examples
116    ///
117    /// ```no_run
118    /// use rusty_cat::api::DownloadRangeGetCtx;
119    ///
120    /// fn inspect_range_ctx(ctx: &DownloadRangeGetCtx<'_>) {
121    ///     let _ = (ctx.range_value, ctx.task.url());
122    /// }
123    /// ```
124    fn merge_range_get_headers(&self, ctx: DownloadRangeGetCtx<'_>) -> Result<(), MeowError> {
125        let _ = self;
126        crate::http_breakpoint::insert_header(ctx.base, "Range", ctx.range_value);
127        let accept = ctx
128            .task
129            .breakpoint_download_http()
130            .map(|c| c.range_accept.as_str())
131            .unwrap_or(crate::http_breakpoint::DEFAULT_RANGE_ACCEPT);
132        crate::http_breakpoint::insert_header(ctx.base, "Accept", accept);
133        Ok(())
134    }
135
136    /// Parses total resource size from successful HEAD response headers.
137    ///
138    /// Default implementation requires valid `Content-Length > 0`.
139    ///
140    /// # Errors
141    ///
142    /// Returns `MissingOrInvalidContentLengthFromHead` when total size cannot
143    /// be parsed from response headers.
144    ///
145    /// # Examples
146    ///
147    /// ```no_run
148    /// use reqwest::header::{HeaderMap, HeaderValue, CONTENT_LENGTH};
149    /// use rusty_cat::api::{BreakpointDownload, StandardRangeDownload};
150    ///
151    /// let mut headers = HeaderMap::new();
152    /// headers.insert(CONTENT_LENGTH, HeaderValue::from_static("1024"));
153    /// let total = StandardRangeDownload.total_size_from_head(&headers)?;
154    /// assert_eq!(total, 1024);
155    /// # Ok::<(), rusty_cat::api::MeowError>(())
156    /// ```
157    fn total_size_from_head(&self, headers: &HeaderMap) -> Result<u64, MeowError> {
158        headers
159            .get(reqwest::header::CONTENT_LENGTH)
160            .and_then(|v| v.to_str().ok())
161            .and_then(|s| s.parse::<u64>().ok())
162            .filter(|&n| n > 0)
163            .ok_or_else(|| {
164                MeowError::from_code_str(
165                    InnerErrorCode::MissingOrInvalidContentLengthFromHead,
166                    "missing or invalid content-length from HEAD",
167                )
168            })
169    }
170}