Skip to main content

zoi_lua/api/
download.rs

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}