1use crate::config::ProxyConfig;
43use crate::optimization::Optimizer;
44use hex;
45use indicatif::{ProgressBar, ProgressStyle};
46use rayon::prelude::*;
47use reqwest::blocking::Client;
48use sha2::{Digest, Sha256};
49use std::error::Error;
50use std::fs::File;
51use std::io::{BufReader, Read, Seek, SeekFrom, Write};
52use std::path::Path;
53use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
54use std::sync::{Arc, Mutex};
55use std::time::{Duration, Instant};
56
57#[cfg(target_family = "unix")]
58use std::os::unix::fs::FileExt;
59#[cfg(target_family = "windows")]
60use std::os::windows::fs::FileExt;
61
62const MIN_CHUNK_SIZE: u64 = 4 * 1024 * 1024;
64const MAX_RETRIES: usize = 3;
66
67pub struct AdvancedDownloader {
121 client: Client,
122 url: String,
123 output_path: String,
124 quiet_mode: bool,
125 #[allow(dead_code)]
126 proxy: ProxyConfig,
127 optimizer: Optimizer,
128 progress_callback: Option<Arc<dyn Fn(f32) + Send + Sync>>,
129 status_callback: Option<Arc<dyn Fn(String) + Send + Sync>>,
130 cancel_token: Arc<AtomicBool>,
131 expected_sha256: Option<String>,
132}
133
134impl AdvancedDownloader {
135 pub fn new(
159 url: String,
160 output_path: String,
161 quiet_mode: bool,
162 proxy_config: ProxyConfig,
163 optimizer: Optimizer,
164 ) -> Self {
165 let _is_iso = url.to_lowercase().ends_with(".iso");
166
167 let mut client_builder = Client::builder()
168 .timeout(std::time::Duration::from_secs(300))
169 .connect_timeout(std::time::Duration::from_secs(20))
170 .user_agent("KGet/1.0")
171 .no_gzip()
172 .no_deflate();
173
174 if proxy_config.enabled {
175 if let Some(proxy_url) = &proxy_config.url {
176 let proxy = match proxy_config.proxy_type {
177 crate::config::ProxyType::Http => reqwest::Proxy::http(proxy_url),
178 crate::config::ProxyType::Https => reqwest::Proxy::https(proxy_url),
179 crate::config::ProxyType::Socks5 => reqwest::Proxy::all(proxy_url),
180 };
181
182 if let Ok(mut proxy) = proxy {
183 if let (Some(username), Some(password)) =
184 (&proxy_config.username, &proxy_config.password)
185 {
186 proxy = proxy.basic_auth(username, password);
187 }
188 client_builder = client_builder.proxy(proxy);
189 }
190 }
191 }
192
193 let client = client_builder
194 .build()
195 .expect("Failed to create HTTP client");
196
197 Self {
198 client,
199 url,
200 output_path,
201 quiet_mode,
202 proxy: proxy_config,
203 optimizer,
204 progress_callback: None,
205 status_callback: None,
206 cancel_token: Arc::new(AtomicBool::new(false)),
207 expected_sha256: None,
208 }
209 }
210
211 pub fn set_cancel_token(&mut self, token: Arc<AtomicBool>) {
233 self.cancel_token = token;
234 }
235
236 pub fn is_cancelled(&self) -> bool {
238 self.cancel_token.load(Ordering::Relaxed)
239 }
240
241 pub fn set_progress_callback(&mut self, callback: impl Fn(f32) + Send + Sync + 'static) {
255 self.progress_callback = Some(Arc::new(callback));
256 }
257
258 pub fn set_status_callback(&mut self, callback: impl Fn(String) + Send + Sync + 'static) {
272 self.status_callback = Some(Arc::new(callback));
273 }
274
275 pub fn set_expected_sha256(&mut self, expected_sha256: impl Into<String>) {
277 self.expected_sha256 = Some(expected_sha256.into());
278 }
279
280 fn send_status(&self, msg: &str) {
281 if let Some(cb) = &self.status_callback {
282 cb(msg.to_string());
283 }
284 if !self.quiet_mode {
285 println!("{}", msg);
286 }
287 }
288
289 pub fn download(&self) -> Result<(), Box<dyn Error + Send + Sync>> {
308 let is_iso = self.url.to_lowercase().ends_with(".iso");
309 if !self.quiet_mode {
310 println!("Starting advanced download for: {}", self.url);
311 if is_iso {
312 println!(
313 "Warning: ISO mode active. Disabling optimizations that could corrupt binary data."
314 );
315 }
316 }
317
318 let existing_size = if Path::new(&self.output_path).exists() {
320 let size = std::fs::metadata(&self.output_path)?.len();
321 if !self.quiet_mode {
322 println!("Existing file found with size: {} bytes", size);
323 }
324 Some(size)
325 } else {
326 if !self.quiet_mode {
327 println!("Output file does not exist, starting fresh download");
328 }
329 None
330 };
331
332 if !self.quiet_mode {
334 println!("Querying server for file size and range support...");
335 }
336 let (total_size, supports_range) = self.get_file_size_and_range()?;
337 if !self.quiet_mode {
338 println!("Total file size: {} bytes", total_size);
339 println!("Server supports range requests: {}", supports_range);
340 }
341
342 if let Some(size) = existing_size {
343 if size > total_size {
344 return Err("Existing file is larger than remote; aborting".into());
345 }
346 if !self.quiet_mode {
347 println!("Resuming download from byte: {}", size);
348 }
349 }
350
351 let progress = if !self.quiet_mode || self.progress_callback.is_some() {
353 let bar = ProgressBar::new(total_size);
354 if self.quiet_mode {
355 bar.set_draw_target(indicatif::ProgressDrawTarget::hidden());
356 } else {
357 bar.set_style(ProgressStyle::with_template(
358 "{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})"
359 ).unwrap().progress_chars("#>-"));
360 }
361 Some(Arc::new(Mutex::new(bar)))
362 } else {
363 None
364 };
365
366 if !self.quiet_mode {
368 println!("Preparing output file: {}", self.output_path);
369 }
370 let file = if existing_size.is_some() {
371 File::options()
372 .read(true)
373 .write(true)
374 .open(&self.output_path)?
375 } else {
376 File::create(&self.output_path)?
377 };
378 file.set_len(total_size)?;
379 if !self.quiet_mode {
380 println!("File preallocated to {} bytes", total_size);
381 }
382
383 if !supports_range {
385 if !self.quiet_mode {
386 println!("Range requests not supported, falling back to single-threaded download");
387 }
388 self.download_whole(&file, existing_size.unwrap_or(0), progress.clone())?;
389 if let Some(ref bar) = progress {
390 bar.lock()
391 .unwrap()
392 .finish_with_message("Download completed");
393 }
394 if !self.quiet_mode {
395 println!("Single-threaded download completed");
396 }
397 return Ok(());
398 }
399
400 if !self.quiet_mode {
402 println!("Calculating download chunks...");
403 }
404 let chunks = self.calculate_chunks(total_size, existing_size)?;
405 if !self.quiet_mode {
406 println!("Download will be split into {} chunks", chunks.len());
407 }
408
409 if !self.quiet_mode {
411 println!("Starting parallel chunk downloads...");
412 }
413 self.download_chunks_parallel(chunks, &file, progress.clone())?;
414
415 if let Some(ref bar) = progress {
416 bar.lock()
417 .unwrap()
418 .finish_with_message("Download completed");
419 }
420
421 if !self.quiet_mode || self.status_callback.is_some() {
423 if is_iso || self.expected_sha256.is_some() {
424 let should_verify = if self.status_callback.is_some() {
425 true
426 } else if self.expected_sha256.is_some() {
427 true
428 } else {
429 println!(
430 "\nThis is an ISO file. Would you like to verify its integrity? (y/N)"
431 );
432 let mut input = String::new();
433 std::io::stdin().read_line(&mut input).is_ok()
434 && input.trim().to_lowercase() == "y"
435 };
436
437 if should_verify {
438 self.verify_integrity(total_size)?;
439 }
440 } else {
441 let metadata = std::fs::metadata(&self.output_path)?;
442 if metadata.len() != total_size {
443 return Err(format!(
444 "File size mismatch: expected {} bytes, got {} bytes",
445 total_size,
446 metadata.len()
447 )
448 .into());
449 }
450 }
451 self.send_status("Advanced download completed successfully!");
452 }
453
454 Ok(())
455 }
456
457 fn get_file_size_and_range(&self) -> Result<(u64, bool), Box<dyn Error + Send + Sync>> {
458 let response = self.client.head(&self.url).send()?;
459 let content_length = response
460 .headers()
461 .get(reqwest::header::CONTENT_LENGTH)
462 .and_then(|v| v.to_str().ok())
463 .and_then(|s| s.parse::<u64>().ok())
464 .ok_or("Could not determine file size")?;
465
466 let accepts_range = response
467 .headers()
468 .get(reqwest::header::ACCEPT_RANGES)
469 .and_then(|v| v.to_str().ok())
470 .map(|s| s.eq_ignore_ascii_case("bytes"))
471 .unwrap_or(false);
472
473 Ok((content_length, accepts_range))
474 }
475
476 fn calculate_chunks(
477 &self,
478 total_size: u64,
479 existing_size: Option<u64>,
480 ) -> Result<Vec<(u64, u64)>, Box<dyn Error + Send + Sync>> {
481 let mut chunks = Vec::new();
482 let start_from = existing_size.unwrap_or(0);
483
484 let configured_parallelism = self.optimizer.max_connections() as u64;
485 let runtime_parallelism = rayon::current_num_threads() as u64;
486 let parallelism = configured_parallelism.min(runtime_parallelism).max(1);
487 let target_chunks = parallelism.saturating_mul(2).max(2); let chunk_size = ((total_size / target_chunks).max(MIN_CHUNK_SIZE)).min(64 * 1024 * 1024);
489
490 let mut start = start_from;
491 while start < total_size {
492 let end = (start + chunk_size).min(total_size);
493 chunks.push((start, end));
494 start = end;
495 }
496
497 Ok(chunks)
498 }
499
500 fn download_whole(
501 &self,
502 file: &File,
503 offset: u64,
504 progress: Option<Arc<Mutex<ProgressBar>>>,
505 ) -> Result<(), Box<dyn Error + Send + Sync>> {
506 let response = self.client.get(&self.url).send()?;
507 if offset > 0 {
508 return Err("Server does not support range; cannot resume partial file".into());
510 }
511
512 let mut reader = BufReader::new(response);
513 let mut f = file.try_clone()?;
514 f.seek(SeekFrom::Start(0))?;
515
516 struct ProgressWriter<'a, W> {
517 inner: W,
518 progress: Option<Arc<Mutex<ProgressBar>>>,
519 callback: Option<&'a Arc<dyn Fn(f32) + Send + Sync>>,
520 }
521
522 impl<'a, W: Write> Write for ProgressWriter<'a, W> {
523 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
524 let n = self.inner.write(buf)?;
525 if let Some(ref bar) = self.progress {
526 let guard = bar.lock().unwrap();
527 guard.inc(n as u64);
528 if let Some(cb) = self.callback {
529 let pos = guard.position();
530 let len = guard.length().unwrap_or(1);
531 drop(guard);
532 (cb)(pos as f32 / len as f32);
533 }
534 }
535 Ok(n)
536 }
537
538 fn flush(&mut self) -> std::io::Result<()> {
539 self.inner.flush()
540 }
541 }
542
543 let mut writer = ProgressWriter {
544 inner: f,
545 progress,
546 callback: self.progress_callback.as_ref(),
547 };
548 std::io::copy(&mut reader, &mut writer)?;
549
550 Ok(())
551 }
552
553 fn download_chunks_parallel(
554 &self,
555 chunks: Vec<(u64, u64)>,
556 file: &File,
557 progress: Option<Arc<Mutex<ProgressBar>>>,
558 ) -> Result<(), Box<dyn Error + Send + Sync>> {
559 let file = Arc::new(file);
560 let client = Arc::new(self.client.clone());
561 let url = Arc::new(self.url.clone());
562 let _optimizer = Arc::new(self.optimizer.clone());
563 let progress_callback = self.progress_callback.clone();
564 let cancel_token = self.cancel_token.clone();
565
566 let total_bytes: u64 = chunks.iter().map(|(s, e)| e - s).sum();
568 let downloaded_bytes = Arc::new(AtomicU64::new(0));
569 let last_print_time = Arc::new(Mutex::new(Instant::now()));
570
571 chunks.par_iter().try_for_each(|&(start, end)| {
572 if cancel_token.load(Ordering::Relaxed) {
574 return Err("Download cancelled".into());
575 }
576
577 let range = format!("bytes={}-{}", start, end - 1);
578 let range_header = reqwest::header::HeaderValue::from_str(&range)
579 .map_err(|e| format!("Invalid range header {}: {}", range, e))?;
580
581 for retry in 0..=MAX_RETRIES {
582 if cancel_token.load(Ordering::Relaxed) {
584 return Err("Download cancelled".into());
585 }
586
587 let request = client.get(url.as_str());
588 let request = request.header(reqwest::header::RANGE, range_header.clone());
589
590 match request.send() {
591 Ok(mut response) => {
592 let status = response.status();
593 if status == reqwest::StatusCode::PARTIAL_CONTENT {
594 let mut current_pos = start;
598 let mut buffer = [0u8; 16384];
599
600 while current_pos < end {
601 if cancel_token.load(Ordering::Relaxed) {
603 return Err("Download cancelled".into());
604 }
605
606 let limit = (end - current_pos).min(buffer.len() as u64);
607 let n = response.read(&mut buffer[..limit as usize])?;
608 if n == 0 {
609 break;
610 }
611
612 #[cfg(target_family = "unix")]
613 file.write_at(&buffer[..n], current_pos)?;
614
615 #[cfg(target_family = "windows")]
616 file.seek_write(&buffer[..n], current_pos)?;
617
618 current_pos += n as u64;
619
620 let new_downloaded = downloaded_bytes
622 .fetch_add(n as u64, Ordering::Relaxed)
623 + n as u64;
624
625 {
627 let mut last_time = last_print_time.lock().unwrap();
628 if last_time.elapsed() >= Duration::from_millis(200) {
629 let percent = (new_downloaded as f64 / total_bytes as f64
630 * 100.0)
631 .min(100.0);
632 println!(
634 "PROGRESS: {:.1}% ({}/{})",
635 percent, new_downloaded, total_bytes
636 );
637 *last_time = Instant::now();
638 }
639 }
640
641 if let Some(ref bar) = progress {
642 let guard = bar.lock().unwrap();
643 guard.inc(n as u64);
644 if let Some(ref cb) = progress_callback {
645 let pos = guard.position();
646 let len = guard.length().unwrap_or(1);
647 drop(guard);
648 (cb)(pos as f32 / len as f32);
649 }
650 }
651 }
652
653 return Ok::<(), Box<dyn Error + Send + Sync>>(());
654 } else if status == reqwest::StatusCode::OK {
655 return Err(format!(
656 "Server ignored range request for chunk {}-{}; refusing to write mismatched data",
657 start, end
658 )
659 .into());
660 } else if status.as_u16() == 416 {
661 if retry == MAX_RETRIES {
662 return Err(format!(
663 "Failed to download chunk {}-{}: HTTP {}",
664 start, end, status
665 )
666 .into());
667 }
668 std::thread::sleep(Duration::from_millis(250 * (retry as u64 + 1)));
669 }
670 }
671 Err(e) => {
672 if retry == MAX_RETRIES {
673 return Err(format!(
674 "Failed to download chunk {}-{}: {}",
675 start, end, e
676 )
677 .into());
678 }
679 std::thread::sleep(Duration::from_millis(250 * (retry as u64 + 1)));
680 }
681 }
682 }
683 Err(format!("Failed to download chunk {}-{} after retries", start, end).into())
684 })?;
685
686 Ok(())
687 }
688
689 fn verify_integrity(&self, expected_size: u64) -> Result<(), Box<dyn Error + Send + Sync>> {
690 let metadata = std::fs::metadata(&self.output_path)?;
691 let actual_size = metadata.len();
692
693 if actual_size != expected_size {
694 return Err(format!(
695 "File size mismatch: expected {} bytes, got {} bytes",
696 expected_size, actual_size
697 )
698 .into());
699 }
700
701 self.send_status(&format!("File size verified: {} bytes", actual_size));
702
703 self.send_status("Calculating SHA256 hash...");
705
706 let mut file = File::open(&self.output_path)?;
707 let mut hasher = Sha256::new();
708 let mut buffer = [0; 8192];
709 loop {
710 let n = file.read(&mut buffer)?;
711 if n == 0 {
712 break;
713 }
714 hasher.update(&buffer[..n]);
715 }
716 let hash = hasher.finalize();
717 let hash_hex = hex::encode(hash);
718
719 self.send_status(&format!("SHA256 hash: {}", hash_hex));
720 if let Some(expected_sha256) = &self.expected_sha256 {
721 let expected_sha256 = expected_sha256.trim().to_ascii_lowercase();
722 if hash_hex != expected_sha256 {
723 return Err(format!(
724 "SHA256 mismatch: expected {}, got {}",
725 expected_sha256, hash_hex
726 )
727 .into());
728 }
729 self.send_status("SHA256 matches expected hash.");
730 }
731 self.send_status("Integrity check passed - file is not corrupted");
732
733 Ok(())
734 }
735}