Skip to main content

ripsync_core/
walk.rs

1//! Parallel filesystem walk built on `jwalk`.
2//!
3//! Produces a flat, sorted list of [`Entry`] values describing a tree relative to
4//! its root. Symlinks are recorded as symlinks (their target is read but never
5//! followed), which keeps the walk safe and cheap.
6
7use std::path::{Path, PathBuf};
8
9use filetime::FileTime;
10use rayon::prelude::*;
11
12use crate::filter::Filter;
13
14use crate::meta::{FileTypeKind, meta_min};
15use crate::{Error, Result, RunControl};
16
17/// What a walked entry is.
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub enum EntryKind {
20    /// A directory.
21    Dir,
22    /// A regular file of the given byte length.
23    File,
24    /// A symbolic link with the given target (recorded verbatim, never followed).
25    Symlink(PathBuf),
26}
27
28/// One entry in a walked tree.
29#[derive(Debug, Clone)]
30pub struct Entry {
31    /// Path relative to the walk root (never contains `..`).
32    pub rel: PathBuf,
33    /// What kind of entry this is.
34    pub kind: EntryKind,
35    /// Byte length (0 for directories and symlinks).
36    pub len: u64,
37    /// Modification time (of the link itself for symlinks).
38    pub mtime: FileTime,
39    /// Unix permission+type bits (0 on platforms without them).
40    pub mode: u32,
41    /// Inode number (for hardlink detection and the index).
42    pub ino: u64,
43    /// Device id.
44    pub dev: u64,
45    /// User id.
46    pub uid: u32,
47    /// Group id.
48    pub gid: u32,
49}
50
51impl Entry {
52    /// Whether this entry is a regular file.
53    #[must_use]
54    pub fn is_file(&self) -> bool {
55        matches!(self.kind, EntryKind::File)
56    }
57
58    /// Whether this entry is a directory.
59    #[must_use]
60    pub fn is_dir(&self) -> bool {
61        matches!(self.kind, EntryKind::Dir)
62    }
63}
64
65/// Walk `root` in parallel, returning entries sorted by relative path (so every
66/// parent directory precedes its children).
67///
68/// `threads` sets the worker-pool size (0 ⇒ `jwalk` default). `filter` drops
69/// any file/symlink whose relative path is excluded.
70///
71/// # Errors
72///
73/// Returns an error if `root` cannot be read, or if any entry's metadata cannot
74/// be stat-ed.
75pub fn walk(root: &Path, threads: usize, filter: &Filter) -> Result<Vec<Entry>> {
76    walk_controlled(root, threads, filter, &RunControl::default())
77}
78
79/// Walk a tree with a cooperative checkpoint before processing each entry.
80///
81/// # Errors
82///
83/// Returns an I/O, containment, metadata, or cancellation error.
84pub fn walk_controlled(
85    root: &Path,
86    threads: usize,
87    filter: &Filter,
88    control: &RunControl,
89) -> Result<Vec<Entry>> {
90    let parallelism = if threads == 0 {
91        jwalk::Parallelism::RayonDefaultPool {
92            busy_timeout: std::time::Duration::from_secs(1),
93        }
94    } else {
95        jwalk::Parallelism::RayonNewPool(threads)
96    };
97
98    let mut entries = Vec::new();
99
100    for dent in jwalk::WalkDir::new(root)
101        .parallelism(parallelism)
102        .skip_hidden(false)
103        .follow_links(false)
104    {
105        control.checkpoint()?;
106        let dent = dent.map_err(|e| Error::io(root, std::io::Error::other(e.to_string())))?;
107        let path = dent.path();
108        if path == root {
109            continue;
110        }
111        let rel = path
112            .strip_prefix(root)
113            .map_err(|_| Error::Containment(path.clone()))?
114            .to_path_buf();
115
116        if !filter.is_empty() && filter.is_excluded(&rel, dent.file_type().is_dir()) {
117            continue;
118        }
119
120        let m = meta_min(&path)?;
121        let kind = match m.kind {
122            FileTypeKind::Symlink => {
123                let target = std::fs::read_link(&path).map_err(|e| Error::io(&path, e))?;
124                EntryKind::Symlink(target)
125            }
126            FileTypeKind::Dir => EntryKind::Dir,
127            FileTypeKind::File => EntryKind::File,
128            // Sockets/FIFOs/devices: skip — not supported in this milestone.
129            FileTypeKind::Other => continue,
130        };
131
132        let len = if matches!(kind, EntryKind::File) {
133            m.len
134        } else {
135            0
136        };
137
138        entries.push(Entry {
139            rel,
140            kind,
141            len,
142            mtime: m.mtime,
143            mode: m.mode,
144            ino: m.ino,
145            dev: m.dev,
146            uid: m.uid,
147            gid: m.gid,
148        });
149    }
150
151    entries.par_sort_unstable_by(|a, b| a.rel.cmp(&b.rel));
152    Ok(entries)
153}