Skip to main content

flodl_cli/util/
http.rs

1//! HTTP file downloads via curl or wget.
2//!
3//! Shells out to curl/wget because TLS in pure std is impractical without
4//! crates, and these tools are ubiquitous (curl ships with Windows 10+).
5
6use std::path::Path;
7use std::process::{Command, Stdio};
8
9use super::system::has_command;
10
11/// Detected download tool.
12enum Downloader {
13    Curl,
14    Wget,
15}
16
17fn detect_downloader() -> Result<Downloader, String> {
18    if has_command("curl") {
19        Ok(Downloader::Curl)
20    } else if has_command("wget") {
21        Ok(Downloader::Wget)
22    } else {
23        Err("Neither curl nor wget is installed.\n\
24             Install one of them:\n\
25             \n\
26             \x20 Ubuntu/Debian:  sudo apt install curl\n\
27             \x20 Fedora/RHEL:    sudo dnf install curl\n\
28             \x20 macOS:          available by default\n\
29             \x20 Windows 10+:    curl.exe is built-in"
30            .into())
31    }
32}
33
34/// Download a file from `url` to `dest`, showing progress on the terminal.
35///
36/// Overwrites `dest` if it exists. Creates parent directories.
37pub fn download_file(url: &str, dest: &Path) -> Result<(), String> {
38    let dl = detect_downloader()?;
39
40    if let Some(parent) = dest.parent() {
41        std::fs::create_dir_all(parent)
42            .map_err(|e| format!("cannot create directory {}: {}", parent.display(), e))?;
43    }
44
45    let dest_str = dest
46        .to_str()
47        .ok_or_else(|| "destination path is not valid UTF-8".to_string())?;
48
49    let status = match dl {
50        Downloader::Curl => Command::new("curl")
51            .args(["-L", "--progress-bar", "-o", dest_str, url])
52            .stdout(Stdio::inherit())
53            .stderr(Stdio::inherit())
54            .status(),
55        Downloader::Wget => Command::new("wget")
56            .args(["-q", "--show-progress", "-O", dest_str, url])
57            .stdout(Stdio::inherit())
58            .stderr(Stdio::inherit())
59            .status(),
60    };
61
62    match status {
63        Ok(s) if s.success() => Ok(()),
64        Ok(s) => Err(format!(
65            "download failed (exit code {})\n  URL: {}",
66            s.code().unwrap_or(-1),
67            url
68        )),
69        Err(e) => Err(format!("failed to run download command: {}", e)),
70    }
71}