use std::process::Command; use std::path::Path; use std::fs; use std::fmt; use std::error::Error; use std::io; // Define a custom error type for download operations #[derive(Debug)] pub enum DownloadError { CreateDirectoryFailed(io::Error), CurlExecutionFailed(io::Error), DownloadFailed(String), FileMetadataError(io::Error), FileTooSmall(i64, i64), RemoveFileFailed(io::Error), ExtractionFailed(String), CommandExecutionFailed(io::Error), InvalidUrl(String), NotAFile(String), PlatformNotSupported(String), InstallationFailed(String), } // Implement Display for DownloadError impl fmt::Display for DownloadError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { DownloadError::CreateDirectoryFailed(e) => write!(f, "Error creating directories: {}", e), DownloadError::CurlExecutionFailed(e) => write!(f, "Error executing curl: {}", e), DownloadError::DownloadFailed(url) => write!(f, "Error downloading url: {}", url), DownloadError::FileMetadataError(e) => write!(f, "Error getting file metadata: {}", e), DownloadError::FileTooSmall(size, min) => write!(f, "Error: Downloaded file is too small ({}KB < {}KB)", size, min), DownloadError::RemoveFileFailed(e) => write!(f, "Error removing file: {}", e), DownloadError::ExtractionFailed(e) => write!(f, "Error extracting archive: {}", e), DownloadError::CommandExecutionFailed(e) => write!(f, "Error executing command: {}", e), DownloadError::InvalidUrl(url) => write!(f, "Invalid URL: {}", url), DownloadError::NotAFile(path) => write!(f, "Not a file: {}", path), DownloadError::PlatformNotSupported(msg) => write!(f, "{}", msg), DownloadError::InstallationFailed(msg) => write!(f, "{}", msg), } } } // Implement Error trait for DownloadError impl Error for DownloadError { fn source(&self) -> Option<&(dyn Error + 'static)> { match self { DownloadError::CreateDirectoryFailed(e) => Some(e), DownloadError::CurlExecutionFailed(e) => Some(e), DownloadError::FileMetadataError(e) => Some(e), DownloadError::RemoveFileFailed(e) => Some(e), DownloadError::CommandExecutionFailed(e) => Some(e), _ => None, } } } /** * Download a file from URL to destination using the curl command. * This function is primarily intended for downloading archives that will be extracted * to a directory. * * # Arguments * * * `url` - The URL to download from * * `dest` - The destination directory where the file will be saved or extracted * * `min_size_kb` - Minimum required file size in KB (0 for no minimum) * * # Returns * * * `Ok(String)` - The path where the file was saved or extracted * * `Err(DownloadError)` - An error if the download failed * * # Examples * * ``` * // Download a file with no minimum size requirement * let path = download("https://example.com/file.txt", "/tmp/", 0)?; * * // Download a file with minimum size requirement of 100KB * let path = download("https://example.com/file.zip", "/tmp/", 100)?; * ``` * * # Notes * * If the URL ends with .tar.gz, .tgz, .tar, or .zip, the file will be automatically * extracted to the destination directory. */ pub fn download(url: &str, dest: &str, min_size_kb: i64) -> Result { // Create parent directories if they don't exist let dest_path = Path::new(dest); fs::create_dir_all(dest_path).map_err(DownloadError::CreateDirectoryFailed)?; // Extract filename from URL let filename = match url.split('/').last() { Some(name) => name, None => return Err(DownloadError::InvalidUrl("cannot extract filename".to_string())) }; // Create a full path for the downloaded file let file_path = format!("{}/{}", dest.trim_end_matches('/'), filename); // Create a temporary path for downloading let temp_path = format!("{}.download", file_path); // Use curl to download the file with progress bar println!("Downloading {} to {}", url, file_path); let output = Command::new("curl") .args(&["--progress-bar", "--location", "--fail", "--output", &temp_path, url]) .status() .map_err(DownloadError::CurlExecutionFailed)?; if !output.success() { return Err(DownloadError::DownloadFailed(url.to_string())); } // Show file size after download match fs::metadata(&temp_path) { Ok(metadata) => { let size_bytes = metadata.len(); let size_kb = size_bytes / 1024; let size_mb = size_kb / 1024; if size_mb > 1 { println!("Download complete! File size: {:.2} MB", size_bytes as f64 / (1024.0 * 1024.0)); } else { println!("Download complete! File size: {:.2} KB", size_bytes as f64 / 1024.0); } }, Err(_) => println!("Download complete!"), } // Check file size if minimum size is specified if min_size_kb > 0 { let metadata = fs::metadata(&temp_path).map_err(DownloadError::FileMetadataError)?; let size_kb = metadata.len() as i64 / 1024; if size_kb < min_size_kb { fs::remove_file(&temp_path).map_err(DownloadError::RemoveFileFailed)?; return Err(DownloadError::FileTooSmall(size_kb, min_size_kb)); } } // Check if it's a compressed file that needs extraction let lower_url = url.to_lowercase(); let is_archive = lower_url.ends_with(".tar.gz") || lower_url.ends_with(".tgz") || lower_url.ends_with(".tar") || lower_url.ends_with(".zip"); if is_archive { // Extract the file using the appropriate command with progress indication println!("Extracting {} to {}", temp_path, dest); let output = if lower_url.ends_with(".zip") { Command::new("unzip") .args(&["-o", &temp_path, "-d", dest]) // Removed -q for verbosity .status() } else if lower_url.ends_with(".tar.gz") || lower_url.ends_with(".tgz") { Command::new("tar") .args(&["-xzvf", &temp_path, "-C", dest]) // Added v for verbosity .status() } else { Command::new("tar") .args(&["-xvf", &temp_path, "-C", dest]) // Added v for verbosity .status() }; match output { Ok(status) => { if !status.success() { return Err(DownloadError::ExtractionFailed("Error extracting archive".to_string())); } }, Err(e) => return Err(DownloadError::CommandExecutionFailed(e)), } // Show number of extracted files match fs::read_dir(dest) { Ok(entries) => { let count = entries.count(); println!("Extraction complete! Extracted {} files/directories", count); }, Err(_) => println!("Extraction complete!"), } // Remove the temporary file fs::remove_file(&temp_path).map_err(DownloadError::RemoveFileFailed)?; Ok(dest.to_string()) } else { // Just rename the temporary file to the final destination fs::rename(&temp_path, &file_path).map_err(|e| DownloadError::CreateDirectoryFailed(e))?; Ok(file_path) } } /** * Download a file from URL to a specific file destination using the curl command. * * # Arguments * * * `url` - The URL to download from * * `dest` - The destination file path where the file will be saved * * `min_size_kb` - Minimum required file size in KB (0 for no minimum) * * # Returns * * * `Ok(String)` - The path where the file was saved * * `Err(DownloadError)` - An error if the download failed * * # Examples * * ``` * // Download a file with no minimum size requirement * let path = download_file("https://example.com/file.txt", "/tmp/file.txt", 0)?; * * // Download a file with minimum size requirement of 100KB * let path = download_file("https://example.com/file.zip", "/tmp/file.zip", 100)?; * ``` */ pub fn download_file(url: &str, dest: &str, min_size_kb: i64) -> Result { // Create parent directories if they don't exist let dest_path = Path::new(dest); if let Some(parent) = dest_path.parent() { fs::create_dir_all(parent).map_err(DownloadError::CreateDirectoryFailed)?; } // Create a temporary path for downloading let temp_path = format!("{}.download", dest); // Use curl to download the file with progress bar println!("Downloading {} to {}", url, dest); let output = Command::new("curl") .args(&["--progress-bar", "--location", "--fail", "--output", &temp_path, url]) .status() .map_err(DownloadError::CurlExecutionFailed)?; if !output.success() { return Err(DownloadError::DownloadFailed(url.to_string())); } // Show file size after download match fs::metadata(&temp_path) { Ok(metadata) => { let size_bytes = metadata.len(); let size_kb = size_bytes / 1024; let size_mb = size_kb / 1024; if size_mb > 1 { println!("Download complete! File size: {:.2} MB", size_bytes as f64 / (1024.0 * 1024.0)); } else { println!("Download complete! File size: {:.2} KB", size_bytes as f64 / 1024.0); } }, Err(_) => println!("Download complete!"), } // Check file size if minimum size is specified if min_size_kb > 0 { let metadata = fs::metadata(&temp_path).map_err(DownloadError::FileMetadataError)?; let size_kb = metadata.len() as i64 / 1024; if size_kb < min_size_kb { fs::remove_file(&temp_path).map_err(DownloadError::RemoveFileFailed)?; return Err(DownloadError::FileTooSmall(size_kb, min_size_kb)); } } // Rename the temporary file to the final destination fs::rename(&temp_path, dest).map_err(|e| DownloadError::CreateDirectoryFailed(e))?; Ok(dest.to_string()) } /** * Make a file executable (equivalent to chmod +x). * * # Arguments * * * `path` - The path to the file to make executable * * # Returns * * * `Ok(String)` - A success message including the path * * `Err(DownloadError)` - An error if the operation failed * * # Examples * * ``` * // Make a file executable * chmod_exec("/path/to/file")?; * ``` */ pub fn chmod_exec(path: &str) -> Result { let path_obj = Path::new(path); // Check if the path exists and is a file if !path_obj.exists() { return Err(DownloadError::NotAFile(format!("Path does not exist: {}", path))); } if !path_obj.is_file() { return Err(DownloadError::NotAFile(format!("Path is not a file: {}", path))); } // Get current permissions let metadata = fs::metadata(path).map_err(DownloadError::FileMetadataError)?; let mut permissions = metadata.permissions(); // Set executable bit for user, group, and others #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; let mode = permissions.mode(); // Add executable bit for user, group, and others (equivalent to +x) let new_mode = mode | 0o111; permissions.set_mode(new_mode); } #[cfg(not(unix))] { // On non-Unix platforms, we can't set executable bit directly // Just return success with a warning return Ok(format!("Made {} executable (note: non-Unix platform, may not be fully supported)", path)); } // Apply the new permissions fs::set_permissions(path, permissions).map_err(|e| DownloadError::CommandExecutionFailed(io::Error::new( io::ErrorKind::Other, format!("Failed to set executable permissions: {}", e) )) )?; Ok(format!("Made {} executable", path)) } /** * Download a file and install it if it's a supported package format. * * # Arguments * * * `url` - The URL to download from * * `min_size_kb` - Minimum required file size in KB (0 for no minimum) * * # Returns * * * `Ok(String)` - The path where the file was saved or extracted * * `Err(DownloadError)` - An error if the download or installation failed * * # Examples * * ``` * // Download and install a .deb package * let result = download_install("https://example.com/package.deb", 100)?; * ``` * * # Notes * * Currently only supports .deb packages on Debian-based systems. * For other file types, it behaves the same as the download function. */ pub fn download_install(url: &str, min_size_kb: i64) -> Result { // Extract filename from URL let filename = match url.split('/').last() { Some(name) => name, None => return Err(DownloadError::InvalidUrl("cannot extract filename".to_string())) }; // Create a proper destination path let dest_path = format!("/tmp/{}", filename); // Check if it's a compressed file that needs extraction let lower_url = url.to_lowercase(); let is_archive = lower_url.ends_with(".tar.gz") || lower_url.ends_with(".tgz") || lower_url.ends_with(".tar") || lower_url.ends_with(".zip"); let download_result = if is_archive { // For archives, use the directory-based download function download(url, "/tmp", min_size_kb)? } else { // For regular files, use the file-specific download function download_file(url, &dest_path, min_size_kb)? }; // Check if the downloaded result is a file let path = Path::new(&dest_path); if !path.is_file() { return Ok(download_result); // Not a file, might be an extracted directory } // Check if it's a .deb package if dest_path.to_lowercase().ends_with(".deb") { // Check if we're on a Debian-based platform let platform_check = Command::new("sh") .arg("-c") .arg("command -v dpkg > /dev/null && command -v apt > /dev/null || test -f /etc/debian_version") .status(); match platform_check { Ok(status) => { if !status.success() { return Err(DownloadError::PlatformNotSupported( "Cannot install .deb package: not on a Debian-based system".to_string() )); } }, Err(_) => return Err(DownloadError::PlatformNotSupported( "Failed to check system compatibility for .deb installation".to_string() )), } // Install the .deb package non-interactively println!("Installing package: {}", dest_path); let install_result = Command::new("sudo") .args(&["dpkg", "--install", &dest_path]) .status(); match install_result { Ok(status) => { if !status.success() { // If dpkg fails, try to fix dependencies and retry println!("Attempting to resolve dependencies..."); let fix_deps = Command::new("sudo") .args(&["apt-get", "install", "-f", "-y"]) .status(); if let Ok(fix_status) = fix_deps { if !fix_status.success() { return Err(DownloadError::InstallationFailed( "Failed to resolve package dependencies".to_string() )); } } else { return Err(DownloadError::InstallationFailed( "Failed to resolve package dependencies".to_string() )); } } println!("Package installation completed successfully"); }, Err(e) => return Err(DownloadError::CommandExecutionFailed(e)), } } Ok(download_result) }