- 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.
523 lines
17 KiB
Rust
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)
|
|
}
|