1use std::borrow::Cow;
2use std::path::{Path, PathBuf};
3
4use tempfile::NamedTempFile;
5use tracing::warn;
6
7pub use crate::locked_file::*;
8pub use crate::path::*;
9
10pub mod cachedir;
11mod locked_file;
12mod path;
13pub mod which;
14
15pub fn with_added_extension<'a>(path: &'a Path, extension: &str) -> Cow<'a, Path> {
23 let Some(name) = path.file_name() else {
24 return Cow::Borrowed(path);
26 };
27 let mut name = name.to_os_string();
28 name.push(".");
29 name.push(extension.trim_start_matches('.'));
30 Cow::Owned(path.with_file_name(name))
31}
32
33pub fn is_same_file_allow_missing(left: &Path, right: &Path) -> Option<bool> {
37 if left == right {
39 return Some(true);
40 }
41
42 if let Ok(value) = same_file::is_same_file(left, right) {
44 return Some(value);
45 }
46
47 if let (Some(left_parent), Some(right_parent), Some(left_name), Some(right_name)) = (
49 left.parent(),
50 right.parent(),
51 left.file_name(),
52 right.file_name(),
53 ) {
54 match same_file::is_same_file(left_parent, right_parent) {
55 Ok(true) => return Some(left_name == right_name),
56 Ok(false) => return Some(false),
57 _ => (),
58 }
59 }
60
61 None
63}
64
65#[cfg(feature = "tokio")]
75pub async fn read_to_string_transcode(path: impl AsRef<Path>) -> std::io::Result<String> {
76 use std::io::Read;
77
78 use encoding_rs_io::DecodeReaderBytes;
79
80 let path = path.as_ref();
81 let raw = if path == Path::new("-") {
82 let mut buf = Vec::with_capacity(1024);
83 std::io::stdin().read_to_end(&mut buf)?;
84 buf
85 } else {
86 fs_err::tokio::read(path).await?
87 };
88 let mut buf = String::with_capacity(1024);
89 DecodeReaderBytes::new(&*raw)
90 .read_to_string(&mut buf)
91 .map_err(|err| {
92 let path = path.display();
93 std::io::Error::other(format!("failed to decode file {path}: {err}"))
94 })?;
95 Ok(buf)
96}
97
98#[cfg(windows)]
108pub fn replace_symlink(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
109 if src.as_ref().is_file() {
111 return Err(std::io::Error::new(
112 std::io::ErrorKind::InvalidInput,
113 format!(
114 "Cannot create a junction for {}: is not a directory",
115 src.as_ref().display()
116 ),
117 ));
118 }
119
120 match junction::delete(dunce::simplified(dst.as_ref())) {
122 Ok(()) => match fs_err::remove_dir_all(dst.as_ref()) {
123 Ok(()) => {}
124 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
125 Err(err) => return Err(err),
126 },
127 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
128 Err(err) => return Err(err),
129 }
130
131 junction::create(
133 dunce::simplified(src.as_ref()),
134 dunce::simplified(dst.as_ref()),
135 )
136}
137
138#[cfg(unix)]
142pub fn replace_symlink(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
143 match fs_err::os::unix::fs::symlink(src.as_ref(), dst.as_ref()) {
145 Ok(()) => Ok(()),
146 Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
147 let temp_dir = tempfile::tempdir_in(dst.as_ref().parent().unwrap())?;
149 let temp_file = temp_dir.path().join("link");
150 fs_err::os::unix::fs::symlink(src, &temp_file)?;
151
152 fs_err::rename(&temp_file, dst.as_ref())?;
154
155 Ok(())
156 }
157 Err(err) => Err(err),
158 }
159}
160
161#[cfg(windows)]
169pub fn create_symlink(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
170 if src.as_ref().is_file() {
172 return Err(std::io::Error::new(
173 std::io::ErrorKind::InvalidInput,
174 format!(
175 "Cannot create a junction for {}: is not a directory",
176 src.as_ref().display()
177 ),
178 ));
179 }
180
181 junction::create(
182 dunce::simplified(src.as_ref()),
183 dunce::simplified(dst.as_ref()),
184 )
185}
186
187#[cfg(unix)]
189pub fn create_symlink(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
190 fs_err::os::unix::fs::symlink(src.as_ref(), dst.as_ref())
191}
192
193#[cfg(unix)]
194pub fn remove_symlink(path: impl AsRef<Path>) -> std::io::Result<()> {
195 fs_err::remove_file(path.as_ref())
196}
197
198pub fn symlink_or_copy_file(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
207 #[cfg(windows)]
208 {
209 fs_err::copy(src.as_ref(), dst.as_ref())?;
210 }
211 #[cfg(unix)]
212 {
213 fs_err::os::unix::fs::symlink(src.as_ref(), dst.as_ref())?;
214 }
215
216 Ok(())
217}
218
219#[cfg(windows)]
220pub fn remove_symlink(path: impl AsRef<Path>) -> std::io::Result<()> {
221 match junction::delete(dunce::simplified(path.as_ref())) {
222 Ok(()) => match fs_err::remove_dir_all(path.as_ref()) {
223 Ok(()) => Ok(()),
224 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
225 Err(err) => Err(err),
226 },
227 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
228 Err(err) => Err(err),
229 }
230}
231
232#[cfg(unix)]
237pub fn tempfile_in(path: &Path) -> std::io::Result<NamedTempFile> {
238 use std::os::unix::fs::PermissionsExt;
239 tempfile::Builder::new()
240 .permissions(std::fs::Permissions::from_mode(0o666))
241 .tempfile_in(path)
242}
243
244#[cfg(not(unix))]
246pub fn tempfile_in(path: &Path) -> std::io::Result<NamedTempFile> {
247 tempfile::Builder::new().tempfile_in(path)
248}
249
250#[cfg(feature = "tokio")]
252pub async fn write_atomic(path: impl AsRef<Path>, data: impl AsRef<[u8]>) -> std::io::Result<()> {
253 let temp_file = tempfile_in(
254 path.as_ref()
255 .parent()
256 .expect("Write path must have a parent"),
257 )?;
258 fs_err::tokio::write(&temp_file, &data).await?;
259 persist_with_retry(temp_file, path.as_ref()).await
260}
261
262pub fn write_atomic_sync(path: impl AsRef<Path>, data: impl AsRef<[u8]>) -> std::io::Result<()> {
264 let temp_file = tempfile_in(
265 path.as_ref()
266 .parent()
267 .expect("Write path must have a parent"),
268 )?;
269 fs_err::write(&temp_file, &data)?;
270 persist_with_retry_sync(temp_file, path.as_ref())
271}
272
273pub fn copy_atomic_sync(from: impl AsRef<Path>, to: impl AsRef<Path>) -> std::io::Result<()> {
275 let temp_file = tempfile_in(to.as_ref().parent().expect("Write path must have a parent"))?;
276 fs_err::copy(from.as_ref(), &temp_file)?;
277 persist_with_retry_sync(temp_file, to.as_ref())
278}
279
280#[cfg(windows)]
281fn backoff_file_move() -> backon::ExponentialBackoff {
282 use backon::BackoffBuilder;
283 backon::ExponentialBuilder::default()
288 .with_min_delay(std::time::Duration::from_millis(10))
289 .with_max_times(9)
290 .build()
291}
292
293#[cfg(feature = "tokio")]
295pub async fn rename_with_retry(
296 from: impl AsRef<Path>,
297 to: impl AsRef<Path>,
298) -> Result<(), std::io::Error> {
299 #[cfg(windows)]
300 {
301 use backon::Retryable;
302 let from = from.as_ref();
308 let to = to.as_ref();
309
310 let rename = async || fs_err::rename(from, to);
311
312 rename
313 .retry(backoff_file_move())
314 .sleep(tokio::time::sleep)
315 .when(|e| e.kind() == std::io::ErrorKind::PermissionDenied)
316 .notify(|err, _dur| {
317 warn!(
318 "Retrying rename from {} to {} due to transient error: {}",
319 from.display(),
320 to.display(),
321 err
322 );
323 })
324 .await
325 }
326 #[cfg(not(windows))]
327 {
328 fs_err::tokio::rename(from, to).await
329 }
330}
331
332#[cfg_attr(not(windows), allow(unused_variables))]
335pub fn with_retry_sync(
336 from: impl AsRef<Path>,
337 to: impl AsRef<Path>,
338 operation_name: &str,
339 operation: impl Fn() -> Result<(), std::io::Error>,
340) -> Result<(), std::io::Error> {
341 #[cfg(windows)]
342 {
343 use backon::BlockingRetryable;
344 let from = from.as_ref();
350 let to = to.as_ref();
351
352 operation
353 .retry(backoff_file_move())
354 .sleep(std::thread::sleep)
355 .when(|err| err.kind() == std::io::ErrorKind::PermissionDenied)
356 .notify(|err, _dur| {
357 warn!(
358 "Retrying {} from {} to {} due to transient error: {}",
359 operation_name,
360 from.display(),
361 to.display(),
362 err
363 );
364 })
365 .call()
366 .map_err(|err| {
367 std::io::Error::other(format!(
368 "Failed {} {} to {}: {}",
369 operation_name,
370 from.display(),
371 to.display(),
372 err
373 ))
374 })
375 }
376 #[cfg(not(windows))]
377 {
378 operation()
379 }
380}
381
382#[cfg(windows)]
384enum PersistRetryError {
385 Persist(String),
387 LostState,
389}
390
391pub async fn persist_with_retry(
393 from: NamedTempFile,
394 to: impl AsRef<Path>,
395) -> Result<(), std::io::Error> {
396 #[cfg(windows)]
397 {
398 use backon::Retryable;
399 let to = to.as_ref();
405
406 let from = std::sync::Arc::new(std::sync::Mutex::new(Some(from)));
426 let persist = || {
427 let from2 = from.clone();
429
430 async move {
431 let maybe_file: Option<NamedTempFile> = from2
432 .lock()
433 .map_err(|_| PersistRetryError::LostState)?
434 .take();
435 if let Some(file) = maybe_file {
436 file.persist(to).map_err(|err| {
437 let error_message: String = err.to_string();
438 if let Ok(mut guard) = from2.lock() {
440 *guard = Some(err.file);
441 PersistRetryError::Persist(error_message)
442 } else {
443 PersistRetryError::LostState
444 }
445 })
446 } else {
447 Err(PersistRetryError::LostState)
448 }
449 }
450 };
451
452 let persisted = persist
453 .retry(backoff_file_move())
454 .sleep(tokio::time::sleep)
455 .when(|err| matches!(err, PersistRetryError::Persist(_)))
456 .notify(|err, _dur| {
457 if let PersistRetryError::Persist(error_message) = err {
458 warn!(
459 "Retrying to persist temporary file to {}: {}",
460 to.display(),
461 error_message,
462 );
463 }
464 })
465 .await;
466
467 match persisted {
468 Ok(_) => Ok(()),
469 Err(PersistRetryError::Persist(error_message)) => Err(std::io::Error::other(format!(
470 "Failed to persist temporary file to {}: {}",
471 to.display(),
472 error_message,
473 ))),
474 Err(PersistRetryError::LostState) => Err(std::io::Error::other(format!(
475 "Failed to retrieve temporary file while trying to persist to {}",
476 to.display()
477 ))),
478 }
479 }
480 #[cfg(not(windows))]
481 {
482 async { fs_err::rename(from, to) }.await
483 }
484}
485
486pub fn persist_with_retry_sync(
488 from: NamedTempFile,
489 to: impl AsRef<Path>,
490) -> Result<(), std::io::Error> {
491 #[cfg(windows)]
492 {
493 use backon::BlockingRetryable;
494 let to = to.as_ref();
500
501 let mut from = Some(from);
506 let persist = || {
507 if let Some(file) = from.take() {
509 file.persist(to).map_err(|err| {
510 let error_message = err.to_string();
511 from = Some(err.file);
513 PersistRetryError::Persist(error_message)
514 })
515 } else {
516 Err(PersistRetryError::LostState)
517 }
518 };
519
520 let persisted = persist
521 .retry(backoff_file_move())
522 .sleep(std::thread::sleep)
523 .when(|err| matches!(err, PersistRetryError::Persist(_)))
524 .notify(|err, _dur| {
525 if let PersistRetryError::Persist(error_message) = err {
526 warn!(
527 "Retrying to persist temporary file to {}: {}",
528 to.display(),
529 error_message,
530 );
531 }
532 })
533 .call();
534
535 match persisted {
536 Ok(_) => Ok(()),
537 Err(PersistRetryError::Persist(error_message)) => Err(std::io::Error::other(format!(
538 "Failed to persist temporary file to {}: {}",
539 to.display(),
540 error_message,
541 ))),
542 Err(PersistRetryError::LostState) => Err(std::io::Error::other(format!(
543 "Failed to retrieve temporary file while trying to persist to {}",
544 to.display()
545 ))),
546 }
547 }
548 #[cfg(not(windows))]
549 {
550 fs_err::rename(from, to)
551 }
552}
553
554pub fn directories(
558 path: impl AsRef<Path>,
559) -> Result<impl Iterator<Item = PathBuf>, std::io::Error> {
560 let entries = match path.as_ref().read_dir() {
561 Ok(entries) => Some(entries),
562 Err(err) if err.kind() == std::io::ErrorKind::NotFound => None,
563 Err(err) => return Err(err),
564 };
565 Ok(entries
566 .into_iter()
567 .flatten()
568 .filter_map(|entry| match entry {
569 Ok(entry) => Some(entry),
570 Err(err) => {
571 warn!("Failed to read entry: {err}");
572 None
573 }
574 })
575 .filter(|entry| entry.file_type().is_ok_and(|file_type| file_type.is_dir()))
576 .map(|entry| entry.path()))
577}
578
579pub fn entries(path: impl AsRef<Path>) -> Result<impl Iterator<Item = PathBuf>, std::io::Error> {
583 let entries = match path.as_ref().read_dir() {
584 Ok(entries) => Some(entries),
585 Err(err) if err.kind() == std::io::ErrorKind::NotFound => None,
586 Err(err) => return Err(err),
587 };
588 Ok(entries
589 .into_iter()
590 .flatten()
591 .filter_map(|entry| match entry {
592 Ok(entry) => Some(entry),
593 Err(err) => {
594 warn!("Failed to read entry: {err}");
595 None
596 }
597 })
598 .map(|entry| entry.path()))
599}
600
601pub fn files(path: impl AsRef<Path>) -> Result<impl Iterator<Item = PathBuf>, std::io::Error> {
605 let entries = match path.as_ref().read_dir() {
606 Ok(entries) => Some(entries),
607 Err(err) if err.kind() == std::io::ErrorKind::NotFound => None,
608 Err(err) => return Err(err),
609 };
610 Ok(entries
611 .into_iter()
612 .flatten()
613 .filter_map(|entry| match entry {
614 Ok(entry) => Some(entry),
615 Err(err) => {
616 warn!("Failed to read entry: {err}");
617 None
618 }
619 })
620 .filter(|entry| entry.file_type().is_ok_and(|file_type| file_type.is_file()))
621 .map(|entry| entry.path()))
622}
623
624pub fn is_temporary(path: impl AsRef<Path>) -> bool {
626 path.as_ref()
627 .file_name()
628 .and_then(|name| name.to_str())
629 .is_some_and(|name| name.starts_with(".tmp"))
630}
631
632pub fn is_virtualenv_executable(executable: impl AsRef<Path>) -> bool {
639 executable
640 .as_ref()
641 .parent()
642 .and_then(Path::parent)
643 .is_some_and(is_virtualenv_base)
644}
645
646pub fn is_virtualenv_base(path: impl AsRef<Path>) -> bool {
653 path.as_ref().join("pyvenv.cfg").is_file()
654}
655
656fn is_known_already_locked_error(err: &std::fs::TryLockError) -> bool {
658 match err {
659 std::fs::TryLockError::WouldBlock => true,
660 std::fs::TryLockError::Error(err) => {
661 if cfg!(windows) && err.raw_os_error() == Some(33) {
663 return true;
664 }
665 false
666 }
667 }
668}
669
670#[cfg(feature = "tokio")]
672pub struct ProgressReader<Reader: tokio::io::AsyncRead + Unpin, Callback: Fn(usize) + Unpin> {
673 reader: Reader,
674 callback: Callback,
675}
676
677#[cfg(feature = "tokio")]
678impl<Reader: tokio::io::AsyncRead + Unpin, Callback: Fn(usize) + Unpin>
679 ProgressReader<Reader, Callback>
680{
681 pub fn new(reader: Reader, callback: Callback) -> Self {
683 Self { reader, callback }
684 }
685}
686
687#[cfg(feature = "tokio")]
688impl<Reader: tokio::io::AsyncRead + Unpin, Callback: Fn(usize) + Unpin> tokio::io::AsyncRead
689 for ProgressReader<Reader, Callback>
690{
691 fn poll_read(
692 mut self: std::pin::Pin<&mut Self>,
693 cx: &mut std::task::Context<'_>,
694 buf: &mut tokio::io::ReadBuf<'_>,
695 ) -> std::task::Poll<std::io::Result<()>> {
696 std::pin::Pin::new(&mut self.as_mut().reader)
697 .poll_read(cx, buf)
698 .map_ok(|()| {
699 (self.callback)(buf.filled().len());
700 })
701 }
702}
703
704pub fn copy_dir_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
706 fs_err::create_dir_all(&dst)?;
707 for entry in fs_err::read_dir(src.as_ref())? {
708 let entry = entry?;
709 let ty = entry.file_type()?;
710 if ty.is_dir() {
711 copy_dir_all(entry.path(), dst.as_ref().join(entry.file_name()))?;
712 } else {
713 fs_err::copy(entry.path(), dst.as_ref().join(entry.file_name()))?;
714 }
715 }
716 Ok(())
717}
718
719#[cfg(test)]
720mod tests {
721 use super::*;
722 use std::path::PathBuf;
723
724 #[test]
725 fn test_with_added_extension() {
726 let path = PathBuf::from("python");
728 let result = with_added_extension(&path, "exe");
729 assert_eq!(result, PathBuf::from("python.exe"));
730
731 let path = PathBuf::from("awslabs.cdk-mcp-server");
733 let result = with_added_extension(&path, "exe");
734 assert_eq!(result, PathBuf::from("awslabs.cdk-mcp-server.exe"));
735
736 let path = PathBuf::from("org.example.tool");
738 let result = with_added_extension(&path, "exe");
739 assert_eq!(result, PathBuf::from("org.example.tool.exe"));
740
741 let path = PathBuf::from("script");
743 let result = with_added_extension(&path, "ps1");
744 assert_eq!(result, PathBuf::from("script.ps1"));
745
746 let path = PathBuf::from("some/path/to/awslabs.cdk-mcp-server");
748 let result = with_added_extension(&path, "exe");
749 assert_eq!(
750 result,
751 PathBuf::from("some/path/to/awslabs.cdk-mcp-server.exe")
752 );
753
754 let path = PathBuf::new();
756 let result = with_added_extension(&path, "exe");
757 assert_eq!(result, path); }
759}