Skip to main content

trash_cli_core/
fs.rs

1use crate::errors::CoreError;
2use std::fs::{self, DirEntry, Metadata};
3use std::io;
4use std::path::{Path, PathBuf};
5use std::time::SystemTime;
6
7/// Filesystem abstraction boundary for command implementations.
8///
9/// Keeping this trait narrow makes it easy to write deterministic tests and
10/// allows alternative backends (e.g. in-memory fs) if command crates need it.
11pub trait FileSystem: Send + Sync {
12    /// Returns the current time in wall-clock format.
13    fn now(&self) -> SystemTime;
14
15    /// Returns true when path exists (symlink-aware).
16    fn exists(&self, path: &Path) -> bool;
17
18    /// Reads file metadata.
19    fn metadata(&self, path: &Path) -> crate::Result<Metadata>;
20
21    /// Reads symlink metadata.
22    fn symlink_metadata(&self, path: &Path) -> crate::Result<Metadata>;
23
24    /// Creates a directory and all missing parent directories.
25    fn create_dir_all(&self, path: &Path) -> crate::Result<()>;
26
27    /// Creates a directory.
28    fn create_dir(&self, path: &Path) -> crate::Result<()>;
29
30    /// Writes raw bytes atomically (truncate + replace).
31    fn write(&self, path: &Path, data: &[u8]) -> crate::Result<()>;
32
33    /// Writes UTF-8 text.
34    fn write_to_string(&self, path: &Path, content: &str) -> crate::Result<()>;
35
36    /// Reads UTF-8 text.
37    fn read_to_string(&self, path: &Path) -> crate::Result<String>;
38
39    /// Removes a file.
40    fn remove_file(&self, path: &Path) -> crate::Result<()>;
41
42    /// Renames/moves a path.
43    fn rename(&self, from: &Path, to: &Path) -> crate::Result<()>;
44
45    /// Lists directory children as concrete paths.
46    fn list_dir(&self, path: &Path) -> crate::Result<Vec<PathBuf>>;
47
48    /// Removes an empty directory.
49    fn remove_dir(&self, path: &Path) -> crate::Result<()>;
50}
51
52/// Default filesystem implementation backed by `std::fs`.
53#[derive(Debug, Default, Clone, Copy)]
54pub struct RealFileSystem;
55
56impl FileSystem for RealFileSystem {
57    fn now(&self) -> SystemTime {
58        SystemTime::now()
59    }
60
61    fn exists(&self, path: &Path) -> bool {
62        path.exists()
63    }
64
65    fn metadata(&self, path: &Path) -> crate::Result<Metadata> {
66        fs::metadata(path).map_err(|err| CoreError::io(path, err))
67    }
68
69    fn symlink_metadata(&self, path: &Path) -> crate::Result<Metadata> {
70        fs::symlink_metadata(path).map_err(|err| CoreError::io(path, err))
71    }
72
73    fn create_dir_all(&self, path: &Path) -> crate::Result<()> {
74        fs::create_dir_all(path).map_err(|err| CoreError::io(path, err))
75    }
76
77    fn create_dir(&self, path: &Path) -> crate::Result<()> {
78        fs::create_dir(path).map_err(|err| CoreError::io(path, err))
79    }
80
81    fn write(&self, path: &Path, data: &[u8]) -> crate::Result<()> {
82        fs::write(path, data).map_err(|err| CoreError::io(path, err))
83    }
84
85    fn write_to_string(&self, path: &Path, content: &str) -> crate::Result<()> {
86        fs::write(path, content).map_err(|err| CoreError::io(path, err))
87    }
88
89    fn read_to_string(&self, path: &Path) -> crate::Result<String> {
90        fs::read_to_string(path).map_err(|err| CoreError::io(path, err))
91    }
92
93    fn remove_file(&self, path: &Path) -> crate::Result<()> {
94        fs::remove_file(path).map_err(|err| CoreError::io(path, err))
95    }
96
97    fn rename(&self, from: &Path, to: &Path) -> crate::Result<()> {
98        fs::rename(from, to).map_err(|err| CoreError::io(from, err))
99    }
100
101    fn list_dir(&self, path: &Path) -> crate::Result<Vec<PathBuf>> {
102        fs::read_dir(path)
103            .map_err(|err| CoreError::io(path, err))?
104            .map(|entry| entry.map(|v| v.path()))
105            .collect::<Result<Vec<PathBuf>, io::Error>>()
106            .map_err(|err| CoreError::io(path, err))
107    }
108
109    fn remove_dir(&self, path: &Path) -> crate::Result<()> {
110        fs::remove_dir(path).map_err(|err| CoreError::io(path, err))
111    }
112}
113
114/// Convenience helper for command implementations that repeatedly need the first
115/// file-system entry when iterating directories.
116pub fn first_entry_file_name(entries: &[DirEntry]) -> Option<String> {
117    entries.first().and_then(|entry| {
118        entry
119            .file_name()
120            .to_str()
121            .map(|name| name.to_ascii_lowercase())
122    })
123}