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}