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_DISPOSITION, 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, Instant};
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>> {
89 if filename.is_empty() {
90 return Err("Filename cannot be empty".into());
91 }
92 if filename.contains('/') || filename.contains('\\') {
93 return Err("Filename cannot contain directory separators".into());
94 }
95 if filename.contains('\0') {
96 return Err("Filename cannot contain null bytes".into());
97 }
98 if filename.contains("..") {
99 return Err("Filename cannot contain path traversal sequences".into());
100 }
101 if filename.len() > 255 {
102 return Err("Filename exceeds maximum length of 255 bytes".into());
103 }
104 let stem = filename.split('.').next().unwrap_or(filename);
106 const RESERVED: &[&str] = &[
107 "CON", "PRN", "AUX", "NUL",
108 "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
109 "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
110 ];
111 if RESERVED.iter().any(|r| stem.eq_ignore_ascii_case(r)) {
112 return Err(format!("'{}' is a reserved filename on Windows", stem).into());
113 }
114 Ok(())
115}
116
117pub fn download(
157 target: &str,
158 proxy: ProxyConfig,
159 optimizer: Optimizer,
160 options: DownloadOptions,
161 status_callback: Option<&(dyn Fn(String) + Send + Sync)>,
162) -> Result<(), Box<dyn Error + Send + Sync>> {
163 let quiet_mode = options.quiet_mode;
164
165 let mut client_builder = Client::builder()
166 .timeout(Duration::from_secs(30))
167 .user_agent(concat!("KGet/", env!("CARGO_PKG_VERSION")))
168 .no_gzip()
169 .no_deflate();
170
171 if proxy.enabled {
172 if let Some(proxy_url) = &proxy.url {
173 let proxy_client = match proxy.proxy_type {
174 crate::config::ProxyType::Http => reqwest::Proxy::http(proxy_url),
175 crate::config::ProxyType::Https => reqwest::Proxy::https(proxy_url),
176 crate::config::ProxyType::Socks5 => reqwest::Proxy::all(proxy_url),
177 };
178 if let Ok(mut proxy_client) = proxy_client {
179 if let (Some(username), Some(password)) = (&proxy.username, &proxy.password) {
180 proxy_client = proxy_client.basic_auth(username, password);
181 }
182 client_builder = client_builder.proxy(proxy_client);
183 }
184 }
185 }
186
187 let client = client_builder.build()?;
188
189 let mut retries = 0;
190 let response = loop {
191 let mut req = client.get(target);
192 for (name, value) in &options.extra_headers {
193 if let (Ok(n), Ok(v)) = (
194 reqwest::header::HeaderName::from_bytes(name.as_bytes()),
195 reqwest::header::HeaderValue::from_str(value),
196 ) {
197 req = req.header(n, v);
198 }
199 }
200 match req.send() {
201 Ok(resp) => {
202 let status = resp.status();
203 if status.is_success() {
204 break resp;
205 } else if status.is_server_error() {
206 retries += 1;
208 if retries >= MAX_RETRIES {
209 return Err(
210 format!("HTTP {} after {} attempts", status, MAX_RETRIES).into()
211 );
212 }
213 print(
214 &format!(
215 "Server error {} on attempt {}, retrying in {} seconds...",
216 status,
217 retries,
218 RETRY_DELAY.as_secs()
219 ),
220 quiet_mode,
221 );
222 std::thread::sleep(RETRY_DELAY);
223 } else {
224 return Err(format!("HTTP error: {}", status).into());
226 }
227 }
228 Err(e) => {
229 retries += 1;
230 if retries >= MAX_RETRIES {
231 return Err(format!("Failed after {} attempts: {}", MAX_RETRIES, e).into());
232 }
233 print(
234 &format!(
235 "Attempt {} failed, retrying in {} seconds...",
236 retries,
237 RETRY_DELAY.as_secs()
238 ),
239 quiet_mode,
240 );
241 std::thread::sleep(RETRY_DELAY);
242 }
243 }
244 };
245
246 print(
247 &format!("HTTP request sent... {}", response.status()),
248 quiet_mode,
249 );
250
251 let content_length = response
252 .headers()
253 .get(CONTENT_LENGTH)
254 .and_then(|ct_len| ct_len.to_str().ok())
255 .and_then(|s| s.parse::<u64>().ok());
256
257 let content_type = response
258 .headers()
259 .get(CONTENT_TYPE)
260 .and_then(|ct| ct.to_str().ok())
261 .and_then(|s| s.parse::<Mime>().ok());
262
263 let server_filename = response
264 .headers()
265 .get(CONTENT_DISPOSITION)
266 .and_then(|v| v.to_str().ok())
267 .and_then(parse_content_disposition_filename);
268
269 if let Some(len) = content_length {
270 print(
271 &format!("Length: {} ({})", len, format_size(len, DECIMAL)),
272 quiet_mode,
273 );
274 } else {
275 print("Length: unknown", quiet_mode);
276 }
277
278 if let Some(ref ct) = content_type {
279 print(&format!("Type: {}", ct), quiet_mode);
280 }
281
282 let is_iso = target.to_lowercase().ends_with(".iso")
283 || content_type.as_ref().map_or(false, |ct| {
284 ct.essence_str() == "application/x-iso9660-image"
285 || ct.essence_str() == "application/x-cd-image"
286 });
287
288 if is_iso {
289 print(
290 "ISO file detected. Ensuring raw download to prevent corruption...",
291 quiet_mode,
292 );
293 }
294
295 let tentative_path: PathBuf;
296
297 if let Some(output_arg_str) = options.output_path {
298 let user_path = PathBuf::from(output_arg_str.clone());
299
300 let is_target_dir =
301 user_path.is_dir() || output_arg_str.ends_with(std::path::MAIN_SEPARATOR);
302
303 if is_target_dir {
304 let base_filename = utils::get_filename_from_url_or_default(target, "downloaded_file");
305 validate_filename(&base_filename)?;
306 tentative_path = user_path.join(base_filename);
307 } else {
308 if let Some(file_name_osstr) = user_path.file_name() {
309 if let Some(file_name_str) = file_name_osstr.to_str() {
310 if file_name_str.is_empty() {
311 return Err(format!(
312 "Invalid output path, does not specify a file name: {}",
313 user_path.display()
314 )
315 .into());
316 }
317 validate_filename(file_name_str)?;
318 } else {
319 return Err("Output filename contains invalid characters (non-UTF-8)".into());
320 }
321 } else {
322 return Err(format!(
323 "Invalid output path, does not specify a file name: {}",
324 user_path.display()
325 )
326 .into());
327 }
328 tentative_path = user_path;
329 }
330 } else {
331 let base_filename = if let Some(ref name) = server_filename {
332 name.clone()
333 } else {
334 utils::get_filename_from_url_or_default(target, "downloaded_file")
335 };
336 validate_filename(&base_filename)?;
337 tentative_path = PathBuf::from(base_filename);
338 }
339
340 let final_path: PathBuf = if tentative_path.is_absolute() {
341 tentative_path
342 } else {
343 let current_dir = std::env::current_dir()
344 .map_err(|e| format!("Failed to get current directory: {}", e))?;
345 current_dir.join(tentative_path)
346 };
347
348 if let Some(parent_dir) = final_path.parent() {
349 if !parent_dir.as_os_str().is_empty()
350 && parent_dir != Path::new("/")
351 && !parent_dir.exists()
352 {
353 std::fs::create_dir_all(parent_dir).map_err(|e| {
354 format!("Failed to create directory {}: {}", parent_dir.display(), e)
355 })?;
356 if !quiet_mode {
357 print(
358 &format!("Created directory: {}", parent_dir.display()),
359 quiet_mode,
360 );
361 }
362 }
363 }
364
365 if !quiet_mode {
366 print(&format!("Saving to: {}", final_path.display()), quiet_mode);
367 }
368
369 if let Some(len) = content_length {
370 check_disk_space(&final_path, len)?;
371 }
372
373 let mut dest = File::create(&final_path)
374 .map_err(|e| format!("Failed to create file {}: {}", final_path.display(), e))?;
375
376 let response_content_length = response.content_length();
377 let progress_bar_filename = final_path
378 .file_name()
379 .unwrap_or_default()
380 .to_string_lossy()
381 .into_owned();
382 let progress = create_progress_bar(
383 quiet_mode,
384 progress_bar_filename,
385 response_content_length,
386 false,
387 );
388
389 let mut source = response.take(response_content_length.unwrap_or(u64::MAX));
390 let mut buffered_reader = progress.wrap_read(&mut source);
391
392 let mut buffer = [0u8; 8192];
394 let mut downloaded: u64 = 0;
395 let started_at = Instant::now();
396 loop {
397 let n = buffered_reader.read(&mut buffer)?;
398 if n == 0 {
399 break;
400 }
401 dest.write_all(&buffer[..n])?;
402 downloaded += n as u64;
403
404 if let Some(total) = response_content_length {
405 if let Some(cb) = status_callback {
406 let percent = downloaded as f64 / total.max(1) as f64 * 100.0;
407 cb(format!(
408 "PROGRESS: {:.1}% ({}/{})",
409 percent, downloaded, total
410 ));
411 }
412 }
413
414 throttle_download(downloaded, started_at, optimizer.speed_limit);
415 }
416
417 progress.finish_with_message("Download completed\n");
418
419 if is_iso && options.verify_iso {
420 verify_file_sha256(
421 &final_path,
422 options.expected_sha256.as_deref(),
423 status_callback,
424 )?;
425 } else if let Some(expected) = options.expected_sha256.as_deref() {
426 verify_file_sha256(&final_path, Some(expected), status_callback)?;
427 }
428
429 Ok(())
430}
431
432pub fn parse_content_disposition_filename(header_value: &str) -> Option<String> {
437 let mut plain: Option<String> = None;
438
439 for part in header_value.split(';') {
440 let part = part.trim();
441
442 if let Some(val) = part.strip_prefix("filename*=") {
444 let val = val.trim().trim_matches('"');
445 let mut parts = val.splitn(3, '\'');
447 let _charset = parts.next();
448 let _language = parts.next();
449 if let Some(encoded) = parts.next() {
450 if let Ok(decoded) = urlencoding::decode(encoded) {
451 let name = decoded.into_owned();
452 if !name.is_empty() {
453 return Some(name);
454 }
455 }
456 }
457 }
458
459 if plain.is_none() {
461 if let Some(val) = part.strip_prefix("filename=") {
462 let name = val.trim().trim_matches('"').to_string();
463 if !name.is_empty() {
464 plain = Some(name);
465 }
466 }
467 }
468 }
469
470 plain
471}
472
473fn throttle_download(downloaded: u64, started_at: Instant, speed_limit: Option<u64>) {
474 let Some(limit) = speed_limit else { return };
475 if limit == 0 {
476 return;
477 }
478
479 let expected_elapsed = Duration::from_secs_f64(downloaded as f64 / limit as f64);
480 let actual_elapsed = started_at.elapsed();
481 if expected_elapsed > actual_elapsed {
482 std::thread::sleep(expected_elapsed - actual_elapsed);
483 }
484}
485
486pub fn verify_iso_integrity(
512 path: &Path,
513 callback: Option<&(dyn Fn(String) + Send + Sync)>,
514) -> Result<(), Box<dyn Error + Send + Sync>> {
515 verify_file_sha256(path, None, callback).map(|_| ())
516}
517
518pub fn verify_file_sha256(
520 path: &Path,
521 expected_hash: Option<&str>,
522 callback: Option<&(dyn Fn(String) + Send + Sync)>,
523) -> Result<String, Box<dyn Error + Send + Sync>> {
524 let send = |msg: &str| {
525 if let Some(cb) = callback {
526 cb(msg.to_string());
527 }
528 };
529
530 send("Calculating SHA256 hash... (this may take a while for large ISOs)");
531
532 let mut file = File::open(path)?;
533 let mut hasher = sha2::Sha256::new();
534 let mut buffer = [0; 8192];
535 loop {
536 let n = file.read(&mut buffer)?;
537 if n == 0 {
538 break;
539 }
540 hasher.update(&buffer[..n]);
541 }
542 let hash = hex::encode(hasher.finalize());
543
544 send("Integrity check finished.");
545 send(&format!("SHA256: {}", hash));
546
547 if let Some(expected_hash) = expected_hash {
548 let expected_hash = expected_hash.trim().to_ascii_lowercase();
549 if hash != expected_hash {
550 return Err(
551 format!("SHA256 mismatch: expected {}, got {}", expected_hash, hash).into(),
552 );
553 }
554 send("SHA256 matches expected hash.");
555 }
556
557 Ok(hash)
558}