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