Skip to main content

kget/
download.rs

1//! Simple HTTP/HTTPS download functionality.
2//!
3//! This module provides basic download capabilities with automatic retry,
4//! progress tracking, and ISO integrity verification.
5//!
6//! For advanced features like parallel connections and resume support,
7//! see [`AdvancedDownloader`](crate::AdvancedDownloader).
8//!
9//! # Example
10//!
11//! ```rust,no_run
12//! use kget::{download, DownloadOptions, ProxyConfig, Optimizer};
13//!
14//! let options = DownloadOptions {
15//!     quiet_mode: false,
16//!     output_path: Some("./file.zip".to_string()),
17//!     verify_iso: false,
18//! };
19//!
20//! download(
21//!     "https://example.com/file.zip",
22//!     ProxyConfig::default(),
23//!     Optimizer::new(),
24//!     options,
25//!     None,
26//! ).unwrap();
27//! ```
28
29use 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
48/// Check if there's enough disk space for the download.
49///
50/// # Arguments
51///
52/// * `path` - Target file path
53/// * `required_size` - Required space in bytes
54///
55/// # Errors
56///
57/// Returns an error if available space is less than required.
58pub 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
72/// Validate that a filename is safe and valid.
73///
74/// # Errors
75///
76/// Returns an error if the filename:
77/// - Contains directory separators
78/// - Is empty
79pub 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
89/// Download a file from a URL with automatic retry and progress tracking.
90///
91/// This is the simple download function for basic use cases. For parallel
92/// connections and resume support, use [`AdvancedDownloader`](crate::AdvancedDownloader).
93///
94/// # Arguments
95///
96/// * `target` - URL to download
97/// * `proxy` - Proxy configuration (use `ProxyConfig::default()` for no proxy)
98/// * `_optimizer` - Optimizer instance (reserved for future use)
99/// * `options` - Download options (quiet mode, output path, ISO verification)
100/// * `status_callback` - Optional callback for status messages
101///
102/// # Example
103///
104/// ```rust,no_run
105/// use kget::{download, DownloadOptions, ProxyConfig, Optimizer};
106///
107/// download(
108///     "https://releases.ubuntu.com/22.04/ubuntu-22.04-desktop-amd64.iso",
109///     ProxyConfig::default(),
110///     Optimizer::new(),
111///     DownloadOptions {
112///         quiet_mode: false,
113///         output_path: None, // Uses filename from URL
114///         verify_iso: true,  // Verify SHA256 after download
115///     },
116///     None,
117/// ).unwrap();
118/// ```
119///
120/// # Errors
121///
122/// Returns an error if:
123/// - Network connection fails after MAX_RETRIES attempts
124/// - HTTP response indicates an error
125/// - Insufficient disk space
126/// - File cannot be created
127pub 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    // Stream data instead of reading all into memory
289    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
306/// Verify the integrity of an ISO file by calculating its SHA-256 hash.
307///
308/// After download, this function calculates the SHA-256 checksum of the file
309/// and displays it for manual comparison with the source.
310///
311/// # Arguments
312///
313/// * `path` - Path to the ISO file
314/// * `callback` - Optional callback for status messages
315///
316/// # Example
317///
318/// ```rust,no_run
319/// use kget::verify_iso_integrity;
320/// use std::path::Path;
321///
322/// verify_iso_integrity(
323///     Path::new("ubuntu-22.04-desktop-amd64.iso"),
324///     Some(&|msg| println!("Status: {}", msg)),
325/// ).unwrap();
326/// ```
327///
328/// # Output
329///
330/// Prints the SHA256 hash to stdout and sends it via callback if provided.
331pub 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}