Skip to main content

snapbox/dir/
ops.rs

1/// Recursively walk a path
2///
3/// Note: Ignores `.keep` files
4#[cfg(feature = "dir")]
5pub struct Walk {
6    inner: walkdir::IntoIter,
7}
8
9#[cfg(feature = "dir")]
10impl Walk {
11    pub fn new(path: &std::path::Path) -> Self {
12        Self {
13            inner: walkdir::WalkDir::new(path).into_iter(),
14        }
15    }
16}
17
18#[cfg(feature = "dir")]
19impl Iterator for Walk {
20    type Item = Result<std::path::PathBuf, std::io::Error>;
21
22    fn next(&mut self) -> Option<Self::Item> {
23        while let Some(entry) = self.inner.next().map(|e| {
24            e.map(walkdir::DirEntry::into_path)
25                .map_err(std::io::Error::from)
26        }) {
27            if entry.as_ref().ok().and_then(|e| e.file_name())
28                != Some(std::ffi::OsStr::new(".keep"))
29            {
30                return Some(entry);
31            }
32        }
33        None
34    }
35}
36
37/// Copy a template into a [`DirRoot`][super::DirRoot]
38///
39/// Note: Generally you'll use [`DirRoot::with_template`][super::DirRoot::with_template] instead.
40///
41/// Note: Ignores `.keep` files
42#[cfg(feature = "dir")]
43pub fn copy_template(
44    source: impl AsRef<std::path::Path>,
45    dest: impl AsRef<std::path::Path>,
46) -> Result<(), crate::assert::Error> {
47    let source = source.as_ref();
48    let dest = dest.as_ref();
49    let source = canonicalize(source)
50        .map_err(|e| format!("Failed to canonicalize {}: {}", source.display(), e))?;
51    std::fs::create_dir_all(dest)
52        .map_err(|e| format!("Failed to create {}: {}", dest.display(), e))?;
53    let dest = canonicalize(dest)
54        .map_err(|e| format!("Failed to canonicalize {}: {}", dest.display(), e))?;
55
56    for current in Walk::new(&source) {
57        let current = current.map_err(|e| e.to_string())?;
58        let rel = current.strip_prefix(&source).unwrap();
59        let target = dest.join(rel);
60
61        shallow_copy(&current, &target)?;
62    }
63
64    Ok(())
65}
66
67/// Copy a file system entry, without recursing
68pub(crate) fn shallow_copy(
69    source: &std::path::Path,
70    dest: &std::path::Path,
71) -> Result<(), crate::assert::Error> {
72    let meta = source
73        .symlink_metadata()
74        .map_err(|e| format!("Failed to read metadata from {}: {}", source.display(), e))?;
75    if meta.is_dir() {
76        std::fs::create_dir_all(dest)
77            .map_err(|e| format!("Failed to create {}: {}", dest.display(), e))?;
78    } else if meta.is_file() {
79        std::fs::copy(source, dest).map_err(|e| {
80            format!(
81                "Failed to copy {} to {}: {}",
82                source.display(),
83                dest.display(),
84                e
85            )
86        })?;
87        // Avoid a mtime check race where:
88        // - Copy files
89        // - Test checks mtime
90        // - Test writes
91        // - Test checks mtime
92        //
93        // If all of this happens too close to each other, then the second mtime check will think
94        // nothing was written by the test.
95        //
96        // Instead of just setting 1s in the past, we'll just respect the existing mtime.
97        copy_stats(&meta, dest).map_err(|e| {
98            format!(
99                "Failed to copy {} metadata to {}: {}",
100                source.display(),
101                dest.display(),
102                e
103            )
104        })?;
105    } else if let Ok(target) = std::fs::read_link(source) {
106        symlink_to_file(dest, &target)
107            .map_err(|e| format!("Failed to create symlink {}: {}", dest.display(), e))?;
108    }
109
110    Ok(())
111}
112
113#[cfg(feature = "dir")]
114fn copy_stats(
115    source_meta: &std::fs::Metadata,
116    dest: &std::path::Path,
117) -> Result<(), std::io::Error> {
118    let src_mtime = filetime::FileTime::from_last_modification_time(source_meta);
119    filetime::set_file_mtime(dest, src_mtime)?;
120
121    Ok(())
122}
123
124#[cfg(not(feature = "dir"))]
125fn copy_stats(
126    _source_meta: &std::fs::Metadata,
127    _dest: &std::path::Path,
128) -> Result<(), std::io::Error> {
129    Ok(())
130}
131
132#[cfg(windows)]
133fn symlink_to_file(link: &std::path::Path, target: &std::path::Path) -> Result<(), std::io::Error> {
134    std::os::windows::fs::symlink_file(target, link)
135}
136
137#[cfg(not(windows))]
138fn symlink_to_file(link: &std::path::Path, target: &std::path::Path) -> Result<(), std::io::Error> {
139    std::os::unix::fs::symlink(target, link)
140}
141
142pub fn resolve_dir(
143    path: impl AsRef<std::path::Path>,
144) -> Result<std::path::PathBuf, std::io::Error> {
145    let path = path.as_ref();
146    let meta = std::fs::symlink_metadata(path)?;
147    if meta.is_dir() {
148        canonicalize(path)
149    } else if meta.is_file() {
150        // Git might checkout symlinks as files
151        let target = std::fs::read_to_string(path)?;
152        let target_path = path.parent().unwrap().join(target);
153        resolve_dir(target_path)
154    } else {
155        canonicalize(path)
156    }
157}
158
159pub(crate) fn canonicalize(path: &std::path::Path) -> Result<std::path::PathBuf, std::io::Error> {
160    #[cfg(feature = "dir")]
161    {
162        dunce::canonicalize(path)
163    }
164    #[cfg(not(feature = "dir"))]
165    {
166        // Hope for the best
167        Ok(strip_trailing_slash(path).to_owned())
168    }
169}
170
171pub fn strip_trailing_slash(path: &std::path::Path) -> &std::path::Path {
172    path.components().as_path()
173}
174
175/// Normalize a path, removing things like `.` and `..`.
176///
177/// CAUTION: This does not resolve symlinks (unlike
178/// [`std::fs::canonicalize`]). This may cause incorrect or surprising
179/// behavior at times. This should be used carefully. Unfortunately,
180/// [`std::fs::canonicalize`] can be hard to use correctly, since it can often
181/// fail, or on Windows returns annoying device paths. This is a problem Cargo
182/// needs to improve on.
183pub(crate) fn normalize_path(path: &std::path::Path) -> std::path::PathBuf {
184    use std::path::Component;
185
186    let mut components = path.components().peekable();
187    let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
188        components.next();
189        std::path::PathBuf::from(c.as_os_str())
190    } else {
191        std::path::PathBuf::new()
192    };
193
194    for component in components {
195        match component {
196            Component::Prefix(..) => unreachable!(),
197            Component::RootDir => {
198                ret.push(Component::RootDir);
199            }
200            Component::CurDir => {}
201            Component::ParentDir => {
202                if ret.ends_with(Component::ParentDir) {
203                    ret.push(Component::ParentDir);
204                } else {
205                    let popped = ret.pop();
206                    if !popped && !ret.has_root() {
207                        ret.push(Component::ParentDir);
208                    }
209                }
210            }
211            Component::Normal(c) => {
212                ret.push(c);
213            }
214        }
215    }
216    ret
217}
218
219pub(crate) fn display_relpath(path: impl AsRef<std::path::Path>) -> String {
220    let path = path.as_ref();
221    let relpath = if let Ok(cwd) = std::env::current_dir() {
222        match path.strip_prefix(cwd) {
223            Ok(path) => path,
224            Err(_) => path,
225        }
226    } else {
227        path
228    };
229    relpath.display().to_string()
230}