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
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
use crate::describer::Describable;
use crate::errors::ProtoError;
use crate::helpers::is_offline;
use crate::resolver::Resolvable;
use starbase_utils::fs::{self, FsError};
use std::io;
use std::path::{Path, PathBuf};
use tracing::{debug, trace};

#[async_trait::async_trait]
pub trait Downloadable<'tool>: Send + Sync + Describable<'tool> + Resolvable<'tool> {
    /// Return an absolute file path to the downloaded file.
    /// This may not exist, as the path is composed ahead of time.
    /// This is typically `~/.proto/temp/<file>`.
    fn get_download_path(&self) -> Result<PathBuf, ProtoError>;

    /// Return a URL to download the tool's archive from a registry.
    fn get_download_url(&self) -> Result<String, ProtoError>;

    /// Download the tool (as an archive) from its distribution registry
    /// into the `~/.proto/temp` folder and return an absolute file path.
    /// A custom URL that points to the downloadable archive can be
    /// provided as the 2nd argument.
    async fn download(&self, to_file: &Path, from_url: Option<&str>) -> Result<bool, ProtoError> {
        if to_file.exists() {
            debug!(tool = self.get_id(), "Tool already downloaded, continuing");

            return Ok(false);
        }

        let from_url = match from_url {
            Some(url) => url.to_owned(),
            None => self.get_download_url()?,
        };

        debug!(
            tool = self.get_id(),
            url = from_url,
            "Attempting to download tool from URL"
        );

        download_from_url(&from_url, &to_file).await?;

        debug!(tool = self.get_id(), "Successfully downloaded tool");

        Ok(true)
    }
}

#[tracing::instrument(skip_all)]
pub async fn download_from_url<U, F>(url: U, dest_file: F) -> Result<(), ProtoError>
where
    U: AsRef<str>,
    F: AsRef<Path>,
{
    if is_offline() {
        return Err(ProtoError::InternetConnectionRequired);
    }

    let url = url.as_ref();
    let dest_file = dest_file.as_ref();
    let handle_io_error = |error: io::Error| FsError::Create {
        path: dest_file.to_path_buf(),
        error,
    };
    let handle_http_error = |error: reqwest::Error| ProtoError::Http {
        url: url.to_owned(),
        error,
    };

    trace!(
        dest_file = ?dest_file,
        url,
        "Downloading file from URL",
    );

    // Ensure parent directories exist
    if let Some(parent) = dest_file.parent() {
        fs::create_dir_all(parent)?;
    }

    // Fetch the file from the HTTP source
    let response = reqwest::get(url).await.map_err(handle_http_error)?;
    let status = response.status();

    if status.as_u16() == 404 {
        return Err(ProtoError::DownloadNotFound(url.to_owned()));
    }

    if !status.is_success() {
        return Err(ProtoError::DownloadFailed(
            url.to_owned(),
            status.to_string(),
        ));
    }

    // Write the bytes to our local file
    let mut contents = io::Cursor::new(response.bytes().await.map_err(handle_http_error)?);
    let mut file = fs::create_file(dest_file)?;

    io::copy(&mut contents, &mut file).map_err(handle_io_error)?;

    Ok(())
}