Skip to main content

dagger_sdk/core/
downloader.rs

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