1use std::collections::VecDeque;
2use std::fs::DirEntry;
3use std::os::unix::fs::PermissionsExt as _;
4use std::path::Path;
5use std::path::PathBuf;
6use std::process::Command;
7use std::process::Stdio;
8
9use chrono::Utc;
10use rootcause::prelude::ResultExt as _;
11use rootcause::report;
12use serde::Serialize;
13use ytil_cmd::CmdExt as _;
14
15#[derive(Clone, Serialize)]
17pub enum FileCmdOutput {
18 BinaryFile(String),
20 TextFile(String),
22 Directory(String),
24 NotFound(String),
26 Unknown(String),
28}
29
30pub fn exec_file_cmd(path: &str) -> rootcause::Result<FileCmdOutput> {
45 let stdout_bytes = Command::new("file").args(["-I", path]).exec()?.stdout;
46 let stdout = std::str::from_utf8(&stdout_bytes)?.to_lowercase();
47 if stdout.contains(" inode/directory;") {
48 return Ok(FileCmdOutput::Directory(path.to_owned()));
49 }
50 if stdout.contains(" text/") || stdout.contains(" application/json") {
51 return Ok(FileCmdOutput::TextFile(path.to_owned()));
52 }
53 if stdout.contains(" application/") {
54 return Ok(FileCmdOutput::BinaryFile(path.to_owned()));
55 }
56 if stdout.contains(" no such file or directory") {
57 return Ok(FileCmdOutput::NotFound(path.to_owned()));
58 }
59 Ok(FileCmdOutput::Unknown(path.to_owned()))
60}
61
62pub fn ln_sf<P: AsRef<Path>>(target: &P, link: &P) -> rootcause::Result<()> {
69 match std::fs::remove_file(link.as_ref()) {
71 Ok(()) => {}
72 Err(ref e) if e.kind() == std::io::ErrorKind::NotFound => {}
73 Err(e) => {
74 Err(e)
75 .context("error removing existing link")
76 .attach_with(|| format!("link={}", link.as_ref().display()))?;
77 }
78 }
79 std::os::unix::fs::symlink(target.as_ref(), link.as_ref())
80 .context("error creating symlink")
81 .attach_with(|| format!("target={} link={}", target.as_ref().display(), link.as_ref().display()))?;
82 Ok(())
83}
84
85pub fn ln_sf_files_in_dir<P: AsRef<std::path::Path>>(target_dir: P, link_dir: P) -> rootcause::Result<()> {
92 for target in std::fs::read_dir(&target_dir)
93 .context("error reading directory")
94 .attach_with(|| format!("path={}", target_dir.as_ref().display()))?
95 {
96 let target = target.context("error getting target entry")?.path();
97 if target.is_file() {
98 let target_name = target
99 .file_name()
100 .ok_or_else(|| report!("error missing filename for target"))
101 .attach_with(|| format!("path={}", target.display()))?;
102 let link = link_dir.as_ref().join(target_name);
103 ln_sf(&target, &link)
104 .context("error linking file from directory")
105 .attach_with(|| format!("target={} link={}", target.display(), link.display()))?;
106 }
107 }
108 Ok(())
109}
110
111pub fn cp_to_system_clipboard(content: &mut &[u8]) -> rootcause::Result<()> {
117 let cmd = "pbcopy";
118
119 let mut pbcopy_child = ytil_cmd::silent_cmd(cmd)
120 .stdin(Stdio::piped())
121 .spawn()
122 .context("error spawning cmd")
123 .attach_with(|| format!("cmd={cmd:?}"))?;
124
125 std::io::copy(
126 content,
127 pbcopy_child
128 .stdin
129 .as_mut()
130 .ok_or_else(|| report!("error getting cmd child stdin"))
131 .attach_with(|| format!("cmd={cmd:?}"))?,
132 )
133 .context("error copying content to stdin")
134 .attach_with(|| format!("cmd={cmd:?}"))?;
135
136 if !pbcopy_child
137 .wait()
138 .context("error waiting for cmd")
139 .attach_with(|| format!("cmd={cmd:?}"))?
140 .success()
141 {
142 Err(report!("error copying to system clipboard"))
143 .attach_with(|| format!("cmd={cmd:?} content={content:#?}"))?;
144 }
145
146 Ok(())
147}
148
149pub fn chmod_x<P: AsRef<Path>>(path: P) -> rootcause::Result<()> {
156 let mut perms = std::fs::metadata(&path)
157 .context("error reading metadata")
158 .attach_with(|| format!("path={}", path.as_ref().display()))?
159 .permissions();
160
161 perms.set_mode(0o755);
162
163 std::fs::set_permissions(&path, perms)
164 .context("error setting permissions")
165 .attach_with(|| format!("path={}", path.as_ref().display()))?;
166
167 Ok(())
168}
169
170pub fn chmod_x_files_in_dir<P: AsRef<Path>>(dir: P) -> rootcause::Result<()> {
177 for target_res in std::fs::read_dir(&dir)
178 .context("error reading directory")
179 .attach_with(|| format!("path={}", dir.as_ref().display()))?
180 {
181 let target = target_res.context("error getting directory entry")?.path();
182 if target.is_file() {
183 chmod_x(&target)
184 .context("error setting file permissions in directory")
185 .attach_with(|| format!("path={}", target.display()))?;
186 }
187 }
188 Ok(())
189}
190
191pub fn atomic_cp(from: &Path, to: &Path) -> rootcause::Result<()> {
204 let tmp_name = format!(
207 "{}.tmp-{}-{}",
208 to.file_name()
209 .ok_or_else(|| report!("error getting file name"))
210 .attach_with(|| format!("path={}", to.display()))?
211 .to_string_lossy(),
212 std::process::id(),
213 Utc::now().to_rfc3339()
214 );
215 let tmp_path = to
216 .parent()
217 .ok_or_else(|| report!("error missing parent directory"))
218 .attach_with(|| format!("path={}", to.display()))?
219 .join(tmp_name);
220
221 std::fs::copy(from, &tmp_path)
222 .context("error copying file to temp")
223 .attach_with(|| format!("from={} temp={}", from.display(), tmp_path.display()))?;
224 std::fs::rename(&tmp_path, to)
225 .context("error renaming file")
226 .attach_with(|| format!("from={} to={}", tmp_path.display(), to.display()))?;
227
228 Ok(())
229}
230
231pub fn find_matching_recursively_in_dir(
239 dir: &Path,
240 matching_file_fn: impl Fn(&DirEntry) -> bool,
241 skip_dir_fn: impl Fn(&DirEntry) -> bool,
242) -> rootcause::Result<Vec<PathBuf>> {
243 let mut manifests = Vec::new();
244 let mut queue = VecDeque::from([dir.to_path_buf()]);
245
246 while let Some(dir) = queue.pop_front() {
247 for entry in std::fs::read_dir(&dir)
248 .context("error reading directory")
249 .attach_with(|| format!("path={}", dir.display()))?
250 {
251 let entry = entry.context("error getting entry")?;
252 let path = entry.path();
253 let file_type = entry
254 .file_type()
255 .context("error getting file type")
256 .attach_with(|| format!("entry={}", path.display()))?;
257
258 if file_type.is_file() {
259 if matching_file_fn(&entry) {
260 manifests.push(path);
261 }
262 continue;
263 }
264
265 if skip_dir_fn(&entry) {
266 continue;
267 }
268 queue.push_back(path);
269 }
270 }
271
272 Ok(manifests)
273}
274
275#[cfg(test)]
276mod tests {
277 use super::*;
278
279 #[test]
280 fn atomic_cp_copies_file_contents() {
281 let dir = tempfile::tempdir().unwrap();
282 let src = dir.path().join("src.txt");
283 let dst = dir.path().join("dst.txt");
284 std::fs::write(&src, b"hello").unwrap();
285
286 let res = atomic_cp(&src, &dst);
287
288 assert2::assert!(let Ok(()) = res);
289 assert_eq!(std::fs::read(&dst).unwrap(), b"hello");
290 }
291
292 #[test]
293 fn atomic_cp_errors_when_missing_source() {
294 let dir = tempfile::tempdir().unwrap();
295 let src = dir.path().join("missing.txt");
296 let dst = dir.path().join("dst.txt");
297
298 let res = atomic_cp(&src, &dst);
299
300 assert2::assert!(let Err(err) = res);
301 assert!(err.to_string().contains("error copying file to temp"));
303 }
304
305 #[test]
306 fn find_matching_recursively_in_dir_returns_the_expected_paths() {
307 let dir = tempfile::tempdir().unwrap();
308 std::fs::create_dir(dir.path().join("a")).unwrap();
310 std::fs::create_dir(dir.path().join("a/b")).unwrap();
311 std::fs::write(dir.path().join("c.txt"), b"c").unwrap();
312 std::fs::write(dir.path().join("a/b/d.txt"), b"d").unwrap();
313
314 let res = find_matching_recursively_in_dir(
315 dir.path(),
316 |e| e.path().extension().and_then(|s| s.to_str()) == Some("txt"),
317 |_| false,
318 );
319 assert2::assert!(let Ok(mut found) = res);
320 found.sort();
321
322 let mut expected = vec![dir.path().join("c.txt"), dir.path().join("a/b/d.txt")];
323 expected.sort();
324 assert_eq!(found, expected);
325 }
326}