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