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