1use std::{fs, io};
4use std::cmp::min;
5use std::fs::File;
6use std::io::{Read, Write};
7#[cfg(unix)]
8use std::os::unix::fs::PermissionsExt;
9use std::path::PathBuf;
10
11use anyhow::{anyhow, bail, Context};
12use flate2::read::GzDecoder;
13use indicatif::{ProgressBar, ProgressStyle};
14use reqwest::Client;
15use serde_json::Value;
16use sha2::{Digest, Sha256};
17use tracing::{debug, info};
18
19use futures_util::StreamExt;
20
21use crate::plugin_models::PactPluginManifest;
22use crate::utils::os_and_arch;
23use tar::Archive;
24
25pub(crate) async fn fetch_json_from_url(source: &str, http_client: &Client) -> anyhow::Result<Value> {
26 info!(%source, "Fetching root document for source");
27 let response: Value = http_client.get(source)
28 .header("accept", "application/json")
29 .send()
30 .await.context("Fetching root document for source")?
31 .json()
32 .await.context("Parsing root JSON document for source")?;
33 debug!(?response, "Got response");
34 Ok(response)
35}
36
37pub async fn download_json_from_github(
39 http_client: &Client,
40 base_url: &str,
41 tag: &String,
42 filename: &str
43) -> anyhow::Result<Value> {
44 let url = format!("{}/download/{}/{}", base_url, tag, filename);
45 debug!("Downloading JSON file from {}", url);
46 Ok(http_client.get(url)
47 .send()
48 .await?
49 .json()
50 .await?)
51}
52
53pub async fn download_plugin_executable(
55 manifest: &PactPluginManifest,
56 plugin_dir: &PathBuf,
57 http_client: &Client,
58 base_url: &str,
59 tag: &String,
60 display_progress: bool
61) -> anyhow::Result<PathBuf> {
62 let (os, arch) = os_and_arch()?;
63
64 let ext = if os == "windows" { ".exe" } else { "" };
66 let gz_file = format!("pact-{}-plugin-{}-{}{}.gz", manifest.name, os, arch, ext);
67 let sha_file = format!("pact-{}-plugin-{}-{}{}.gz.sha256", manifest.name, os, arch, ext);
68 if github_file_exists(http_client, base_url, tag, gz_file.as_str()).await? {
69 debug!(file = %gz_file, "Found a GZipped file");
70 let file = download_file_from_github(http_client, base_url, tag, gz_file.as_str(), plugin_dir, display_progress).await?;
71
72 if github_file_exists(http_client, base_url, tag, sha_file.as_str()).await? {
73 let sha_file = download_file_from_github(http_client, base_url, tag, sha_file.as_str(), plugin_dir, display_progress).await?;
74 check_sha(&file, &sha_file)?;
75 fs::remove_file(sha_file)?;
76 }
77
78 let file = gunzip_file(&file, plugin_dir, manifest, ext)?;
79 #[cfg(unix)]
80 {
81 let mut perms = fs::metadata(&file)?.permissions();
82 perms.set_mode(0o775);
83 fs::set_permissions(&file, perms)?;
84 }
85
86 return Ok(file);
87 }
88
89 let zip_file = format!("pact-{}-plugin-{}-{}.zip", manifest.name, os, arch);
91 let zip_sha_file = format!("pact-{}-plugin-{}-{}.zip.sha256", manifest.name, os, arch);
92 if github_file_exists(http_client, base_url, tag, zip_file.as_str()).await? {
93 return download_zip_file(plugin_dir, http_client, base_url, tag, zip_file, zip_sha_file, display_progress).await;
94 }
95
96 let zip_file = format!("pact-{}-plugin.zip", manifest.name);
98 let zip_sha_file = format!("pact-{}-plugin.zip.sha256", manifest.name);
99 if github_file_exists(http_client, base_url, tag, zip_file.as_str()).await? {
100 return download_zip_file(plugin_dir, http_client, base_url, tag, zip_file, zip_sha_file, display_progress).await;
101 }
102
103 let tar_gz_file = format!("pact-{}-plugin.tar.gz", manifest.name);
105 let tar_gz_sha_file = format!("pact-{}-plugin.tar.gz.sha256", manifest.name);
106 if github_file_exists(http_client, base_url, tag, tar_gz_file.as_str()).await? {
107 return download_tar_gz_file(plugin_dir, http_client, base_url, tag, tar_gz_file, tar_gz_sha_file, display_progress).await;
108 }
109
110 let tar_gz_file = format!("pact-{}-plugin-{}-{}.tar.gz", manifest.name, os, arch);
112 let tar_gz_sha_file = format!("pact-{}-plugin-{}-{}.tar.gz.sha256", manifest.name, os, arch);
113 if github_file_exists(http_client, base_url, tag, tar_gz_file.as_str()).await? {
114 return download_tar_gz_file(plugin_dir, http_client, base_url, tag, tar_gz_file, tar_gz_sha_file, display_progress).await;
115 }
116
117 let tgz_file = format!("pact-{}-plugin-{}-{}.tgz", manifest.name, os, arch);
119 let tgz_sha_file = format!("pact-{}-plugin-{}-{}.tgz.sha256", manifest.name, os, arch);
120 if github_file_exists(http_client, base_url, tag, tgz_file.as_str()).await? {
121 return download_tar_gz_file(plugin_dir, http_client, base_url, tag, tgz_file, tgz_sha_file, display_progress).await;
122 }
123
124 let tgz_file = format!("pact-{}-plugin.tgz", manifest.name);
126 let tgz_sha_file = format!("pact-{}-plugin.tgz.sha256", manifest.name);
127 if github_file_exists(http_client, base_url, tag, tgz_file.as_str()).await? {
128 return download_tar_gz_file(plugin_dir, http_client, base_url, tag, tgz_file, tgz_sha_file, display_progress).await;
129 }
130
131 bail!("Did not find a matching file pattern on GitHub to install")
132}
133
134async fn github_file_exists(http_client: &Client, base_url: &str, tag: &String, filename: &str) -> anyhow::Result<bool> {
135 let url = format!("{}/download/{}/{}", base_url, tag, filename);
136 debug!("Checking existence of file from {}", url);
137 Ok(http_client.head(url)
138 .send()
139 .await?
140 .status().is_success())
141}
142
143pub async fn download_zip_file(
145 plugin_dir: &PathBuf,
146 http_client: &Client,
147 base_url: &str,
148 tag: &String,
149 zip_file: String,
150 zip_sha_file: String,
151 display_progress: bool
152) -> anyhow::Result<PathBuf> {
153 debug!(file = %zip_file, "Found a Zip file");
154 let file = download_file_from_github(http_client, base_url, tag, zip_file.as_str(), plugin_dir, display_progress).await?;
155
156 if github_file_exists(http_client, base_url, tag, zip_sha_file.as_str()).await? {
157 let sha_file = download_file_from_github(http_client, base_url, tag, zip_sha_file.as_str(), plugin_dir, display_progress).await?;
158 check_sha(&file, &sha_file)?;
159 fs::remove_file(sha_file)?;
160 }
161
162 unzip_file(&file, plugin_dir)
163}
164
165pub async fn download_tar_gz_file(
167 plugin_dir: &PathBuf,
168 http_client: &Client,
169 base_url: &str,
170 tag: &String,
171 tar_gz_file: String,
172 tar_gz_sha_file: String,
173 display_progress: bool
174) -> anyhow::Result<PathBuf> {
175 debug!(file = %tar_gz_file, "Found a tar gz file");
176 let file = download_file_from_github(http_client, base_url, tag, tar_gz_file.as_str(), plugin_dir, display_progress).await?;
177
178 if github_file_exists(http_client, base_url, tag, tar_gz_sha_file.as_str()).await? {
179 let sha_file = download_file_from_github(http_client, base_url, tag, tar_gz_sha_file.as_str(), plugin_dir, display_progress).await?;
180 check_sha(&file, &sha_file)?;
181 fs::remove_file(sha_file)?;
182 }
183
184 extract_tar_gz(&file, plugin_dir)
185}
186
187fn unzip_file(zip_file: &PathBuf, plugin_dir: &PathBuf) -> anyhow::Result<PathBuf> {
188 let mut archive = zip::ZipArchive::new(File::open(zip_file)?)?;
189
190 for i in 0..archive.len() {
191 let mut file = archive.by_index(i).unwrap();
192 let outpath = match file.enclosed_name() {
193 Some(path) => plugin_dir.join(path),
194 None => continue
195 };
196
197 if (*file.name()).ends_with('/') {
198 debug!("Dir {} extracted to \"{}\"", i, outpath.display());
199 fs::create_dir_all(&outpath)?;
200 } else {
201 debug!("File {} extracted to \"{}\" ({} bytes)", i, outpath.display(), file.size());
202 if let Some(p) = outpath.parent() {
203 if !p.exists() {
204 fs::create_dir_all(&p)?;
205 }
206 }
207 let mut outfile = File::create(&outpath)?;
208 io::copy(&mut file, &mut outfile)?;
209 }
210
211 #[cfg(unix)]
212 {
213 if let Some(mode) = file.unix_mode() {
214 fs::set_permissions(&outpath, fs::Permissions::from_mode(mode))?;
215 }
216 }
217 }
218
219 Ok(plugin_dir.clone())
220}
221
222fn gunzip_file(
223 gz_file: &PathBuf,
224 plugin_dir: &PathBuf,
225 manifest: &PactPluginManifest,
226 ext: &str
227) -> anyhow::Result<PathBuf> {
228 let file = if ext.is_empty() {
229 plugin_dir.join(&manifest.entry_point)
230 } else {
231 plugin_dir.join(&manifest.entry_point)
232 .with_extension(ext.strip_prefix('.').unwrap_or(ext))
233 };
234 let mut f = File::create(file.clone())?;
235 let mut gz = GzDecoder::new(File::open(gz_file)?);
236
237 let bytes = io::copy(&mut gz, &mut f)?;
238 debug!(file = %file.display(), "Wrote {} bytes", bytes);
239 fs::remove_file(gz_file)?;
240
241 Ok(file)
242}
243
244fn extract_tar_gz(tar_gz_file: &PathBuf, plugin_dir: &PathBuf) -> anyhow::Result<PathBuf> {
245 let file = File::open(tar_gz_file)?;
246 let gz_decoder = GzDecoder::new(file);
247 let mut archive = Archive::new(gz_decoder);
248
249 archive.unpack(plugin_dir)?;
250 debug!("Unpacked {:?} plugin", tar_gz_file);
251 fs::remove_file(tar_gz_file)?;
252 Ok(tar_gz_file.clone())
253}
254
255pub async fn download_file_from_github(
257 http_client: &Client,
258 base_url: &str,
259 tag: &String,
260 filename: &str,
261 plugin_dir: &PathBuf,
262 display_progress: bool
263) -> anyhow::Result<PathBuf> {
264 let url = format!("{}/download/{}/{}", base_url, tag, filename);
265 debug!("Downloading file from {}", url);
266
267 let res = http_client.get(url.as_str()).send().await?;
268 let total_size = res.content_length()
269 .ok_or(anyhow!("Failed to get content length from '{}'", url))?;
270
271 let pb = ProgressBar::new(total_size);
272 if display_progress {
273 pb.set_style(
274 ProgressStyle::with_template("{msg}\n{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})")
275 .unwrap()
276 .progress_chars("#>-"));
277 pb.set_message(format!("Downloading {}", url));
278 }
279
280 let path = plugin_dir.join(filename);
281 let mut file = File::create(path.clone())?;
282 let mut downloaded: u64 = 0;
283 let mut stream = res.bytes_stream();
284
285 while let Some(item) = stream.next().await {
286 let chunk = item?;
287 file.write_all(&chunk)?;
288 let new = min(downloaded + (chunk.len() as u64), total_size);
289 downloaded = new;
290 if display_progress {
291 pb.set_position(new);
292 }
293 }
294
295 if display_progress {
296 pb.finish_with_message(format!("Downloaded {} to {}", url, path.display()));
297 }
298 debug!(url, downloaded_bytes = downloaded, "File downloaded OK");
299 Ok(path.clone())
300}
301
302pub fn check_sha(file: &PathBuf, sha_file: &PathBuf) -> anyhow::Result<()> {
304 debug!(file = %file.display(), sha_file = %sha_file.display(), "Checking SHA of downloaded file");
305 let sha = fs::read_to_string(sha_file).context("Could not read SHA file")?;
306 let sha = sha.split(' ').next().ok_or(anyhow!("SHA file is not correctly formatted"))?;
307 debug!("Downloaded SHA {}", sha);
308
309 let mut hasher = Sha256::new();
310 let mut f = File::open(file.clone())?;
311 let mut buffer = [0_u8; 256];
312 let mut done = false;
313
314 while !done {
315 let amount = f.read(&mut buffer)?;
316 if amount == 0 {
317 done = true;
318 } else if amount == 256 {
319 hasher.update(&buffer);
320 } else {
321 let b = &buffer[0..amount];
322 hasher.update(b);
323 }
324 }
325
326 let result = hasher.finalize();
327 let calculated = format!("{:x}", result);
328 debug!("Calculated SHA {}", calculated);
329 if calculated == sha {
330 Ok(())
331 } else {
332 Err(anyhow!("Downloaded file {} has a checksum mismatch: {} != {}",
333 file.display(), sha, calculated))
334 }
335}