1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
use std::path::Path;

use anyhow::Context;
use cfg_match::cfg_match;
use reqwest::{Client, IntoUrl, Url};
use serde::de::DeserializeOwned;

cfg_match! {
	target_os = "windows" => {
		/// Sensible open file descriptor limit for asynchronous transfers
		pub const FD_SENSIBLE_LIMIT: usize = 64;
	}
	_ => {
		/// Sensible open file descriptor limit for asynchronous transfers
		pub const FD_SENSIBLE_LIMIT: usize = 16;
	}
}

/// Downloads data from a remote location
pub async fn download(url: impl IntoUrl, client: &Client) -> anyhow::Result<reqwest::Response> {
	let resp = client
		.get(url)
		.send()
		.await
		.context("Failed to send request")?
		.error_for_status()
		.context("Server reported an error")?;

	Ok(resp)
}

/// Downloads and returns text
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)
}

/// Downloads and returns bytes
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)
}

/// Downloads and puts the contents in a file
pub async fn file(url: impl IntoUrl, path: &Path, client: &Client) -> anyhow::Result<()> {
	let bytes = bytes(url, client)
		.await
		.context("Failed to download data")?;
	tokio::fs::write(path, bytes).await.with_context(|| {
		format!(
			"Failed to write downloaded contents to path {}",
			path.display()
		)
	})?;

	Ok(())
}

/// Downloads and deserializes the contents into JSON
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")
}

/// Validates a URL with a helpful error message
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(())
}