Skip to main content

rok_utils/
fs.rs

1//! File system utilities.
2
3use anyhow::Result;
4use std::fs;
5use std::path::Path;
6
7/// Read a file to a `String`.
8pub fn read_to_string<P: AsRef<Path>>(path: P) -> Result<String> {
9    Ok(fs::read_to_string(path)?)
10}
11
12/// Read raw bytes from a file.
13pub fn read_bytes<P: AsRef<Path>>(path: P) -> Result<Vec<u8>> {
14    Ok(fs::read(path)?)
15}
16
17/// Write `contents` to `path` atomically via a temporary sibling file.
18///
19/// On success the temp file is renamed to `path`, which on most file systems
20/// is an atomic operation.  The temporary file is removed on failure.
21pub fn write_atomic<P: AsRef<Path>>(path: P, contents: impl AsRef<[u8]>) -> Result<()> {
22    let path = path.as_ref();
23    let parent = path.parent().unwrap_or(Path::new("."));
24
25    // Write to a temp file in the same directory so the rename is on the same FS.
26    let tmp = tempfile(parent)?;
27    fs::write(&tmp, contents)?;
28    fs::rename(&tmp, path)?;
29    Ok(())
30}
31
32/// Ensure `path` and all its parents exist (like `mkdir -p`).
33pub fn ensure_dir<P: AsRef<Path>>(path: P) -> Result<()> {
34    Ok(fs::create_dir_all(path)?)
35}
36
37/// Return `true` if `path` exists and is a regular file.
38pub fn is_file<P: AsRef<Path>>(path: P) -> bool {
39    path.as_ref().is_file()
40}
41
42/// Return `true` if `path` exists and is a directory.
43pub fn is_dir<P: AsRef<Path>>(path: P) -> bool {
44    path.as_ref().is_dir()
45}
46
47/// Recursively copy a directory tree from `src` to `dst`.
48///
49/// `dst` is created if it does not exist.  Existing files at `dst` are
50/// overwritten without warning.
51///
52/// ```rust
53/// use rok_utils::fs::copy_dir_all;
54/// use std::fs;
55///
56/// let src = tempfile::tempdir().unwrap();
57/// let dst = tempfile::tempdir().unwrap();
58/// fs::write(src.path().join("hello.txt"), b"hi").unwrap();
59/// copy_dir_all(src.path(), dst.path()).unwrap();
60/// assert!(dst.path().join("hello.txt").exists());
61/// ```
62pub fn copy_dir_all<P: AsRef<Path>, Q: AsRef<Path>>(src: P, dst: Q) -> Result<()> {
63    let src = src.as_ref();
64    let dst = dst.as_ref();
65    fs::create_dir_all(dst)?;
66    for entry in fs::read_dir(src)? {
67        let entry = entry?;
68        let ty = entry.file_type()?;
69        if ty.is_dir() {
70            copy_dir_all(entry.path(), dst.join(entry.file_name()))?;
71        } else {
72            fs::copy(entry.path(), dst.join(entry.file_name()))?;
73        }
74    }
75    Ok(())
76}
77
78/// Recursively find all files under `dir` with the given extension (without
79/// the leading dot).
80///
81/// Returns paths in the order they are discovered (directory-order, not
82/// sorted).  Returns an empty `Vec` if `dir` does not exist.
83///
84/// ```rust
85/// use rok_utils::fs::find_files;
86/// use std::fs;
87///
88/// let dir = tempfile::tempdir().unwrap();
89/// fs::write(dir.path().join("a.txt"), b"").unwrap();
90/// fs::write(dir.path().join("b.rs"), b"").unwrap();
91/// let txt = find_files(dir.path(), "txt").unwrap();
92/// assert_eq!(txt.len(), 1);
93/// assert!(txt[0].ends_with("a.txt"));
94/// ```
95pub fn find_files<P: AsRef<Path>>(dir: P, ext: &str) -> Result<Vec<std::path::PathBuf>> {
96    let mut result = Vec::new();
97    if dir.as_ref().is_dir() {
98        find_files_inner(dir.as_ref(), ext, &mut result)?;
99    }
100    Ok(result)
101}
102
103fn find_files_inner(dir: &Path, ext: &str, acc: &mut Vec<std::path::PathBuf>) -> Result<()> {
104    for entry in fs::read_dir(dir)? {
105        let entry = entry?;
106        let path = entry.path();
107        if path.is_dir() {
108            find_files_inner(&path, ext, acc)?;
109        } else if path.extension().and_then(|e| e.to_str()) == Some(ext) {
110            acc.push(path);
111        }
112    }
113    Ok(())
114}
115
116// ── internal helper ──────────────────────────────────────────────────────────
117
118fn tempfile(dir: &Path) -> Result<std::path::PathBuf> {
119    use std::time::{SystemTime, UNIX_EPOCH};
120    let ts = SystemTime::now()
121        .duration_since(UNIX_EPOCH)
122        .map(|d| d.subsec_nanos())
123        .unwrap_or(0);
124    Ok(dir.join(format!(".tmp.rok.{ts}")))
125}
126
127// ── tests ────────────────────────────────────────────────────────────────────
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132    use std::fs;
133
134    #[test]
135    fn round_trip_atomic_write() {
136        let dir = tempfile::tempdir().unwrap();
137        let p = dir.path().join("hello.txt");
138        write_atomic(&p, b"hello").unwrap();
139        assert_eq!(read_to_string(&p).unwrap(), "hello");
140    }
141
142    #[test]
143    fn ensure_dir_creates_nested() {
144        let dir = tempfile::tempdir().unwrap();
145        let nested = dir.path().join("a").join("b").join("c");
146        ensure_dir(&nested).unwrap();
147        assert!(nested.is_dir());
148    }
149
150    #[test]
151    fn is_file_and_is_dir() {
152        let dir = tempfile::tempdir().unwrap();
153        let f = dir.path().join("f.txt");
154        fs::write(&f, "x").unwrap();
155        assert!(is_file(&f));
156        assert!(!is_dir(&f));
157        assert!(is_dir(dir.path()));
158        assert!(!is_file(dir.path()));
159    }
160
161    #[test]
162    fn copy_dir_all_copies_nested() {
163        let src = tempfile::tempdir().unwrap();
164        let dst = tempfile::tempdir().unwrap();
165        let sub = src.path().join("sub");
166        fs::create_dir_all(&sub).unwrap();
167        fs::write(src.path().join("root.txt"), b"root").unwrap();
168        fs::write(sub.join("nested.txt"), b"nested").unwrap();
169
170        copy_dir_all(src.path(), dst.path()).unwrap();
171
172        assert_eq!(read_to_string(dst.path().join("root.txt")).unwrap(), "root");
173        assert_eq!(
174            read_to_string(dst.path().join("sub/nested.txt")).unwrap(),
175            "nested"
176        );
177    }
178
179    #[test]
180    fn find_files_by_extension() {
181        let dir = tempfile::tempdir().unwrap();
182        fs::write(dir.path().join("a.txt"), b"").unwrap();
183        fs::write(dir.path().join("b.rs"), b"").unwrap();
184        fs::write(dir.path().join("c.txt"), b"").unwrap();
185
186        let txt = find_files(dir.path(), "txt").unwrap();
187        assert_eq!(txt.len(), 2);
188        let rs = find_files(dir.path(), "rs").unwrap();
189        assert_eq!(rs.len(), 1);
190    }
191
192    #[test]
193    fn find_files_nonexistent_dir_returns_empty() {
194        let result = find_files("/nonexistent/path/xyz", "rs").unwrap();
195        assert!(result.is_empty());
196    }
197}