fluidattacks_core_git/
download.rs1use anyhow::{bail, Context, Result};
2use std::path::Path;
3use tracing::warn;
4
5const MAX_RETRIES: u32 = 3;
6const RETRY_SLEEP_SECS: u64 = 60;
7
8pub async fn download(url: &str) -> Result<Vec<u8>> {
9 let mut last_err = None;
10
11 for attempt in 0..MAX_RETRIES {
12 match do_download(url).await {
13 Ok(data) => return Ok(data),
14 Err(e) => {
15 warn!(
16 attempt = attempt.saturating_add(1),
17 max = MAX_RETRIES,
18 error = %e,
19 "download failed, retrying"
20 );
21 last_err = Some(e);
22 if attempt < MAX_RETRIES - 1 {
23 tokio::time::sleep(std::time::Duration::from_secs(RETRY_SLEEP_SECS)).await;
24 }
25 }
26 }
27 }
28
29 Err(last_err.unwrap_or_else(|| anyhow::anyhow!("download failed")))
30}
31
32async fn do_download(url: &str) -> Result<Vec<u8>> {
33 let resp = reqwest::Client::new()
34 .get(url)
35 .send()
36 .await
37 .context("HTTP request failed")?;
38
39 let status = resp.status();
40 if !status.is_success() {
41 bail!("HTTP {status}");
42 }
43
44 let data = resp.bytes().await.context("reading response body")?;
45
46 if data.is_empty() || !is_gzip(&data) {
47 bail!(
48 "response is not a valid gzip file (got {} bytes)",
49 data.len()
50 );
51 }
52
53 Ok(data.to_vec())
54}
55
56const fn is_gzip(data: &[u8]) -> bool {
57 matches!(data, [0x1f, 0x8b, ..])
58}
59
60fn find_extracted_dir(tmp_path: &Path) -> Option<std::path::PathBuf> {
61 std::fs::read_dir(tmp_path)
62 .ok()?
63 .filter_map(std::result::Result::ok)
64 .find(|e| e.file_type().is_ok_and(|t| t.is_dir()))
65 .map(|e| e.path())
66}
67
68fn move_or_copy(from: &Path, to: &Path) -> bool {
69 if let Err(e) = std::fs::rename(from, to) {
70 if let Err(e2) = super::fs::copy_dir_all(from, to) {
71 warn!(rename_error = %e, copy_error = %e2, "failed to move extracted dir");
72 return false;
73 }
74 }
75 true
76}
77
78pub async fn download_and_extract_repo(url: &str, dest: &Path, git_ignore: &[String]) -> bool {
81 if let Some(parent) = dest.parent() {
82 if let Err(e) = std::fs::create_dir_all(parent) {
83 warn!(error = %e, "failed to create parent dir");
84 return false;
85 }
86 }
87
88 let data = match download(url).await {
89 Ok(d) => d,
90 Err(e) => {
91 warn!(error = %e, "failed to download");
92 return false;
93 }
94 };
95
96 let tmp_dir = match tempfile::tempdir() {
97 Ok(d) => d,
98 Err(e) => {
99 warn!(error = %e, "failed to create temp dir");
100 return false;
101 }
102 };
103
104 if let Err(e) = super::tar::extract(&data, tmp_dir.path()) {
105 warn!(error = %e, "failed to extract archive");
106 return false;
107 }
108
109 let Some(extracted_dir) = find_extracted_dir(tmp_dir.path()) else {
110 warn!("no directory found in extracted archive");
111 return false;
112 };
113
114 if dest.exists() {
115 let _ = std::fs::remove_dir_all(dest);
116 }
117 if !move_or_copy(&extracted_dir, dest) {
118 return false;
119 }
120
121 let dest_str = dest.to_string_lossy().to_string();
122 let _ = super::cli::set_safe_directory().await;
123 if super::cli::reset_hard(&dest_str).await.is_err() {
124 let _ = std::fs::remove_dir_all(dest);
125 return false;
126 }
127 let _ = super::fs::remove_symlinks(dest);
128 let _ = super::fs::delete_gitignored_files(dest, git_ignore);
129
130 true
131}