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
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
73pub enum ResumePolicy {
74 #[default]
76 Ask,
77 AlwaysResume,
79 AlwaysRestart,
81}
82
83struct TokenBucket {
88 limit_bps: u64,
89 tokens: f64,
90 last_refill: Instant,
91}
92
93impl TokenBucket {
94 fn new(limit_bps: u64) -> Self {
95 Self {
96 limit_bps,
97 tokens: limit_bps as f64,
98 last_refill: Instant::now(),
99 }
100 }
101
102 fn consume(&mut self, n: u64) -> Option<Duration> {
106 let elapsed = self.last_refill.elapsed();
107 let refill = elapsed.as_secs_f64() * self.limit_bps as f64;
108 self.tokens = (self.tokens + refill).min(self.limit_bps as f64);
109 self.last_refill = Instant::now();
110
111 if self.tokens >= n as f64 {
112 self.tokens -= n as f64;
113 None
114 } else {
115 let deficit = n as f64 - self.tokens;
116 self.tokens = 0.0;
117 Some(Duration::from_secs_f64(deficit / self.limit_bps as f64))
118 }
119 }
120}
121
122pub struct AdvancedDownloader {
176 client: Client,
177 url: String,
178 output_path: String,
179 quiet_mode: bool,
180 #[allow(dead_code)]
181 proxy: ProxyConfig,
182 optimizer: Optimizer,
183 progress_callback: Option<Arc<dyn Fn(f32) + Send + Sync>>,
184 status_callback: Option<Arc<dyn Fn(String) + Send + Sync>>,
185 cancel_token: Arc<AtomicBool>,
186 expected_sha256: Option<String>,
187 extra_headers: Vec<(String, String)>,
188 resume_policy: ResumePolicy,
189}
190
191impl AdvancedDownloader {
192 pub fn new(
216 url: String,
217 output_path: String,
218 quiet_mode: bool,
219 proxy_config: ProxyConfig,
220 optimizer: Optimizer,
221 ) -> Result<Self, Box<dyn Error + Send + Sync>> {
222 let _is_iso = url.to_lowercase().ends_with(".iso");
223
224 let mut client_builder = Client::builder()
225 .timeout(std::time::Duration::from_secs(300))
226 .connect_timeout(std::time::Duration::from_secs(20))
227 .user_agent(concat!("KGet/", env!("CARGO_PKG_VERSION")))
228 .no_gzip()
229 .no_deflate();
230
231 if proxy_config.enabled {
232 if let Some(proxy_url) = &proxy_config.url {
233 let proxy = match proxy_config.proxy_type {
234 crate::config::ProxyType::Http => reqwest::Proxy::http(proxy_url),
235 crate::config::ProxyType::Https => reqwest::Proxy::https(proxy_url),
236 crate::config::ProxyType::Socks5 => reqwest::Proxy::all(proxy_url),
237 };
238
239 if let Ok(mut proxy) = proxy {
240 if let (Some(username), Some(password)) =
241 (&proxy_config.username, &proxy_config.password)
242 {
243 proxy = proxy.basic_auth(username, password);
244 }
245 client_builder = client_builder.proxy(proxy);
246 }
247 }
248 }
249
250 let client = client_builder
251 .build()
252 .map_err(|e| format!("Failed to create HTTP client: {}", e))?;
253
254 Ok(Self {
255 client,
256 url,
257 output_path,
258 quiet_mode,
259 proxy: proxy_config,
260 optimizer,
261 progress_callback: None,
262 status_callback: None,
263 cancel_token: Arc::new(AtomicBool::new(false)),
264 expected_sha256: None,
265 extra_headers: Vec::new(),
266 resume_policy: ResumePolicy::default(),
267 })
268 }
269
270 pub fn set_cancel_token(&mut self, token: Arc<AtomicBool>) {
292 self.cancel_token = token;
293 }
294
295 pub fn is_cancelled(&self) -> bool {
297 self.cancel_token.load(Ordering::Relaxed)
298 }
299
300 pub fn set_progress_callback(&mut self, callback: impl Fn(f32) + Send + Sync + 'static) {
314 self.progress_callback = Some(Arc::new(callback));
315 }
316
317 pub fn set_status_callback(&mut self, callback: impl Fn(String) + Send + Sync + 'static) {
331 self.status_callback = Some(Arc::new(callback));
332 }
333
334 pub fn set_expected_sha256(&mut self, expected_sha256: impl Into<String>) {
336 self.expected_sha256 = Some(expected_sha256.into());
337 }
338
339 pub fn set_extra_headers(&mut self, headers: Vec<(String, String)>) {
341 self.extra_headers = headers;
342 }
343
344 pub fn set_resume_policy(&mut self, policy: ResumePolicy) {
349 self.resume_policy = policy;
350 }
351
352 fn apply_headers(&self, mut req: reqwest::blocking::RequestBuilder) -> reqwest::blocking::RequestBuilder {
354 for (name, value) in &self.extra_headers {
355 if let (Ok(n), Ok(v)) = (
356 reqwest::header::HeaderName::from_bytes(name.as_bytes()),
357 reqwest::header::HeaderValue::from_str(value),
358 ) {
359 req = req.header(n, v);
360 }
361 }
362 req
363 }
364
365 fn send_status(&self, msg: &str) {
366 if let Some(cb) = &self.status_callback {
367 cb(msg.to_string());
368 }
369 if !self.quiet_mode {
370 println!("{}", msg);
371 }
372 }
373
374 pub fn download(&self) -> Result<(), Box<dyn Error + Send + Sync>> {
393 let is_iso = self.url.to_lowercase().ends_with(".iso");
394 if !self.quiet_mode {
395 println!("Starting advanced download for: {}", self.url);
396 if is_iso {
397 println!(
398 "Warning: ISO mode active. Disabling optimizations that could corrupt binary data."
399 );
400 }
401 }
402
403 let existing_size = if Path::new(&self.output_path).exists() {
405 let size = std::fs::metadata(&self.output_path)?.len();
406 if !self.quiet_mode {
407 println!("Existing file found with size: {} bytes", size);
408 }
409 Some(size)
410 } else {
411 if !self.quiet_mode {
412 println!("Output file does not exist, starting fresh download");
413 }
414 None
415 };
416
417 if !self.quiet_mode {
419 println!("Querying server for file size and range support...");
420 }
421 let (total_size, supports_range) = self.get_file_size_and_range()?;
422 if !self.quiet_mode {
423 println!("Total file size: {} bytes", total_size);
424 println!("Server supports range requests: {}", supports_range);
425 }
426
427 if let Some(size) = existing_size {
428 if size > total_size {
429 return Err("Existing file is larger than remote; aborting".into());
430 }
431 if !self.quiet_mode {
432 println!("Resuming download from byte: {}", size);
433 }
434 }
435
436 let existing_size = if self.resume_policy == ResumePolicy::AlwaysRestart {
438 None
439 } else {
440 existing_size
441 };
442
443 let progress = if !self.quiet_mode || self.progress_callback.is_some() {
445 let bar = ProgressBar::new(total_size);
446 if let Some(size) = existing_size {
447 bar.set_position(size);
448 }
449 if self.quiet_mode {
450 bar.set_draw_target(indicatif::ProgressDrawTarget::hidden());
451 } else {
452 bar.set_style(ProgressStyle::with_template(
453 "{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})"
454 ).unwrap().progress_chars("#>-"));
455 }
456 Some(Arc::new(Mutex::new(bar)))
457 } else {
458 None
459 };
460
461 if !self.quiet_mode {
463 println!("Preparing output file: {}", self.output_path);
464 }
465 let file = if existing_size.is_some() {
466 File::options()
467 .read(true)
468 .write(true)
469 .open(&self.output_path)?
470 } else {
471 File::create(&self.output_path)?
472 };
473
474 if !supports_range {
476 if !self.quiet_mode {
477 println!("Range requests not supported, falling back to single-threaded download");
478 }
479 self.download_whole(&file, existing_size.unwrap_or(0), progress.clone())?;
480 if let Some(ref bar) = progress {
481 bar.lock()
482 .expect("Progress bar mutex was poisoned")
483 .finish_with_message("Download completed");
484 }
485 if !self.quiet_mode {
486 println!("Single-threaded download completed");
487 }
488 return Ok(());
489 }
490
491 file.set_len(total_size)?;
493 if !self.quiet_mode {
494 println!("File preallocated to {} bytes", total_size);
495 }
496
497 if !self.quiet_mode {
499 println!("Calculating download chunks...");
500 }
501 let chunks = self.calculate_chunks(total_size, existing_size)?;
502 if !self.quiet_mode {
503 println!("Download will be split into {} chunks", chunks.len());
504 }
505
506 let throttle_bucket = self.optimizer.speed_limit.map(|limit| {
508 Arc::new(Mutex::new(TokenBucket::new(limit)))
509 });
510
511 if !self.quiet_mode {
513 println!("Starting parallel chunk downloads...");
514 }
515 self.download_chunks_parallel(
516 chunks,
517 &file,
518 progress.clone(),
519 total_size,
520 existing_size.unwrap_or(0),
521 throttle_bucket,
522 )?;
523
524 if let Some(ref bar) = progress {
525 bar.lock()
526 .expect("Progress bar mutex was poisoned")
527 .finish_with_message("Download completed");
528 }
529
530 if !self.quiet_mode || self.status_callback.is_some() {
532 if is_iso || self.expected_sha256.is_some() {
533 let should_verify = if self.status_callback.is_some()
534 || self.expected_sha256.is_some()
535 {
536 true
537 } else {
538 match self.resume_policy {
539 ResumePolicy::Ask => {
540 println!(
541 "\nThis is an ISO file. Would you like to verify its integrity? (y/N)"
542 );
543 let mut input = String::new();
544 std::io::stdin().read_line(&mut input).is_ok()
545 && input.trim().to_lowercase() == "y"
546 }
547 ResumePolicy::AlwaysResume => true,
548 ResumePolicy::AlwaysRestart => false,
549 }
550 };
551
552 if should_verify {
553 self.verify_integrity(total_size)?;
554 }
555 } else {
556 let metadata = std::fs::metadata(&self.output_path)?;
557 if metadata.len() != total_size {
558 return Err(format!(
559 "File size mismatch: expected {} bytes, got {} bytes",
560 total_size,
561 metadata.len()
562 )
563 .into());
564 }
565 }
566 self.send_status("Advanced download completed successfully!");
567 }
568
569 Ok(())
570 }
571
572 fn get_file_size_and_range(&self) -> Result<(u64, bool), Box<dyn Error + Send + Sync>> {
573 let head_response = self.apply_headers(self.client.head(&self.url)).send();
574 let Ok(response) = head_response else {
575 return self.get_file_size_with_range_probe();
576 };
577
578 if !response.status().is_success() {
579 return self.get_file_size_with_range_probe();
580 }
581
582 let content_length = response
583 .headers()
584 .get(reqwest::header::CONTENT_LENGTH)
585 .and_then(|v| v.to_str().ok())
586 .and_then(|s| s.parse::<u64>().ok());
587
588 let accepts_range = response
589 .headers()
590 .get(reqwest::header::ACCEPT_RANGES)
591 .and_then(|v| v.to_str().ok())
592 .map(|s| s.eq_ignore_ascii_case("bytes"))
593 .unwrap_or(false);
594
595 if let Some(content_length) = content_length {
596 Ok((content_length, accepts_range))
597 } else {
598 self.get_file_size_with_range_probe()
599 }
600 }
601
602 fn get_file_size_with_range_probe(&self) -> Result<(u64, bool), Box<dyn Error + Send + Sync>> {
603 let response = self
604 .apply_headers(self.client.get(&self.url))
605 .header(reqwest::header::RANGE, "bytes=0-0")
606 .send()?;
607
608 if response.status() == reqwest::StatusCode::PARTIAL_CONTENT {
609 if let Some(total) = response
610 .headers()
611 .get(reqwest::header::CONTENT_RANGE)
612 .and_then(|v| v.to_str().ok())
613 .and_then(parse_content_range_total)
614 {
615 return Ok((total, true));
616 }
617 }
618
619 if response.status().is_success() {
620 if let Some(total) = response
621 .headers()
622 .get(reqwest::header::CONTENT_LENGTH)
623 .and_then(|v| v.to_str().ok())
624 .and_then(|s| s.parse::<u64>().ok())
625 {
626 return Ok((total, false));
627 }
628 }
629
630 Err("Could not determine file size".into())
631 }
632
633 fn calculate_chunks(
634 &self,
635 total_size: u64,
636 existing_size: Option<u64>,
637 ) -> Result<Vec<(u64, u64)>, Box<dyn Error + Send + Sync>> {
638 let mut chunks = Vec::new();
639 let start_from = existing_size.unwrap_or(0);
640
641 let configured_parallelism = self.optimizer.max_connections() as u64;
642 let runtime_parallelism = rayon::current_num_threads() as u64;
643 let parallelism = configured_parallelism.min(runtime_parallelism).max(1);
644 let target_chunks = parallelism.saturating_mul(2).max(2); let chunk_size = ((total_size / target_chunks).max(MIN_CHUNK_SIZE)).min(64 * 1024 * 1024);
646
647 let mut start = start_from;
648 while start < total_size {
649 let end = (start + chunk_size).min(total_size);
650 chunks.push((start, end));
651 start = end;
652 }
653
654 Ok(chunks)
655 }
656
657 fn download_whole(
658 &self,
659 file: &File,
660 offset: u64,
661 progress: Option<Arc<Mutex<ProgressBar>>>,
662 ) -> Result<(), Box<dyn Error + Send + Sync>> {
663 let response = self.apply_headers(self.client.get(&self.url)).send()?;
664 if offset > 0 {
665 return Err("Server does not support range; cannot resume partial file".into());
667 }
668
669 let mut reader = BufReader::new(response);
670 let mut f = file.try_clone()?;
671 f.seek(SeekFrom::Start(0))?;
672
673 struct ProgressWriter<'a, W> {
674 inner: W,
675 progress: Option<Arc<Mutex<ProgressBar>>>,
676 callback: Option<&'a Arc<dyn Fn(f32) + Send + Sync>>,
677 }
678
679 impl<'a, W: Write> Write for ProgressWriter<'a, W> {
680 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
681 let n = self.inner.write(buf)?;
682 if let Some(ref bar) = self.progress {
683 let guard = bar.lock().expect("Progress bar mutex was poisoned");
684 guard.inc(n as u64);
685 if let Some(cb) = self.callback {
686 let pos = guard.position();
687 let len = guard.length().unwrap_or(1);
688 drop(guard);
689 (cb)(pos as f32 / len as f32);
690 }
691 }
692 Ok(n)
693 }
694
695 fn flush(&mut self) -> std::io::Result<()> {
696 self.inner.flush()
697 }
698 }
699
700 let mut writer = ProgressWriter {
701 inner: f,
702 progress,
703 callback: self.progress_callback.as_ref(),
704 };
705 std::io::copy(&mut reader, &mut writer)?;
706
707 Ok(())
708 }
709
710 fn download_chunks_parallel(
711 &self,
712 chunks: Vec<(u64, u64)>,
713 file: &File,
714 progress: Option<Arc<Mutex<ProgressBar>>>,
715 total_size: u64,
716 initial_downloaded: u64,
717 throttle_bucket: Option<Arc<Mutex<TokenBucket>>>,
718 ) -> Result<(), Box<dyn Error + Send + Sync>> {
719 let file = Arc::new(file);
720 let client = Arc::new(self.client.clone());
721 let url = Arc::new(self.url.clone());
722 let progress_callback = self.progress_callback.clone();
723 let cancel_token = self.cancel_token.clone();
724 let extra_headers = Arc::new(self.extra_headers.clone());
725
726 let downloaded_bytes = Arc::new(AtomicU64::new(initial_downloaded));
728 let last_print_time = Arc::new(Mutex::new(Instant::now()));
729 let quiet_mode = self.quiet_mode;
730
731 chunks.par_iter().try_for_each(|&(start, end)| {
732 if cancel_token.load(Ordering::Relaxed) {
734 return Err("Download cancelled".into());
735 }
736
737 let range = format!("bytes={}-{}", start, end - 1);
738 let range_header = reqwest::header::HeaderValue::from_str(&range)
739 .map_err(|e| format!("Invalid range header {}: {}", range, e))?;
740
741 for retry in 0..=MAX_RETRIES {
742 if cancel_token.load(Ordering::Relaxed) {
744 return Err("Download cancelled".into());
745 }
746
747 let mut request = client.get(url.as_str());
748 for (name, value) in extra_headers.as_ref() {
749 if let (Ok(n), Ok(v)) = (
750 reqwest::header::HeaderName::from_bytes(name.as_bytes()),
751 reqwest::header::HeaderValue::from_str(value),
752 ) {
753 request = request.header(n, v);
754 }
755 }
756 let request = request.header(reqwest::header::RANGE, range_header.clone());
757
758 match request.send() {
759 Ok(mut response) => {
760 let status = response.status();
761 if status == reqwest::StatusCode::PARTIAL_CONTENT {
762 let mut current_pos = start;
766 let mut buffer = [0u8; 16384];
767
768 while current_pos < end {
769 if cancel_token.load(Ordering::Relaxed) {
771 return Err("Download cancelled".into());
772 }
773
774 let limit = (end - current_pos).min(buffer.len() as u64);
775 let n = response.read(&mut buffer[..limit as usize])?;
776 if n == 0 {
777 break;
778 }
779
780 #[cfg(target_family = "unix")]
781 file.write_at(&buffer[..n], current_pos)?;
782
783 #[cfg(target_family = "windows")]
784 file.seek_write(&buffer[..n], current_pos)?;
785
786 current_pos += n as u64;
787
788 let new_downloaded = downloaded_bytes
790 .fetch_add(n as u64, Ordering::Relaxed)
791 + n as u64;
792
793 {
795 let mut last_time = last_print_time.lock().expect("Timer mutex was poisoned");
796 if !quiet_mode && last_time.elapsed() >= Duration::from_millis(200) {
797 let percent = (new_downloaded as f64 / total_size.max(1) as f64
798 * 100.0)
799 .min(100.0);
800 println!(
802 "PROGRESS: {:.1}% ({}/{})",
803 percent, new_downloaded, total_size
804 );
805 *last_time = Instant::now();
806 }
807 }
808
809 if let Some(ref bucket) = throttle_bucket {
810 let sleep_dur = {
811 let mut guard = bucket
812 .lock()
813 .expect("throttle bucket poisoned");
814 guard.consume(n as u64)
815 };
816 if let Some(dur) = sleep_dur {
817 std::thread::sleep(dur);
818 }
819 }
820
821 if let Some(ref bar) = progress {
822 let guard = bar.lock().expect("Progress bar mutex was poisoned");
823 guard.inc(n as u64);
824 if let Some(ref cb) = progress_callback {
825 let pos = guard.position();
826 let len = guard.length().unwrap_or(1);
827 drop(guard);
828 (cb)(pos as f32 / len as f32);
829 }
830 }
831 }
832
833 return Ok::<(), Box<dyn Error + Send + Sync>>(());
834 } else if status == reqwest::StatusCode::OK {
835 return Err(format!(
836 "Server ignored range request for chunk {}-{}; refusing to write mismatched data",
837 start, end
838 )
839 .into());
840 } else if status.as_u16() == 416 {
841 if retry == MAX_RETRIES {
842 return Err(format!(
843 "Failed to download chunk {}-{}: HTTP {}",
844 start, end, status
845 )
846 .into());
847 }
848 std::thread::sleep(Duration::from_millis(250 * (retry as u64 + 1)));
849 }
850 }
851 Err(e) => {
852 if retry == MAX_RETRIES {
853 return Err(format!(
854 "Failed to download chunk {}-{}: {}",
855 start, end, e
856 )
857 .into());
858 }
859 std::thread::sleep(Duration::from_millis(250 * (retry as u64 + 1)));
860 }
861 }
862 }
863 Err(format!("Failed to download chunk {}-{} after retries", start, end).into())
864 })?;
865
866 Ok(())
867 }
868
869 fn verify_integrity(&self, expected_size: u64) -> Result<(), Box<dyn Error + Send + Sync>> {
870 let metadata = std::fs::metadata(&self.output_path)?;
871 let actual_size = metadata.len();
872
873 if actual_size != expected_size {
874 return Err(format!(
875 "File size mismatch: expected {} bytes, got {} bytes",
876 expected_size, actual_size
877 )
878 .into());
879 }
880
881 self.send_status(&format!("File size verified: {} bytes", actual_size));
882
883 self.send_status("Calculating SHA256 hash...");
885
886 let mut file = File::open(&self.output_path)?;
887 let mut hasher = Sha256::new();
888 let mut buffer = [0; 8192];
889 loop {
890 let n = file.read(&mut buffer)?;
891 if n == 0 {
892 break;
893 }
894 hasher.update(&buffer[..n]);
895 }
896 let hash = hasher.finalize();
897 let hash_hex = hex::encode(hash);
898
899 self.send_status(&format!("SHA256 hash: {}", hash_hex));
900 if let Some(expected_sha256) = &self.expected_sha256 {
901 let expected_sha256 = expected_sha256.trim().to_ascii_lowercase();
902 if hash_hex != expected_sha256 {
903 return Err(format!(
904 "SHA256 mismatch: expected {}, got {}",
905 expected_sha256, hash_hex
906 )
907 .into());
908 }
909 self.send_status("SHA256 matches expected hash.");
910 }
911 self.send_status("Integrity check passed - file is not corrupted");
912
913 Ok(())
914 }
915}
916
917fn parse_content_range_total(value: &str) -> Option<u64> {
918 let (_, total) = value.rsplit_once('/')?;
919 if total == "*" {
920 return None;
921 }
922 total.parse::<u64>().ok()
923}
924