1use std::io;
2use std::path::{Path, PathBuf};
3use std::time::SystemTime;
4
5#[cfg(unix)]
6use std::os::unix::fs::MetadataExt;
7#[cfg(windows)]
8use std::os::windows::io::AsRawHandle;
9
10#[cfg(target_os = "linux")]
11use std::time::{Duration, UNIX_EPOCH};
12
13#[cfg(feature = "tokio")]
14use std::io::Read;
15
16#[cfg(feature = "tokio")]
17use encoding_rs_io::DecodeReaderBytes;
18#[cfg(target_os = "linux")]
19use rustix::fs::{AtFlags, CWD as RUSTIX_CWD, StatxFlags, statx};
20use tempfile::NamedTempFile;
21use tracing::{debug, warn};
22#[cfg(windows)]
23use windows::Win32::Foundation::HANDLE;
24#[cfg(windows)]
25use windows::Win32::Storage::FileSystem::{BY_HANDLE_FILE_INFORMATION, GetFileInformationByHandle};
26
27pub use crate::locked_file::*;
28pub use crate::path::*;
29pub use crate::read::ValidatedReader;
30pub use crate::space::{PhysicalSpaceError, physical_space, supports_fine_grained_accounting};
31
32pub mod cachedir;
33#[cfg(target_os = "macos")]
34mod hardlink_macos;
35pub mod link;
36mod locked_file;
37mod path;
38mod read;
39mod space;
40pub mod which;
41
42#[cfg(unix)]
44pub fn hardlink_count(path: &Path) -> io::Result<u64> {
45 Ok(fs_err::metadata(path)?.nlink())
46}
47
48#[cfg(windows)]
50#[expect(unsafe_code)]
51pub fn hardlink_count(path: &Path) -> io::Result<u64> {
52 let file = fs_err::File::open(path)?;
53 let mut information = BY_HANDLE_FILE_INFORMATION::default();
54 unsafe { GetFileInformationByHandle(HANDLE(file.as_raw_handle()), &raw mut information) }?;
57 Ok(u64::from(information.nNumberOfLinks))
58}
59
60#[cfg(not(any(unix, windows)))]
62pub fn hardlink_count(_path: &Path) -> io::Result<u64> {
63 Err(io::Error::new(
64 io::ErrorKind::Unsupported,
65 "hardlink counts are not supported on this platform",
66 ))
67}
68
69pub fn files_with_one_hardlink(path: &Path) -> io::Result<Option<Vec<PathBuf>>> {
78 #[cfg(target_os = "macos")]
79 {
80 hardlink_macos::files_with_one_hardlink(path)
81 }
82 #[cfg(not(target_os = "macos"))]
83 {
84 let _ = path;
85 Ok(None)
86 }
87}
88
89pub fn created_time(path: &Path, metadata: &std::fs::Metadata) -> io::Result<SystemTime> {
92 #[cfg(target_os = "linux")]
93 {
94 let _ = metadata;
95
96 let metadata = statx(
97 RUSTIX_CWD,
98 path,
99 AtFlags::empty(),
100 StatxFlags::BASIC_STATS | StatxFlags::BTIME,
101 )?;
102
103 if metadata.stx_mask & StatxFlags::BTIME.bits() == 0 {
104 return Err(io::Error::new(
105 io::ErrorKind::Unsupported,
106 "creation time is not available for the filesystem",
107 ));
108 }
109
110 let birth_time = metadata.stx_btime;
111 let seconds = Duration::from_secs(birth_time.tv_sec.unsigned_abs());
112 let created = if birth_time.tv_sec < 0 {
113 UNIX_EPOCH.checked_sub(seconds)
114 } else {
115 UNIX_EPOCH.checked_add(seconds)
116 };
117
118 created
119 .filter(|_| birth_time.tv_nsec < 1_000_000_000)
120 .and_then(|created| {
121 created.checked_add(Duration::from_nanos(u64::from(birth_time.tv_nsec)))
122 })
123 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "invalid creation time"))
124 }
125
126 #[cfg(not(target_os = "linux"))]
127 {
128 let _ = path;
129 metadata.created()
130 }
131}
132
133pub fn is_same_file_allow_missing(left: &Path, right: &Path) -> Option<bool> {
137 if left == right {
139 return Some(true);
140 }
141
142 if let Ok(value) = same_file::is_same_file(left, right) {
144 return Some(value);
145 }
146
147 if let (Some(left_parent), Some(right_parent), Some(left_name), Some(right_name)) = (
149 left.parent(),
150 right.parent(),
151 left.file_name(),
152 right.file_name(),
153 ) {
154 match same_file::is_same_file(left_parent, right_parent) {
155 Ok(true) => return Some(left_name == right_name),
156 Ok(false) => return Some(false),
157 _ => (),
158 }
159 }
160
161 None
163}
164
165#[cfg(feature = "tokio")]
175pub async fn read_to_string_transcode(path: impl AsRef<Path>) -> std::io::Result<String> {
176 let path = path.as_ref();
177 let raw = if path == Path::new("-") {
178 let mut buf = Vec::with_capacity(1024);
179 std::io::stdin().read_to_end(&mut buf)?;
180 buf
181 } else {
182 fs_err::tokio::read(path).await?
183 };
184 let mut buf = String::with_capacity(1024);
185 DecodeReaderBytes::new(&*raw)
186 .read_to_string(&mut buf)
187 .map_err(|err| {
188 let path = path.display();
189 std::io::Error::other(format!("failed to decode file {path}: {err}"))
190 })?;
191 Ok(buf)
192}
193
194#[cfg(windows)]
201fn create_junction(target: &Path, path: &Path) -> std::io::Result<()> {
202 use windows::Win32::Foundation::{
203 ERROR_ALREADY_EXISTS, ERROR_INVALID_NAME, ERROR_INVALID_PARAMETER,
204 ERROR_INVALID_REPARSE_DATA, ERROR_NOT_A_REPARSE_POINT, WIN32_ERROR,
205 };
206
207 let create_result = junction::create(target, path);
208
209 match path.metadata() {
210 Ok(_) if create_result.is_ok() => Ok(()),
211 Ok(_) => {
212 if let Err(ref create_err) = create_result {
215 if !matches!(
216 create_err
217 .raw_os_error()
218 .map(|err| WIN32_ERROR(err.cast_unsigned())),
219 Some(ERROR_ALREADY_EXISTS)
220 ) {
221 let _ = fs_err::remove_dir(path);
224 }
225 }
226 create_result
227 }
228 Err(err)
229 if matches!(
230 err.raw_os_error()
231 .map(|err| WIN32_ERROR(err.cast_unsigned())),
232 Some(
233 ERROR_INVALID_PARAMETER
234 | ERROR_INVALID_NAME
235 | ERROR_NOT_A_REPARSE_POINT
236 | ERROR_INVALID_REPARSE_DATA
237 )
238 ) =>
239 {
240 let _ = fs_err::remove_dir(path);
242 Err(create_result.err().unwrap_or(err))
243 }
244 Err(err) => Err(create_result.err().unwrap_or(err)),
245 }
246}
247
248#[cfg(windows)]
263pub fn replace_symlink(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
264 let src = src.as_ref();
265 let dst = dst.as_ref();
266
267 if src.is_file() {
268 return Err(std::io::Error::new(
269 std::io::ErrorKind::InvalidInput,
270 format!(
271 "Cannot create a directory link for {}: is not a directory",
272 src.display()
273 ),
274 ));
275 }
276
277 if uv_windows::is_wine() {
278 replace_with_symlink_dir(src, dst)
279 } else {
280 replace_with_junction(src, dst)
281 }
282}
283
284#[cfg(windows)]
285fn replace_with_junction(src: &Path, dst: &Path) -> std::io::Result<()> {
286 match fs_err::remove_dir(dst) {
288 Ok(()) => {}
289 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
290 Err(err) => return Err(err),
291 }
292
293 create_junction(src, dst)
295}
296
297#[cfg(windows)]
298fn replace_with_symlink_dir(src: &Path, dst: &Path) -> std::io::Result<()> {
299 match fs_err::remove_dir_all(dst) {
303 Ok(()) => {}
304 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
305 Err(_) => match fs_err::remove_file(dst) {
306 Ok(()) => {}
307 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
308 Err(err) => return Err(err),
309 },
310 }
311
312 fs_err::os::windows::fs::symlink_dir(dunce::simplified(src), dunce::simplified(dst))
313}
314
315#[cfg(unix)]
319pub fn replace_symlink(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
320 match fs_err::os::unix::fs::symlink(src.as_ref(), dst.as_ref()) {
322 Ok(()) => Ok(()),
323 Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
324 let temp_dir = tempfile::tempdir_in(dst.as_ref().parent().unwrap())?;
326 let temp_file = temp_dir.path().join("link");
327 fs_err::os::unix::fs::symlink(src, &temp_file)?;
328
329 fs_err::rename(&temp_file, dst.as_ref())?;
331
332 Ok(())
333 }
334 Err(err) => Err(err),
335 }
336}
337
338#[cfg(windows)]
348pub fn create_symlink(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
349 let src = src.as_ref();
350 let dst = dst.as_ref();
351
352 if src.is_file() {
353 return Err(std::io::Error::new(
354 std::io::ErrorKind::InvalidInput,
355 format!(
356 "Cannot create a directory link for {}: is not a directory",
357 src.display()
358 ),
359 ));
360 }
361
362 if uv_windows::is_wine() {
363 fs_err::os::windows::fs::symlink_dir(dunce::simplified(src), dunce::simplified(dst))
364 } else {
365 create_junction(src, dst)
366 }
367}
368
369#[cfg(unix)]
371pub fn create_symlink(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
372 fs_err::os::unix::fs::symlink(src.as_ref(), dst.as_ref())
373}
374
375pub fn remove_symlink(path: impl AsRef<Path>) -> io::Result<()> {
377 let path = path.as_ref();
378
379 #[cfg(windows)]
380 {
381 use std::os::windows::fs::FileTypeExt;
382
383 if fs_err::symlink_metadata(path)?.file_type().is_symlink_dir() {
384 return fs_err::remove_dir(path);
385 }
386 }
387
388 fs_err::remove_file(path)
389}
390
391#[cfg(all(test, windows))]
392mod windows_tests {
393 use std::assert_matches;
394 use std::os::windows::ffi::OsStrExt;
395
396 use super::*;
397
398 #[test]
399 fn fs_err_read_link_reads_created_directory_link() -> std::io::Result<()> {
400 let tempdir = tempfile::tempdir()?;
401 let target = tempdir.path().join("target");
402 fs_err::create_dir(&target)?;
403 let link = tempdir.path().join("link");
404
405 create_symlink(&target, &link)?;
406
407 assert_eq!(
408 verbatim_path(&fs_err::read_link(&link)?),
409 verbatim_path(&target)
410 );
411 Ok(())
412 }
413
414 #[test]
415 fn fs_err_read_link_reads_long_junction_target() -> std::io::Result<()> {
416 let tempdir = tempfile::tempdir()?;
417 let mut target = tempdir.path().join("target");
418 while target.as_os_str().encode_wide().count() < 257 {
419 target.push("long-path-component");
420 }
421 fs_err::create_dir_all(&target)?;
422 let link = tempdir.path().join("link");
423
424 create_symlink(&target, &link)?;
425
426 let link_target = fs_err::read_link(&link)?;
427 assert_eq!(verbatim_path(&link_target), verbatim_path(&target));
428 Ok(())
429 }
430
431 #[test]
432 fn create_junction_from_smb_failure_removes_directory() -> std::io::Result<()> {
433 #[expect(clippy::print_stderr)]
434 let Some(smb_fs) = std::env::var(uv_static::EnvVars::UV_INTERNAL__TEST_SMB_FS).ok() else {
435 eprintln!("Skipping: UV_INTERNAL__TEST_SMB_FS not set");
436 return Ok(());
437 };
438 fs_err::create_dir_all(&smb_fs)?;
439 let alt_tempdir = tempfile::tempdir_in(smb_fs)?;
440 let tempdir = tempfile::tempdir()?;
441 let link = tempdir.path().join("link");
442 let target = alt_tempdir.path().join("target");
443 fs_err::create_dir(&target)?;
444
445 let err = create_junction(&target, &link).unwrap_err();
446 assert_eq!(err.kind(), std::io::ErrorKind::InvalidFilename);
447 assert_matches!(
448 fs_err::symlink_metadata(&link),
449 Err(err) if err.kind() == std::io::ErrorKind::NotFound
450 );
451 Ok(())
452 }
453}
454
455pub fn symlink_or_copy_file(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
464 cfg_select! {
465 windows => {
466 fs_err::copy(src.as_ref(), dst.as_ref())?;
467 },
468 unix => {
469 fs_err::os::unix::fs::symlink(src.as_ref(), dst.as_ref())?;
470 },
471 }
472
473 Ok(())
474}
475
476#[cfg(unix)]
481pub fn tempfile_in(path: &Path) -> std::io::Result<NamedTempFile> {
482 use std::os::unix::fs::PermissionsExt;
483 tempfile::Builder::new()
484 .permissions(std::fs::Permissions::from_mode(0o666))
485 .tempfile_in(path)
486}
487
488#[cfg(not(unix))]
490pub fn tempfile_in(path: &Path) -> std::io::Result<NamedTempFile> {
491 tempfile::Builder::new().tempfile_in(path)
492}
493
494#[cfg(feature = "tokio")]
496pub async fn write_atomic(path: impl AsRef<Path>, data: impl AsRef<[u8]>) -> std::io::Result<()> {
497 let temp_file = tempfile_in(
498 path.as_ref()
499 .parent()
500 .expect("Write path must have a parent"),
501 )?;
502 fs_err::tokio::write(&temp_file, &data).await?;
503 persist_with_retry(temp_file, path.as_ref()).await
504}
505
506pub fn write_atomic_sync(path: impl AsRef<Path>, data: impl AsRef<[u8]>) -> std::io::Result<()> {
508 let temp_file = tempfile_in(
509 path.as_ref()
510 .parent()
511 .expect("Write path must have a parent"),
512 )?;
513 fs_err::write(&temp_file, &data)?;
514 persist_with_retry_sync(temp_file, path.as_ref())
515}
516
517pub fn copy_atomic_sync(from: impl AsRef<Path>, to: impl AsRef<Path>) -> std::io::Result<()> {
519 let temp_file = tempfile_in(to.as_ref().parent().expect("Write path must have a parent"))?;
520 fs_err::copy(from.as_ref(), &temp_file)?;
521 persist_with_retry_sync(temp_file, to.as_ref())
522}
523
524#[cfg(windows)]
525fn backoff_file_move() -> backon::ExponentialBackoff {
526 use backon::BackoffBuilder;
527 backon::ExponentialBuilder::default()
533 .with_min_delay(std::time::Duration::from_millis(10))
534 .with_max_times(10)
535 .build()
536}
537
538#[cfg(feature = "tokio")]
540pub async fn rename_with_retry(
541 from: impl AsRef<Path>,
542 to: impl AsRef<Path>,
543) -> Result<(), std::io::Error> {
544 #[cfg(windows)]
545 {
546 use backon::Retryable;
547 let from = from.as_ref();
553 let to = to.as_ref();
554
555 let rename = async || fs_err::rename(from, to);
556
557 rename
558 .retry(backoff_file_move())
559 .sleep(tokio::time::sleep)
560 .when(|e| e.kind() == std::io::ErrorKind::PermissionDenied)
561 .notify(|err, _dur| {
562 warn!(
563 "Retrying rename from {} to {} due to transient error: {}",
564 from.display(),
565 to.display(),
566 err
567 );
568 })
569 .await
570 }
571 #[cfg(not(windows))]
572 {
573 fs_err::tokio::rename(from, to).await
574 }
575}
576
577#[cfg_attr(not(windows), allow(unused_variables))]
581pub fn with_retry_sync(
582 from: impl AsRef<Path>,
583 to: impl AsRef<Path>,
584 operation_name: &str,
585 operation: impl Fn() -> Result<(), std::io::Error>,
586) -> Result<(), std::io::Error> {
587 #[cfg(windows)]
588 {
589 use backon::BlockingRetryable;
590 let from = from.as_ref();
596 let to = to.as_ref();
597
598 operation
599 .retry(backoff_file_move())
600 .sleep(std::thread::sleep)
601 .when(|err| err.kind() == std::io::ErrorKind::PermissionDenied)
602 .notify(|err, _dur| {
603 warn!(
604 "Retrying {} from {} to {} due to transient error: {}",
605 operation_name,
606 from.display(),
607 to.display(),
608 err
609 );
610 })
611 .call()
612 .map_err(|err| {
613 std::io::Error::other(format!(
614 "Failed {} {} to {}: {}",
615 operation_name,
616 from.display(),
617 to.display(),
618 err
619 ))
620 })
621 }
622 #[cfg(not(windows))]
623 {
624 operation()
625 }
626}
627
628#[cfg(windows)]
630enum PersistRetryError {
631 Persist(String),
633 LostState,
635}
636
637#[cfg(feature = "tokio")]
640async fn persist_with_retry(
641 from: NamedTempFile,
642 to: impl AsRef<Path>,
643) -> Result<(), std::io::Error> {
644 #[cfg(windows)]
645 {
646 use backon::Retryable;
647 let to = to.as_ref();
653
654 let from = std::sync::Arc::new(std::sync::Mutex::new(Some(from)));
674 let persist = || {
675 let from2 = from.clone();
677
678 async move {
679 let maybe_file: Option<NamedTempFile> = from2
680 .lock()
681 .map_err(|_| PersistRetryError::LostState)?
682 .take();
683 if let Some(file) = maybe_file {
684 file.persist(to).map_err(|err| {
685 let error_message: String = err.to_string();
686 if let Ok(mut guard) = from2.lock() {
688 *guard = Some(err.file);
689 PersistRetryError::Persist(error_message)
690 } else {
691 PersistRetryError::LostState
692 }
693 })
694 } else {
695 Err(PersistRetryError::LostState)
696 }
697 }
698 };
699
700 let persisted = persist
701 .retry(backoff_file_move())
702 .sleep(tokio::time::sleep)
703 .when(|err| matches!(err, PersistRetryError::Persist(_)))
704 .notify(|err, _dur| {
705 if let PersistRetryError::Persist(error_message) = err {
706 warn!(
707 "Retrying to persist temporary file to {}: {}",
708 to.display(),
709 error_message,
710 );
711 }
712 })
713 .await;
714
715 match persisted {
716 Ok(_) => Ok(()),
717 Err(PersistRetryError::Persist(error_message)) => Err(std::io::Error::other(format!(
718 "Failed to persist temporary file to {}: {}",
719 to.display(),
720 error_message,
721 ))),
722 Err(PersistRetryError::LostState) => Err(std::io::Error::other(format!(
723 "Failed to retrieve temporary file while trying to persist to {}",
724 to.display()
725 ))),
726 }
727 }
728 #[cfg(not(windows))]
729 {
730 async { fs_err::rename(from, to) }.await
731 }
732}
733
734pub fn persist_with_retry_sync(
739 from: NamedTempFile,
740 to: impl AsRef<Path>,
741) -> Result<(), std::io::Error> {
742 #[cfg(windows)]
743 {
744 use backon::BlockingRetryable;
745 let to = to.as_ref();
751
752 let mut from = Some(from);
757 let persist = || {
758 if let Some(file) = from.take() {
760 file.persist(to).map_err(|err| {
761 let error_message = err.to_string();
762 from = Some(err.file);
764 PersistRetryError::Persist(error_message)
765 })
766 } else {
767 Err(PersistRetryError::LostState)
768 }
769 };
770
771 let persisted = persist
772 .retry(backoff_file_move())
773 .sleep(std::thread::sleep)
774 .when(|err| matches!(err, PersistRetryError::Persist(_)))
775 .notify(|err, _dur| {
776 if let PersistRetryError::Persist(error_message) = err {
777 warn!(
778 "Retrying to persist temporary file to {}: {}",
779 to.display(),
780 error_message,
781 );
782 }
783 })
784 .call();
785
786 match persisted {
787 Ok(_) => Ok(()),
788 Err(PersistRetryError::Persist(error_message)) => Err(std::io::Error::other(format!(
789 "Failed to persist temporary file to {}: {}",
790 to.display(),
791 error_message,
792 ))),
793 Err(PersistRetryError::LostState) => Err(std::io::Error::other(format!(
794 "Failed to retrieve temporary file while trying to persist to {}",
795 to.display()
796 ))),
797 }
798 }
799 #[cfg(not(windows))]
800 {
801 fs_err::rename(from, to)
802 }
803}
804
805pub fn directories(
809 path: impl AsRef<Path>,
810) -> Result<impl Iterator<Item = PathBuf>, std::io::Error> {
811 let entries = match path.as_ref().read_dir() {
812 Ok(entries) => Some(entries),
813 Err(err) if err.kind() == std::io::ErrorKind::NotFound => None,
814 Err(err) => return Err(err),
815 };
816 Ok(entries
817 .into_iter()
818 .flatten()
819 .filter_map(|entry| match entry {
820 Ok(entry) => Some(entry),
821 Err(err) => {
822 warn!("Failed to read entry: {err}");
823 None
824 }
825 })
826 .filter(|entry| entry.file_type().is_ok_and(|file_type| file_type.is_dir()))
827 .map(|entry| entry.path()))
828}
829
830pub fn entries(path: impl AsRef<Path>) -> Result<impl Iterator<Item = PathBuf>, std::io::Error> {
834 let entries = match path.as_ref().read_dir() {
835 Ok(entries) => Some(entries),
836 Err(err) if err.kind() == std::io::ErrorKind::NotFound => None,
837 Err(err) => return Err(err),
838 };
839 Ok(entries
840 .into_iter()
841 .flatten()
842 .filter_map(|entry| match entry {
843 Ok(entry) => Some(entry),
844 Err(err) => {
845 warn!("Failed to read entry: {err}");
846 None
847 }
848 })
849 .map(|entry| entry.path()))
850}
851
852pub fn files(path: impl AsRef<Path>) -> Result<impl Iterator<Item = PathBuf>, std::io::Error> {
856 let entries = match path.as_ref().read_dir() {
857 Ok(entries) => Some(entries),
858 Err(err) if err.kind() == std::io::ErrorKind::NotFound => None,
859 Err(err) => return Err(err),
860 };
861 Ok(entries
862 .into_iter()
863 .flatten()
864 .filter_map(|entry| match entry {
865 Ok(entry) => Some(entry),
866 Err(err) => {
867 warn!("Failed to read entry: {err}");
868 None
869 }
870 })
871 .filter(|entry| entry.file_type().is_ok_and(|file_type| file_type.is_file()))
872 .map(|entry| entry.path()))
873}
874
875pub fn is_temporary(path: impl AsRef<Path>) -> bool {
877 path.as_ref()
878 .file_name()
879 .and_then(|name| name.to_str())
880 .is_some_and(|name| name.starts_with(".tmp"))
881}
882
883pub fn is_virtualenv_executable(executable: impl AsRef<Path>) -> bool {
890 executable
891 .as_ref()
892 .parent()
893 .and_then(Path::parent)
894 .is_some_and(is_virtualenv_base)
895}
896
897pub fn is_virtualenv_base(path: impl AsRef<Path>) -> bool {
904 path.as_ref().join("pyvenv.cfg").is_file()
905}
906
907fn is_known_already_locked_error(err: &std::fs::TryLockError) -> bool {
909 match err {
910 std::fs::TryLockError::WouldBlock => true,
911 std::fs::TryLockError::Error(err) => {
912 if cfg!(windows) && err.raw_os_error() == Some(33) {
914 return true;
915 }
916 false
917 }
918 }
919}
920
921#[cfg(feature = "tokio")]
923pub struct ProgressReader<Reader: tokio::io::AsyncRead + Unpin, Callback: Fn(usize) + Unpin> {
924 reader: Reader,
925 callback: Callback,
926}
927
928#[cfg(feature = "tokio")]
929impl<Reader: tokio::io::AsyncRead + Unpin, Callback: Fn(usize) + Unpin>
930 ProgressReader<Reader, Callback>
931{
932 pub fn new(reader: Reader, callback: Callback) -> Self {
934 Self { reader, callback }
935 }
936}
937
938#[cfg(feature = "tokio")]
939impl<Reader: tokio::io::AsyncRead + Unpin, Callback: Fn(usize) + Unpin> tokio::io::AsyncRead
940 for ProgressReader<Reader, Callback>
941{
942 fn poll_read(
943 mut self: std::pin::Pin<&mut Self>,
944 cx: &mut std::task::Context<'_>,
945 buf: &mut tokio::io::ReadBuf<'_>,
946 ) -> std::task::Poll<std::io::Result<()>> {
947 std::pin::Pin::new(&mut self.as_mut().reader)
948 .poll_read(cx, buf)
949 .map_ok(|()| {
950 (self.callback)(buf.filled().len());
951 })
952 }
953}
954
955pub fn copy_dir_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
957 fs_err::create_dir_all(&dst)?;
958 for entry in fs_err::read_dir(src.as_ref())? {
959 let entry = entry?;
960 let ty = entry.file_type()?;
961 if ty.is_dir() {
962 copy_dir_all(entry.path(), dst.as_ref().join(entry.file_name()))?;
963 } else {
964 fs_err::copy(entry.path(), dst.as_ref().join(entry.file_name()))?;
965 }
966 }
967 Ok(())
968}
969
970pub fn remove_virtualenv(location: &Path) -> io::Result<()> {
974 if !fs_err::symlink_metadata(location)?.is_dir() {
975 return remove_symlink(location);
976 }
977
978 #[cfg(windows)]
981 if let Ok(itself) = std::env::current_exe() {
982 let target = std::path::absolute(location)?;
983 if itself.starts_with(&target) {
984 debug!("Detected self-delete of executable: {}", itself.display());
985 self_replace::self_delete_outside_path(location)?;
986 }
987 }
988
989 for entry in fs_err::read_dir(location)? {
992 let entry = entry?;
993 let path = entry.path();
994 if path == location.join("pyvenv.cfg") {
995 continue;
996 }
997 if path.is_dir() {
998 fs_err::remove_dir_all(&path)?;
999 } else {
1000 fs_err::remove_file(&path)?;
1001 }
1002 }
1003
1004 match fs_err::remove_file(location.join("pyvenv.cfg")) {
1005 Ok(()) => {}
1006 Err(err) if err.kind() == io::ErrorKind::NotFound => {}
1007 Err(err) => return Err(err),
1008 }
1009
1010 match fs_err::remove_dir_all(location) {
1012 Ok(()) => {}
1013 Err(err) if err.kind() == io::ErrorKind::NotFound => {}
1014 Err(err) if err.kind() == io::ErrorKind::ResourceBusy => {
1017 debug!(
1018 "Skipping removal of `{}` directory due to {err}",
1019 location.display(),
1020 );
1021 }
1022 Err(err) => return Err(err),
1023 }
1024
1025 Ok(())
1026}
1027
1028pub fn clear_virtualenv(location: &Path) -> io::Result<bool> {
1032 let location = location
1033 .canonicalize()
1034 .unwrap_or_else(|_| location.to_path_buf());
1035 let cleared = match remove_virtualenv(&location) {
1036 Ok(()) => true,
1037 Err(err) if err.kind() == io::ErrorKind::NotFound => false,
1038 Err(err) => return Err(err),
1039 };
1040 fs_err::create_dir_all(location)?;
1041 Ok(cleared)
1042}
1043
1044#[cfg(test)]
1045mod tests {
1046 use std::assert_matches;
1047
1048 use super::*;
1049
1050 #[test]
1051 fn remove_symlink_removes_directory_link_without_removing_target() -> io::Result<()> {
1052 let tempdir = tempfile::tempdir()?;
1053 let target = tempdir.path().join("target");
1054 fs_err::create_dir(&target)?;
1055 fs_err::write(target.join("file"), "content")?;
1056 let link = tempdir.path().join("link");
1057
1058 create_symlink(&target, &link)?;
1059 remove_symlink(&link)?;
1060
1061 assert_matches!(
1062 fs_err::symlink_metadata(&link),
1063 Err(err) if err.kind() == io::ErrorKind::NotFound
1064 );
1065 assert_eq!(fs_err::read_to_string(target.join("file"))?, "content");
1066 Ok(())
1067 }
1068
1069 #[test]
1070 fn remove_virtualenv_removes_directory_link_without_removing_target() -> io::Result<()> {
1071 let tempdir = tempfile::tempdir()?;
1072 let target = tempdir.path().join("target");
1073 fs_err::create_dir(&target)?;
1074 let marker = target.join("marker");
1075 fs_err::write(&marker, "")?;
1076 let environment = tempdir.path().join("environment");
1077 create_symlink(&target, &environment)?;
1078
1079 remove_virtualenv(&environment)?;
1080
1081 assert_matches!(
1082 fs_err::symlink_metadata(environment),
1083 Err(err) if err.kind() == io::ErrorKind::NotFound
1084 );
1085 assert!(marker.is_file());
1086 Ok(())
1087 }
1088
1089 #[test]
1090 fn clear_virtualenv_recreates_missing_directory() -> io::Result<()> {
1091 let tempdir = tempfile::tempdir()?;
1092 let environment = tempdir.path().join("environment");
1093
1094 assert!(!clear_virtualenv(&environment)?);
1095 assert!(environment.is_dir());
1096 Ok(())
1097 }
1098}