Skip to main content

kithara_stream/dl/
cmd.rs

1use std::io;
2
3use bon::Builder;
4use kithara_events::RequestMethod;
5use kithara_net::{Headers, NetError, NetResult, RangeSpec};
6use tokio_util::sync::CancellationToken;
7use url::Url;
8
9/// Per-command body writer. Downloader calls it for each chunk.
10pub type WriterFn = Box<dyn FnMut(&[u8]) -> io::Result<()> + Send>;
11
12/// Per-command response callback. Fires once on the streaming path
13/// when the HTTP response is in hand — past validation, headers
14/// available, body about to stream. Mirrors [`OnCompleteFn`] on the
15/// other end of the fetch lifecycle: peers use it to seed metadata
16/// (Content-Length, Content-Type) eagerly so a reader blocked on the
17/// first byte already sees a populated coord.
18pub type OnResponseFn = Box<dyn FnOnce(&Headers) + Send>;
19
20/// Per-command completion handler. Called when the fetch completes.
21///
22/// Receives `(bytes_written, response_headers, error)`. Headers are
23/// `Some` once the HTTP response made it past validation (so `Content-Type`,
24/// `Content-Length`, etc. can be captured); `None` when the fetch failed
25/// before headers were received.
26pub type OnCompleteFn = Box<dyn FnOnce(u64, Option<&Headers>, Option<&NetError>) + Send>;
27
28/// Optional response-header validator for a single `FetchCmd`.
29///
30/// Invoked with the response headers after a successful HTTP response.
31/// Returning `Err` rejects the response before the body is consumed.
32pub(super) type ResponseValidator = fn(&Headers) -> NetResult<()>;
33
34/// A single download command.
35///
36/// Built by protocol code via [`FetchCmd::get`] / [`FetchCmd::head`]
37/// constructors and executed via
38/// [`PeerHandle::execute`](super::PeerHandle::execute). The downloader
39/// establishes the HTTP connection and returns a
40/// [`FetchResponse`](super::FetchResponse) with headers and a body stream.
41#[derive(Builder)]
42#[builder(state_mod(vis = "pub"))]
43#[non_exhaustive]
44pub struct FetchCmd {
45    /// Epoch cancel token from the Peer. When set, the Downloader
46    /// combines it with the track-level cancel via [`CancelGroup`].
47    pub cancel: Option<CancellationToken>,
48    /// Additional HTTP headers for this request.
49    pub headers: Option<Headers>,
50    /// Streaming path completion handler. `None` for channel path (`execute`/`batch`).
51    pub on_complete: Option<OnCompleteFn>,
52    /// Streaming path response callback — fires once when the
53    /// response is ready, before the body streams. `None` for the
54    /// channel path (`execute`/`batch`).
55    pub on_response: Option<OnResponseFn>,
56    /// Optional byte range (HTTP Range request).
57    pub range: Option<RangeSpec>,
58    /// Optional per-request response validator.
59    /// Called with the response headers after a successful HTTP response.
60    /// Return `Err` to reject the response before the body is consumed.
61    pub validator: Option<ResponseValidator>,
62    /// Streaming path body writer. `None` for channel path (`execute`/`batch`).
63    pub writer: Option<WriterFn>,
64    /// HTTP method.
65    pub method: RequestMethod,
66    /// URL to fetch.
67    pub url: Url,
68}
69
70impl FetchCmd {
71    /// Builder for an HTTP GET command targeting the given URL.
72    pub fn get(
73        url: Url,
74    ) -> FetchCmdBuilder<fetch_cmd_builder::SetUrl<fetch_cmd_builder::SetMethod>> {
75        Self::builder().method(RequestMethod::Get).url(url)
76    }
77
78    /// Builder for an HTTP HEAD command targeting the given URL.
79    pub fn head(
80        url: Url,
81    ) -> FetchCmdBuilder<fetch_cmd_builder::SetUrl<fetch_cmd_builder::SetMethod>> {
82        Self::builder().method(RequestMethod::Head).url(url)
83    }
84}
85
86impl std::fmt::Debug for FetchCmd {
87    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88        f.debug_struct("FetchCmd")
89            .field("method", &self.method)
90            .field("url", &self.url)
91            .field("range", &self.range)
92            .finish_non_exhaustive()
93    }
94}
95
96/// Reject responses with `content-type: text/html`.
97///
98/// Protects against CDN soft-error pages that return `200 OK` with an HTML
99/// body. Pass as the validator argument to [`FetchCmd::validator`].
100///
101/// # Errors
102///
103/// Returns [`NetError::InvalidContentType`] when the response `content-type`
104/// header starts with `text/html`.
105pub fn reject_html_response(headers: &Headers) -> NetResult<()> {
106    if let Some(ct) = headers.get("content-type")
107        && ct.starts_with("text/html")
108    {
109        return Err(NetError::InvalidContentType(ct.to_string()));
110    }
111    Ok(())
112}