...
This commit is contained in:
289
src/os/download.rs
Normal file
289
src/os/download.rs
Normal file
@@ -0,0 +1,289 @@
|
||||
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.
|
||||
*
|
||||
* # Arguments
|
||||
*
|
||||
* * `url` - The URL to download from
|
||||
* * `dest` - The destination 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 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/file.txt", 0)?;
|
||||
*
|
||||
* // Download a file with minimum size requirement of 100KB
|
||||
* let path = download("https://example.com/file.zip", "/tmp/file.zip", 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<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));
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// Create the destination directory
|
||||
fs::create_dir_all(dest).map_err(DownloadError::CreateDirectoryFailed)?;
|
||||
|
||||
// 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, dest).map_err(|e| DownloadError::CreateDirectoryFailed(e))?;
|
||||
|
||||
Ok(dest.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<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);
|
||||
|
||||
let download_result = download(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)
|
||||
}
|
Reference in New Issue
Block a user