idt/downloaders/
curl.rs

1use std::fs::File;
2use std::io::Write;
3use std::path::Path;
4use std::path::PathBuf;
5use std::process::Command;
6use std::process::Stdio;
7
8use color_eyre::eyre::eyre;
9
10pub enum CurlDownloaderOption<'a> {
11    PipeIntoZcat {
12        dest_path: &'a Path,
13    },
14    PipeIntoTar {
15        dest_dir: &'a Path,
16        // Option because not all the downloaded archives has a:
17        // - stable name (i.e. `shellcheck`)
18        // - a usable binary outside the archive (i.e. `elixir_ls` or `lua_ls`)
19        // In these cases `dest_name` is set to None
20        dest_name: Option<&'a str>,
21    },
22    WriteTo {
23        dest_path: &'a Path,
24    },
25}
26
27/// Downloads a file from the given URL using curl with the specified [`CurlDownloaderOption`].
28///
29/// # Errors
30/// - Executing the `curl` command fails or returns a non-zero exit status.
31/// - Executing a decompression command (`zcat`, `tar`) fails or returns a non-zero exit status.
32/// - The spawned `curl` process does not expose a stdout pipe (missing piped handle).
33/// - A filesystem operation (create/read/write/remove) fails.
34pub fn run(url: &str, opt: &CurlDownloaderOption) -> color_eyre::Result<PathBuf> {
35    let mut curl_cmd = ytil_cmd::silent_cmd("curl");
36    let silent_flag = cfg!(debug_assertions).then(|| "S").unwrap_or("");
37    curl_cmd.args([&format!("-L{silent_flag}"), url]);
38
39    let target = match opt {
40        CurlDownloaderOption::PipeIntoZcat { dest_path } => {
41            let curl_stdout = get_cmd_stdout(&mut curl_cmd)?;
42
43            let output = Command::new("zcat").stdin(curl_stdout).output()?;
44            output.status.exit_ok()?;
45
46            let mut file = File::create(dest_path)?;
47            file.write_all(&output.stdout)?;
48
49            dest_path.into()
50        }
51        CurlDownloaderOption::PipeIntoTar { dest_dir, dest_name } => {
52            let curl_stdout = get_cmd_stdout(&mut curl_cmd)?;
53
54            let mut tar_cmd = Command::new("tar");
55            tar_cmd.args(["-xz", "-C", &dest_dir.to_string_lossy()]);
56            if let Some(dest_name) = dest_name {
57                tar_cmd.arg(dest_name);
58            }
59            tar_cmd.stdin(curl_stdout).status()?.exit_ok()?;
60
61            dest_name.map_or_else(|| dest_dir.into(), |dn| dest_dir.join(dn))
62        }
63        CurlDownloaderOption::WriteTo { dest_path } => {
64            curl_cmd.arg("--output");
65            curl_cmd.arg(dest_path);
66            curl_cmd.status()?.exit_ok()?;
67
68            dest_path.into()
69        }
70    };
71
72    Ok(target)
73}
74
75/// Get cmd standard output.
76fn get_cmd_stdout(cmd: &mut Command) -> color_eyre::Result<Stdio> {
77    let stdout = cmd
78        .stdout(Stdio::piped())
79        .spawn()?
80        .stdout
81        .ok_or_else(|| eyre!("missing child stdout | cmd={cmd:#?}"))?;
82
83    Ok(Stdio::from(stdout))
84}