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 color_eyre::eyre::Context as _;
11use color_eyre::eyre::bail;
12use color_eyre::eyre::eyre;
13use serde::Serialize;
14use ytil_cmd::CmdExt as _;
15
16#[derive(Serialize)]
18pub enum FileCmdOutput {
19 BinaryFile(String),
21 TextFile(String),
23 Directory(String),
25 NotFound(String),
27 Unknown(String),
29}
30
31pub fn exec_file_cmd(path: &str) -> color_eyre::Result<FileCmdOutput> {
46 let stdout_bytes = Command::new("sh")
47 .arg("-c")
48 .arg(format!("file {path} -I"))
49 .exec()?
50 .stdout;
51 let stdout = std::str::from_utf8(&stdout_bytes)?.to_lowercase();
52 if stdout.contains(" inode/directory;") {
53 return Ok(FileCmdOutput::Directory(path.to_owned()));
54 }
55 if stdout.contains(" text/") || stdout.contains(" application/json") {
56 return Ok(FileCmdOutput::TextFile(path.to_owned()));
57 }
58 if stdout.contains(" application/") {
59 return Ok(FileCmdOutput::BinaryFile(path.to_owned()));
60 }
61 if stdout.contains(" no such file or directory") {
62 return Ok(FileCmdOutput::NotFound(path.to_owned()));
63 }
64 Ok(FileCmdOutput::Unknown(path.to_owned()))
65}
66
67pub fn ln_sf<P: AsRef<Path>>(target: &P, link: &P) -> color_eyre::Result<()> {
74 if link
75 .as_ref()
76 .try_exists()
77 .wrap_err_with(|| eyre!("error checking if link exists | link={}", link.as_ref().display()))?
78 {
79 std::fs::remove_file(link.as_ref())
80 .wrap_err_with(|| eyre!("error removing existing link | link={}", link.as_ref().display()))?;
81 }
82 std::os::unix::fs::symlink(target.as_ref(), link.as_ref()).wrap_err_with(|| {
83 eyre!(
84 "error creating symlink for target={} link={}",
85 target.as_ref().display(),
86 link.as_ref().display()
87 )
88 })?;
89 Ok(())
90}
91
92pub fn ln_sf_files_in_dir<P: AsRef<std::path::Path>>(target_dir: P, link_dir: P) -> color_eyre::Result<()> {
99 for target in std::fs::read_dir(&target_dir)
100 .wrap_err_with(|| eyre!("error reading directory | path={}", target_dir.as_ref().display()))?
101 {
102 let target = target.wrap_err_with(|| eyre!("error getting target entry"))?.path();
103 if target.is_file() {
104 let target_name = target
105 .file_name()
106 .ok_or_else(|| eyre!("error missing filename for target | path={}", target.display()))?;
107 let link = link_dir.as_ref().join(target_name);
108 ln_sf(&target, &link).wrap_err_with(|| {
109 eyre!(
110 "error creating symlink | target={} link={}",
111 target.display(),
112 link.display()
113 )
114 })?;
115 }
116 }
117 Ok(())
118}
119
120pub fn cp_to_system_clipboard(content: &mut &[u8]) -> color_eyre::Result<()> {
126 let cmd = "pbcopy";
127
128 let mut pbcopy_child = ytil_cmd::silent_cmd(cmd)
129 .stdin(Stdio::piped())
130 .spawn()
131 .wrap_err_with(|| eyre!("error spawning cmd | cmd={cmd:?}"))?;
132
133 std::io::copy(
134 content,
135 pbcopy_child
136 .stdin
137 .as_mut()
138 .ok_or_else(|| eyre!("error getting cmd child stdin | cmd={cmd:?}"))?,
139 )
140 .wrap_err_with(|| eyre!("error copying content to stdin | cmd={cmd:?}"))?;
141
142 if !pbcopy_child
143 .wait()
144 .wrap_err_with(|| eyre!("error waiting for cmd | cmd={cmd:?}"))?
145 .success()
146 {
147 bail!("error copying to system clipboard | cmd={cmd:?} content={content:#?}");
148 }
149
150 Ok(())
151}
152
153pub fn chmod_x<P: AsRef<Path>>(path: P) -> color_eyre::Result<()> {
160 let mut perms = std::fs::metadata(&path)
161 .wrap_err_with(|| eyre!("error reading metadata | path={}", path.as_ref().display()))?
162 .permissions();
163
164 perms.set_mode(0o755);
165
166 std::fs::set_permissions(&path, perms)
167 .wrap_err_with(|| eyre!("error setting permissions | path={}", path.as_ref().display()))?;
168
169 Ok(())
170}
171
172pub fn chmod_x_files_in_dir<P: AsRef<Path>>(dir: P) -> color_eyre::Result<()> {
179 for target_res in
180 std::fs::read_dir(&dir).wrap_err_with(|| eyre!("error reading directory | path={}", dir.as_ref().display()))?
181 {
182 let target = target_res
183 .wrap_err_with(|| eyre!("error getting directory entry"))?
184 .path();
185 if target.is_file() {
186 chmod_x(&target).wrap_err_with(|| eyre!("error setting permissions | path={}", target.display()))?;
187 }
188 }
189 Ok(())
190}
191
192pub fn atomic_cp(from: &Path, to: &Path) -> color_eyre::Result<()> {
205 if !from.exists() {
206 return Err(eyre!("error missing source file | path={}", from.display()));
207 }
208
209 let tmp_name = format!(
210 "{}.tmp-{}-{}",
211 to.file_name()
212 .ok_or_else(|| eyre!("error getting file name | path={}", to.display()))?
213 .to_string_lossy(),
214 std::process::id(),
215 Utc::now().to_rfc3339()
216 );
217 let tmp_path = to
218 .parent()
219 .ok_or_else(|| eyre!("error missing parent directory | path={}", to.display()))?
220 .join(tmp_name);
221
222 std::fs::copy(from, &tmp_path).with_context(|| {
223 format!(
224 "error copying file to temp | from={} temp={}",
225 from.display(),
226 tmp_path.display()
227 )
228 })?;
229 std::fs::rename(&tmp_path, to)
230 .with_context(|| format!("error renaming file | from={} to={}", tmp_path.display(), to.display()))?;
231
232 Ok(())
233}
234
235pub fn find_matching_recursively_in_dir(
253 dir: &Path,
254 matching_file_fn: impl Fn(&DirEntry) -> bool,
255 skip_dir_fn: impl Fn(&DirEntry) -> bool,
256) -> color_eyre::Result<Vec<PathBuf>> {
257 let mut manifests = Vec::new();
258 let mut queue = VecDeque::from([dir.to_path_buf()]);
259
260 while let Some(dir) = queue.pop_front() {
261 for entry in
262 std::fs::read_dir(&dir).wrap_err_with(|| eyre!("error reading directory | path={}", dir.display()))?
263 {
264 let entry = entry.wrap_err_with(|| eyre!("error getting entry"))?;
265 let path = entry.path();
266 let file_type = entry
267 .file_type()
268 .wrap_err_with(|| eyre!("error getting file type | entry={}", path.display()))?;
269
270 if file_type.is_file() {
271 if matching_file_fn(&entry) {
272 manifests.push(path);
273 }
274 continue;
275 }
276
277 if skip_dir_fn(&entry) {
278 continue;
279 }
280 queue.push_back(path);
281 }
282 }
283
284 Ok(manifests)
285}
286
287#[cfg(test)]
288mod tests {
289 use super::*;
290
291 #[test]
292 fn atomic_cp_copies_file_contents() {
293 let dir = tempfile::tempdir().unwrap();
294 let src = dir.path().join("src.txt");
295 let dst = dir.path().join("dst.txt");
296 std::fs::write(&src, b"hello").unwrap();
297
298 let res = atomic_cp(&src, &dst);
299
300 assert2::let_assert!(Ok(()) = res);
301 assert_eq!(std::fs::read(&dst).unwrap(), b"hello");
302 }
303
304 #[test]
305 fn atomic_cp_errors_when_missing_source() {
306 let dir = tempfile::tempdir().unwrap();
307 let src = dir.path().join("missing.txt");
308 let dst = dir.path().join("dst.txt");
309
310 let res = atomic_cp(&src, &dst);
311
312 assert2::let_assert!(Err(err) = res);
313 assert!(err.to_string().contains("error missing source file"));
314 }
315
316 #[test]
317 fn find_matching_recursively_in_dir_returns_the_expected_paths() {
318 let dir = tempfile::tempdir().unwrap();
319 std::fs::create_dir(dir.path().join("a")).unwrap();
321 std::fs::create_dir(dir.path().join("a/b")).unwrap();
322 std::fs::write(dir.path().join("c.txt"), b"c").unwrap();
323 std::fs::write(dir.path().join("a/b/d.txt"), b"d").unwrap();
324
325 let res = find_matching_recursively_in_dir(
326 dir.path(),
327 |e| e.path().extension().and_then(|s| s.to_str()) == Some("txt"),
328 |_| false,
329 );
330 assert2::let_assert!(Ok(mut found) = res);
331 found.sort();
332
333 let mut expected = vec![dir.path().join("c.txt"), dir.path().join("a/b/d.txt")];
334 expected.sort();
335 assert_eq!(found, expected);
336 }
337}