This commit is contained in:
2025-04-02 07:33:57 +02:00
parent d8de0d7ebf
commit 5678a9aa35
16 changed files with 2467 additions and 0 deletions

289
src/os/download.rs Normal file
View 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)
}

580
src/os/fs.rs Normal file
View File

@@ -0,0 +1,580 @@
use std::fs;
use std::path::Path;
use std::process::Command;
use std::fmt;
use std::error::Error;
use std::io;
// Define a custom error type for file system operations
#[derive(Debug)]
pub enum FsError {
DirectoryNotFound(String),
FileNotFound(String),
CreateDirectoryFailed(io::Error),
CopyFailed(io::Error),
DeleteFailed(io::Error),
CommandFailed(String),
CommandExecutionError(io::Error),
InvalidGlobPattern(glob::PatternError),
NotADirectory(String),
NotAFile(String),
UnknownFileType(String),
MetadataError(io::Error),
}
// Implement Display for FsError
impl fmt::Display for FsError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FsError::DirectoryNotFound(dir) => write!(f, "Directory '{}' does not exist", dir),
FsError::FileNotFound(pattern) => write!(f, "No files found matching '{}'", pattern),
FsError::CreateDirectoryFailed(e) => write!(f, "Failed to create parent directories: {}", e),
FsError::CopyFailed(e) => write!(f, "Failed to copy file: {}", e),
FsError::DeleteFailed(e) => write!(f, "Failed to delete: {}", e),
FsError::CommandFailed(e) => write!(f, "{}", e),
FsError::CommandExecutionError(e) => write!(f, "Failed to execute command: {}", e),
FsError::InvalidGlobPattern(e) => write!(f, "Invalid glob pattern: {}", e),
FsError::NotADirectory(path) => write!(f, "Path '{}' exists but is not a directory", path),
FsError::NotAFile(path) => write!(f, "Path '{}' is not a regular file", path),
FsError::UnknownFileType(path) => write!(f, "Unknown file type at '{}'", path),
FsError::MetadataError(e) => write!(f, "Failed to get file metadata: {}", e),
}
}
}
// Implement Error trait for FsError
impl Error for FsError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
FsError::CreateDirectoryFailed(e) => Some(e),
FsError::CopyFailed(e) => Some(e),
FsError::DeleteFailed(e) => Some(e),
FsError::CommandExecutionError(e) => Some(e),
FsError::InvalidGlobPattern(e) => Some(e),
FsError::MetadataError(e) => Some(e),
_ => None,
}
}
}
/**
* Recursively copy a file or directory from source to destination.
*
* # Arguments
*
* * `src` - The source path, which can include wildcards
* * `dest` - The destination path
*
* # Returns
*
* * `Ok(String)` - A success message indicating what was copied
* * `Err(FsError)` - An error if the copy operation failed
*
* # Examples
*
* ```
* // Copy a single file
* let result = copy("file.txt", "backup/file.txt")?;
*
* // Copy multiple files using wildcards
* let result = copy("*.txt", "backup/")?;
*
* // Copy a directory recursively
* let result = copy("src_dir", "dest_dir")?;
* ```
*/
pub fn copy(src: &str, dest: &str) -> Result<String, FsError> {
let dest_path = Path::new(dest);
// Check if source path contains wildcards
if src.contains('*') || src.contains('?') || src.contains('[') {
// Create parent directories for destination if needed
if let Some(parent) = dest_path.parent() {
fs::create_dir_all(parent).map_err(FsError::CreateDirectoryFailed)?;
}
// Use glob to expand wildcards
let entries = glob::glob(src).map_err(FsError::InvalidGlobPattern)?;
let paths: Vec<_> = entries
.filter_map(Result::ok)
.collect();
if paths.is_empty() {
return Err(FsError::FileNotFound(src.to_string()));
}
let mut success_count = 0;
let dest_is_dir = dest_path.exists() && dest_path.is_dir();
for path in paths {
let target_path = if dest_is_dir {
// If destination is a directory, copy the file into it
dest_path.join(path.file_name().unwrap_or_default())
} else {
// Otherwise use the destination as is (only makes sense for single file)
dest_path.to_path_buf()
};
if path.is_file() {
// Copy file
if let Err(e) = fs::copy(&path, &target_path) {
println!("Warning: Failed to copy {}: {}", path.display(), e);
} else {
success_count += 1;
}
} else if path.is_dir() {
// For directories, use platform-specific command
#[cfg(target_os = "windows")]
let output = Command::new("xcopy")
.args(&["/E", "/I", "/H", "/Y",
&path.to_string_lossy(),
&target_path.to_string_lossy()])
.status();
#[cfg(not(target_os = "windows"))]
let output = Command::new("cp")
.args(&["-R",
&path.to_string_lossy(),
&target_path.to_string_lossy()])
.status();
match output {
Ok(status) => {
if status.success() {
success_count += 1;
}
},
Err(e) => println!("Warning: Failed to copy directory {}: {}", path.display(), e),
}
}
}
if success_count > 0 {
Ok(format!("Successfully copied {} items from '{}' to '{}'",
success_count, src, dest))
} else {
Err(FsError::CommandFailed(format!("Failed to copy any files from '{}' to '{}'", src, dest)))
}
} else {
// Handle non-wildcard paths normally
let src_path = Path::new(src);
// Check if source exists
if !src_path.exists() {
return Err(FsError::FileNotFound(src.to_string()));
}
// Create parent directories if they don't exist
if let Some(parent) = dest_path.parent() {
fs::create_dir_all(parent).map_err(FsError::CreateDirectoryFailed)?;
}
// Copy based on source type
if src_path.is_file() {
// Copy file
fs::copy(src_path, dest_path).map_err(FsError::CopyFailed)?;
Ok(format!("Successfully copied file '{}' to '{}'", src, dest))
} else if src_path.is_dir() {
// For directories, use platform-specific command
#[cfg(target_os = "windows")]
let output = Command::new("xcopy")
.args(&["/E", "/I", "/H", "/Y", src, dest])
.output();
#[cfg(not(target_os = "windows"))]
let output = Command::new("cp")
.args(&["-R", src, dest])
.output();
match output {
Ok(out) => {
if out.status.success() {
Ok(format!("Successfully copied directory '{}' to '{}'", src, dest))
} else {
let error = String::from_utf8_lossy(&out.stderr);
Err(FsError::CommandFailed(format!("Failed to copy directory: {}", error)))
}
},
Err(e) => Err(FsError::CommandExecutionError(e)),
}
} else {
Err(FsError::UnknownFileType(src.to_string()))
}
}
}
/**
* Check if a file or directory exists.
*
* # Arguments
*
* * `path` - The path to check
*
* # Returns
*
* * `bool` - True if the path exists, false otherwise
*
* # Examples
*
* ```
* if exist("file.txt") {
* println!("File exists");
* }
* ```
*/
pub fn exist(path: &str) -> bool {
Path::new(path).exists()
}
/**
* Find a file in a directory (with support for wildcards).
*
* # Arguments
*
* * `dir` - The directory to search in
* * `filename` - The filename pattern to search for (can include wildcards)
*
* # Returns
*
* * `Ok(String)` - The path to the found file
* * `Err(FsError)` - An error if no file is found or multiple files are found
*
* # Examples
*
* ```
* let file_path = find_file("/path/to/dir", "*.txt")?;
* println!("Found file: {}", file_path);
* ```
*/
pub fn find_file(dir: &str, filename: &str) -> Result<String, FsError> {
let dir_path = Path::new(dir);
// Check if directory exists
if !dir_path.exists() || !dir_path.is_dir() {
return Err(FsError::DirectoryNotFound(dir.to_string()));
}
// Use glob to find files - use recursive pattern to find in subdirectories too
let pattern = format!("{}/**/{}", dir, filename);
let entries = glob::glob(&pattern).map_err(FsError::InvalidGlobPattern)?;
let files: Vec<_> = entries
.filter_map(Result::ok)
.filter(|path| path.is_file())
.collect();
match files.len() {
0 => Err(FsError::FileNotFound(filename.to_string())),
1 => Ok(files[0].to_string_lossy().to_string()),
_ => {
// If multiple matches, just return the first one instead of erroring
// This makes wildcard searches more practical
println!("Note: Multiple files found matching '{}', returning first match", filename);
Ok(files[0].to_string_lossy().to_string())
}
}
}
/**
* Find multiple files in a directory (recursive, with support for wildcards).
*
* # Arguments
*
* * `dir` - The directory to search in
* * `filename` - The filename pattern to search for (can include wildcards)
*
* # Returns
*
* * `Ok(Vec<String>)` - A vector of paths to the found files
* * `Err(FsError)` - An error if the directory doesn't exist or the pattern is invalid
*
* # Examples
*
* ```
* let files = find_files("/path/to/dir", "*.txt")?;
* for file in files {
* println!("Found file: {}", file);
* }
* ```
*/
pub fn find_files(dir: &str, filename: &str) -> Result<Vec<String>, FsError> {
let dir_path = Path::new(dir);
// Check if directory exists
if !dir_path.exists() || !dir_path.is_dir() {
return Err(FsError::DirectoryNotFound(dir.to_string()));
}
// Use glob to find files
let pattern = format!("{}/**/{}", dir, filename);
let entries = glob::glob(&pattern).map_err(FsError::InvalidGlobPattern)?;
let files: Vec<String> = entries
.filter_map(Result::ok)
.filter(|path| path.is_file())
.map(|path| path.to_string_lossy().to_string())
.collect();
Ok(files)
}
/**
* Find a directory in a parent directory (with support for wildcards).
*
* # Arguments
*
* * `dir` - The parent directory to search in
* * `dirname` - The directory name pattern to search for (can include wildcards)
*
* # Returns
*
* * `Ok(String)` - The path to the found directory
* * `Err(FsError)` - An error if no directory is found or multiple directories are found
*
* # Examples
*
* ```
* let dir_path = find_dir("/path/to/parent", "sub*")?;
* println!("Found directory: {}", dir_path);
* ```
*/
pub fn find_dir(dir: &str, dirname: &str) -> Result<String, FsError> {
let dir_path = Path::new(dir);
// Check if directory exists
if !dir_path.exists() || !dir_path.is_dir() {
return Err(FsError::DirectoryNotFound(dir.to_string()));
}
// Use glob to find directories
let pattern = format!("{}/{}", dir, dirname);
let entries = glob::glob(&pattern).map_err(FsError::InvalidGlobPattern)?;
let dirs: Vec<_> = entries
.filter_map(Result::ok)
.filter(|path| path.is_dir())
.collect();
match dirs.len() {
0 => Err(FsError::DirectoryNotFound(dirname.to_string())),
1 => Ok(dirs[0].to_string_lossy().to_string()),
_ => Err(FsError::CommandFailed(format!("Multiple directories found matching '{}', expected only one", dirname))),
}
}
/**
* Find multiple directories in a parent directory (recursive, with support for wildcards).
*
* # Arguments
*
* * `dir` - The parent directory to search in
* * `dirname` - The directory name pattern to search for (can include wildcards)
*
* # Returns
*
* * `Ok(Vec<String>)` - A vector of paths to the found directories
* * `Err(FsError)` - An error if the parent directory doesn't exist or the pattern is invalid
*
* # Examples
*
* ```
* let dirs = find_dirs("/path/to/parent", "sub*")?;
* for dir in dirs {
* println!("Found directory: {}", dir);
* }
* ```
*/
pub fn find_dirs(dir: &str, dirname: &str) -> Result<Vec<String>, FsError> {
let dir_path = Path::new(dir);
// Check if directory exists
if !dir_path.exists() || !dir_path.is_dir() {
return Err(FsError::DirectoryNotFound(dir.to_string()));
}
// Use glob to find directories
let pattern = format!("{}/**/{}", dir, dirname);
let entries = glob::glob(&pattern).map_err(FsError::InvalidGlobPattern)?;
let dirs: Vec<String> = entries
.filter_map(Result::ok)
.filter(|path| path.is_dir())
.map(|path| path.to_string_lossy().to_string())
.collect();
Ok(dirs)
}
/**
* Delete a file or directory (defensive - doesn't error if file doesn't exist).
*
* # Arguments
*
* * `path` - The path to delete
*
* # Returns
*
* * `Ok(String)` - A success message indicating what was deleted
* * `Err(FsError)` - An error if the deletion failed
*
* # Examples
*
* ```
* // Delete a file
* let result = delete("file.txt")?;
*
* // Delete a directory and all its contents
* let result = delete("directory/")?;
* ```
*/
pub fn delete(path: &str) -> Result<String, FsError> {
let path_obj = Path::new(path);
// Check if path exists
if !path_obj.exists() {
return Ok(format!("Nothing to delete at '{}'", path));
}
// Delete based on path type
if path_obj.is_file() || path_obj.is_symlink() {
fs::remove_file(path_obj).map_err(FsError::DeleteFailed)?;
Ok(format!("Successfully deleted file '{}'", path))
} else if path_obj.is_dir() {
fs::remove_dir_all(path_obj).map_err(FsError::DeleteFailed)?;
Ok(format!("Successfully deleted directory '{}'", path))
} else {
Err(FsError::UnknownFileType(path.to_string()))
}
}
/**
* Create a directory and all parent directories (defensive - doesn't error if directory exists).
*
* # Arguments
*
* * `path` - The path of the directory to create
*
* # Returns
*
* * `Ok(String)` - A success message indicating the directory was created
* * `Err(FsError)` - An error if the creation failed
*
* # Examples
*
* ```
* let result = mkdir("path/to/new/directory")?;
* println!("{}", result);
* ```
*/
pub fn mkdir(path: &str) -> Result<String, FsError> {
let path_obj = Path::new(path);
// Check if path already exists
if path_obj.exists() {
if path_obj.is_dir() {
return Ok(format!("Directory '{}' already exists", path));
} else {
return Err(FsError::NotADirectory(path.to_string()));
}
}
// Create directory and parents
fs::create_dir_all(path_obj).map_err(FsError::CreateDirectoryFailed)?;
Ok(format!("Successfully created directory '{}'", path))
}
/**
* Get the size of a file in bytes.
*
* # Arguments
*
* * `path` - The path of the file
*
* # Returns
*
* * `Ok(i64)` - The size of the file in bytes
* * `Err(FsError)` - An error if the file doesn't exist or isn't a regular file
*
* # Examples
*
* ```
* let size = file_size("file.txt")?;
* println!("File size: {} bytes", size);
* ```
*/
pub fn file_size(path: &str) -> Result<i64, FsError> {
let path_obj = Path::new(path);
// Check if file exists
if !path_obj.exists() {
return Err(FsError::FileNotFound(path.to_string()));
}
// Check if it's a regular file
if !path_obj.is_file() {
return Err(FsError::NotAFile(path.to_string()));
}
// Get file metadata
let metadata = fs::metadata(path_obj).map_err(FsError::MetadataError)?;
Ok(metadata.len() as i64)
}
/**
* Sync directories using rsync (or platform equivalent).
*
* # Arguments
*
* * `src` - The source directory
* * `dest` - The destination directory
*
* # Returns
*
* * `Ok(String)` - A success message indicating the directories were synced
* * `Err(FsError)` - An error if the sync failed
*
* # Examples
*
* ```
* let result = rsync("source_dir/", "backup_dir/")?;
* println!("{}", result);
* ```
*/
pub fn rsync(src: &str, dest: &str) -> Result<String, FsError> {
let src_path = Path::new(src);
let dest_path = Path::new(dest);
// Check if source exists
if !src_path.exists() {
return Err(FsError::FileNotFound(src.to_string()));
}
// Create parent directories if they don't exist
if let Some(parent) = dest_path.parent() {
fs::create_dir_all(parent).map_err(FsError::CreateDirectoryFailed)?;
}
// Use platform-specific command for syncing
#[cfg(target_os = "windows")]
let output = Command::new("robocopy")
.args(&[src, dest, "/MIR", "/NFL", "/NDL"])
.output();
#[cfg(any(target_os = "macos", target_os = "linux"))]
let output = Command::new("rsync")
.args(&["-a", "--delete", src, dest])
.output();
match output {
Ok(out) => {
if out.status.success() || out.status.code() == Some(1) { // rsync and robocopy return 1 for some non-error cases
Ok(format!("Successfully synced '{}' to '{}'", src, dest))
} else {
let error = String::from_utf8_lossy(&out.stderr);
Err(FsError::CommandFailed(format!("Failed to sync directories: {}", error)))
}
},
Err(e) => Err(FsError::CommandExecutionError(e)),
}
}

2
src/os/mod.rs Normal file
View File

@@ -0,0 +1,2 @@
use fs::*;
use download::*;