Skip to main content

sal_os/
fs.rs

1use dirs;
2use libc;
3use std::error::Error;
4use std::fmt;
5use std::fs;
6use std::io;
7#[cfg(not(target_os = "windows"))]
8use std::os::unix::fs::PermissionsExt;
9use std::path::Path;
10use std::process::Command;
11
12// Define a custom error type for file system operations
13#[derive(Debug)]
14pub enum FsError {
15    DirectoryNotFound(String),
16    FileNotFound(String),
17    CreateDirectoryFailed(io::Error),
18    CopyFailed(io::Error),
19    DeleteFailed(io::Error),
20    CommandFailed(String),
21    CommandNotFound(String),
22    CommandExecutionError(io::Error),
23    InvalidGlobPattern(glob::PatternError),
24    NotADirectory(String),
25    NotAFile(String),
26    UnknownFileType(String),
27    MetadataError(io::Error),
28    ChangeDirFailed(io::Error),
29    ReadFailed(io::Error),
30    WriteFailed(io::Error),
31    AppendFailed(io::Error),
32}
33
34// Implement Display for FsError
35impl fmt::Display for FsError {
36    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37        match self {
38            FsError::DirectoryNotFound(dir) => write!(f, "Directory '{}' does not exist", dir),
39            FsError::FileNotFound(pattern) => write!(f, "No files found matching '{}'", pattern),
40            FsError::CreateDirectoryFailed(e) => {
41                write!(f, "Failed to create parent directories: {}", e)
42            }
43            FsError::CopyFailed(e) => write!(f, "Failed to copy file: {}", e),
44            FsError::DeleteFailed(e) => write!(f, "Failed to delete: {}", e),
45            FsError::CommandFailed(e) => write!(f, "{}", e),
46            FsError::CommandNotFound(e) => write!(f, "Command not found: {}", e),
47            FsError::CommandExecutionError(e) => write!(f, "Failed to execute command: {}", e),
48            FsError::InvalidGlobPattern(e) => write!(f, "Invalid glob pattern: {}", e),
49            FsError::NotADirectory(path) => {
50                write!(f, "Path '{}' exists but is not a directory", path)
51            }
52            FsError::NotAFile(path) => write!(f, "Path '{}' is not a regular file", path),
53            FsError::UnknownFileType(path) => write!(f, "Unknown file type at '{}'", path),
54            FsError::MetadataError(e) => write!(f, "Failed to get file metadata: {}", e),
55            FsError::ChangeDirFailed(e) => write!(f, "Failed to change directory: {}", e),
56            FsError::ReadFailed(e) => write!(f, "Failed to read file: {}", e),
57            FsError::WriteFailed(e) => write!(f, "Failed to write to file: {}", e),
58            FsError::AppendFailed(e) => write!(f, "Failed to append to file: {}", e),
59        }
60    }
61}
62
63// Implement Error trait for FsError
64impl Error for FsError {
65    fn source(&self) -> Option<&(dyn Error + 'static)> {
66        match self {
67            FsError::CreateDirectoryFailed(e) => Some(e),
68            FsError::CopyFailed(e) => Some(e),
69            FsError::DeleteFailed(e) => Some(e),
70            FsError::CommandExecutionError(e) => Some(e),
71            FsError::InvalidGlobPattern(e) => Some(e),
72            FsError::MetadataError(e) => Some(e),
73            FsError::ChangeDirFailed(e) => Some(e),
74            FsError::ReadFailed(e) => Some(e),
75            FsError::WriteFailed(e) => Some(e),
76            FsError::AppendFailed(e) => Some(e),
77            _ => None,
78        }
79    }
80}
81
82#[cfg(not(target_os = "windows"))]
83fn set_executable(path: &Path) -> Result<(), io::Error> {
84    let mut perms = fs::metadata(path)?.permissions();
85    perms.set_mode(0o755); // rwxr-xr-x
86    fs::set_permissions(path, perms)
87}
88
89fn copy_internal(src: &str, dest: &str, make_executable: bool) -> Result<String, FsError> {
90    let dest_path = Path::new(dest);
91
92    // Check if source path contains wildcards
93    if src.contains('*') || src.contains('?') || src.contains('[') {
94        // Create parent directories for destination if needed
95        if let Some(parent) = dest_path.parent() {
96            fs::create_dir_all(parent).map_err(FsError::CreateDirectoryFailed)?;
97        }
98
99        // Use glob to expand wildcards
100        let entries = glob::glob(src).map_err(FsError::InvalidGlobPattern)?;
101
102        let paths: Vec<_> = entries.filter_map(Result::ok).collect();
103
104        if paths.is_empty() {
105            return Err(FsError::FileNotFound(src.to_string()));
106        }
107
108        let mut success_count = 0;
109        let dest_is_dir = dest_path.exists() && dest_path.is_dir();
110
111        for path in paths {
112            let target_path = if dest_is_dir {
113                // If destination is a directory, copy the file into it
114                if path.is_file() {
115                    // For files, just use the filename
116                    dest_path.join(path.file_name().unwrap_or_default())
117                } else if path.is_dir() {
118                    // For directories, use the directory name
119                    dest_path.join(path.file_name().unwrap_or_default())
120                } else {
121                    // Fallback
122                    dest_path.join(path.file_name().unwrap_or_default())
123                }
124            } else {
125                // Otherwise use the destination as is (only makes sense for single file)
126                dest_path.to_path_buf()
127            };
128
129            if path.is_file() {
130                // Copy file
131                if let Err(e) = fs::copy(&path, &target_path) {
132                    println!("Warning: Failed to copy {}: {}", path.display(), e);
133                } else {
134                    success_count += 1;
135                    if make_executable {
136                        #[cfg(not(target_os = "windows"))]
137                        {
138                            if let Err(e) = set_executable(&target_path) {
139                                println!(
140                                    "Warning: Failed to make {} executable: {}",
141                                    target_path.display(),
142                                    e
143                                );
144                            }
145                        }
146                    }
147                }
148            } else if path.is_dir() {
149                // For directories, use platform-specific command
150                #[cfg(target_os = "windows")]
151                let output = Command::new("xcopy")
152                    .args(&[
153                        "/E",
154                        "/I",
155                        "/H",
156                        "/Y",
157                        &path.to_string_lossy(),
158                        &target_path.to_string_lossy(),
159                    ])
160                    .status();
161
162                #[cfg(not(target_os = "windows"))]
163                let output = Command::new("cp")
164                    .args(&[
165                        "-R",
166                        &path.to_string_lossy(),
167                        &target_path.to_string_lossy(),
168                    ])
169                    .status();
170
171                match output {
172                    Ok(status) => {
173                        if status.success() {
174                            success_count += 1;
175                        }
176                    }
177                    Err(e) => println!(
178                        "Warning: Failed to copy directory {}: {}",
179                        path.display(),
180                        e
181                    ),
182                }
183            }
184        }
185
186        if success_count > 0 {
187            Ok(format!(
188                "Successfully copied {} items from '{}' to '{}'",
189                success_count, src, dest
190            ))
191        } else {
192            Err(FsError::CommandFailed(format!(
193                "Failed to copy any files from '{}' to '{}'",
194                src, dest
195            )))
196        }
197    } else {
198        // Handle non-wildcard paths normally
199        let src_path = Path::new(src);
200
201        // Check if source exists
202        if !src_path.exists() {
203            return Err(FsError::FileNotFound(src.to_string()));
204        }
205
206        // Create parent directories if they don't exist
207        if let Some(parent) = dest_path.parent() {
208            fs::create_dir_all(parent).map_err(FsError::CreateDirectoryFailed)?;
209        }
210
211        // Copy based on source type
212        if src_path.is_file() {
213            // If destination is a directory, copy the file into it
214            if dest_path.exists() && dest_path.is_dir() {
215                let file_name = src_path.file_name().unwrap_or_default();
216                let new_dest_path = dest_path.join(file_name);
217                fs::copy(src_path, &new_dest_path).map_err(FsError::CopyFailed)?;
218                if make_executable {
219                    #[cfg(not(target_os = "windows"))]
220                    {
221                        if let Err(e) = set_executable(&new_dest_path) {
222                            println!(
223                                "Warning: Failed to make {} executable: {}",
224                                new_dest_path.display(),
225                                e
226                            );
227                        }
228                    }
229                }
230                Ok(format!(
231                    "Successfully copied file '{}' to '{}/{}'",
232                    src,
233                    dest,
234                    file_name.to_string_lossy()
235                ))
236            } else {
237                // Otherwise copy file to the specified destination
238                fs::copy(src_path, dest_path).map_err(FsError::CopyFailed)?;
239                if make_executable {
240                    #[cfg(not(target_os = "windows"))]
241                    {
242                        if let Err(e) = set_executable(dest_path) {
243                            println!(
244                                "Warning: Failed to make {} executable: {}",
245                                dest_path.display(),
246                                e
247                            );
248                        }
249                    }
250                }
251                Ok(format!("Successfully copied file '{}' to '{}'", src, dest))
252            }
253        } else if src_path.is_dir() {
254            // For directories, use platform-specific command
255            #[cfg(target_os = "windows")]
256            let output = Command::new("xcopy")
257                .args(&["/E", "/I", "/H", "/Y", src, dest])
258                .output();
259
260            #[cfg(not(target_os = "windows"))]
261            let output = Command::new("cp").args(&["-R", src, dest]).output();
262
263            match output {
264                Ok(out) => {
265                    if out.status.success() {
266                        Ok(format!(
267                            "Successfully copied directory '{}' to '{}'",
268                            src, dest
269                        ))
270                    } else {
271                        let error = String::from_utf8_lossy(&out.stderr);
272                        Err(FsError::CommandFailed(format!(
273                            "Failed to copy directory: {}",
274                            error
275                        )))
276                    }
277                }
278                Err(e) => Err(FsError::CommandExecutionError(e)),
279            }
280        } else {
281            Err(FsError::UnknownFileType(src.to_string()))
282        }
283    }
284}
285
286/**
287 * Recursively copy a file or directory from source to destination.
288 *
289 * # Arguments
290 *
291 * * `src` - The source path, which can include wildcards
292 * * `dest` - The destination path
293 *
294 * # Returns
295 *
296 * * `Ok(String)` - A success message indicating what was copied
297 * * `Err(FsError)` - An error if the copy operation failed
298 *
299 * # Examples
300 *
301 * ```no_run
302 * use sal_os::copy;
303 *
304 * fn main() -> Result<(), Box<dyn std::error::Error>> {
305 *     // Copy a single file
306 *     let result = copy("file.txt", "backup/file.txt")?;
307 *
308 *     // Copy multiple files using wildcards
309 *     let result = copy("*.txt", "backup/")?;
310 *
311 *     // Copy a directory recursively
312 *     let result = copy("src_dir", "dest_dir")?;
313 *
314 *     Ok(())
315 * }
316 * ```
317 */
318pub fn copy(src: &str, dest: &str) -> Result<String, FsError> {
319    copy_internal(src, dest, false)
320}
321
322/**
323 * Copy a binary to the correct location based on OS and user privileges.
324 *
325 * # Arguments
326 *
327 * * `src` - The source file path
328 *
329 * # Returns
330 *
331 * * `Ok(String)` - A success message indicating where the file was copied
332 * * `Err(FsError)` - An error if the copy operation failed
333 *
334 * # Examples
335 *
336 * ```no_run
337 * use sal_os::copy_bin;
338 *
339 * fn main() -> Result<(), Box<dyn std::error::Error>> {
340 *     // Copy a binary
341 *     let result = copy_bin("my_binary")?;
342 *     Ok(())
343 * }
344 * ```
345 */
346pub fn copy_bin(src: &str) -> Result<String, FsError> {
347    let dest_path = if cfg!(target_os = "linux") && unsafe { libc::getuid() } == 0 {
348        Path::new("/usr/local/bin").to_path_buf()
349    } else {
350        dirs::home_dir()
351            .ok_or_else(|| FsError::DirectoryNotFound("Home directory not found".to_string()))?
352            .join("hero/bin")
353    };
354
355    let result = copy_internal(src, dest_path.to_str().unwrap(), true);
356    if let Ok(msg) = &result {
357        println!("{}", msg);
358    }
359    result
360}
361
362/**
363 * Check if a file or directory exists.
364 *
365 * # Arguments
366 *
367 * * `path` - The path to check
368 *
369 * # Returns
370 *
371 * * `bool` - True if the path exists, false otherwise
372 *
373 * # Examples
374 *
375 * ```
376 * use sal_os::exist;
377 *
378 * if exist("file.txt") {
379 *     println!("File exists");
380 * }
381 * ```
382 */
383pub fn exist(path: &str) -> bool {
384    Path::new(path).exists()
385}
386
387/**
388 * Find a file in a directory (with support for wildcards).
389 *
390 * # Arguments
391 *
392 * * `dir` - The directory to search in
393 * * `filename` - The filename pattern to search for (can include wildcards)
394 *
395 * # Returns
396 *
397 * * `Ok(String)` - The path to the found file
398 * * `Err(FsError)` - An error if no file is found or multiple files are found
399 *
400 * # Examples
401 *
402 * ```no_run
403 * use sal_os::find_file;
404 *
405 * fn main() -> Result<(), Box<dyn std::error::Error>> {
406 *     let file_path = find_file("/path/to/dir", "*.txt")?;
407 *     println!("Found file: {}", file_path);
408 *     Ok(())
409 * }
410 * ```
411 */
412pub fn find_file(dir: &str, filename: &str) -> Result<String, FsError> {
413    let dir_path = Path::new(dir);
414
415    // Check if directory exists
416    if !dir_path.exists() || !dir_path.is_dir() {
417        return Err(FsError::DirectoryNotFound(dir.to_string()));
418    }
419
420    // Use glob to find files - use recursive pattern to find in subdirectories too
421    let pattern = format!("{}/**/{}", dir, filename);
422    let entries = glob::glob(&pattern).map_err(FsError::InvalidGlobPattern)?;
423
424    let files: Vec<_> = entries
425        .filter_map(Result::ok)
426        .filter(|path| path.is_file())
427        .collect();
428
429    match files.len() {
430        0 => Err(FsError::FileNotFound(filename.to_string())),
431        1 => Ok(files[0].to_string_lossy().to_string()),
432        _ => {
433            // If multiple matches, just return the first one instead of erroring
434            // This makes wildcard searches more practical
435            println!(
436                "Note: Multiple files found matching '{}', returning first match",
437                filename
438            );
439            Ok(files[0].to_string_lossy().to_string())
440        }
441    }
442}
443
444/**
445 * Find multiple files in a directory (recursive, with support for wildcards).
446 *
447 * # Arguments
448 *
449 * * `dir` - The directory to search in
450 * * `filename` - The filename pattern to search for (can include wildcards)
451 *
452 * # Returns
453 *
454 * * `Ok(Vec<String>)` - A vector of paths to the found files
455 * * `Err(FsError)` - An error if the directory doesn't exist or the pattern is invalid
456 *
457 * # Examples
458 *
459 * ```no_run
460 * use sal_os::find_files;
461 *
462 * fn main() -> Result<(), Box<dyn std::error::Error>> {
463 *     let files = find_files("/path/to/dir", "*.txt")?;
464 *     for file in files {
465 *         println!("Found file: {}", file);
466 *     }
467 *     Ok(())
468 * }
469 * ```
470 */
471pub fn find_files(dir: &str, filename: &str) -> Result<Vec<String>, FsError> {
472    let dir_path = Path::new(dir);
473
474    // Check if directory exists
475    if !dir_path.exists() || !dir_path.is_dir() {
476        return Err(FsError::DirectoryNotFound(dir.to_string()));
477    }
478
479    // Use glob to find files
480    let pattern = format!("{}/**/{}", dir, filename);
481    let entries = glob::glob(&pattern).map_err(FsError::InvalidGlobPattern)?;
482
483    let files: Vec<String> = entries
484        .filter_map(Result::ok)
485        .filter(|path| path.is_file())
486        .map(|path| path.to_string_lossy().to_string())
487        .collect();
488
489    Ok(files)
490}
491
492/**
493 * Find a directory in a parent directory (with support for wildcards).
494 *
495 * # Arguments
496 *
497 * * `dir` - The parent directory to search in
498 * * `dirname` - The directory name pattern to search for (can include wildcards)
499 *
500 * # Returns
501 *
502 * * `Ok(String)` - The path to the found directory
503 * * `Err(FsError)` - An error if no directory is found or multiple directories are found
504 *
505 * # Examples
506 *
507 * ```no_run
508 * use sal_os::find_dir;
509 *
510 * fn main() -> Result<(), Box<dyn std::error::Error>> {
511 *     let dir_path = find_dir("/path/to/parent", "sub*")?;
512 *     println!("Found directory: {}", dir_path);
513 *     Ok(())
514 * }
515 * ```
516 */
517pub fn find_dir(dir: &str, dirname: &str) -> Result<String, FsError> {
518    let dir_path = Path::new(dir);
519
520    // Check if directory exists
521    if !dir_path.exists() || !dir_path.is_dir() {
522        return Err(FsError::DirectoryNotFound(dir.to_string()));
523    }
524
525    // Use glob to find directories
526    let pattern = format!("{}/{}", dir, dirname);
527    let entries = glob::glob(&pattern).map_err(FsError::InvalidGlobPattern)?;
528
529    let dirs: Vec<_> = entries
530        .filter_map(Result::ok)
531        .filter(|path| path.is_dir())
532        .collect();
533
534    match dirs.len() {
535        0 => Err(FsError::DirectoryNotFound(dirname.to_string())),
536        1 => Ok(dirs[0].to_string_lossy().to_string()),
537        _ => Err(FsError::CommandFailed(format!(
538            "Multiple directories found matching '{}', expected only one",
539            dirname
540        ))),
541    }
542}
543
544/**
545 * Find multiple directories in a parent directory (recursive, with support for wildcards).
546 *
547 * # Arguments
548 *
549 * * `dir` - The parent directory to search in
550 * * `dirname` - The directory name pattern to search for (can include wildcards)
551 *
552 * # Returns
553 *
554 * * `Ok(Vec<String>)` - A vector of paths to the found directories
555 * * `Err(FsError)` - An error if the parent directory doesn't exist or the pattern is invalid
556 *
557 * # Examples
558 *
559 * ```no_run
560 * use sal_os::find_dirs;
561 *
562 * fn main() -> Result<(), Box<dyn std::error::Error>> {
563 *     let dirs = find_dirs("/path/to/parent", "sub*")?;
564 *     for dir in dirs {
565 *         println!("Found directory: {}", dir);
566 *     }
567 *     Ok(())
568 * }
569 * ```
570 */
571pub fn find_dirs(dir: &str, dirname: &str) -> Result<Vec<String>, FsError> {
572    let dir_path = Path::new(dir);
573
574    // Check if directory exists
575    if !dir_path.exists() || !dir_path.is_dir() {
576        return Err(FsError::DirectoryNotFound(dir.to_string()));
577    }
578
579    // Use glob to find directories
580    let pattern = format!("{}/**/{}", dir, dirname);
581    let entries = glob::glob(&pattern).map_err(FsError::InvalidGlobPattern)?;
582
583    let dirs: Vec<String> = entries
584        .filter_map(Result::ok)
585        .filter(|path| path.is_dir())
586        .map(|path| path.to_string_lossy().to_string())
587        .collect();
588
589    Ok(dirs)
590}
591
592/**
593 * Delete a file or directory (defensive - doesn't error if file doesn't exist).
594 *
595 * # Arguments
596 *
597 * * `path` - The path to delete
598 *
599 * # Returns
600 *
601 * * `Ok(String)` - A success message indicating what was deleted
602 * * `Err(FsError)` - An error if the deletion failed
603 *
604 * # Examples
605 *
606 * ```
607 * use sal_os::delete;
608 *
609 * fn main() -> Result<(), Box<dyn std::error::Error>> {
610 *     // Delete a file
611 *     let result = delete("file.txt")?;
612 *
613 *     // Delete a directory and all its contents
614 *     let result = delete("directory/")?;
615 *
616 *     Ok(())
617 * }
618 * ```
619 */
620pub fn delete(path: &str) -> Result<String, FsError> {
621    let path_obj = Path::new(path);
622
623    // Check if path exists
624    if !path_obj.exists() {
625        return Ok(format!("Nothing to delete at '{}'", path));
626    }
627
628    // Delete based on path type
629    if path_obj.is_file() || path_obj.is_symlink() {
630        fs::remove_file(path_obj).map_err(FsError::DeleteFailed)?;
631        Ok(format!("Successfully deleted file '{}'", path))
632    } else if path_obj.is_dir() {
633        fs::remove_dir_all(path_obj).map_err(FsError::DeleteFailed)?;
634        Ok(format!("Successfully deleted directory '{}'", path))
635    } else {
636        Err(FsError::UnknownFileType(path.to_string()))
637    }
638}
639
640/**
641 * Create a directory and all parent directories (defensive - doesn't error if directory exists).
642 *
643 * # Arguments
644 *
645 * * `path` - The path of the directory to create
646 *
647 * # Returns
648 *
649 * * `Ok(String)` - A success message indicating the directory was created
650 * * `Err(FsError)` - An error if the creation failed
651 *
652 * # Examples
653 *
654 * ```
655 * use sal_os::mkdir;
656 *
657 * fn main() -> Result<(), Box<dyn std::error::Error>> {
658 *     let result = mkdir("path/to/new/directory")?;
659 *     println!("{}", result);
660 *     Ok(())
661 * }
662 * ```
663 */
664pub fn mkdir(path: &str) -> Result<String, FsError> {
665    let path_obj = Path::new(path);
666
667    // Check if path already exists
668    if path_obj.exists() {
669        if path_obj.is_dir() {
670            return Ok(format!("Directory '{}' already exists", path));
671        } else {
672            return Err(FsError::NotADirectory(path.to_string()));
673        }
674    }
675
676    // Create directory and parents
677    fs::create_dir_all(path_obj).map_err(FsError::CreateDirectoryFailed)?;
678    Ok(format!("Successfully created directory '{}'", path))
679}
680
681/**
682 * Get the size of a file in bytes.
683 *
684 * # Arguments
685 *
686 * * `path` - The path of the file
687 *
688 * # Returns
689 *
690 * * `Ok(i64)` - The size of the file in bytes
691 * * `Err(FsError)` - An error if the file doesn't exist or isn't a regular file
692 *
693 * # Examples
694 *
695 * ```no_run
696 * use sal_os::file_size;
697 *
698 * fn main() -> Result<(), Box<dyn std::error::Error>> {
699 *     let size = file_size("file.txt")?;
700 *     println!("File size: {} bytes", size);
701 *     Ok(())
702 * }
703 * ```
704 */
705pub fn file_size(path: &str) -> Result<i64, FsError> {
706    let path_obj = Path::new(path);
707
708    // Check if file exists
709    if !path_obj.exists() {
710        return Err(FsError::FileNotFound(path.to_string()));
711    }
712
713    // Check if it's a regular file
714    if !path_obj.is_file() {
715        return Err(FsError::NotAFile(path.to_string()));
716    }
717
718    // Get file metadata
719    let metadata = fs::metadata(path_obj).map_err(FsError::MetadataError)?;
720    Ok(metadata.len() as i64)
721}
722
723/**
724 * Sync directories using rsync (or platform equivalent).
725 *
726 * # Arguments
727 *
728 * * `src` - The source directory
729 * * `dest` - The destination directory
730 *
731 * # Returns
732 *
733 * * `Ok(String)` - A success message indicating the directories were synced
734 * * `Err(FsError)` - An error if the sync failed
735 *
736 * # Examples
737 *
738 * ```no_run
739 * use sal_os::rsync;
740 *
741 * fn main() -> Result<(), Box<dyn std::error::Error>> {
742 *     let result = rsync("source_dir/", "backup_dir/")?;
743 *     println!("{}", result);
744 *     Ok(())
745 * }
746 * ```
747 */
748pub fn rsync(src: &str, dest: &str) -> Result<String, FsError> {
749    let src_path = Path::new(src);
750    let dest_path = Path::new(dest);
751
752    // Check if source exists
753    if !src_path.exists() {
754        return Err(FsError::FileNotFound(src.to_string()));
755    }
756
757    // Create parent directories if they don't exist
758    if let Some(parent) = dest_path.parent() {
759        fs::create_dir_all(parent).map_err(FsError::CreateDirectoryFailed)?;
760    }
761
762    // Use platform-specific command for syncing
763    #[cfg(target_os = "windows")]
764    let output = Command::new("robocopy")
765        .args(&[src, dest, "/MIR", "/NFL", "/NDL"])
766        .output();
767
768    #[cfg(any(target_os = "macos", target_os = "linux"))]
769    let output = Command::new("rsync")
770        .args(&["-a", "--delete", src, dest])
771        .output();
772
773    match output {
774        Ok(out) => {
775            if out.status.success() || out.status.code() == Some(1) {
776                // rsync and robocopy return 1 for some non-error cases
777                Ok(format!("Successfully synced '{}' to '{}'", src, dest))
778            } else {
779                let error = String::from_utf8_lossy(&out.stderr);
780                Err(FsError::CommandFailed(format!(
781                    "Failed to sync directories: {}",
782                    error
783                )))
784            }
785        }
786        Err(e) => Err(FsError::CommandExecutionError(e)),
787    }
788}
789
790/**
791 * Change the current working directory.
792 *
793 * # Arguments
794 *
795 * * `path` - The path to change to
796 *
797 * # Returns
798 *
799 * * `Ok(String)` - A success message indicating the directory was changed
800 * * `Err(FsError)` - An error if the directory change failed
801 *
802 * # Examples
803 *
804 * ```no_run
805 * use sal_os::chdir;
806 *
807 * fn main() -> Result<(), Box<dyn std::error::Error>> {
808 *     let result = chdir("/path/to/directory")?;
809 *     println!("{}", result);
810 *     Ok(())
811 * }
812 * ```
813 */
814pub fn chdir(path: &str) -> Result<String, FsError> {
815    let path_obj = Path::new(path);
816
817    // Check if directory exists
818    if !path_obj.exists() {
819        return Err(FsError::DirectoryNotFound(path.to_string()));
820    }
821
822    // Check if it's a directory
823    if !path_obj.is_dir() {
824        return Err(FsError::NotADirectory(path.to_string()));
825    }
826
827    // Change directory
828    std::env::set_current_dir(path_obj).map_err(FsError::ChangeDirFailed)?;
829
830    Ok(format!("Successfully changed directory to '{}'", path))
831}
832
833/**
834 * Read the contents of a file.
835 *
836 * # Arguments
837 *
838 * * `path` - The path of the file to read
839 *
840 * # Returns
841 *
842 * * `Ok(String)` - The contents of the file
843 * * `Err(FsError)` - An error if the file doesn't exist or can't be read
844 *
845 * # Examples
846 *
847 * ```no_run
848 * use sal_os::file_read;
849 *
850 * fn main() -> Result<(), Box<dyn std::error::Error>> {
851 *     let content = file_read("file.txt")?;
852 *     println!("File content: {}", content);
853 *     Ok(())
854 * }
855 * ```
856 */
857pub fn file_read(path: &str) -> Result<String, FsError> {
858    let path_obj = Path::new(path);
859
860    // Check if file exists
861    if !path_obj.exists() {
862        return Err(FsError::FileNotFound(path.to_string()));
863    }
864
865    // Check if it's a regular file
866    if !path_obj.is_file() {
867        return Err(FsError::NotAFile(path.to_string()));
868    }
869
870    // Read file content
871    fs::read_to_string(path_obj).map_err(FsError::ReadFailed)
872}
873
874/**
875 * Write content to a file (creates the file if it doesn't exist, overwrites if it does).
876 *
877 * # Arguments
878 *
879 * * `path` - The path of the file to write to
880 * * `content` - The content to write to the file
881 *
882 * # Returns
883 *
884 * * `Ok(String)` - A success message indicating the file was written
885 * * `Err(FsError)` - An error if the file can't be written
886 *
887 * # Examples
888 *
889 * ```
890 * use sal_os::file_write;
891 *
892 * fn main() -> Result<(), Box<dyn std::error::Error>> {
893 *     let result = file_write("file.txt", "Hello, world!")?;
894 *     println!("{}", result);
895 *     Ok(())
896 * }
897 * ```
898 */
899pub fn file_write(path: &str, content: &str) -> Result<String, FsError> {
900    let path_obj = Path::new(path);
901
902    // Create parent directories if they don't exist
903    if let Some(parent) = path_obj.parent() {
904        fs::create_dir_all(parent).map_err(FsError::CreateDirectoryFailed)?;
905    }
906
907    // Write content to file
908    fs::write(path_obj, content).map_err(FsError::WriteFailed)?;
909
910    Ok(format!("Successfully wrote to file '{}'", path))
911}
912
913/**
914 * Append content to a file (creates the file if it doesn't exist).
915 *
916 * # Arguments
917 *
918 * * `path` - The path of the file to append to
919 * * `content` - The content to append to the file
920 *
921 * # Returns
922 *
923 * * `Ok(String)` - A success message indicating the content was appended
924 * * `Err(FsError)` - An error if the file can't be appended to
925 *
926 * # Examples
927 *
928 * ```
929 * use sal_os::file_write_append;
930 *
931 * fn main() -> Result<(), Box<dyn std::error::Error>> {
932 *     let result = file_write_append("log.txt", "New log entry\n")?;
933 *     println!("{}", result);
934 *     Ok(())
935 * }
936 * ```
937 */
938pub fn file_write_append(path: &str, content: &str) -> Result<String, FsError> {
939    let path_obj = Path::new(path);
940
941    // Create parent directories if they don't exist
942    if let Some(parent) = path_obj.parent() {
943        fs::create_dir_all(parent).map_err(FsError::CreateDirectoryFailed)?;
944    }
945
946    // Open file in append mode (or create if it doesn't exist)
947    let mut file = fs::OpenOptions::new()
948        .create(true)
949        .append(true)
950        .open(path_obj)
951        .map_err(FsError::AppendFailed)?;
952
953    // Append content to file
954    use std::io::Write;
955    file.write_all(content.as_bytes())
956        .map_err(FsError::AppendFailed)?;
957
958    Ok(format!("Successfully appended to file '{}'", path))
959}
960
961/**
962 * Move a file or directory from source to destination.
963 *
964 * # Arguments
965 *
966 * * `src` - The source path
967 * * `dest` - The destination path
968 *
969 * # Returns
970 *
971 * * `Ok(String)` - A success message indicating what was moved
972 * * `Err(FsError)` - An error if the move operation failed
973 *
974 * # Examples
975 *
976 * ```no_run
977 * use sal_os::mv;
978 *
979 * fn main() -> Result<(), Box<dyn std::error::Error>> {
980 *     // Move a file
981 *     let result = mv("file.txt", "new_location/file.txt")?;
982 *
983 *     // Move a directory
984 *     let result = mv("src_dir", "dest_dir")?;
985 *
986 *     // Rename a file
987 *     let result = mv("old_name.txt", "new_name.txt")?;
988 *
989 *     Ok(())
990 * }
991 * ```
992 */
993pub fn mv(src: &str, dest: &str) -> Result<String, FsError> {
994    let src_path = Path::new(src);
995    let dest_path = Path::new(dest);
996
997    // Check if source exists
998    if !src_path.exists() {
999        return Err(FsError::FileNotFound(src.to_string()));
1000    }
1001
1002    // Create parent directories if they don't exist
1003    if let Some(parent) = dest_path.parent() {
1004        fs::create_dir_all(parent).map_err(FsError::CreateDirectoryFailed)?;
1005    }
1006
1007    // Handle the case where destination is a directory and exists
1008    let final_dest_path = if dest_path.exists() && dest_path.is_dir() && src_path.is_file() {
1009        // If destination is a directory and source is a file, move the file into the directory
1010        let file_name = src_path.file_name().unwrap_or_default();
1011        dest_path.join(file_name)
1012    } else {
1013        dest_path.to_path_buf()
1014    };
1015
1016    // Clone the path for use in the error handler
1017    let final_dest_path_clone = final_dest_path.clone();
1018
1019    // Perform the move operation
1020    fs::rename(src_path, &final_dest_path).map_err(|e| {
1021        // If rename fails (possibly due to cross-device link), try copy and delete
1022        if e.kind() == std::io::ErrorKind::CrossesDevices {
1023            // For cross-device moves, we need to copy and then delete
1024            if src_path.is_file() {
1025                // Copy file
1026                match fs::copy(src_path, &final_dest_path_clone) {
1027                    Ok(_) => {
1028                        // Delete source after successful copy
1029                        if let Err(del_err) = fs::remove_file(src_path) {
1030                            return FsError::DeleteFailed(del_err);
1031                        }
1032                        return FsError::CommandFailed("".to_string()); // This is a hack to trigger the success message
1033                    }
1034                    Err(copy_err) => return FsError::CopyFailed(copy_err),
1035                }
1036            } else if src_path.is_dir() {
1037                // For directories, use platform-specific command
1038                #[cfg(target_os = "windows")]
1039                let output = Command::new("xcopy")
1040                    .args(&["/E", "/I", "/H", "/Y", src, dest])
1041                    .status();
1042
1043                #[cfg(not(target_os = "windows"))]
1044                let output = Command::new("cp").args(&["-R", src, dest]).status();
1045
1046                match output {
1047                    Ok(status) => {
1048                        if status.success() {
1049                            // Delete source after successful copy
1050                            if let Err(del_err) = fs::remove_dir_all(src_path) {
1051                                return FsError::DeleteFailed(del_err);
1052                            }
1053                            return FsError::CommandFailed("".to_string()); // This is a hack to trigger the success message
1054                        } else {
1055                            return FsError::CommandFailed(
1056                                "Failed to copy directory for move operation".to_string(),
1057                            );
1058                        }
1059                    }
1060                    Err(cmd_err) => return FsError::CommandExecutionError(cmd_err),
1061                }
1062            }
1063        }
1064        FsError::CommandFailed(format!("Failed to move '{}' to '{}': {}", src, dest, e))
1065    })?;
1066
1067    // If we get here, either the rename was successful or our copy-delete hack worked
1068    if src_path.is_file() {
1069        Ok(format!("Successfully moved file '{}' to '{}'", src, dest))
1070    } else {
1071        Ok(format!(
1072            "Successfully moved directory '{}' to '{}'",
1073            src, dest
1074        ))
1075    }
1076}
1077
1078/**
1079 * Check if a command exists in the system PATH.
1080 *
1081 * # Arguments
1082 *
1083 * * `command` - The command to check
1084 *
1085 * # Returns
1086 *
1087 * * `String` - Empty string if the command doesn't exist, path to the command if it does
1088 *
1089 * # Examples
1090 *
1091 * ```
1092 * use sal_os::which;
1093 *
1094 * let cmd_path = which("ls");
1095 * if cmd_path != "" {
1096 *     println!("ls is available at: {}", cmd_path);
1097 * }
1098 * ```
1099 */
1100pub fn which(command: &str) -> String {
1101    // Use the appropriate command based on the platform
1102    #[cfg(target_os = "windows")]
1103    let output = Command::new("where").arg(command).output();
1104
1105    #[cfg(not(target_os = "windows"))]
1106    let output = Command::new("which").arg(command).output();
1107
1108    match output {
1109        Ok(out) => {
1110            if out.status.success() {
1111                let path = String::from_utf8_lossy(&out.stdout).trim().to_string();
1112                path
1113            } else {
1114                String::new()
1115            }
1116        }
1117        Err(_) => String::new(),
1118    }
1119}
1120
1121/**
1122 * Ensure that one or more commands exist in the system PATH.
1123 * If any command doesn't exist, an error is thrown.
1124 *
1125 * # Arguments
1126 *
1127 * * `commands` - The command(s) to check, comma-separated for multiple commands
1128 *
1129 * # Returns
1130 *
1131 * * `Ok(String)` - A success message indicating all commands exist
1132 * * `Err(FsError)` - An error if any command doesn't exist
1133 *
1134 * # Examples
1135 *
1136 * ```no_run
1137 * use sal_os::cmd_ensure_exists;
1138 *
1139 * fn main() -> Result<(), Box<dyn std::error::Error>> {
1140 *     // Check if a single command exists
1141 *     let result = cmd_ensure_exists("ls")?;
1142 *
1143 *     // Check if multiple commands exist
1144 *     let result = cmd_ensure_exists("ls,cat,grep")?;
1145 *
1146 *     Ok(())
1147 * }
1148 * ```
1149 */
1150pub fn cmd_ensure_exists(commands: &str) -> Result<String, FsError> {
1151    // Split the input by commas to handle multiple commands
1152    let command_list: Vec<&str> = commands
1153        .split(',')
1154        .map(|s| s.trim())
1155        .filter(|s| !s.is_empty())
1156        .collect();
1157
1158    if command_list.is_empty() {
1159        return Err(FsError::CommandFailed(
1160            "No commands specified to check".to_string(),
1161        ));
1162    }
1163
1164    let mut missing_commands = Vec::new();
1165
1166    // Check each command
1167    for cmd in &command_list {
1168        let cmd_path = which(cmd);
1169        if cmd_path.is_empty() {
1170            missing_commands.push(cmd.to_string());
1171        }
1172    }
1173
1174    // If any commands are missing, return an error
1175    if !missing_commands.is_empty() {
1176        return Err(FsError::CommandNotFound(missing_commands.join(", ")));
1177    }
1178
1179    // All commands exist
1180    if command_list.len() == 1 {
1181        Ok(format!("Command '{}' exists", command_list[0]))
1182    } else {
1183        Ok(format!("All commands exist: {}", command_list.join(", ")))
1184    }
1185}