1use std::io;
2use std::path::{Path, PathBuf};
3
4#[cfg(feature = "tokio")]
5use std::io::Read;
6
7#[cfg(feature = "tokio")]
8use encoding_rs_io::DecodeReaderBytes;
9use tempfile::NamedTempFile;
10use tracing::{debug, warn};
11
12pub use crate::locked_file::*;
13pub use crate::path::*;
14
15pub mod cachedir;
16pub mod link;
17mod locked_file;
18mod path;
19pub mod which;
20
21pub fn is_same_file_allow_missing(left: &Path, right: &Path) -> Option<bool> {
25 if left == right {
27 return Some(true);
28 }
29
30 if let Ok(value) = same_file::is_same_file(left, right) {
32 return Some(value);
33 }
34
35 if let (Some(left_parent), Some(right_parent), Some(left_name), Some(right_name)) = (
37 left.parent(),
38 right.parent(),
39 left.file_name(),
40 right.file_name(),
41 ) {
42 match same_file::is_same_file(left_parent, right_parent) {
43 Ok(true) => return Some(left_name == right_name),
44 Ok(false) => return Some(false),
45 _ => (),
46 }
47 }
48
49 None
51}
52
53#[cfg(feature = "tokio")]
63pub async fn read_to_string_transcode(path: impl AsRef<Path>) -> std::io::Result<String> {
64 let path = path.as_ref();
65 let raw = if path == Path::new("-") {
66 let mut buf = Vec::with_capacity(1024);
67 std::io::stdin().read_to_end(&mut buf)?;
68 buf
69 } else {
70 fs_err::tokio::read(path).await?
71 };
72 let mut buf = String::with_capacity(1024);
73 DecodeReaderBytes::new(&*raw)
74 .read_to_string(&mut buf)
75 .map_err(|err| {
76 let path = path.display();
77 std::io::Error::other(format!("failed to decode file {path}: {err}"))
78 })?;
79 Ok(buf)
80}
81
82#[cfg(windows)]
89fn create_junction(target: &Path, path: &Path) -> std::io::Result<()> {
90 use windows::Win32::Foundation::{
91 ERROR_ALREADY_EXISTS, ERROR_INVALID_NAME, ERROR_INVALID_PARAMETER,
92 ERROR_INVALID_REPARSE_DATA, ERROR_NOT_A_REPARSE_POINT, WIN32_ERROR,
93 };
94
95 let create_result = junction::create(target, path);
96
97 match path.metadata() {
98 Ok(_) if create_result.is_ok() => Ok(()),
99 Ok(_) => {
100 if let Err(ref create_err) = create_result {
103 if !matches!(
104 create_err
105 .raw_os_error()
106 .map(|err| WIN32_ERROR(err.cast_unsigned())),
107 Some(ERROR_ALREADY_EXISTS)
108 ) {
109 let _ = fs_err::remove_dir(path);
112 }
113 }
114 create_result
115 }
116 Err(err)
117 if matches!(
118 err.raw_os_error()
119 .map(|err| WIN32_ERROR(err.cast_unsigned())),
120 Some(
121 ERROR_INVALID_PARAMETER
122 | ERROR_INVALID_NAME
123 | ERROR_NOT_A_REPARSE_POINT
124 | ERROR_INVALID_REPARSE_DATA
125 )
126 ) =>
127 {
128 if junction::delete(path).is_ok() {
131 let _ = fs_err::remove_dir(path);
132 }
133 Err(create_result.err().unwrap_or(err))
134 }
135 Err(err) => Err(create_result.err().unwrap_or(err)),
136 }
137}
138
139#[cfg(windows)]
154pub fn replace_symlink(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
155 let src = src.as_ref();
156 let dst = dst.as_ref();
157
158 if src.is_file() {
159 return Err(std::io::Error::new(
160 std::io::ErrorKind::InvalidInput,
161 format!(
162 "Cannot create a directory link for {}: is not a directory",
163 src.display()
164 ),
165 ));
166 }
167
168 if uv_windows::is_wine() {
169 replace_with_symlink_dir(src, dst)
170 } else {
171 replace_with_junction(src, dst)
172 }
173}
174
175#[cfg(windows)]
176fn replace_with_junction(src: &Path, dst: &Path) -> std::io::Result<()> {
177 match junction::delete(dunce::simplified(dst)) {
179 Ok(()) => match fs_err::remove_dir_all(dst) {
180 Ok(()) => {}
181 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
182 Err(err) => return Err(err),
183 },
184 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
185 Err(err) => return Err(err),
186 }
187
188 create_junction(src, dst)
190}
191
192#[cfg(windows)]
193fn replace_with_symlink_dir(src: &Path, dst: &Path) -> std::io::Result<()> {
194 match fs_err::remove_dir_all(dst) {
198 Ok(()) => {}
199 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
200 Err(_) => match fs_err::remove_file(dst) {
201 Ok(()) => {}
202 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
203 Err(err) => return Err(err),
204 },
205 }
206
207 fs_err::os::windows::fs::symlink_dir(dunce::simplified(src), dunce::simplified(dst))
208}
209
210#[cfg(windows)]
219pub fn read_link(path: impl AsRef<Path>) -> std::io::Result<PathBuf> {
220 let path = path.as_ref();
221
222 if uv_windows::is_wine() {
223 fs_err::read_link(path)
224 } else {
225 junction::get_target(dunce::simplified(path))
226 }
227}
228
229#[cfg(unix)]
233pub fn replace_symlink(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
234 match fs_err::os::unix::fs::symlink(src.as_ref(), dst.as_ref()) {
236 Ok(()) => Ok(()),
237 Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
238 let temp_dir = tempfile::tempdir_in(dst.as_ref().parent().unwrap())?;
240 let temp_file = temp_dir.path().join("link");
241 fs_err::os::unix::fs::symlink(src, &temp_file)?;
242
243 fs_err::rename(&temp_file, dst.as_ref())?;
245
246 Ok(())
247 }
248 Err(err) => Err(err),
249 }
250}
251
252#[cfg(windows)]
262pub fn create_symlink(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
263 let src = src.as_ref();
264 let dst = dst.as_ref();
265
266 if src.is_file() {
267 return Err(std::io::Error::new(
268 std::io::ErrorKind::InvalidInput,
269 format!(
270 "Cannot create a directory link for {}: is not a directory",
271 src.display()
272 ),
273 ));
274 }
275
276 if uv_windows::is_wine() {
277 fs_err::os::windows::fs::symlink_dir(dunce::simplified(src), dunce::simplified(dst))
278 } else {
279 create_junction(src, dst)
280 }
281}
282
283#[cfg(unix)]
285pub fn create_symlink(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
286 fs_err::os::unix::fs::symlink(src.as_ref(), dst.as_ref())
287}
288
289#[cfg(all(test, windows))]
290mod tests {
291 use super::*;
292
293 #[test]
294 fn read_link_reads_created_directory_link() -> std::io::Result<()> {
295 let tempdir = tempfile::tempdir()?;
296 let target = tempdir.path().join("target");
297 fs_err::create_dir(&target)?;
298 let link = tempdir.path().join("link");
299
300 create_symlink(&target, &link)?;
301
302 assert_eq!(read_link(&link)?, dunce::simplified(&target));
303 Ok(())
304 }
305
306 #[test]
307 fn create_junction_from_smb_failure_removes_directory() -> std::io::Result<()> {
308 #[expect(clippy::print_stderr)]
309 let Some(smb_fs) = std::env::var(uv_static::EnvVars::UV_INTERNAL__TEST_SMB_FS).ok() else {
310 eprintln!("Skipping: UV_INTERNAL__TEST_SMB_FS not set");
311 return Ok(());
312 };
313 fs_err::create_dir_all(&smb_fs)?;
314 let alt_tempdir = tempfile::tempdir_in(smb_fs)?;
315 let tempdir = tempfile::tempdir()?;
316 let link = tempdir.path().join("link");
317 let target = alt_tempdir.path().join("target");
318 fs_err::create_dir(&target)?;
319
320 let err = create_junction(&target, &link).unwrap_err();
321 assert_eq!(err.kind(), std::io::ErrorKind::InvalidFilename);
322 assert!(!link.exists());
323 Ok(())
324 }
325}
326
327pub fn symlink_or_copy_file(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
336 #[cfg(windows)]
337 {
338 fs_err::copy(src.as_ref(), dst.as_ref())?;
339 }
340 #[cfg(unix)]
341 {
342 fs_err::os::unix::fs::symlink(src.as_ref(), dst.as_ref())?;
343 }
344
345 Ok(())
346}
347
348#[cfg(unix)]
353pub fn tempfile_in(path: &Path) -> std::io::Result<NamedTempFile> {
354 use std::os::unix::fs::PermissionsExt;
355 tempfile::Builder::new()
356 .permissions(std::fs::Permissions::from_mode(0o666))
357 .tempfile_in(path)
358}
359
360#[cfg(not(unix))]
362pub fn tempfile_in(path: &Path) -> std::io::Result<NamedTempFile> {
363 tempfile::Builder::new().tempfile_in(path)
364}
365
366#[cfg(feature = "tokio")]
368pub async fn write_atomic(path: impl AsRef<Path>, data: impl AsRef<[u8]>) -> std::io::Result<()> {
369 let temp_file = tempfile_in(
370 path.as_ref()
371 .parent()
372 .expect("Write path must have a parent"),
373 )?;
374 fs_err::tokio::write(&temp_file, &data).await?;
375 persist_with_retry(temp_file, path.as_ref()).await
376}
377
378pub fn write_atomic_sync(path: impl AsRef<Path>, data: impl AsRef<[u8]>) -> std::io::Result<()> {
380 let temp_file = tempfile_in(
381 path.as_ref()
382 .parent()
383 .expect("Write path must have a parent"),
384 )?;
385 fs_err::write(&temp_file, &data)?;
386 persist_with_retry_sync(temp_file, path.as_ref())
387}
388
389pub fn copy_atomic_sync(from: impl AsRef<Path>, to: impl AsRef<Path>) -> std::io::Result<()> {
391 let temp_file = tempfile_in(to.as_ref().parent().expect("Write path must have a parent"))?;
392 fs_err::copy(from.as_ref(), &temp_file)?;
393 persist_with_retry_sync(temp_file, to.as_ref())
394}
395
396#[cfg(windows)]
397fn backoff_file_move() -> backon::ExponentialBackoff {
398 use backon::BackoffBuilder;
399 backon::ExponentialBuilder::default()
405 .with_min_delay(std::time::Duration::from_millis(10))
406 .with_max_times(10)
407 .build()
408}
409
410#[cfg(feature = "tokio")]
412pub async fn rename_with_retry(
413 from: impl AsRef<Path>,
414 to: impl AsRef<Path>,
415) -> Result<(), std::io::Error> {
416 #[cfg(windows)]
417 {
418 use backon::Retryable;
419 let from = from.as_ref();
425 let to = to.as_ref();
426
427 let rename = async || fs_err::rename(from, to);
428
429 rename
430 .retry(backoff_file_move())
431 .sleep(tokio::time::sleep)
432 .when(|e| e.kind() == std::io::ErrorKind::PermissionDenied)
433 .notify(|err, _dur| {
434 warn!(
435 "Retrying rename from {} to {} due to transient error: {}",
436 from.display(),
437 to.display(),
438 err
439 );
440 })
441 .await
442 }
443 #[cfg(not(windows))]
444 {
445 fs_err::tokio::rename(from, to).await
446 }
447}
448
449#[cfg_attr(not(windows), allow(unused_variables))]
453pub fn with_retry_sync(
454 from: impl AsRef<Path>,
455 to: impl AsRef<Path>,
456 operation_name: &str,
457 operation: impl Fn() -> Result<(), std::io::Error>,
458) -> Result<(), std::io::Error> {
459 #[cfg(windows)]
460 {
461 use backon::BlockingRetryable;
462 let from = from.as_ref();
468 let to = to.as_ref();
469
470 operation
471 .retry(backoff_file_move())
472 .sleep(std::thread::sleep)
473 .when(|err| err.kind() == std::io::ErrorKind::PermissionDenied)
474 .notify(|err, _dur| {
475 warn!(
476 "Retrying {} from {} to {} due to transient error: {}",
477 operation_name,
478 from.display(),
479 to.display(),
480 err
481 );
482 })
483 .call()
484 .map_err(|err| {
485 std::io::Error::other(format!(
486 "Failed {} {} to {}: {}",
487 operation_name,
488 from.display(),
489 to.display(),
490 err
491 ))
492 })
493 }
494 #[cfg(not(windows))]
495 {
496 operation()
497 }
498}
499
500#[cfg(windows)]
502enum PersistRetryError {
503 Persist(String),
505 LostState,
507}
508
509#[cfg(feature = "tokio")]
512async fn persist_with_retry(
513 from: NamedTempFile,
514 to: impl AsRef<Path>,
515) -> Result<(), std::io::Error> {
516 #[cfg(windows)]
517 {
518 use backon::Retryable;
519 let to = to.as_ref();
525
526 let from = std::sync::Arc::new(std::sync::Mutex::new(Some(from)));
546 let persist = || {
547 let from2 = from.clone();
549
550 async move {
551 let maybe_file: Option<NamedTempFile> = from2
552 .lock()
553 .map_err(|_| PersistRetryError::LostState)?
554 .take();
555 if let Some(file) = maybe_file {
556 file.persist(to).map_err(|err| {
557 let error_message: String = err.to_string();
558 if let Ok(mut guard) = from2.lock() {
560 *guard = Some(err.file);
561 PersistRetryError::Persist(error_message)
562 } else {
563 PersistRetryError::LostState
564 }
565 })
566 } else {
567 Err(PersistRetryError::LostState)
568 }
569 }
570 };
571
572 let persisted = persist
573 .retry(backoff_file_move())
574 .sleep(tokio::time::sleep)
575 .when(|err| matches!(err, PersistRetryError::Persist(_)))
576 .notify(|err, _dur| {
577 if let PersistRetryError::Persist(error_message) = err {
578 warn!(
579 "Retrying to persist temporary file to {}: {}",
580 to.display(),
581 error_message,
582 );
583 }
584 })
585 .await;
586
587 match persisted {
588 Ok(_) => Ok(()),
589 Err(PersistRetryError::Persist(error_message)) => Err(std::io::Error::other(format!(
590 "Failed to persist temporary file to {}: {}",
591 to.display(),
592 error_message,
593 ))),
594 Err(PersistRetryError::LostState) => Err(std::io::Error::other(format!(
595 "Failed to retrieve temporary file while trying to persist to {}",
596 to.display()
597 ))),
598 }
599 }
600 #[cfg(not(windows))]
601 {
602 async { fs_err::rename(from, to) }.await
603 }
604}
605
606pub fn persist_with_retry_sync(
611 from: NamedTempFile,
612 to: impl AsRef<Path>,
613) -> Result<(), std::io::Error> {
614 #[cfg(windows)]
615 {
616 use backon::BlockingRetryable;
617 let to = to.as_ref();
623
624 let mut from = Some(from);
629 let persist = || {
630 if let Some(file) = from.take() {
632 file.persist(to).map_err(|err| {
633 let error_message = err.to_string();
634 from = Some(err.file);
636 PersistRetryError::Persist(error_message)
637 })
638 } else {
639 Err(PersistRetryError::LostState)
640 }
641 };
642
643 let persisted = persist
644 .retry(backoff_file_move())
645 .sleep(std::thread::sleep)
646 .when(|err| matches!(err, PersistRetryError::Persist(_)))
647 .notify(|err, _dur| {
648 if let PersistRetryError::Persist(error_message) = err {
649 warn!(
650 "Retrying to persist temporary file to {}: {}",
651 to.display(),
652 error_message,
653 );
654 }
655 })
656 .call();
657
658 match persisted {
659 Ok(_) => Ok(()),
660 Err(PersistRetryError::Persist(error_message)) => Err(std::io::Error::other(format!(
661 "Failed to persist temporary file to {}: {}",
662 to.display(),
663 error_message,
664 ))),
665 Err(PersistRetryError::LostState) => Err(std::io::Error::other(format!(
666 "Failed to retrieve temporary file while trying to persist to {}",
667 to.display()
668 ))),
669 }
670 }
671 #[cfg(not(windows))]
672 {
673 fs_err::rename(from, to)
674 }
675}
676
677pub fn directories(
681 path: impl AsRef<Path>,
682) -> Result<impl Iterator<Item = PathBuf>, std::io::Error> {
683 let entries = match path.as_ref().read_dir() {
684 Ok(entries) => Some(entries),
685 Err(err) if err.kind() == std::io::ErrorKind::NotFound => None,
686 Err(err) => return Err(err),
687 };
688 Ok(entries
689 .into_iter()
690 .flatten()
691 .filter_map(|entry| match entry {
692 Ok(entry) => Some(entry),
693 Err(err) => {
694 warn!("Failed to read entry: {err}");
695 None
696 }
697 })
698 .filter(|entry| entry.file_type().is_ok_and(|file_type| file_type.is_dir()))
699 .map(|entry| entry.path()))
700}
701
702pub fn entries(path: impl AsRef<Path>) -> Result<impl Iterator<Item = PathBuf>, std::io::Error> {
706 let entries = match path.as_ref().read_dir() {
707 Ok(entries) => Some(entries),
708 Err(err) if err.kind() == std::io::ErrorKind::NotFound => None,
709 Err(err) => return Err(err),
710 };
711 Ok(entries
712 .into_iter()
713 .flatten()
714 .filter_map(|entry| match entry {
715 Ok(entry) => Some(entry),
716 Err(err) => {
717 warn!("Failed to read entry: {err}");
718 None
719 }
720 })
721 .map(|entry| entry.path()))
722}
723
724pub fn files(path: impl AsRef<Path>) -> Result<impl Iterator<Item = PathBuf>, std::io::Error> {
728 let entries = match path.as_ref().read_dir() {
729 Ok(entries) => Some(entries),
730 Err(err) if err.kind() == std::io::ErrorKind::NotFound => None,
731 Err(err) => return Err(err),
732 };
733 Ok(entries
734 .into_iter()
735 .flatten()
736 .filter_map(|entry| match entry {
737 Ok(entry) => Some(entry),
738 Err(err) => {
739 warn!("Failed to read entry: {err}");
740 None
741 }
742 })
743 .filter(|entry| entry.file_type().is_ok_and(|file_type| file_type.is_file()))
744 .map(|entry| entry.path()))
745}
746
747pub fn is_temporary(path: impl AsRef<Path>) -> bool {
749 path.as_ref()
750 .file_name()
751 .and_then(|name| name.to_str())
752 .is_some_and(|name| name.starts_with(".tmp"))
753}
754
755pub fn is_virtualenv_executable(executable: impl AsRef<Path>) -> bool {
762 executable
763 .as_ref()
764 .parent()
765 .and_then(Path::parent)
766 .is_some_and(is_virtualenv_base)
767}
768
769pub fn is_virtualenv_base(path: impl AsRef<Path>) -> bool {
776 path.as_ref().join("pyvenv.cfg").is_file()
777}
778
779fn is_known_already_locked_error(err: &std::fs::TryLockError) -> bool {
781 match err {
782 std::fs::TryLockError::WouldBlock => true,
783 std::fs::TryLockError::Error(err) => {
784 if cfg!(windows) && err.raw_os_error() == Some(33) {
786 return true;
787 }
788 false
789 }
790 }
791}
792
793#[cfg(feature = "tokio")]
795pub struct ProgressReader<Reader: tokio::io::AsyncRead + Unpin, Callback: Fn(usize) + Unpin> {
796 reader: Reader,
797 callback: Callback,
798}
799
800#[cfg(feature = "tokio")]
801impl<Reader: tokio::io::AsyncRead + Unpin, Callback: Fn(usize) + Unpin>
802 ProgressReader<Reader, Callback>
803{
804 pub fn new(reader: Reader, callback: Callback) -> Self {
806 Self { reader, callback }
807 }
808}
809
810#[cfg(feature = "tokio")]
811impl<Reader: tokio::io::AsyncRead + Unpin, Callback: Fn(usize) + Unpin> tokio::io::AsyncRead
812 for ProgressReader<Reader, Callback>
813{
814 fn poll_read(
815 mut self: std::pin::Pin<&mut Self>,
816 cx: &mut std::task::Context<'_>,
817 buf: &mut tokio::io::ReadBuf<'_>,
818 ) -> std::task::Poll<std::io::Result<()>> {
819 std::pin::Pin::new(&mut self.as_mut().reader)
820 .poll_read(cx, buf)
821 .map_ok(|()| {
822 (self.callback)(buf.filled().len());
823 })
824 }
825}
826
827pub fn copy_dir_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
829 fs_err::create_dir_all(&dst)?;
830 for entry in fs_err::read_dir(src.as_ref())? {
831 let entry = entry?;
832 let ty = entry.file_type()?;
833 if ty.is_dir() {
834 copy_dir_all(entry.path(), dst.as_ref().join(entry.file_name()))?;
835 } else {
836 fs_err::copy(entry.path(), dst.as_ref().join(entry.file_name()))?;
837 }
838 }
839 Ok(())
840}
841
842pub fn remove_virtualenv(location: &Path) -> io::Result<()> {
844 #[cfg(windows)]
847 if let Ok(itself) = std::env::current_exe() {
848 let target = std::path::absolute(location)?;
849 if itself.starts_with(&target) {
850 debug!("Detected self-delete of executable: {}", itself.display());
851 self_replace::self_delete_outside_path(location)?;
852 }
853 }
854
855 for entry in fs_err::read_dir(location)? {
858 let entry = entry?;
859 let path = entry.path();
860 if path == location.join("pyvenv.cfg") {
861 continue;
862 }
863 if path.is_dir() {
864 fs_err::remove_dir_all(&path)?;
865 } else {
866 fs_err::remove_file(&path)?;
867 }
868 }
869
870 match fs_err::remove_file(location.join("pyvenv.cfg")) {
871 Ok(()) => {}
872 Err(err) if err.kind() == io::ErrorKind::NotFound => {}
873 Err(err) => return Err(err),
874 }
875
876 match fs_err::remove_dir_all(location) {
878 Ok(()) => {}
879 Err(err) if err.kind() == io::ErrorKind::NotFound => {}
880 Err(err) if err.kind() == io::ErrorKind::ResourceBusy => {
883 debug!(
884 "Skipping removal of `{}` directory due to {err}",
885 location.display(),
886 );
887 }
888 Err(err) => return Err(err),
889 }
890
891 Ok(())
892}