dagger_sdk/core/
downloader.rs1use 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!()
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}