Skip to main content

wowlab_fs/
directory.rs

1//! Directory mutation and symlink-aware inspection.
2
3use std::time::SystemTime;
4
5use crate::{
6    error::{Error, Operation, Result},
7    path::{Path, PathBuf},
8};
9
10/// Kind of entry observed without following its final symlink.
11#[derive(Clone, Copy, Debug, Eq, PartialEq)]
12#[non_exhaustive]
13pub enum EntryKind {
14    /// A regular file.
15    File,
16    /// A directory.
17    Directory,
18    /// A symbolic link.
19    Symlink,
20    /// A platform-specific entry kind.
21    Other,
22}
23
24/// Snapshot of one filesystem entry.
25#[derive(Clone, Debug)]
26pub struct EntryInfo {
27    path: PathBuf,
28    kind: EntryKind,
29    len: u64,
30    modified: Option<SystemTime>,
31}
32
33impl EntryInfo {
34    /// Return the entry path.
35    #[must_use]
36    pub fn path(&self) -> &Path {
37        &self.path
38    }
39
40    /// Return the entry kind.
41    #[must_use]
42    pub const fn kind(&self) -> EntryKind {
43        self.kind
44    }
45
46    /// Return the reported byte length.
47    #[must_use]
48    pub const fn len(&self) -> u64 {
49        self.len
50    }
51
52    /// Report whether the entry has zero reported bytes.
53    #[must_use]
54    pub const fn is_empty(&self) -> bool {
55        self.len == 0
56    }
57
58    /// Return the last-modified time when the platform provides it.
59    #[must_use]
60    pub const fn modified(&self) -> Option<SystemTime> {
61        self.modified
62    }
63}
64
65/// Create a directory and every missing ancestor.
66///
67/// # Errors
68///
69/// Returns a contextual error when the directory cannot be ensured.
70pub fn ensure(path: &Path) -> Result<()> {
71    std::fs::create_dir_all(path)
72        .map_err(|source| Error::new(Operation::CreateDirectory, path, source))
73}
74
75/// Create exactly one directory.
76///
77/// # Errors
78///
79/// Returns a contextual error when the directory cannot be created.
80pub fn create(path: &Path) -> Result<()> {
81    std::fs::create_dir(path).map_err(|source| Error::new(Operation::CreateDirectory, path, source))
82}
83
84/// Inspect a path while following its final symlink.
85///
86/// A missing path is represented by `Ok(None)`.
87///
88/// # Errors
89///
90/// Returns a contextual error when metadata cannot be read.
91pub fn inspect(path: &Path) -> Result<Option<EntryInfo>> {
92    inspect_with(path, true)
93}
94
95/// Inspect a path without following its final symlink.
96///
97/// A missing path is represented by `Ok(None)`.
98///
99/// # Errors
100///
101/// Returns a contextual error when metadata cannot be read.
102pub fn inspect_link(path: &Path) -> Result<Option<EntryInfo>> {
103    inspect_with(path, false)
104}
105
106fn inspect_with(path: &Path, follow: bool) -> Result<Option<EntryInfo>> {
107    let result = if follow {
108        std::fs::metadata(path)
109    } else {
110        std::fs::symlink_metadata(path)
111    };
112    let metadata = match result {
113        Ok(metadata) => metadata,
114        Err(source) if source.kind() == std::io::ErrorKind::NotFound => return Ok(None),
115        Err(source) => return Err(Error::new(Operation::Inspect, path, source)),
116    };
117    let file_type = metadata.file_type();
118    let kind = if file_type.is_file() {
119        EntryKind::File
120    } else if file_type.is_dir() {
121        EntryKind::Directory
122    } else if file_type.is_symlink() {
123        EntryKind::Symlink
124    } else {
125        EntryKind::Other
126    };
127
128    Ok(Some(EntryInfo {
129        path: path.to_path_buf(),
130        kind,
131        len: metadata.len(),
132        modified: metadata.modified().ok(),
133    }))
134}
135
136/// Read and sort all immediate directory entries.
137///
138/// Entry kinds are captured without following final symlinks.
139///
140/// # Errors
141///
142/// Returns a contextual error for opening the directory or reading any entry.
143pub fn entries(path: &Path) -> Result<Vec<EntryInfo>> {
144    let entries = std::fs::read_dir(path)
145        .map_err(|source| Error::new(Operation::ReadDirectory, path, source))?;
146    let mut result = Vec::new();
147
148    for entry in entries {
149        let entry = entry.map_err(|source| Error::new(Operation::ReadDirectory, path, source))?;
150        let entry_path = PathBuf::from(entry.path());
151        let info = inspect_link(&entry_path)?.ok_or_else(|| {
152            Error::new(
153                Operation::Inspect,
154                &entry_path,
155                std::io::Error::new(
156                    std::io::ErrorKind::NotFound,
157                    "directory entry disappeared during inspection",
158                ),
159            )
160        })?;
161
162        result.push(info);
163    }
164
165    result.sort_by(|left, right| left.path.cmp(&right.path));
166
167    Ok(result)
168}
169
170/// Remove a file when present.
171///
172/// Returns whether an entry was removed.
173///
174/// # Errors
175///
176/// Returns a contextual error for failures other than a missing path.
177pub fn remove_file_if_exists(path: &Path) -> Result<bool> {
178    match std::fs::remove_file(path) {
179        Ok(()) => Ok(true),
180        Err(source) if source.kind() == std::io::ErrorKind::NotFound => Ok(false),
181        Err(source) => Err(Error::new(Operation::RemoveFile, path, source)),
182    }
183}
184
185/// Remove an empty directory.
186///
187/// # Errors
188///
189/// Returns a contextual error when the directory cannot be removed.
190pub fn remove_if_empty(path: &Path) -> Result<()> {
191    std::fs::remove_dir(path).map_err(|source| Error::new(Operation::RemoveDirectory, path, source))
192}
193
194/// Remove a directory tree when present.
195///
196/// Returns whether an entry was removed.
197///
198/// # Errors
199///
200/// Returns a contextual error for failures other than a missing path.
201pub fn remove_tree_if_exists(path: &Path) -> Result<bool> {
202    match std::fs::remove_dir_all(path) {
203        Ok(()) => Ok(true),
204        Err(source) if source.kind() == std::io::ErrorKind::NotFound => Ok(false),
205        Err(source) => Err(Error::new(Operation::RemoveDirectory, path, source)),
206    }
207}
208
209/// Canonicalize a path through the host filesystem.
210///
211/// Prefer a confined resolver for untrusted relative paths.
212///
213/// # Errors
214///
215/// Returns a contextual error when the path cannot be resolved.
216pub fn canonicalize(path: &Path) -> Result<PathBuf> {
217    std::fs::canonicalize(path)
218        .map(PathBuf::from)
219        .map_err(|source| Error::new(Operation::Canonicalize, path, source))
220}
221
222/// Rename one entry.
223///
224/// # Errors
225///
226/// Returns a contextual error with both paths when the rename fails.
227pub fn rename(from: &Path, to: &Path) -> Result<()> {
228    std::fs::rename(from, to).map_err(|source| Error::between(Operation::Rename, from, to, source))
229}