mirrors_arch/
lib.rs

1#![cfg_attr(docsrs, feature(doc_cfg))]
2#![warn(
3    missing_docs,
4    rustdoc::broken_intra_doc_links,
5    missing_debug_implementations
6)]
7
8//! # mirrors-arch
9use std::time::{Duration, Instant};
10
11use futures::{future::BoxFuture, FutureExt};
12use log::{info, trace};
13use reqwest::{header::LOCATION, ClientBuilder, Response, StatusCode};
14
15use crate::response::external::Root;
16
17#[cfg(test)]
18mod test;
19
20mod errors;
21pub use errors::Error;
22
23pub use reqwest::Client;
24
25mod response;
26#[cfg(feature = "time")]
27#[cfg_attr(docsrs, doc(cfg(feature = "time")))]
28#[doc(no_inline)]
29pub use chrono;
30
31pub use response::{external::Protocol, internal::*};
32
33type Result<T> = std::result::Result<T, Error>;
34
35pub(crate) const FILE_PATH: &str = "core/os/x86_64/core.db.tar.gz";
36
37/// Get ArchLinux mirrors from an `json` endpoint and return them in a [minified](ArchLinux) format
38///
39/// # Parameters
40///
41/// - `source` - The URL to query for a mirrorlist
42/// - `with_timeout` - Connection timeout (in seconds) to be used in network requests
43///
44/// # Example
45///
46/// ```rust
47/// # use mirrors_arch::get_mirrors;
48/// # async fn foo()->Result<(), Box<dyn std::error::Error>>{
49/// let arch_mirrors = get_mirrors("https://archlinux.org/mirrors/status/json/", None).await?;
50/// println!("{arch_mirrors:?}");
51/// #    Ok(())
52/// # }
53/// ```
54pub async fn get_mirrors(source: &str, with_timeout: Option<u64>) -> Result<ArchLinux> {
55    let response = get_response(source, with_timeout).await?;
56
57    let root: Root = response.json().await?;
58
59    let body = ArchLinux::from(root);
60    let count = body.countries.len();
61    info!("located mirrors from {count} countries");
62    Ok(body)
63}
64
65async fn get_response(source: &str, with_timeout: Option<u64>) -> Result<Response> {
66    trace!("creating http client");
67    let client = get_client(with_timeout)?;
68
69    trace!("sending request");
70    let response = client.get(source).send().await?;
71
72    Ok(response)
73}
74
75/// The same as [get_mirrors] but returns a tuple including the json as a
76/// `String`
77///
78/// # Example
79///
80/// ```rust
81/// # use mirrors_arch::get_mirrors_with_raw;
82/// # async fn foo()->Result<(), Box<dyn std::error::Error>>{
83/// let timeout = Some(10);
84/// let arch_mirrors = get_mirrors_with_raw("https://my-url.com/json/", timeout).await?;
85/// println!("{arch_mirrors:?}");
86/// #    Ok(())
87/// # }
88/// ```
89pub async fn get_mirrors_with_raw(
90    source: &str,
91    with_timeout: Option<u64>,
92) -> Result<(ArchLinux, String)> {
93    let response = get_response(source, with_timeout).await?;
94    deserialise_mirrors(response).await
95}
96
97async fn deserialise_mirrors(response: Response) -> Result<(ArchLinux, String)> {
98    let root: Root = response.json().await?;
99
100    let value = serde_json::to_string(&root)?;
101    Ok((ArchLinux::from(root), value))
102}
103
104/// The same as [get_mirrors_with_raw] but uses a specified
105/// [Client] for requests
106pub async fn get_mirrors_with_client(source: &str, client: Client) -> Result<(ArchLinux, String)> {
107    let response = client.get(source).send().await?;
108    deserialise_mirrors(response).await
109}
110
111/// Parses a `string slice` to the [ArchLinux] type
112///
113/// # Parameters
114/// - `contents` - A `json` string slice to be parsed and returned as a [mirrorlist](ArchLinux)
115///
116/// # Example
117///
118/// ```rust
119/// # use mirrors_arch::parse_local;
120/// # async fn foo()->Result<(), Box<dyn std::error::Error>>{
121/// let json = std::fs::read_to_string("archmirrors.json")?;
122/// let arch_mirrors = parse_local(&json)?;
123/// println!("{arch_mirrors:?}");
124/// #  Ok(())
125/// # }
126/// ```
127pub fn parse_local(contents: &str) -> Result<ArchLinux> {
128    let vals = ArchLinux::from(serde_json::from_str::<Root>(contents)?);
129    Ok(vals)
130}
131
132/// Gets a client that can be used to rate mirrors
133///
134/// # Parameters
135/// - `with_timeout` - an optional connection timeout to be used when rating the mirrors
136///
137/// # Example
138///
139/// ```rust
140/// # use mirrors_arch::get_client;
141/// # async fn foo()->Result<(), Box<dyn std::error::Error>>{
142/// let timeout = Some(5);
143/// let client = get_client(timeout);
144/// #  Ok(())
145/// # }
146/// ```
147pub fn get_client(with_timeout: Option<u64>) -> Result<Client> {
148    let timeout = with_timeout.map(Duration::from_secs);
149
150    let mut client_builder = ClientBuilder::new();
151    if let Some(timeout) = timeout {
152        client_builder = client_builder.timeout(timeout).connect_timeout(timeout);
153    }
154
155    Ok(client_builder.build()?)
156}
157
158/// Queries a mirrorlist and calculates how long it took to get a response
159///
160/// # Parameters
161/// - `url` - The mirrorlist
162/// - `client` - The client returned from [get_client]
163///
164/// # Example
165///
166/// ```rust
167/// # use mirrors_arch::{get_client, rate_mirror};
168/// # async fn foo()->Result<(), Box<dyn std::error::Error>>{
169/// # let url = String::default();
170/// # let client = get_client(Some(5))?;
171/// let (duration, url) = rate_mirror(url, client).await?;
172/// #  Ok(())
173/// # }
174/// ```
175pub fn rate_mirror(url: String, client: Client) -> BoxFuture<'static, Result<(Duration, String)>> {
176    async move {
177        let uri = format!("{url}{FILE_PATH}");
178
179        let now = Instant::now();
180
181        let response = client.get(&uri).send().await?;
182
183        if response.status() == StatusCode::OK {
184            Ok((now.elapsed(), url))
185        } else if response.status() == StatusCode::MOVED_PERMANENTLY {
186            if let Some(new_uri) = response.headers().get(LOCATION) {
187                let new_url = String::from_utf8_lossy(new_uri.as_bytes()).replace(FILE_PATH, "");
188                rate_mirror(new_url.to_string(), client.clone()).await
189            } else {
190                Err(Error::Rate {
191                    qualified_url: uri,
192                    url,
193                    status_code: response.status(),
194                })
195            }
196        } else {
197            Err(Error::Rate {
198                qualified_url: uri,
199                url,
200                status_code: response.status(),
201            })
202        }
203    }
204    .boxed()
205}
206
207/// Gets a mirror's last sync time
208/// # Parameters
209/// - `mirror` - The mirror to get the last sync time for
210/// - `client` - A [reqwest::Client]
211///
212/// # Example
213///
214/// ```rust
215/// # use mirrors_arch::{get_client, get_last_sync};
216/// # async fn foo()->Result<(), Box<dyn std::error::Error>>{
217/// # let mirror = String::default();
218/// # let client = get_client(Some(5))?;
219/// let (date_time, mirror) = get_last_sync(mirror, client).await?;
220/// #  Ok(())
221/// # }
222/// ```
223
224#[cfg(feature = "time")]
225#[cfg_attr(docsrs, doc(cfg(feature = "time")))]
226pub async fn get_last_sync(
227    mirror: impl Into<String>,
228    client: Client,
229) -> Result<(chrono::DateTime<chrono::Utc>, String)> {
230    let mirror = mirror.into();
231    let lastsync_url = format!("{mirror}lastsync");
232
233    let timestamp = client
234        .get(&lastsync_url)
235        .send()
236        .await
237        .map_err(|e| Error::Request(e.to_string()))?
238        .text()
239        .await?;
240
241    let result = chrono::NaiveDateTime::parse_from_str(&timestamp, "%s")
242        .map(|res| chrono::DateTime::<chrono::Utc>::from_naive_utc_and_offset(res, chrono::Utc))
243        .map_err(Error::TimeError)?;
244
245    Ok((result, mirror))
246}