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}