stackify_common/
download.rs

1use std::{
2    fmt::Debug,
3    fs::{self, File, Permissions},
4    io::{copy, BufReader, Write},
5    os::unix::fs::PermissionsExt,
6    path::{Path, PathBuf},
7    time::Duration,
8};
9
10use color_eyre::eyre::{eyre, Result};
11use flate2::bufread::GzDecoder;
12use reqwest::{header, Client, Url};
13use tar::Archive;
14
15pub async fn download_file<P: AsRef<Path> + Debug>(
16    url_str: &str,
17    tmp_dir: &P,
18    dl_start: impl FnOnce(u64),
19    mut dl_chunk: impl FnMut(u64, u64),
20) -> Result<PathBuf> {
21    let url = Url::parse(&url_str)?;
22    let filename = url
23        .path_segments()
24        .unwrap()
25        .last()
26        .expect("Could not determine filename.");
27
28    let tmp_file_path = tmp_dir.as_ref().join(filename);
29
30    let download_size = get_download_size(url).await?;
31    dl_start(download_size);
32
33    let mut tmp_file = File::create(&tmp_file_path)?;
34
35    let client = Client::builder()
36        .timeout(Duration::from_secs(500))
37        .build()?;
38
39    let request = client.get(url_str);
40    let mut download = request.send().await?;
41    while let Some(chunk) = download.chunk().await? {
42        dl_chunk(chunk.len() as u64, download_size);
43        tmp_file.write(&chunk)?;
44    }
45
46    tmp_file.flush()?;
47
48    Ok(tmp_file_path)
49}
50
51pub async fn download_bitcoin_core_binaries<P: AsRef<Path> + Debug>(
52    version: &str,
53    tmp_dir: &P,
54    dest_dir: &P,
55    dl_start: impl FnOnce(u64),
56    mut dl_chunk: impl FnMut(u64, u64),
57    dl_finished: impl FnOnce(),
58) -> Result<()> {
59    let filename = format!("bitcoin-{version}-x86_64-linux-gnu.tar.gz");
60    let url_str = format!("https://bitcoincore.org/bin/bitcoin-core-{version}/{filename}");
61
62    let url = Url::parse(&url_str)?;
63
64    let tmp_file_path = tmp_dir.as_ref().join(filename);
65
66    let download_size = get_download_size(url).await?;
67    dl_start(download_size);
68
69    let mut tmp_file = File::create(&tmp_file_path)?;
70
71    let client = Client::builder()
72        .timeout(Duration::from_secs(500))
73        .build()?;
74
75    let request = client.get(&url_str);
76    let mut download = request.send().await?;
77    while let Some(chunk) = download.chunk().await? {
78        dl_chunk(chunk.len() as u64, download_size);
79        tmp_file.write(&chunk)?;
80    }
81
82    tmp_file.flush()?;
83    dl_finished();
84
85    let tmp_file = File::open(&tmp_file_path)?;
86    let gz = GzDecoder::new(BufReader::new(tmp_file));
87
88    //let tmp_dir = tempfile::tempdir()?;
89    Archive::new(gz).unpack(&tmp_dir)?;
90
91    let bin_dir = tmp_dir
92        .as_ref()
93        .join(format!("bitcoin-{version}"))
94        .join("bin");
95
96    let bitcoin_cli_src = bin_dir.join("bitcoin-cli");
97    let bitcoin_cli_dest = dest_dir.as_ref().join("bitcoin-cli");
98    inner_copy(&bitcoin_cli_src, &bitcoin_cli_dest)?;
99    let bitcoind_src = bin_dir.join("bitcoind");
100    let bitcoind_dest = dest_dir.as_ref().join("bitcoind");
101    inner_copy(&bitcoind_src, &bitcoind_dest)?;
102
103    fs::remove_dir_all(&tmp_dir)?;
104
105    set_executable(&bitcoin_cli_dest)?;
106    set_executable(&bitcoind_dest)?;
107
108    Ok(())
109}
110
111pub fn download_dasel_binary<P: AsRef<Path>>(version: &str, dest_dir: P) -> Result<()> {
112    let url = format!(
113        "https://github.com/TomWright/dasel/releases/download/v{version}/dasel_linux_amd64"
114    );
115    let dest = dest_dir.as_ref().join("dasel");
116    let mut dest_file = std::fs::File::create(&dest)?;
117    let response = reqwest::blocking::get(&url)?;
118    copy(&mut response.text()?.as_bytes(), &mut dest_file)?;
119
120    set_executable(&dest)?;
121
122    Ok(())
123}
124
125pub fn inner_copy<P: AsRef<Path>>(src: &P, dest: &P) -> Result<()> {
126    let mut src_file = std::fs::File::open(src)?;
127    let mut dest_file = std::fs::File::create(dest)?;
128
129    copy(&mut src_file, &mut dest_file)?;
130
131    Ok(())
132}
133
134pub fn set_executable<P: AsRef<Path>>(path: &P) -> Result<()> {
135    let perm = Permissions::from_mode(0o744);
136    let file = File::open(path)?;
137    file.set_permissions(perm)?;
138    Ok(())
139}
140
141// Help from https://github.com/benkay86/async-applied/blob/master/indicatif-reqwest-tokio/src/bin/indicatif-reqwest-tokio-single.rs
142async fn get_download_size(url: Url) -> Result<u64> {
143    let client = reqwest::Client::new();
144    // We need to determine the file size before we download so we can create a ProgressBar
145    // A Header request for the CONTENT_LENGTH header gets us the file size
146    let download_size = {
147        let resp = client.head(url.as_str()).send().await?;
148        if resp.status().is_success() {
149            resp.headers() // Gives is the HeaderMap
150                .get(header::CONTENT_LENGTH) // Gives us an Option containing the HeaderValue
151                .and_then(|ct_len| ct_len.to_str().ok()) // Unwraps the Option as &str
152                .and_then(|ct_len| ct_len.parse().ok()) // Parses the Option as u64
153                .unwrap_or(0) // Fallback to 0
154        } else {
155            // We return an Error if something goes wrong here
156            return Err(
157                eyre!("Couldn't download URL: {}. Error: {:?}", url, resp.status(),).into(),
158            );
159        }
160    };
161
162    Ok(download_size as u64)
163}