1use std::path::{Path, PathBuf};
2
3#[cfg(feature = "tokio")]
4use std::io::Read;
5
6#[cfg(feature = "tokio")]
7use encoding_rs_io::DecodeReaderBytes;
8use tempfile::NamedTempFile;
9use tracing::warn;
10
11pub use crate::locked_file::*;
12pub use crate::path::*;
13
14pub mod cachedir;
15pub mod link;
16mod locked_file;
17mod path;
18pub mod which;
19
20pub fn is_same_file_allow_missing(left: &Path, right: &Path) -> Option<bool> {
24 if left == right {
26 return Some(true);
27 }
28
29 if let Ok(value) = same_file::is_same_file(left, right) {
31 return Some(value);
32 }
33
34 if let (Some(left_parent), Some(right_parent), Some(left_name), Some(right_name)) = (
36 left.parent(),
37 right.parent(),
38 left.file_name(),
39 right.file_name(),
40 ) {
41 match same_file::is_same_file(left_parent, right_parent) {
42 Ok(true) => return Some(left_name == right_name),
43 Ok(false) => return Some(false),
44 _ => (),
45 }
46 }
47
48 None
50}
51
52#[cfg(feature = "tokio")]
62pub async fn read_to_string_transcode(path: impl AsRef<Path>) -> std::io::Result<String> {
63 let path = path.as_ref();
64 let raw = if path == Path::new("-") {
65 let mut buf = Vec::with_capacity(1024);
66 std::io::stdin().read_to_end(&mut buf)?;
67 buf
68 } else {
69 fs_err::tokio::read(path).await?
70 };
71 let mut buf = String::with_capacity(1024);
72 DecodeReaderBytes::new(&*raw)
73 .read_to_string(&mut buf)
74 .map_err(|err| {
75 let path = path.display();
76 std::io::Error::other(format!("failed to decode file {path}: {err}"))
77 })?;
78 Ok(buf)
79}
80
81#[cfg(windows)]
91pub fn replace_symlink(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
92 if src.as_ref().is_file() {
94 return Err(std::io::Error::new(
95 std::io::ErrorKind::InvalidInput,
96 format!(
97 "Cannot create a junction for {}: is not a directory",
98 src.as_ref().display()
99 ),
100 ));
101 }
102
103 match junction::delete(dunce::simplified(dst.as_ref())) {
105 Ok(()) => match fs_err::remove_dir_all(dst.as_ref()) {
106 Ok(()) => {}
107 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
108 Err(err) => return Err(err),
109 },
110 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
111 Err(err) => return Err(err),
112 }
113
114 junction::create(
116 dunce::simplified(src.as_ref()),
117 dunce::simplified(dst.as_ref()),
118 )
119}
120
121#[cfg(unix)]
125pub fn replace_symlink(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
126 match fs_err::os::unix::fs::symlink(src.as_ref(), dst.as_ref()) {
128 Ok(()) => Ok(()),
129 Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
130 let temp_dir = tempfile::tempdir_in(dst.as_ref().parent().unwrap())?;
132 let temp_file = temp_dir.path().join("link");
133 fs_err::os::unix::fs::symlink(src, &temp_file)?;
134
135 fs_err::rename(&temp_file, dst.as_ref())?;
137
138 Ok(())
139 }
140 Err(err) => Err(err),
141 }
142}
143
144#[cfg(windows)]
152pub fn create_symlink(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
153 if src.as_ref().is_file() {
155 return Err(std::io::Error::new(
156 std::io::ErrorKind::InvalidInput,
157 format!(
158 "Cannot create a junction for {}: is not a directory",
159 src.as_ref().display()
160 ),
161 ));
162 }
163
164 junction::create(
165 dunce::simplified(src.as_ref()),
166 dunce::simplified(dst.as_ref()),
167 )
168}
169
170#[cfg(unix)]
172pub fn create_symlink(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
173 fs_err::os::unix::fs::symlink(src.as_ref(), dst.as_ref())
174}
175
176#[cfg(unix)]
177pub fn remove_symlink(path: impl AsRef<Path>) -> std::io::Result<()> {
178 fs_err::remove_file(path.as_ref())
179}
180
181pub fn symlink_or_copy_file(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
190 #[cfg(windows)]
191 {
192 fs_err::copy(src.as_ref(), dst.as_ref())?;
193 }
194 #[cfg(unix)]
195 {
196 fs_err::os::unix::fs::symlink(src.as_ref(), dst.as_ref())?;
197 }
198
199 Ok(())
200}
201
202#[cfg(windows)]
203pub fn remove_symlink(path: impl AsRef<Path>) -> std::io::Result<()> {
204 match junction::delete(dunce::simplified(path.as_ref())) {
205 Ok(()) => match fs_err::remove_dir_all(path.as_ref()) {
206 Ok(()) => Ok(()),
207 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
208 Err(err) => Err(err),
209 },
210 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
211 Err(err) => Err(err),
212 }
213}
214
215#[cfg(unix)]
220pub fn tempfile_in(path: &Path) -> std::io::Result<NamedTempFile> {
221 use std::os::unix::fs::PermissionsExt;
222 tempfile::Builder::new()
223 .permissions(std::fs::Permissions::from_mode(0o666))
224 .tempfile_in(path)
225}
226
227#[cfg(not(unix))]
229pub fn tempfile_in(path: &Path) -> std::io::Result<NamedTempFile> {
230 tempfile::Builder::new().tempfile_in(path)
231}
232
233#[cfg(feature = "tokio")]
235pub async fn write_atomic(path: impl AsRef<Path>, data: impl AsRef<[u8]>) -> std::io::Result<()> {
236 let temp_file = tempfile_in(
237 path.as_ref()
238 .parent()
239 .expect("Write path must have a parent"),
240 )?;
241 fs_err::tokio::write(&temp_file, &data).await?;
242 persist_with_retry(temp_file, path.as_ref()).await
243}
244
245pub fn write_atomic_sync(path: impl AsRef<Path>, data: impl AsRef<[u8]>) -> std::io::Result<()> {
247 let temp_file = tempfile_in(
248 path.as_ref()
249 .parent()
250 .expect("Write path must have a parent"),
251 )?;
252 fs_err::write(&temp_file, &data)?;
253 persist_with_retry_sync(temp_file, path.as_ref())
254}
255
256pub fn copy_atomic_sync(from: impl AsRef<Path>, to: impl AsRef<Path>) -> std::io::Result<()> {
258 let temp_file = tempfile_in(to.as_ref().parent().expect("Write path must have a parent"))?;
259 fs_err::copy(from.as_ref(), &temp_file)?;
260 persist_with_retry_sync(temp_file, to.as_ref())
261}
262
263#[cfg(windows)]
264fn backoff_file_move() -> backon::ExponentialBackoff {
265 use backon::BackoffBuilder;
266 backon::ExponentialBuilder::default()
272 .with_min_delay(std::time::Duration::from_millis(10))
273 .with_max_times(10)
274 .build()
275}
276
277#[cfg(feature = "tokio")]
279pub async fn rename_with_retry(
280 from: impl AsRef<Path>,
281 to: impl AsRef<Path>,
282) -> Result<(), std::io::Error> {
283 #[cfg(windows)]
284 {
285 use backon::Retryable;
286 let from = from.as_ref();
292 let to = to.as_ref();
293
294 let rename = async || fs_err::rename(from, to);
295
296 rename
297 .retry(backoff_file_move())
298 .sleep(tokio::time::sleep)
299 .when(|e| e.kind() == std::io::ErrorKind::PermissionDenied)
300 .notify(|err, _dur| {
301 warn!(
302 "Retrying rename from {} to {} due to transient error: {}",
303 from.display(),
304 to.display(),
305 err
306 );
307 })
308 .await
309 }
310 #[cfg(not(windows))]
311 {
312 fs_err::tokio::rename(from, to).await
313 }
314}
315
316#[cfg_attr(not(windows), allow(unused_variables))]
320pub fn with_retry_sync(
321 from: impl AsRef<Path>,
322 to: impl AsRef<Path>,
323 operation_name: &str,
324 operation: impl Fn() -> Result<(), std::io::Error>,
325) -> Result<(), std::io::Error> {
326 #[cfg(windows)]
327 {
328 use backon::BlockingRetryable;
329 let from = from.as_ref();
335 let to = to.as_ref();
336
337 operation
338 .retry(backoff_file_move())
339 .sleep(std::thread::sleep)
340 .when(|err| err.kind() == std::io::ErrorKind::PermissionDenied)
341 .notify(|err, _dur| {
342 warn!(
343 "Retrying {} from {} to {} due to transient error: {}",
344 operation_name,
345 from.display(),
346 to.display(),
347 err
348 );
349 })
350 .call()
351 .map_err(|err| {
352 std::io::Error::other(format!(
353 "Failed {} {} to {}: {}",
354 operation_name,
355 from.display(),
356 to.display(),
357 err
358 ))
359 })
360 }
361 #[cfg(not(windows))]
362 {
363 operation()
364 }
365}
366
367#[cfg(windows)]
369enum PersistRetryError {
370 Persist(String),
372 LostState,
374}
375
376#[cfg(feature = "tokio")]
379pub async fn persist_with_retry(
380 from: NamedTempFile,
381 to: impl AsRef<Path>,
382) -> Result<(), std::io::Error> {
383 #[cfg(windows)]
384 {
385 use backon::Retryable;
386 let to = to.as_ref();
392
393 let from = std::sync::Arc::new(std::sync::Mutex::new(Some(from)));
413 let persist = || {
414 let from2 = from.clone();
416
417 async move {
418 let maybe_file: Option<NamedTempFile> = from2
419 .lock()
420 .map_err(|_| PersistRetryError::LostState)?
421 .take();
422 if let Some(file) = maybe_file {
423 file.persist(to).map_err(|err| {
424 let error_message: String = err.to_string();
425 if let Ok(mut guard) = from2.lock() {
427 *guard = Some(err.file);
428 PersistRetryError::Persist(error_message)
429 } else {
430 PersistRetryError::LostState
431 }
432 })
433 } else {
434 Err(PersistRetryError::LostState)
435 }
436 }
437 };
438
439 let persisted = persist
440 .retry(backoff_file_move())
441 .sleep(tokio::time::sleep)
442 .when(|err| matches!(err, PersistRetryError::Persist(_)))
443 .notify(|err, _dur| {
444 if let PersistRetryError::Persist(error_message) = err {
445 warn!(
446 "Retrying to persist temporary file to {}: {}",
447 to.display(),
448 error_message,
449 );
450 }
451 })
452 .await;
453
454 match persisted {
455 Ok(_) => Ok(()),
456 Err(PersistRetryError::Persist(error_message)) => Err(std::io::Error::other(format!(
457 "Failed to persist temporary file to {}: {}",
458 to.display(),
459 error_message,
460 ))),
461 Err(PersistRetryError::LostState) => Err(std::io::Error::other(format!(
462 "Failed to retrieve temporary file while trying to persist to {}",
463 to.display()
464 ))),
465 }
466 }
467 #[cfg(not(windows))]
468 {
469 async { fs_err::rename(from, to) }.await
470 }
471}
472
473pub fn persist_with_retry_sync(
478 from: NamedTempFile,
479 to: impl AsRef<Path>,
480) -> Result<(), std::io::Error> {
481 #[cfg(windows)]
482 {
483 use backon::BlockingRetryable;
484 let to = to.as_ref();
490
491 let mut from = Some(from);
496 let persist = || {
497 if let Some(file) = from.take() {
499 file.persist(to).map_err(|err| {
500 let error_message = err.to_string();
501 from = Some(err.file);
503 PersistRetryError::Persist(error_message)
504 })
505 } else {
506 Err(PersistRetryError::LostState)
507 }
508 };
509
510 let persisted = persist
511 .retry(backoff_file_move())
512 .sleep(std::thread::sleep)
513 .when(|err| matches!(err, PersistRetryError::Persist(_)))
514 .notify(|err, _dur| {
515 if let PersistRetryError::Persist(error_message) = err {
516 warn!(
517 "Retrying to persist temporary file to {}: {}",
518 to.display(),
519 error_message,
520 );
521 }
522 })
523 .call();
524
525 match persisted {
526 Ok(_) => Ok(()),
527 Err(PersistRetryError::Persist(error_message)) => Err(std::io::Error::other(format!(
528 "Failed to persist temporary file to {}: {}",
529 to.display(),
530 error_message,
531 ))),
532 Err(PersistRetryError::LostState) => Err(std::io::Error::other(format!(
533 "Failed to retrieve temporary file while trying to persist to {}",
534 to.display()
535 ))),
536 }
537 }
538 #[cfg(not(windows))]
539 {
540 fs_err::rename(from, to)
541 }
542}
543
544pub fn directories(
548 path: impl AsRef<Path>,
549) -> Result<impl Iterator<Item = PathBuf>, std::io::Error> {
550 let entries = match path.as_ref().read_dir() {
551 Ok(entries) => Some(entries),
552 Err(err) if err.kind() == std::io::ErrorKind::NotFound => None,
553 Err(err) => return Err(err),
554 };
555 Ok(entries
556 .into_iter()
557 .flatten()
558 .filter_map(|entry| match entry {
559 Ok(entry) => Some(entry),
560 Err(err) => {
561 warn!("Failed to read entry: {err}");
562 None
563 }
564 })
565 .filter(|entry| entry.file_type().is_ok_and(|file_type| file_type.is_dir()))
566 .map(|entry| entry.path()))
567}
568
569pub fn entries(path: impl AsRef<Path>) -> Result<impl Iterator<Item = PathBuf>, std::io::Error> {
573 let entries = match path.as_ref().read_dir() {
574 Ok(entries) => Some(entries),
575 Err(err) if err.kind() == std::io::ErrorKind::NotFound => None,
576 Err(err) => return Err(err),
577 };
578 Ok(entries
579 .into_iter()
580 .flatten()
581 .filter_map(|entry| match entry {
582 Ok(entry) => Some(entry),
583 Err(err) => {
584 warn!("Failed to read entry: {err}");
585 None
586 }
587 })
588 .map(|entry| entry.path()))
589}
590
591pub fn files(path: impl AsRef<Path>) -> Result<impl Iterator<Item = PathBuf>, std::io::Error> {
595 let entries = match path.as_ref().read_dir() {
596 Ok(entries) => Some(entries),
597 Err(err) if err.kind() == std::io::ErrorKind::NotFound => None,
598 Err(err) => return Err(err),
599 };
600 Ok(entries
601 .into_iter()
602 .flatten()
603 .filter_map(|entry| match entry {
604 Ok(entry) => Some(entry),
605 Err(err) => {
606 warn!("Failed to read entry: {err}");
607 None
608 }
609 })
610 .filter(|entry| entry.file_type().is_ok_and(|file_type| file_type.is_file()))
611 .map(|entry| entry.path()))
612}
613
614pub fn is_temporary(path: impl AsRef<Path>) -> bool {
616 path.as_ref()
617 .file_name()
618 .and_then(|name| name.to_str())
619 .is_some_and(|name| name.starts_with(".tmp"))
620}
621
622pub fn is_virtualenv_executable(executable: impl AsRef<Path>) -> bool {
629 executable
630 .as_ref()
631 .parent()
632 .and_then(Path::parent)
633 .is_some_and(is_virtualenv_base)
634}
635
636pub fn is_virtualenv_base(path: impl AsRef<Path>) -> bool {
643 path.as_ref().join("pyvenv.cfg").is_file()
644}
645
646fn is_known_already_locked_error(err: &std::fs::TryLockError) -> bool {
648 match err {
649 std::fs::TryLockError::WouldBlock => true,
650 std::fs::TryLockError::Error(err) => {
651 if cfg!(windows) && err.raw_os_error() == Some(33) {
653 return true;
654 }
655 false
656 }
657 }
658}
659
660#[cfg(feature = "tokio")]
662pub struct ProgressReader<Reader: tokio::io::AsyncRead + Unpin, Callback: Fn(usize) + Unpin> {
663 reader: Reader,
664 callback: Callback,
665}
666
667#[cfg(feature = "tokio")]
668impl<Reader: tokio::io::AsyncRead + Unpin, Callback: Fn(usize) + Unpin>
669 ProgressReader<Reader, Callback>
670{
671 pub fn new(reader: Reader, callback: Callback) -> Self {
673 Self { reader, callback }
674 }
675}
676
677#[cfg(feature = "tokio")]
678impl<Reader: tokio::io::AsyncRead + Unpin, Callback: Fn(usize) + Unpin> tokio::io::AsyncRead
679 for ProgressReader<Reader, Callback>
680{
681 fn poll_read(
682 mut self: std::pin::Pin<&mut Self>,
683 cx: &mut std::task::Context<'_>,
684 buf: &mut tokio::io::ReadBuf<'_>,
685 ) -> std::task::Poll<std::io::Result<()>> {
686 std::pin::Pin::new(&mut self.as_mut().reader)
687 .poll_read(cx, buf)
688 .map_ok(|()| {
689 (self.callback)(buf.filled().len());
690 })
691 }
692}
693
694pub fn copy_dir_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
696 fs_err::create_dir_all(&dst)?;
697 for entry in fs_err::read_dir(src.as_ref())? {
698 let entry = entry?;
699 let ty = entry.file_type()?;
700 if ty.is_dir() {
701 copy_dir_all(entry.path(), dst.as_ref().join(entry.file_name()))?;
702 } else {
703 fs_err::copy(entry.path(), dst.as_ref().join(entry.file_name()))?;
704 }
705 }
706 Ok(())
707}