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