Skip to main content

pistonite_cu/fs/
dir.rs

1//! Directory-only operations (Will error if path is a file or link)
2use std::collections::BTreeSet;
3use std::path::Path;
4
5use crate::pre::*;
6
7/// Check if a directory exists and is empty.
8#[inline(always)]
9pub fn is_empty_dir(path: impl AsRef<Path>) -> crate::Result<bool> {
10    is_empty_dir_impl(path.as_ref())
11}
12fn is_empty_dir_impl(path: &Path) -> crate::Result<bool> {
13    crate::trace!("is_empty_dir '{}'", path.display());
14    let mut x = read_dir_impl(path)?;
15    Ok(x.next().is_none())
16}
17
18/// Ensure `path` exists and is a directory, creating it and all parent directories
19/// if not.
20#[inline(always)]
21pub fn make_dir(path: impl AsRef<Path>) -> crate::Result<()> {
22    make_dir_impl(path.as_ref())
23}
24fn make_dir_impl(path: &Path) -> crate::Result<()> {
25    crate::trace!("make_dir '{}'", path.display());
26    let (exists, is_dir) = match std::fs::metadata(path) {
27        Ok(m) => (true, m.is_dir()),
28        Err(_) => (false, false),
29    };
30    match (exists, is_dir) {
31        (true, false) => {
32            crate::bail!(
33                "failed to create directory: '{}' exists but is not a directory",
34                path.display()
35            );
36        }
37        (true, true) => {} // exists and is dir
38        (false, _) => {
39            crate::trace!("make_dir: creating '{}'", path.display());
40            crate::check!(
41                std::fs::create_dir_all(path),
42                "failed to create directory '{}'",
43                path.display()
44            )?;
45        }
46    }
47    Ok(())
48}
49
50/// Async version of [`make_dir`]
51#[cfg(feature = "coroutine")]
52#[inline(always)]
53pub async fn co_make_dir(path: impl AsRef<Path>) -> crate::Result<()> {
54    co_make_dir_impl(path.as_ref()).await
55}
56#[cfg(feature = "coroutine")]
57async fn co_make_dir_impl(path: &Path) -> crate::Result<()> {
58    crate::trace!("co_make_dir '{}'", path.display());
59    let (exists, is_dir) = match std::fs::metadata(path) {
60        Ok(m) => (true, m.is_dir()),
61        Err(_) => (false, false),
62    };
63    match (exists, is_dir) {
64        (true, false) => {
65            crate::bail!(
66                "failed to create directory: '{}' exists but is not a directory",
67                path.display()
68            );
69        }
70        (true, true) => {} // exists and is dir
71        (false, _) => {
72            crate::trace!("co_make_dir: creating '{}'", path.display());
73            crate::check!(
74                tokio::fs::create_dir_all(path).await,
75                "failed to create directory '{}'",
76                path.display()
77            )?;
78        }
79    }
80    Ok(())
81}
82
83/// Ensure `path` exists and is an empty directory.
84///
85/// If `path` does not exist, it will be created.
86/// Current contents in `path` will be removed.
87pub fn make_dir_empty(path: impl AsRef<Path>) -> crate::Result<()> {
88    let path = path.as_ref();
89    make_dir_impl(path)?;
90    remove_contents_impl(path)
91}
92
93/// Async version of [`make_dir_empty`]
94#[cfg(feature = "coroutine")]
95pub async fn co_make_dir_empty(path: impl AsRef<Path>) -> crate::Result<()> {
96    let path = path.as_ref();
97    co_make_dir_impl(path).await?;
98    co_remove_contents_impl(path).await
99}
100
101/// Ensure `path` either does not exist, or is an empty directory.
102pub fn make_dir_absent_or_empty(path: impl AsRef<Path>) -> crate::Result<()> {
103    let path = path.as_ref();
104    if !path.exists() {
105        return Ok(());
106    }
107    remove_contents(path)
108}
109/// Async version of [`make_dir_absent_or_empty`]
110#[cfg(feature = "coroutine")]
111pub async fn co_make_dir_absent_or_empty(path: impl AsRef<Path>) -> crate::Result<()> {
112    let path = path.as_ref();
113    if !path.exists() {
114        return Ok(());
115    }
116    co_remove_contents(path).await
117}
118
119/// Recursively remove `path` and all of its contents.
120///
121/// No-op if the path does not exist.
122/// Error if the path is a file or a link.
123/// Does not follow symlinks.
124#[inline(always)]
125pub fn rec_remove(path: impl AsRef<Path>) -> crate::Result<()> {
126    rec_remove_impl(path.as_ref())
127}
128fn rec_remove_impl(path: &Path) -> crate::Result<()> {
129    if !path.exists() {
130        crate::trace!("rec_remove: is absent: '{}'", path.display());
131        return Ok(());
132    }
133    crate::trace!("rec_remove '{}'", path.display());
134    if !path.is_dir() {
135        crate::bail!("'{}' is not a directory", path.display());
136    }
137    crate::check!(
138        std::fs::remove_dir_all(path),
139        "failed to recursively remove '{}'",
140        path.display()
141    )
142}
143
144/// Async version of [`rec_remove`]
145#[cfg(feature = "coroutine")]
146#[inline(always)]
147pub async fn co_rec_remove(path: impl AsRef<Path>) -> crate::Result<()> {
148    co_rec_remove_impl(path.as_ref()).await
149}
150#[cfg(feature = "coroutine")]
151async fn co_rec_remove_impl(path: &Path) -> crate::Result<()> {
152    if !path.exists() {
153        crate::trace!("co_rec_remove: is absent: '{}'", path.display());
154        return Ok(());
155    }
156    crate::trace!("co_rec_remove '{}'", path.display());
157    if !path.is_dir() {
158        crate::bail!("'{}' is not a directory", path.display());
159    }
160    crate::check!(
161        tokio::fs::remove_dir_all(path).await,
162        "failed to recursively remove '{}'",
163        path.display()
164    )
165}
166
167/// Remove all of `path`'s contents, but does not remove itself.
168///
169/// Error if the path is not a directory. Does not follow symlinks.
170/// If any of the directory content fails to read, it will propagate
171/// the error.
172#[inline(always)]
173pub fn remove_contents(path: impl AsRef<Path>) -> crate::Result<()> {
174    remove_contents_impl(path.as_ref())
175}
176fn remove_contents_impl(path: &Path) -> crate::Result<()> {
177    crate::trace!("remove_contents '{}'", path.display());
178    if !path.is_dir() {
179        if !path.exists() {
180            crate::bail!("'{}' does not exist", path.display());
181        }
182        crate::bail!("'{}' is not a directory", path.display());
183    }
184    for entry in read_dir(path)? {
185        let entry = crate::check!(entry, "failed to read entry inside '{}'", path.display())?;
186        let entry_path = entry.path();
187        let file_type = crate::check!(
188            entry.file_type(),
189            "failed to read entry type for '{}'",
190            entry_path.display()
191        )?;
192        if file_type.is_dir() {
193            rec_remove(entry_path)?;
194        } else {
195            crate::fs::remove(entry_path)?;
196        }
197    }
198    Ok(())
199}
200
201/// Async version of [`remove_contents`]. Note that this is not fail-fast.
202/// If some entry fails to delete, all entries will still be attempted to be deleted.
203#[cfg(feature = "coroutine")]
204#[inline(always)]
205pub async fn co_remove_contents(path: impl AsRef<Path>) -> crate::Result<()> {
206    co_remove_contents_impl(path.as_ref()).await
207}
208#[cfg(feature = "coroutine")]
209async fn co_remove_contents_impl(path: &Path) -> crate::Result<()> {
210    crate::trace!("remove_contents '{}'", path.display());
211    if !path.is_dir() {
212        if !path.exists() {
213            crate::bail!("'{}' does not exist", path.display());
214        }
215        crate::bail!("'{}' is not a directory", path.display());
216    }
217    let mut x = co_read_dir(path).await?;
218    // using a join set to take full advantage of blocking thread pool
219    let mut join_set = tokio::task::JoinSet::new();
220    loop {
221        let entry = x.next_entry().await;
222        let entry = crate::check!(entry, "failed to read entry inside '{}'", path.display())?;
223        let Some(entry) = entry else {
224            break;
225        };
226        let entry_path = entry.path();
227        let file_type = crate::check!(
228            entry.file_type().await,
229            "failed to read entry type for '{}'",
230            entry_path.display()
231        )?;
232        if file_type.is_dir() {
233            join_set.spawn(async move { co_rec_remove(entry_path).await });
234        } else {
235            join_set.spawn(async move { crate::fs::co_remove(entry_path).await });
236        }
237    }
238    let mut has_failure = false;
239    while let Some(x) = join_set.join_next().await {
240        match x {
241            Err(e) => {
242                crate::debug!("failed to remove some entry in '{}': {e}", path.display());
243                has_failure = true;
244            }
245            Ok(Err(e)) => {
246                crate::debug!("failed to remove some entry in '{}': {e}", path.display());
247                has_failure = true;
248            }
249            Ok(Ok(())) => {}
250        }
251    }
252    if has_failure {
253        crate::bail!(
254            "failed to remove one or more entries in '{}'",
255            path.display()
256        );
257    }
258    Ok(())
259}
260
261pub type ReadDir = std::fs::ReadDir;
262#[cfg(feature = "coroutine")]
263pub type CoReadDir = tokio::fs::ReadDir;
264pub type DirEntry = std::fs::DirEntry;
265#[cfg(feature = "coroutine")]
266pub type CoDirEntry = tokio::fs::DirEntry;
267
268/// `std::fs::read_dir` with error reporting
269#[inline(always)]
270pub fn read_dir(path: impl AsRef<Path>) -> crate::Result<ReadDir> {
271    read_dir_impl(path.as_ref())
272}
273fn read_dir_impl(path: &Path) -> crate::Result<ReadDir> {
274    crate::check!(
275        std::fs::read_dir(path),
276        "cannot read directory '{}'",
277        path.display()
278    )
279}
280
281/// Async version of [`read_dir`]
282#[cfg(feature = "coroutine")]
283#[inline(always)]
284pub async fn co_read_dir(path: impl AsRef<Path>) -> crate::Result<CoReadDir> {
285    co_read_dir_impl(path.as_ref()).await
286}
287#[cfg(feature = "coroutine")]
288async fn co_read_dir_impl(path: &Path) -> crate::Result<CoReadDir> {
289    crate::check!(
290        tokio::fs::read_dir(path).await,
291        "cannot read directory '{}'",
292        path.display()
293    )
294}
295
296/// (Inefficiently) recursively copy all files in `from` directory to `to` directory.
297/// Old files in `to` are NOT removed if no corresponding file exists in `from`.
298/// (If that behavior is desired, call [`cu::fs::make_dir_empty`](make_dir_empty) or
299/// [`cu::fs::remove_contents`](remove_contents) first)
300///
301/// The implementation is naive, suitable for a quick invocation in your build scripts
302/// or something. Find other crates if you are looking for an efficient solution
303/// that does not involve cloning the files.
304///
305/// # Behavior
306/// - `to` will be created if does not exist.
307/// - symlinks are not followed (they are copied as-is)
308///
309/// # Will error if:
310/// - `from` is not a directory, or `to` exists and is not a directory
311/// - there is a path in `to` that is a directory and not a directory in `from`, or vice versa
312///
313#[inline(always)]
314pub fn rec_copy_inefficiently(from: impl AsRef<Path>, to: impl AsRef<Path>) -> crate::Result<()> {
315    rec_copy_inefficiently_impl(from.as_ref(), to.as_ref())
316}
317#[crate::context("failed to copy recursively copy '{}' to '{}'", from.display(), to.display())]
318fn rec_copy_inefficiently_impl(from: &Path, to: &Path) -> crate::Result<()> {
319    crate::trace!(
320        "rec_copy_inefficiently from='{}' to='{}'",
321        from.display(),
322        to.display()
323    );
324    if !from.is_dir() {
325        crate::bail!("source does not exist or is not a directory");
326    }
327    let to_meta = to.metadata();
328    if to_meta.is_ok_and(|x| !x.is_dir()) {
329        crate::bail!("target exists and is not a directory");
330    }
331
332    cu::check!(
333        make_dir_impl(to),
334        "rec_copy: failed to create target directory"
335    )?;
336
337    // cache paths that are known to be directories in `to`
338    let mut dir_cache = BTreeSet::new();
339    let mut walker = crate::fs::walker(from);
340    walker.include_dir_entries(true);
341    for entry in walker.walk()? {
342        let entry = entry?;
343        let rel_path = entry.rel_path()?;
344        if !entry.is_dir() {
345            let Some(file_name) = entry.file_name() else {
346                cu::trace!(
347                    "skipping file with no file name: '{}'",
348                    entry.path().display()
349                );
350                continue;
351            };
352            let containing = if entry.depth() > 1 {
353                let parent = cu::check!(
354                    rel_path.parent(),
355                    "failed to get relative path of containing directory for entry"
356                )?;
357                to.join(parent)
358            } else {
359                to.to_path_buf()
360            };
361            let target = containing.join(file_name);
362            // ensure parent directories exists
363            if !dir_cache.contains(&containing) {
364                make_dir_impl(&containing)?;
365                dir_cache.insert(containing);
366            }
367            // copy the file
368            crate::fs::copy(entry.path(), target)?;
369        } else {
370            let target_path = to.join(rel_path);
371            make_dir_impl(&target_path)?;
372            dir_cache.insert(target_path);
373        }
374    }
375
376    Ok(())
377}