dagger_core/
downloader.rs

1use std::{
2    fs::File,
3    io::{copy, Write},
4    os::unix::prelude::PermissionsExt,
5    path::PathBuf,
6};
7
8use eyre::Context;
9use flate2::read::GzDecoder;
10use platform_info::Uname;
11use sha2::Digest;
12use tar::Archive;
13use tempfile::tempfile;
14
15#[allow(dead_code)]
16#[derive(Clone)]
17pub struct Platform {
18    pub os: String,
19    pub arch: String,
20}
21
22impl Platform {
23    pub fn from_system() -> eyre::Result<Self> {
24        let platform = platform_info::PlatformInfo::new()?;
25        let os_name = platform.sysname();
26        let arch = platform.machine().to_lowercase();
27        let normalize_arch = match arch.as_str() {
28            "x86_64" => "amd64",
29            "aarch" => "arm64",
30            "aarch64" => "arm64",
31            arch => arch,
32        };
33
34        Ok(Self {
35            os: os_name.to_lowercase(),
36            arch: normalize_arch.into(),
37        })
38    }
39}
40
41#[allow(dead_code)]
42pub struct TempFile {
43    prefix: String,
44    directory: PathBuf,
45    file: File,
46}
47
48#[allow(dead_code)]
49impl TempFile {
50    pub fn new(prefix: &str, directory: &PathBuf) -> eyre::Result<Self> {
51        let prefix = prefix.to_string();
52        let directory = directory.clone();
53
54        let file = tempfile()?;
55
56        Ok(Self {
57            prefix,
58            directory,
59            file,
60        })
61    }
62}
63
64#[allow(dead_code)]
65pub type CliVersion = String;
66
67#[allow(dead_code)]
68pub struct Downloader {
69    version: CliVersion,
70    platform: Platform,
71}
72#[allow(dead_code)]
73const DEFAULT_CLI_HOST: &str = "dl.dagger.io";
74#[allow(dead_code)]
75const CLI_BIN_PREFIX: &str = "dagger-";
76#[allow(dead_code)]
77const CLI_BASE_URL: &str = "https://dl.dagger.io/dagger/releases";
78
79#[allow(dead_code)]
80impl Downloader {
81    pub fn new(version: CliVersion) -> eyre::Result<Self> {
82        Ok(Self {
83            version,
84            platform: Platform::from_system()?,
85        })
86    }
87
88    pub fn archive_url(&self) -> String {
89        let ext = match self.platform.os.as_str() {
90            "windows" => "zip",
91            _ => "tar.gz",
92        };
93        let version = &self.version;
94        let os = &self.platform.os;
95        let arch = &self.platform.arch;
96
97        format!("{CLI_BASE_URL}/{version}/dagger_v{version}_{os}_{arch}.{ext}")
98    }
99
100    pub fn checksum_url(&self) -> String {
101        let version = &self.version;
102
103        format!("{CLI_BASE_URL}/{version}/checksums.txt")
104    }
105
106    pub fn cache_dir(&self) -> eyre::Result<PathBuf> {
107        let env = std::env::var("XDG_CACHE_HOME").unwrap_or("".into());
108        let env = env.trim();
109        let mut path = match env {
110            "" => dirs::cache_dir().ok_or(eyre::anyhow!(
111                "could not find cache_dir, either in env or XDG_CACHE_HOME"
112            ))?,
113            path => PathBuf::from(path),
114        };
115
116        path.push("dagger");
117
118        std::fs::create_dir_all(&path)?;
119
120        Ok(path)
121    }
122
123    pub async fn get_cli(&self) -> eyre::Result<PathBuf> {
124        let version = &self.version;
125        let mut cli_bin_path = self.cache_dir()?;
126        cli_bin_path.push(format!("{CLI_BIN_PREFIX}{version}"));
127        if self.platform.os == "windows" {
128            cli_bin_path = cli_bin_path.with_extension("exe")
129        }
130
131        if !cli_bin_path.exists() {
132            cli_bin_path = self
133                .download(cli_bin_path)
134                .await
135                .context("failed to download CLI from archive")?;
136        }
137
138        Ok(cli_bin_path)
139    }
140
141    async fn download(&self, path: PathBuf) -> eyre::Result<PathBuf> {
142        let expected_checksum = self.expected_checksum().await?;
143
144        let mut bytes = vec![];
145        let actual_hash = self.extract_cli_archive(&mut bytes).await?;
146
147        if expected_checksum != actual_hash {
148            eyre::bail!("downloaded CLI binary checksum doesn't match checksum from checksums.txt")
149        }
150
151        let mut file = std::fs::File::create(&path)?;
152        let meta = file.metadata()?;
153        let mut perm = meta.permissions();
154        perm.set_mode(0o700);
155        file.set_permissions(perm)?;
156        file.write_all(bytes.as_slice())?;
157
158        Ok(path)
159    }
160
161    async fn expected_checksum(&self) -> eyre::Result<String> {
162        let archive_url = &self.archive_url();
163        let archive_path = PathBuf::from(&archive_url);
164        let archive_name = archive_path
165            .file_name()
166            .ok_or(eyre::anyhow!("could not get file_name from archive_url"))?;
167        let resp = reqwest::get(self.checksum_url()).await?;
168        let resp = resp.error_for_status()?;
169        for line in resp.text().await?.lines() {
170            let mut content = line.split_whitespace();
171            let checksum = content
172                .next()
173                .ok_or(eyre::anyhow!("could not find checksum in checksums.txt"))?;
174            let file_name = content
175                .next()
176                .ok_or(eyre::anyhow!("could not find file_name in checksums.txt"))?;
177
178            if file_name == archive_name {
179                return Ok(checksum.to_string());
180            }
181        }
182
183        eyre::bail!("could not find a matching version or binary in checksums.txt")
184    }
185
186    pub async fn extract_cli_archive(&self, dest: &mut Vec<u8>) -> eyre::Result<String> {
187        let archive_url = self.archive_url();
188        let resp = reqwest::get(&archive_url).await?;
189        let resp = resp.error_for_status()?;
190        let bytes = resp.bytes().await?;
191        let mut hasher = sha2::Sha256::new();
192        hasher.update(&bytes);
193        let res = hasher.finalize();
194
195        if archive_url.ends_with(".zip") {
196            // TODO:  Nothing for now
197            todo!()
198        } else {
199            self.extract_from_tar(&bytes, dest)?;
200        }
201
202        Ok(hex::encode(res))
203    }
204
205    fn extract_from_tar(&self, temp: &[u8], output: &mut Vec<u8>) -> eyre::Result<()> {
206        let decompressed_temp = GzDecoder::new(temp);
207        let mut archive = Archive::new(decompressed_temp);
208
209        for entry in archive.entries()? {
210            let mut entry = entry?;
211            let path = entry.path()?;
212
213            if path.ends_with("dagger") {
214                copy(&mut entry, output)?;
215
216                return Ok(());
217            }
218        }
219
220        eyre::bail!("could not find a matching file")
221    }
222}
223
224#[cfg(test)]
225mod test {
226    use super::Downloader;
227
228    #[tokio::test]
229    async fn download() {
230        let cli_path = Downloader::new("0.3.10".into())
231            .unwrap()
232            .get_cli()
233            .await
234            .unwrap();
235
236        assert_eq!(
237            Some("dagger-0.3.10"),
238            cli_path.file_name().and_then(|s| s.to_str())
239        )
240    }
241}