1use std::path::{Path, PathBuf};
2
3use tempfile::NamedTempFile;
4use tracing::warn;
5
6pub use crate::locked_file::*;
7pub use crate::path::*;
8
9pub mod cachedir;
10mod locked_file;
11mod path;
12pub mod which;
13
14pub fn is_same_file_allow_missing(left: &Path, right: &Path) -> Option<bool> {
18 if left == right {
20 return Some(true);
21 }
22
23 if let Ok(value) = same_file::is_same_file(left, right) {
25 return Some(value);
26 }
27
28 if let (Some(left_parent), Some(right_parent), Some(left_name), Some(right_name)) = (
30 left.parent(),
31 right.parent(),
32 left.file_name(),
33 right.file_name(),
34 ) {
35 match same_file::is_same_file(left_parent, right_parent) {
36 Ok(true) => return Some(left_name == right_name),
37 Ok(false) => return Some(false),
38 _ => (),
39 }
40 }
41
42 None
44}
45
46#[cfg(feature = "tokio")]
56pub async fn read_to_string_transcode(path: impl AsRef<Path>) -> std::io::Result<String> {
57 use std::io::Read;
58
59 use encoding_rs_io::DecodeReaderBytes;
60
61 let path = path.as_ref();
62 let raw = if path == Path::new("-") {
63 let mut buf = Vec::with_capacity(1024);
64 std::io::stdin().read_to_end(&mut buf)?;
65 buf
66 } else {
67 fs_err::tokio::read(path).await?
68 };
69 let mut buf = String::with_capacity(1024);
70 DecodeReaderBytes::new(&*raw)
71 .read_to_string(&mut buf)
72 .map_err(|err| {
73 let path = path.display();
74 std::io::Error::other(format!("failed to decode file {path}: {err}"))
75 })?;
76 Ok(buf)
77}
78
79#[cfg(windows)]
89pub fn replace_symlink(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
90 if src.as_ref().is_file() {
92 return Err(std::io::Error::new(
93 std::io::ErrorKind::InvalidInput,
94 format!(
95 "Cannot create a junction for {}: is not a directory",
96 src.as_ref().display()
97 ),
98 ));
99 }
100
101 match junction::delete(dunce::simplified(dst.as_ref())) {
103 Ok(()) => match fs_err::remove_dir_all(dst.as_ref()) {
104 Ok(()) => {}
105 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
106 Err(err) => return Err(err),
107 },
108 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
109 Err(err) => return Err(err),
110 }
111
112 junction::create(
114 dunce::simplified(src.as_ref()),
115 dunce::simplified(dst.as_ref()),
116 )
117}
118
119#[cfg(unix)]
123pub fn replace_symlink(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
124 match fs_err::os::unix::fs::symlink(src.as_ref(), dst.as_ref()) {
126 Ok(()) => Ok(()),
127 Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
128 let temp_dir = tempfile::tempdir_in(dst.as_ref().parent().unwrap())?;
130 let temp_file = temp_dir.path().join("link");
131 fs_err::os::unix::fs::symlink(src, &temp_file)?;
132
133 fs_err::rename(&temp_file, dst.as_ref())?;
135
136 Ok(())
137 }
138 Err(err) => Err(err),
139 }
140}
141
142#[cfg(windows)]
150pub fn create_symlink(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
151 if src.as_ref().is_file() {
153 return Err(std::io::Error::new(
154 std::io::ErrorKind::InvalidInput,
155 format!(
156 "Cannot create a junction for {}: is not a directory",
157 src.as_ref().display()
158 ),
159 ));
160 }
161
162 junction::create(
163 dunce::simplified(src.as_ref()),
164 dunce::simplified(dst.as_ref()),
165 )
166}
167
168#[cfg(unix)]
170pub fn create_symlink(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
171 fs_err::os::unix::fs::symlink(src.as_ref(), dst.as_ref())
172}
173
174#[cfg(unix)]
175pub fn remove_symlink(path: impl AsRef<Path>) -> std::io::Result<()> {
176 fs_err::remove_file(path.as_ref())
177}
178
179pub fn symlink_or_copy_file(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
188 #[cfg(windows)]
189 {
190 fs_err::copy(src.as_ref(), dst.as_ref())?;
191 }
192 #[cfg(unix)]
193 {
194 fs_err::os::unix::fs::symlink(src.as_ref(), dst.as_ref())?;
195 }
196
197 Ok(())
198}
199
200#[cfg(windows)]
201pub fn remove_symlink(path: impl AsRef<Path>) -> std::io::Result<()> {
202 match junction::delete(dunce::simplified(path.as_ref())) {
203 Ok(()) => match fs_err::remove_dir_all(path.as_ref()) {
204 Ok(()) => Ok(()),
205 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
206 Err(err) => Err(err),
207 },
208 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
209 Err(err) => Err(err),
210 }
211}
212
213#[cfg(unix)]
218pub fn tempfile_in(path: &Path) -> std::io::Result<NamedTempFile> {
219 use std::os::unix::fs::PermissionsExt;
220 tempfile::Builder::new()
221 .permissions(std::fs::Permissions::from_mode(0o666))
222 .tempfile_in(path)
223}
224
225#[cfg(not(unix))]
227pub fn tempfile_in(path: &Path) -> std::io::Result<NamedTempFile> {
228 tempfile::Builder::new().tempfile_in(path)
229}
230
231#[cfg(feature = "tokio")]
233pub async fn write_atomic(path: impl AsRef<Path>, data: impl AsRef<[u8]>) -> std::io::Result<()> {
234 let temp_file = tempfile_in(
235 path.as_ref()
236 .parent()
237 .expect("Write path must have a parent"),
238 )?;
239 fs_err::tokio::write(&temp_file, &data).await?;
240 persist_with_retry(temp_file, path.as_ref()).await
241}
242
243pub fn write_atomic_sync(path: impl AsRef<Path>, data: impl AsRef<[u8]>) -> std::io::Result<()> {
245 let temp_file = tempfile_in(
246 path.as_ref()
247 .parent()
248 .expect("Write path must have a parent"),
249 )?;
250 fs_err::write(&temp_file, &data)?;
251 persist_with_retry_sync(temp_file, path.as_ref())
252}
253
254pub fn copy_atomic_sync(from: impl AsRef<Path>, to: impl AsRef<Path>) -> std::io::Result<()> {
256 let temp_file = tempfile_in(to.as_ref().parent().expect("Write path must have a parent"))?;
257 fs_err::copy(from.as_ref(), &temp_file)?;
258 persist_with_retry_sync(temp_file, to.as_ref())
259}
260
261#[cfg(windows)]
262fn backoff_file_move() -> backon::ExponentialBackoff {
263 use backon::BackoffBuilder;
264 backon::ExponentialBuilder::default()
270 .with_min_delay(std::time::Duration::from_millis(10))
271 .with_max_times(10)
272 .build()
273}
274
275#[cfg(feature = "tokio")]
277pub async fn rename_with_retry(
278 from: impl AsRef<Path>,
279 to: impl AsRef<Path>,
280) -> Result<(), std::io::Error> {
281 #[cfg(windows)]
282 {
283 use backon::Retryable;
284 let from = from.as_ref();
290 let to = to.as_ref();
291
292 let rename = async || fs_err::rename(from, to);
293
294 rename
295 .retry(backoff_file_move())
296 .sleep(tokio::time::sleep)
297 .when(|e| e.kind() == std::io::ErrorKind::PermissionDenied)
298 .notify(|err, _dur| {
299 warn!(
300 "Retrying rename from {} to {} due to transient error: {}",
301 from.display(),
302 to.display(),
303 err
304 );
305 })
306 .await
307 }
308 #[cfg(not(windows))]
309 {
310 fs_err::tokio::rename(from, to).await
311 }
312}
313
314#[cfg_attr(not(windows), allow(unused_variables))]
317pub fn with_retry_sync(
318 from: impl AsRef<Path>,
319 to: impl AsRef<Path>,
320 operation_name: &str,
321 operation: impl Fn() -> Result<(), std::io::Error>,
322) -> Result<(), std::io::Error> {
323 #[cfg(windows)]
324 {
325 use backon::BlockingRetryable;
326 let from = from.as_ref();
332 let to = to.as_ref();
333
334 operation
335 .retry(backoff_file_move())
336 .sleep(std::thread::sleep)
337 .when(|err| err.kind() == std::io::ErrorKind::PermissionDenied)
338 .notify(|err, _dur| {
339 warn!(
340 "Retrying {} from {} to {} due to transient error: {}",
341 operation_name,
342 from.display(),
343 to.display(),
344 err
345 );
346 })
347 .call()
348 .map_err(|err| {
349 std::io::Error::other(format!(
350 "Failed {} {} to {}: {}",
351 operation_name,
352 from.display(),
353 to.display(),
354 err
355 ))
356 })
357 }
358 #[cfg(not(windows))]
359 {
360 operation()
361 }
362}
363
364#[cfg(windows)]
366enum PersistRetryError {
367 Persist(String),
369 LostState,
371}
372
373pub async fn persist_with_retry(
375 from: NamedTempFile,
376 to: impl AsRef<Path>,
377) -> Result<(), std::io::Error> {
378 #[cfg(windows)]
379 {
380 use backon::Retryable;
381 let to = to.as_ref();
387
388 let from = std::sync::Arc::new(std::sync::Mutex::new(Some(from)));
408 let persist = || {
409 let from2 = from.clone();
411
412 async move {
413 let maybe_file: Option<NamedTempFile> = from2
414 .lock()
415 .map_err(|_| PersistRetryError::LostState)?
416 .take();
417 if let Some(file) = maybe_file {
418 file.persist(to).map_err(|err| {
419 let error_message: String = err.to_string();
420 if let Ok(mut guard) = from2.lock() {
422 *guard = Some(err.file);
423 PersistRetryError::Persist(error_message)
424 } else {
425 PersistRetryError::LostState
426 }
427 })
428 } else {
429 Err(PersistRetryError::LostState)
430 }
431 }
432 };
433
434 let persisted = persist
435 .retry(backoff_file_move())
436 .sleep(tokio::time::sleep)
437 .when(|err| matches!(err, PersistRetryError::Persist(_)))
438 .notify(|err, _dur| {
439 if let PersistRetryError::Persist(error_message) = err {
440 warn!(
441 "Retrying to persist temporary file to {}: {}",
442 to.display(),
443 error_message,
444 );
445 }
446 })
447 .await;
448
449 match persisted {
450 Ok(_) => Ok(()),
451 Err(PersistRetryError::Persist(error_message)) => Err(std::io::Error::other(format!(
452 "Failed to persist temporary file to {}: {}",
453 to.display(),
454 error_message,
455 ))),
456 Err(PersistRetryError::LostState) => Err(std::io::Error::other(format!(
457 "Failed to retrieve temporary file while trying to persist to {}",
458 to.display()
459 ))),
460 }
461 }
462 #[cfg(not(windows))]
463 {
464 async { fs_err::rename(from, to) }.await
465 }
466}
467
468pub fn persist_with_retry_sync(
470 from: NamedTempFile,
471 to: impl AsRef<Path>,
472) -> Result<(), std::io::Error> {
473 #[cfg(windows)]
474 {
475 use backon::BlockingRetryable;
476 let to = to.as_ref();
482
483 let mut from = Some(from);
488 let persist = || {
489 if let Some(file) = from.take() {
491 file.persist(to).map_err(|err| {
492 let error_message = err.to_string();
493 from = Some(err.file);
495 PersistRetryError::Persist(error_message)
496 })
497 } else {
498 Err(PersistRetryError::LostState)
499 }
500 };
501
502 let persisted = persist
503 .retry(backoff_file_move())
504 .sleep(std::thread::sleep)
505 .when(|err| matches!(err, PersistRetryError::Persist(_)))
506 .notify(|err, _dur| {
507 if let PersistRetryError::Persist(error_message) = err {
508 warn!(
509 "Retrying to persist temporary file to {}: {}",
510 to.display(),
511 error_message,
512 );
513 }
514 })
515 .call();
516
517 match persisted {
518 Ok(_) => Ok(()),
519 Err(PersistRetryError::Persist(error_message)) => Err(std::io::Error::other(format!(
520 "Failed to persist temporary file to {}: {}",
521 to.display(),
522 error_message,
523 ))),
524 Err(PersistRetryError::LostState) => Err(std::io::Error::other(format!(
525 "Failed to retrieve temporary file while trying to persist to {}",
526 to.display()
527 ))),
528 }
529 }
530 #[cfg(not(windows))]
531 {
532 fs_err::rename(from, to)
533 }
534}
535
536pub fn directories(
540 path: impl AsRef<Path>,
541) -> Result<impl Iterator<Item = PathBuf>, std::io::Error> {
542 let entries = match path.as_ref().read_dir() {
543 Ok(entries) => Some(entries),
544 Err(err) if err.kind() == std::io::ErrorKind::NotFound => None,
545 Err(err) => return Err(err),
546 };
547 Ok(entries
548 .into_iter()
549 .flatten()
550 .filter_map(|entry| match entry {
551 Ok(entry) => Some(entry),
552 Err(err) => {
553 warn!("Failed to read entry: {err}");
554 None
555 }
556 })
557 .filter(|entry| entry.file_type().is_ok_and(|file_type| file_type.is_dir()))
558 .map(|entry| entry.path()))
559}
560
561pub fn entries(path: impl AsRef<Path>) -> Result<impl Iterator<Item = PathBuf>, std::io::Error> {
565 let entries = match path.as_ref().read_dir() {
566 Ok(entries) => Some(entries),
567 Err(err) if err.kind() == std::io::ErrorKind::NotFound => None,
568 Err(err) => return Err(err),
569 };
570 Ok(entries
571 .into_iter()
572 .flatten()
573 .filter_map(|entry| match entry {
574 Ok(entry) => Some(entry),
575 Err(err) => {
576 warn!("Failed to read entry: {err}");
577 None
578 }
579 })
580 .map(|entry| entry.path()))
581}
582
583pub fn files(path: impl AsRef<Path>) -> Result<impl Iterator<Item = PathBuf>, std::io::Error> {
587 let entries = match path.as_ref().read_dir() {
588 Ok(entries) => Some(entries),
589 Err(err) if err.kind() == std::io::ErrorKind::NotFound => None,
590 Err(err) => return Err(err),
591 };
592 Ok(entries
593 .into_iter()
594 .flatten()
595 .filter_map(|entry| match entry {
596 Ok(entry) => Some(entry),
597 Err(err) => {
598 warn!("Failed to read entry: {err}");
599 None
600 }
601 })
602 .filter(|entry| entry.file_type().is_ok_and(|file_type| file_type.is_file()))
603 .map(|entry| entry.path()))
604}
605
606pub fn is_temporary(path: impl AsRef<Path>) -> bool {
608 path.as_ref()
609 .file_name()
610 .and_then(|name| name.to_str())
611 .is_some_and(|name| name.starts_with(".tmp"))
612}
613
614pub fn is_virtualenv_executable(executable: impl AsRef<Path>) -> bool {
621 executable
622 .as_ref()
623 .parent()
624 .and_then(Path::parent)
625 .is_some_and(is_virtualenv_base)
626}
627
628pub fn is_virtualenv_base(path: impl AsRef<Path>) -> bool {
635 path.as_ref().join("pyvenv.cfg").is_file()
636}
637
638fn is_known_already_locked_error(err: &std::fs::TryLockError) -> bool {
640 match err {
641 std::fs::TryLockError::WouldBlock => true,
642 std::fs::TryLockError::Error(err) => {
643 if cfg!(windows) && err.raw_os_error() == Some(33) {
645 return true;
646 }
647 false
648 }
649 }
650}
651
652#[cfg(feature = "tokio")]
654pub struct ProgressReader<Reader: tokio::io::AsyncRead + Unpin, Callback: Fn(usize) + Unpin> {
655 reader: Reader,
656 callback: Callback,
657}
658
659#[cfg(feature = "tokio")]
660impl<Reader: tokio::io::AsyncRead + Unpin, Callback: Fn(usize) + Unpin>
661 ProgressReader<Reader, Callback>
662{
663 pub fn new(reader: Reader, callback: Callback) -> Self {
665 Self { reader, callback }
666 }
667}
668
669#[cfg(feature = "tokio")]
670impl<Reader: tokio::io::AsyncRead + Unpin, Callback: Fn(usize) + Unpin> tokio::io::AsyncRead
671 for ProgressReader<Reader, Callback>
672{
673 fn poll_read(
674 mut self: std::pin::Pin<&mut Self>,
675 cx: &mut std::task::Context<'_>,
676 buf: &mut tokio::io::ReadBuf<'_>,
677 ) -> std::task::Poll<std::io::Result<()>> {
678 std::pin::Pin::new(&mut self.as_mut().reader)
679 .poll_read(cx, buf)
680 .map_ok(|()| {
681 (self.callback)(buf.filled().len());
682 })
683 }
684}
685
686pub fn copy_dir_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
688 fs_err::create_dir_all(&dst)?;
689 for entry in fs_err::read_dir(src.as_ref())? {
690 let entry = entry?;
691 let ty = entry.file_type()?;
692 if ty.is_dir() {
693 copy_dir_all(entry.path(), dst.as_ref().join(entry.file_name()))?;
694 } else {
695 fs_err::copy(entry.path(), dst.as_ref().join(entry.file_name()))?;
696 }
697 }
698 Ok(())
699}