sal/src/os/download.rs
Mahmoud Emad 1ebd591f19 feat: Enhance documentation and add .gitignore entries
- Add new documentation sections for PostgreSQL installer
  functions and usage examples.  Improves clarity and
  completeness of the documentation.
- Add new files and patterns to .gitignore to prevent
  unnecessary files from being committed to the repository.
  Improves repository cleanliness and reduces clutter.
2025-05-10 08:50:05 +03:00

523 lines
17 KiB
Rust

use std::error::Error;
use std::fmt;
use std::fs;
use std::io;
use std::path::Path;
use std::process::Command;
// 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
*
* ```no_run
* use sal::os::download;
*
* fn main() -> Result<(), Box<dyn std::error::Error>> {
* // 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)?;
*
* Ok(())
* }
* ```
*
* # 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<String, DownloadError> {
// 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
*
* ```no_run
* use sal::os::download_file;
*
* fn main() -> Result<(), Box<dyn std::error::Error>> {
* // 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)?;
*
* Ok(())
* }
* ```
*/
pub fn download_file(url: &str, dest: &str, min_size_kb: i64) -> Result<String, DownloadError> {
// 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
*
* ```no_run
* use sal::os::chmod_exec;
*
* fn main() -> Result<(), Box<dyn std::error::Error>> {
* // Make a file executable
* chmod_exec("/path/to/file")?;
* Ok(())
* }
* ```
*/
pub fn chmod_exec(path: &str) -> Result<String, DownloadError> {
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
*
* ```no_run
* use sal::os::download_install;
*
* fn main() -> Result<(), Box<dyn std::error::Error>> {
* // Download and install a .deb package
* let result = download_install("https://example.com/package.deb", 100)?;
* Ok(())
* }
* ```
*
* # 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<String, DownloadError> {
// 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)
}