1use reqwest::blocking::Client;
30use reqwest::header::{CONTENT_LENGTH, CONTENT_TYPE};
31use std::fs::File;
32use std::time::Duration;
33use std::error::Error;
34use crate::progress::create_progress_bar;
35use crate::utils::{self, print};
36use humansize::{format_size, DECIMAL};
37use mime::Mime;
38use std::io::{Read, Write};
39use std::path::{Path, PathBuf};
40use sha2::Digest;
41use crate::config::ProxyConfig;
42use crate::optimization::Optimizer;
43use crate::DownloadOptions;
44
45const MAX_RETRIES: u32 = 3;
46const RETRY_DELAY: Duration = Duration::from_secs(2);
47
48pub fn check_disk_space(path: &Path, required_size: u64) -> Result<(), Box<dyn Error + Send + Sync>> {
59 let dir = path.parent().unwrap_or(Path::new("."));
60 let available_space = fs2::available_space(dir)?;
61
62 if available_space < required_size {
63 return Err(format!(
64 "Insufficient disk space. Required: {}, Available: {}",
65 format_size(required_size, DECIMAL),
66 format_size(available_space, DECIMAL)
67 ).into());
68 }
69 Ok(())
70}
71
72pub fn validate_filename(filename: &str) -> Result<(), Box<dyn Error + Send + Sync>> {
80 if filename.contains(std::path::MAIN_SEPARATOR) {
81 return Err("Filename cannot contain directory separators".into());
82 }
83 if filename.is_empty() {
84 return Err("Filename cannot be empty".into());
85 }
86 Ok(())
87}
88
89pub fn download(
128 target: &str,
129 proxy: ProxyConfig,
130 _optimizer: Optimizer,
131 options: DownloadOptions,
132 status_callback: Option<&(dyn Fn(String) + Send + Sync)>,
133) -> Result<(), Box<dyn Error + Send + Sync>> {
134 let quiet_mode = options.quiet_mode;
135
136
137 let mut client_builder = Client::builder()
138 .timeout(Duration::from_secs(30))
139 .no_gzip()
140 .no_deflate();
141
142 if proxy.enabled {
143 if let Some(proxy_url) = &proxy.url {
144 let proxy_client = match proxy.proxy_type {
145 crate::config::ProxyType::Http => reqwest::Proxy::http(proxy_url),
146 crate::config::ProxyType::Https => reqwest::Proxy::https(proxy_url),
147 crate::config::ProxyType::Socks5 => reqwest::Proxy::all(proxy_url),
148 };
149 if let Ok(mut proxy_client) = proxy_client {
150 if let (Some(username), Some(password)) = (&proxy.username, &proxy.password) {
151 proxy_client = proxy_client.basic_auth(username, password);
152 }
153 client_builder = client_builder.proxy(proxy_client);
154 }
155 }
156 }
157
158 let client = client_builder.build()?;
159
160 let mut retries = 0;
161 let response = loop {
162 match client.get(target).send() {
163 Ok(resp) => break resp,
164 Err(e) => {
165 retries += 1;
166 if retries >= MAX_RETRIES {
167 return Err(format!("Failed after {} attempts: {}", MAX_RETRIES, e).into());
168 }
169 print(&format!("Attempt {} failed, retrying in {} seconds...",
170 retries, RETRY_DELAY.as_secs()), quiet_mode);
171 std::thread::sleep(RETRY_DELAY);
172 }
173 }
174 };
175
176 print(
177 &format!("HTTP request sent... {}", response.status()),
178 quiet_mode
179 );
180
181 if !response.status().is_success() {
182 return Err(format!("HTTP error: {}", response.status()).into());
183 }
184
185 let content_length = response.headers()
186 .get(CONTENT_LENGTH)
187 .and_then(|ct_len| ct_len.to_str().ok())
188 .and_then(|s| s.parse::<u64>().ok());
189
190 let content_type = response.headers()
191 .get(CONTENT_TYPE)
192 .and_then(|ct| ct.to_str().ok())
193 .and_then(|s| s.parse::<Mime>().ok());
194
195 if let Some(len) = content_length {
196 print(
197 &format!("Length: {} ({})",
198 len,
199 format_size(len, DECIMAL)
200 ),
201 quiet_mode
202 );
203 } else {
204 print("Length: unknown", quiet_mode);
205 }
206
207 if let Some(ref ct) = content_type {
208 print(&format!("Type: {}", ct), quiet_mode);
209 }
210
211 let is_iso = target.to_lowercase().ends_with(".iso")
212 || content_type.as_ref().map_or(false, |ct| ct.essence_str() == "application/x-iso9001" || ct.essence_str() == "application/x-cd-image");
213
214 if is_iso {
215 print("ISO file detected. Ensuring raw download to prevent corruption...", quiet_mode);
216 }
217
218 let tentative_path: PathBuf;
219
220 if let Some(output_arg_str) = options.output_path {
221 let user_path = PathBuf::from(output_arg_str.clone());
222
223 let is_target_dir = user_path.is_dir() ||
224 output_arg_str.ends_with(std::path::MAIN_SEPARATOR);
225
226 if is_target_dir {
227 let base_filename = utils::get_filename_from_url_or_default(target, "downloaded_file");
228 validate_filename(&base_filename)?;
229 tentative_path = user_path.join(base_filename);
230 } else {
231 if let Some(file_name_osstr) = user_path.file_name() {
232 if let Some(file_name_str) = file_name_osstr.to_str() {
233 if file_name_str.is_empty() {
234 return Err(format!("Invalid output path, does not specify a file name: {}", user_path.display()).into());
235 }
236 validate_filename(file_name_str)?;
237 } else {
238 return Err("Output filename contains invalid characters (non-UTF-8)".into());
239 }
240 } else {
241 return Err(format!("Invalid output path, does not specify a file name: {}", user_path.display()).into());
242 }
243 tentative_path = user_path;
244 }
245 } else {
246 let base_filename = utils::get_filename_from_url_or_default(target, "downloaded_file");
247 validate_filename(&base_filename)?;
248 tentative_path = PathBuf::from(base_filename);
249 }
250
251 let final_path: PathBuf = if tentative_path.is_absolute() {
252 tentative_path
253 } else {
254 let current_dir = std::env::current_dir()
255 .map_err(|e| format!("Failed to get current directory: {}", e))?;
256 current_dir.join(tentative_path)
257 };
258
259 if let Some(parent_dir) = final_path.parent() {
260 if !parent_dir.as_os_str().is_empty() && parent_dir != Path::new("/") && !parent_dir.exists() {
261 std::fs::create_dir_all(parent_dir)
262 .map_err(|e| format!("Failed to create directory {}: {}", parent_dir.display(), e))?;
263 if !quiet_mode {
264 print(&format!("Created directory: {}", parent_dir.display()), quiet_mode);
265 }
266 }
267 }
268
269 if !quiet_mode {
270 print(&format!("Saving to: {}", final_path.display()), quiet_mode);
271 }
272
273 if let Some(len) = content_length {
274 check_disk_space(&final_path, len)?;
275 }
276
277 let mut dest = File::create(&final_path).map_err(|e| {
278 format!("Failed to create file {}: {}", final_path.display(), e)
279 })?;
280
281 let response_content_length = response.content_length();
282 let progress_bar_filename = final_path.file_name().unwrap_or_default().to_string_lossy().into_owned();
283 let progress = create_progress_bar(quiet_mode, progress_bar_filename, response_content_length, false);
284
285 let mut source = response.take(response_content_length.unwrap_or(u64::MAX));
286 let mut buffered_reader = progress.wrap_read(&mut source);
287
288 let mut buffer = [0u8; 8192];
290 loop {
291 let n = buffered_reader.read(&mut buffer)?;
292 if n == 0 { break; }
293 dest.write_all(&buffer[..n])?;
294 }
295
296 progress.finish_with_message("Download completed\n");
297
298
299 if is_iso && options.verify_iso {
300 verify_iso_integrity(&final_path, status_callback)?;
301 }
302
303 Ok(())
304}
305
306pub fn verify_iso_integrity(path: &Path, callback: Option<&(dyn Fn(String) + Send + Sync)>) -> Result<(), Box<dyn Error + Send + Sync>> {
332 let msg = "Calculating SHA256 hash... (this may take a while for large ISOs)";
333 if let Some(cb) = callback { cb(msg.to_string()); }
334 println!("{}", msg);
335
336 let mut file = File::open(path)?;
337 let mut hasher = sha2::Sha256::new();
338 let mut buffer = [0; 8192];
339 loop {
340 let n = file.read(&mut buffer)?;
341 if n == 0 { break; }
342 hasher.update(&buffer[..n]);
343 }
344 let hash = hex::encode(hasher.finalize());
345
346 let msg_done = "Integrity check finished.";
347 if let Some(cb) = callback { cb(msg_done.to_string()); }
348 println!("{}", msg_done);
349
350 let msg_hash = format!("SHA256: {}", hash);
351 if let Some(cb) = callback { cb(msg_hash); }
352 println!("SHA256: {}", hash);
353
354 println!("You can compare this hash with the one provided by the source.");
355 Ok(())
356}