Skip to main content

soar_dl/
http.rs

1use tracing::{debug, trace};
2use ureq::{
3    http::{
4        header::{CONTENT_LENGTH, CONTENT_TYPE, LOCATION},
5        Response,
6    },
7    Body,
8};
9
10use crate::{error::DownloadError, http_client::SHARED_AGENT};
11
12pub struct Http;
13
14impl Http {
15    pub fn head(url: &str) -> Result<Response<Body>, DownloadError> {
16        trace!(url = url, "sending HEAD request");
17        let result = SHARED_AGENT.head(url).call().map_err(DownloadError::from);
18        if let Ok(ref resp) = result {
19            trace!(status = resp.status().as_u16(), "HEAD response received");
20            Self::log_response_headers(resp, "HEAD");
21        }
22        result
23    }
24
25    fn log_response_headers(resp: &Response<Body>, method: &str) {
26        let status = resp.status();
27        let headers = resp.headers();
28
29        debug!(
30            "{} {} {}",
31            method,
32            status.as_u16(),
33            status.canonical_reason().unwrap_or("")
34        );
35
36        if let Some(content_length) = headers.get(CONTENT_LENGTH) {
37            if let Ok(len) = content_length.to_str() {
38                trace!("  Content-Length: {}", len);
39            }
40        }
41        if let Some(content_type) = headers.get(CONTENT_TYPE) {
42            if let Ok(ct) = content_type.to_str() {
43                trace!("  Content-Type: {}", ct);
44            }
45        }
46        if let Some(location) = headers.get(LOCATION) {
47            if let Ok(loc) = location.to_str() {
48                debug!("  Location: {}", loc);
49            }
50        }
51    }
52
53    /// Fetches a GET response for the given URL, optionally requesting a byte range and using an ETag for conditional requests.
54    ///
55    /// If `resume_from` is `Some(pos)`, the request includes a `Range: bytes={pos}-` header. If `etag` is `Some(tag)` and a range is requested,
56    /// the request also includes an `If-Range: {tag}` header.
57    ///
58    /// # Returns
59    ///
60    /// `Ok(Response<Body>)` with the HTTP response on success, `Err(DownloadError)` on failure.
61    ///
62    /// # Examples
63    ///
64    /// ```no_run
65    /// # use soar_dl::http::Http;
66    ///
67    /// let resp = Http::fetch("https://example.com/resource", Some(1024), Some("\"etag-value\""), false);
68    /// match resp {
69    ///     Ok(r) => {
70    ///         assert!(r.status().as_u16() < 600); // got a response
71    ///     }
72    ///     Err(e) => panic!("request failed: {:?}", e),
73    /// }
74    /// ```
75    pub fn fetch(
76        url: &str,
77        resume_from: Option<u64>,
78        etag: Option<&str>,
79        ghcr_blob: bool,
80    ) -> Result<Response<Body>, DownloadError> {
81        debug!("GET {}", url);
82        trace!(resume_from = ?resume_from, ghcr_blob = ghcr_blob, "request details");
83        let mut req = SHARED_AGENT.get(url);
84
85        if ghcr_blob {
86            trace!("adding GHCR authorization header");
87            req = req.header("Authorization", "Bearer QQ==");
88        }
89
90        if let Some(pos) = resume_from {
91            debug!("  Range: bytes={}-", pos);
92            req = req.header("Range", &format!("bytes={}-", pos));
93            if let Some(tag) = etag {
94                trace!(etag = tag, "adding If-Range header");
95                req = req.header("If-Range", tag);
96            }
97        }
98
99        let result = req.call().map_err(DownloadError::from);
100        if let Ok(ref resp) = result {
101            Self::log_response_headers(resp, "GET");
102        }
103        result
104    }
105
106    /// Fetches JSON from the given URL and deserializes it into `T`.
107    ///
108    /// Performs an HTTP GET request to `url` and deserializes the response body into the requested type.
109    ///
110    /// # Returns
111    ///
112    /// `T` parsed from the response body on success, `DownloadError` on failure.
113    ///
114    /// # Examples
115    ///
116    /// ```no_run
117    /// use serde_json::Value;
118    /// use soar_dl::http::Http;
119    ///
120    /// let json: Value = Http::json("https://example.com/data.json").unwrap();
121    /// ```
122    pub fn json<T: serde::de::DeserializeOwned>(url: &str) -> Result<T, DownloadError> {
123        debug!(url = url, "fetching JSON");
124        let result = SHARED_AGENT
125            .get(url)
126            .call()?
127            .body_mut()
128            .read_json()
129            .map_err(|_| DownloadError::InvalidResponse);
130        if result.is_ok() {
131            trace!(url = url, "JSON parsed successfully");
132        }
133        result
134    }
135}