wash_lib/start/github/
mod.rs

1//! Reusable code for downloading tarballs from GitHub releases
2
3use anyhow::{anyhow, bail, Result};
4use async_compression::tokio::bufread::GzipDecoder;
5#[cfg(target_family = "unix")]
6use std::os::unix::prelude::PermissionsExt;
7use std::path::{Path, PathBuf};
8use std::{ffi::OsStr, io::Cursor};
9use tokio::fs::{create_dir_all, metadata, File};
10use tokio_stream::StreamExt;
11use tokio_tar::Archive;
12use wasmcloud_core::tls::NativeRootsExt;
13
14pub const DOWNLOAD_CLIENT_USER_AGENT: &str =
15    concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));
16
17pub const GITHUB_WASMCLOUD_ORG: &str = "wasmCloud";
18pub const GITHUB_WASMCLOUD_WASMCLOUD_REPO: &str = "wasmCloud";
19pub const GITHUB_WASMCLOUD_WADM_REPO: &str = "wadm";
20
21mod api;
22pub use api::*;
23
24/// Reusable function to download a release tarball from GitHub and extract an embedded binary to a specified directory
25///
26/// # Arguments
27///
28/// * `url` - URL of the GitHub release artifact tarball (Usually in the form of https://github.com/<owner>/<repo>/releases/download/<tag>/<artifact>.tar.gz)
29/// * `dir` - Directory on disk to install the binary into. This will be created if it doesn't exist
30/// * `bin_name` - Name of the binary inside of the tarball, e.g. `nats-server` or `wadm`
31/// # Examples
32///
33/// ```rust,ignore
34/// # #[tokio::main]
35/// # async fn main() {
36/// let url = "https://github.com/wasmCloud/wadm/releases/download/v0.4.0-alpha.1/wadm-v0.4.0-alpha.1-linux-amd64.tar.gz";
37/// let res = download_binary_from_github(url, "/tmp/", "wadm").await;
38/// assert!(res.is_ok());
39/// assert!(res.unwrap().to_string_lossy() == "/tmp/wadm");
40/// # }
41/// ```
42pub async fn download_binary_from_github<P>(url: &str, dir: P, bin_name: &str) -> Result<PathBuf>
43where
44    P: AsRef<Path>,
45{
46    let bin_path = dir.as_ref().join(bin_name);
47    // Download release tarball
48    let body = match get_download_client()?.get(url).send().await {
49        Ok(resp) => resp.bytes().await?,
50        Err(e) => bail!("Failed to request release tarball: {:?}", e),
51    };
52    let cursor = Cursor::new(body);
53    let mut bin_tarball = Archive::new(Box::new(GzipDecoder::new(cursor)));
54
55    // Look for binary within tarball and only extract that
56    let mut entries = bin_tarball.entries()?;
57    while let Some(res) = entries.next().await {
58        let mut entry = res.map_err(|e| {
59            anyhow!(
60                "Failed to retrieve file from archive, ensure {bin_name} exists. Original error: {e}",
61            )
62        })?;
63        if let Ok(tar_path) = entry.path() {
64            match tar_path.file_name() {
65                Some(name) if name == OsStr::new(bin_name) => {
66                    // Ensure target directory exists
67                    create_dir_all(&dir).await?;
68                    let mut bin_file = File::create(&bin_path).await?;
69                    // Make binary executable
70                    #[cfg(target_family = "unix")]
71                    {
72                        let mut permissions = bin_file.metadata().await?.permissions();
73                        // Read/write/execute for owner and read/execute for others. This is what `cargo install` does
74                        permissions.set_mode(0o755);
75                        bin_file.set_permissions(permissions).await?;
76                    }
77
78                    tokio::io::copy(&mut entry, &mut bin_file).await?;
79                    return Ok(bin_path);
80                }
81                // Ignore all other files in the tarball
82                _ => (),
83            }
84        }
85    }
86
87    bail!("{bin_name} binary could not be installed, please see logs")
88}
89
90/// Helper function to determine if the provided binary is present in a directory
91#[allow(unused)]
92pub(crate) async fn is_bin_installed<P>(dir: P, bin_name: &str) -> bool
93where
94    P: AsRef<Path>,
95{
96    metadata(dir.as_ref().join(bin_name))
97        .await
98        .is_ok_and(|m| m.is_file())
99}
100
101/// Helper function to set up a reqwest client for performing the download
102pub fn get_download_client() -> Result<reqwest::Client> {
103    get_download_client_with_user_agent(DOWNLOAD_CLIENT_USER_AGENT)
104}
105
106/// Helper function to set up a reqwest client for performing the download with a user agent
107pub(crate) fn get_download_client_with_user_agent(user_agent: &str) -> Result<reqwest::Client> {
108    let proxy_username = std::env::var("WASH_PROXY_USERNAME").unwrap_or_default();
109    let proxy_password = std::env::var("WASH_PROXY_PASSWORD").unwrap_or_default();
110
111    let mut builder = reqwest::ClientBuilder::default()
112        .user_agent(user_agent)
113        .with_native_certificates();
114
115    if let Ok(http_proxy) = std::env::var("HTTP_PROXY").or_else(|_| std::env::var("http_proxy")) {
116        let mut proxy = reqwest::Proxy::http(http_proxy)?.no_proxy(reqwest::NoProxy::from_env());
117        if !proxy_username.is_empty() && !proxy_password.is_empty() {
118            proxy = proxy.basic_auth(&proxy_username, &proxy_password);
119        }
120        builder = builder.proxy(proxy);
121    }
122
123    if let Ok(https_proxy) = std::env::var("HTTPS_PROXY").or_else(|_| std::env::var("https_proxy"))
124    {
125        let mut proxy = reqwest::Proxy::https(https_proxy)?.no_proxy(reqwest::NoProxy::from_env());
126        if !proxy_username.is_empty() && !proxy_password.is_empty() {
127            proxy = proxy.basic_auth(&proxy_username, &proxy_password);
128        }
129        builder = builder.proxy(proxy);
130    }
131
132    Ok(builder.build()?)
133}