1use colored::*;
2use indicatif::{ProgressBar, ProgressStyle};
3use std::fs;
4use std::io::{Read, Write};
5use std::path::Path;
6use zoi_core::utils;
7
8pub fn download_with_progress(url: &str, dest_path: &Path, quiet: bool) -> Result<(), mlua::Error> {
9 if url.starts_with("http://") && !quiet {
10 println!(
11 "{}: downloading over insecure HTTP: {}",
12 "Warning:".yellow(),
13 url
14 );
15 }
16
17 let client = utils::get_http_client().map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
18
19 let mut attempt = 0u32;
20 let mut response = loop {
21 attempt += 1;
22 match client.get(url).send() {
23 Ok(resp) => {
24 if !resp.status().is_success() {
25 return Err(mlua::Error::RuntimeError(format!(
26 "Failed to download {}: {}",
27 url,
28 resp.status()
29 )));
30 }
31 break resp;
32 }
33 Err(e) => {
34 if attempt < 3 {
35 if !quiet {
36 eprintln!("Download failed ({}). Retrying...", e);
37 }
38 zoi_core::utils::retry_backoff_sleep(attempt);
39 continue;
40 } else {
41 return Err(mlua::Error::RuntimeError(e.to_string()));
42 }
43 }
44 }
45 };
46
47 let total_size = response.content_length().unwrap_or(0);
48
49 let pb = if !quiet {
50 let pb = ProgressBar::new(total_size);
51 pb.set_style(ProgressStyle::default_bar()
52 .template("{spinner:.green} {msg:30.cyan} [{bar:40.cyan/blue}] {bytes}/{total_bytes} ({bytes_per_sec}, {elapsed_precise})")
53 .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?
54 .progress_chars("=>-"));
55
56 let filename = url.split('/').next_back().unwrap_or("file");
57 pb.set_message(format!("Downloading {}", filename));
58 Some(pb)
59 } else {
60 None
61 };
62
63 let mut dest_file =
64 fs::File::create(dest_path).map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
65
66 let mut buffer = [0; 8192];
67 let mut downloaded = 0;
68
69 while let Ok(n) = response.read(&mut buffer) {
70 if n == 0 {
71 break;
72 }
73 dest_file
74 .write_all(&buffer[..n])
75 .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
76 downloaded += n as u64;
77 if let Some(ref p) = pb {
78 p.set_position(downloaded);
79 }
80 }
81
82 if let Some(p) = pb {
83 p.finish_and_clear();
84 }
85
86 Ok(())
87}
88
89pub fn add_download_util(lua: &mlua::Lua, quiet: bool) -> Result<(), mlua::Error> {
90 let download_fn = lua.create_function(
91 move |lua, (url, out_name, hash): (String, Option<String>, Option<String>)| {
92 let build_dir_str: String = lua.globals().get("BUILD_DIR")?;
93 let build_dir = Path::new(&build_dir_str);
94
95 let filename = out_name.unwrap_or_else(|| {
96 url.split('/')
97 .next_back()
98 .unwrap_or("download.tmp")
99 .to_string()
100 });
101
102 let dest_path = build_dir.join(&filename);
103
104 download_with_progress(&url, &dest_path, quiet)?;
105
106 if let Some(hash_spec) = hash {
107 let parts: Vec<&str> = hash_spec.splitn(2, '-').collect();
108 let (algo, expected_hash) = if parts.len() == 2 {
109 (parts[0], parts[1])
110 } else {
111 ("sha512", hash_spec.as_str())
112 };
113
114 let hash_algo = match zoi_core::hash::HashAlgorithm::from_name(algo) {
115 Some(a) => a,
116 None => {
117 return Err(mlua::Error::RuntimeError(format!(
118 "Unsupported hash algorithm: {}",
119 algo
120 )));
121 }
122 };
123
124 let actual_hash = zoi_core::hash::calculate_file_hash(&dest_path, hash_algo)
125 .map_err(|e| {
126 mlua::Error::RuntimeError(format!("Failed to calculate hash: {}", e))
127 })?;
128
129 if !actual_hash.eq_ignore_ascii_case(expected_hash) {
130 return Err(mlua::Error::RuntimeError(format!(
131 "Hash mismatch for {}. Expected: {}-{}, Got: {}-{}",
132 filename, algo, expected_hash, algo, actual_hash
133 )));
134 }
135 }
136
137 Ok(filename)
138 },
139 )?;
140
141 let utils_table: mlua::Table = lua.globals().get("UTILS")?;
142 utils_table.set("DOWNLOAD", download_fn)?;
143
144 Ok(())
145}