1use std::path::{Path, PathBuf};
2
3use anyhow::{Context, Result};
4
5pub fn is_cross_device_error(e: &std::io::Error) -> bool {
9 #[cfg(unix)]
10 {
11 e.raw_os_error() == Some(libc::EXDEV)
12 }
13 #[cfg(windows)]
14 {
15 e.raw_os_error() == Some(17) }
17}
18
19pub fn symlink(original: &Path, link: &Path) -> std::io::Result<()> {
23 #[cfg(unix)]
24 {
25 std::os::unix::fs::symlink(original, link)
26 }
27 #[cfg(windows)]
28 {
29 if original.is_dir() {
30 std::os::windows::fs::symlink_dir(original, link)
31 } else {
32 std::os::windows::fs::symlink_file(original, link)
33 }
34 }
35}
36
37fn walk_dir(dir: &Path, visitor: &mut dyn FnMut(&Path) -> Result<()>) -> Result<()> {
41 if !dir.exists() {
42 return Ok(());
43 }
44 for entry in std::fs::read_dir(dir)
45 .with_context(|| format!("failed to read directory: {}", dir.display()))?
46 {
47 let entry = entry?;
48 let path = entry.path();
49 if path.is_dir() {
50 walk_dir(&path, visitor)?;
51 } else {
52 visitor(&path)?;
53 }
54 }
55 Ok(())
56}
57
58pub fn walk_files_relative(base: &Path) -> Result<Vec<(String, PathBuf)>> {
60 let mut files = Vec::new();
61 walk_dir(base, &mut |path| {
62 let rel = path
63 .strip_prefix(base)
64 .with_context(|| "failed to compute relative path")?;
65 files.push((rel.to_string_lossy().to_string(), path.to_path_buf()));
66 Ok(())
67 })?;
68 Ok(files)
69}
70
71pub fn walk_files(dir: &Path) -> Result<Vec<PathBuf>> {
73 let mut files = Vec::new();
74 walk_dir(dir, &mut |path| {
75 files.push(path.to_path_buf());
76 Ok(())
77 })?;
78 Ok(files)
79}
80
81pub fn count_files(dir: &Path) -> Result<u64> {
83 let mut count = 0u64;
84 walk_dir(dir, &mut |_| {
85 count += 1;
86 Ok(())
87 })?;
88 Ok(count)
89}
90
91pub async fn symlink_async(original: &Path, link: &Path) -> std::io::Result<()> {
94 #[cfg(unix)]
95 {
96 tokio::fs::symlink(original, link).await
97 }
98 #[cfg(windows)]
99 {
100 if original.is_dir() {
101 tokio::fs::symlink_dir(original, link).await
102 } else {
103 tokio::fs::symlink_file(original, link).await
104 }
105 }
106}
107
108pub fn deploy_symlinks(src: &Path, dst: &Path) -> Result<()> {
110 if !dst.exists() {
111 std::fs::create_dir_all(dst)?;
112 }
113 for entry in std::fs::read_dir(src)
114 .with_context(|| format!("failed to read staging dir: {}", src.display()))?
115 {
116 let entry = entry?;
117 let src_path = entry.path();
118 let dst_path = dst.join(entry.file_name());
119
120 if src_path.is_dir() {
121 std::fs::create_dir_all(&dst_path)?;
122 deploy_symlinks(&src_path, &dst_path)?;
123 } else {
124 if dst_path.exists() || dst_path.symlink_metadata().is_ok() {
125 std::fs::remove_file(&dst_path)?;
126 }
127 symlink(&src_path, &dst_path)?;
128 }
129 }
130 Ok(())
131}
132
133#[cfg(test)]
134mod tests {
135 use super::*;
136 use tempfile::TempDir;
137
138 #[test]
139 fn walk_files_relative_empty() {
140 let tmp = TempDir::new().unwrap();
141 let files = walk_files_relative(tmp.path()).unwrap();
142 assert!(files.is_empty());
143 }
144
145 #[test]
146 fn walk_files_relative_flat() {
147 let tmp = TempDir::new().unwrap();
148 std::fs::write(tmp.path().join("a.txt"), "a").unwrap();
149 std::fs::write(tmp.path().join("b.esp"), "b").unwrap();
150 let mut files = walk_files_relative(tmp.path()).unwrap();
151 files.sort_by(|a, b| a.0.cmp(&b.0));
152 assert_eq!(files.len(), 2);
153 assert_eq!(files[0].0, "a.txt");
154 assert_eq!(files[1].0, "b.esp");
155 }
156
157 #[test]
158 fn walk_files_relative_nested() {
159 let tmp = TempDir::new().unwrap();
160 std::fs::create_dir_all(tmp.path().join("sub/deep")).unwrap();
161 std::fs::write(tmp.path().join("sub/deep/file.txt"), "x").unwrap();
162 std::fs::write(tmp.path().join("top.txt"), "y").unwrap();
163 let files = walk_files_relative(tmp.path()).unwrap();
164 assert_eq!(files.len(), 2);
165 let rels: Vec<&str> = files.iter().map(|(r, _)| r.as_str()).collect();
166 assert!(rels.contains(&"top.txt"));
167 assert!(rels.contains(&"sub/deep/file.txt"));
168 }
169
170 #[test]
171 fn walk_files_relative_nonexistent() {
172 let tmp = TempDir::new().unwrap();
173 let files = walk_files_relative(&tmp.path().join("nope")).unwrap();
174 assert!(files.is_empty());
175 }
176
177 #[test]
178 fn walk_files_flat_test() {
179 let tmp = TempDir::new().unwrap();
180 std::fs::write(tmp.path().join("a"), "a").unwrap();
181 std::fs::write(tmp.path().join("b"), "b").unwrap();
182 let files = walk_files(tmp.path()).unwrap();
183 assert_eq!(files.len(), 2);
184 assert!(files.iter().all(|p| p.is_absolute()));
185 }
186
187 #[test]
188 fn count_files_test() {
189 let tmp = TempDir::new().unwrap();
190 std::fs::create_dir_all(tmp.path().join("sub")).unwrap();
191 std::fs::write(tmp.path().join("a"), "a").unwrap();
192 std::fs::write(tmp.path().join("sub/b"), "b").unwrap();
193 assert_eq!(count_files(tmp.path()).unwrap(), 2);
194 }
195
196 #[test]
197 fn count_files_nonexistent() {
198 let tmp = TempDir::new().unwrap();
199 assert_eq!(count_files(&tmp.path().join("nope")).unwrap(), 0);
200 }
201
202 #[test]
203 fn deploy_symlinks_test() {
204 let tmp = TempDir::new().unwrap();
205 let src = tmp.path().join("src");
206 let dst = tmp.path().join("dst");
207 std::fs::create_dir_all(src.join("sub")).unwrap();
208 std::fs::write(src.join("a.txt"), "a").unwrap();
209 std::fs::write(src.join("sub/b.txt"), "b").unwrap();
210
211 deploy_symlinks(&src, &dst).unwrap();
212
213 assert!(dst.join("a.txt").symlink_metadata().unwrap().file_type().is_symlink());
214 assert!(dst.join("sub/b.txt").symlink_metadata().unwrap().file_type().is_symlink());
215 assert_eq!(std::fs::read_to_string(dst.join("a.txt")).unwrap(), "a");
216 assert_eq!(std::fs::read_to_string(dst.join("sub/b.txt")).unwrap(), "b");
217 }
218}