use std::fs::File;
use std::io::{BufWriter, Cursor, Write};
use std::path::Path;
use anyhow::{Context, ensure};
use nitro_shared::output::MessageContents;
use reqwest::{IntoUrl, Url};
use serde::de::DeserializeOwned;
pub use reqwest::Client;
pub fn user_agent() -> String {
let version = env!("CARGO_PKG_VERSION");
format!("nitro_core_{version}")
}
pub async fn download(url: impl IntoUrl, client: &Client) -> anyhow::Result<reqwest::Response> {
let resp = client
.get(url)
.header("User-Agent", user_agent())
.send()
.await
.context("Failed to send request")?
.error_for_status()
.context("Server reported an error")?;
Ok(resp)
}
pub async fn text(url: impl IntoUrl, client: &Client) -> anyhow::Result<String> {
let text = download(url, client)
.await
.context("Failed to download")?
.text()
.await
.context("Failed to convert download to text")?;
Ok(text)
}
pub async fn bytes(url: impl IntoUrl, client: &Client) -> anyhow::Result<bytes::Bytes> {
let bytes = download(url, client)
.await
.context("Failed to download")?
.bytes()
.await
.context("Failed to convert download to raw bytes")?;
Ok(bytes)
}
pub async fn file(
url: impl IntoUrl,
path: impl AsRef<Path>,
client: &Client,
) -> anyhow::Result<()> {
let bytes = bytes(url, client)
.await
.context("Failed to download data")?;
std::fs::write(path.as_ref(), bytes).with_context(|| {
format!(
"Failed to write downloaded contents to path {}",
path.as_ref().display()
)
})?;
Ok(())
}
pub async fn json<T: DeserializeOwned>(url: impl IntoUrl, client: &Client) -> anyhow::Result<T> {
download(url, client)
.await
.context("Failed to download JSON data")?
.json()
.await
.context("Failed to parse JSON")
}
pub struct ProgressiveDownload<W: Write> {
response: reqwest::Response,
writer: W,
content_length: u64,
bytes_downloaded: usize,
finished: bool,
}
impl<W: Write> ProgressiveDownload<W> {
pub fn from_response(response: reqwest::Response, writer: W) -> Self {
Self {
content_length: response.content_length().unwrap_or_default(),
response,
writer,
bytes_downloaded: 0,
finished: false,
}
}
pub fn get_downloaded(&self) -> usize {
self.bytes_downloaded
}
pub fn get_total_length(&self) -> usize {
self.content_length as usize
}
pub fn get_progress(&self) -> MessageContents {
let current = (self.get_downloaded() / 2) as u32;
let total = (self.get_total_length() / 2) as u32;
MessageContents::Progress { current, total }
}
pub async fn poll_download(&mut self) -> anyhow::Result<()> {
let chunk = self
.response
.chunk()
.await
.context("Failed to download chunk")?;
if let Some(bytes) = chunk {
self.writer
.write_all(&bytes)
.context("Failed to write downloaded bytes")?;
self.bytes_downloaded += bytes.len();
} else {
self.finished = true;
ensure!(
self.get_downloaded() == self.get_total_length(),
"Bytes downloaded did not equal the amount expected"
);
}
Ok(())
}
pub fn is_finished(&self) -> bool {
self.finished
}
}
impl ProgressiveDownload<BufWriter<File>> {
pub async fn file(
url: impl IntoUrl,
path: impl AsRef<Path>,
client: &Client,
) -> anyhow::Result<Self> {
let file = BufWriter::new(File::create(path).context("Failed to open file")?);
let response = download(url, client)
.await
.context("Failed to get response")?;
Ok(Self::from_response(response, file))
}
}
impl ProgressiveDownload<Cursor<Vec<u8>>> {
pub async fn bytes(url: impl IntoUrl, client: &Client) -> anyhow::Result<Self> {
let response = download(url, client)
.await
.context("Failed to get response")?;
let cursor = Cursor::new(Vec::with_capacity(
response.content_length().unwrap_or_default() as usize,
));
Ok(Self::from_response(response, cursor))
}
pub fn finish(self) -> Vec<u8> {
self.writer.into_inner()
}
pub fn finish_json<D: DeserializeOwned>(self) -> anyhow::Result<D> {
simd_json::from_slice(&mut self.finish()).context("Failed to deserialize downloaded output")
}
}
pub fn validate_url(url: &str) -> anyhow::Result<()> {
Url::parse(url).context(
"It may help to make sure that either http:// or https:// is before the domain name",
)?;
Ok(())
}