1use std::io::{self, Write};
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()) {
321 Ok(()) => Ok(()),
322 Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
323 let temp_file = tempfile::Builder::new().make_in(
324 dst.as_ref()
325 .parent()
326 .expect("Symlink path must have a parent"),
327 |path| fs_err::os::unix::fs::symlink(src.as_ref(), path),
328 )?;
329 fs_err::rename(temp_file.path(), dst.as_ref())?;
330
331 Ok(())
332 }
333 Err(err) => Err(err),
334 }
335}
336
337#[cfg(windows)]
347pub fn create_symlink(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
348 let src = src.as_ref();
349 let dst = dst.as_ref();
350
351 if src.is_file() {
352 return Err(std::io::Error::new(
353 std::io::ErrorKind::InvalidInput,
354 format!(
355 "Cannot create a directory link for {}: is not a directory",
356 src.display()
357 ),
358 ));
359 }
360
361 if uv_windows::is_wine() {
362 fs_err::os::windows::fs::symlink_dir(dunce::simplified(src), dunce::simplified(dst))
363 } else {
364 create_junction(src, dst)
365 }
366}
367
368#[cfg(unix)]
370pub fn create_symlink(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
371 fs_err::os::unix::fs::symlink(src.as_ref(), dst.as_ref())
372}
373
374pub fn remove_symlink(path: impl AsRef<Path>) -> io::Result<()> {
376 let path = path.as_ref();
377
378 #[cfg(windows)]
379 {
380 use std::os::windows::fs::FileTypeExt;
381
382 if fs_err::symlink_metadata(path)?.file_type().is_symlink_dir() {
383 return fs_err::remove_dir(path);
384 }
385 }
386
387 fs_err::remove_file(path)
388}
389
390#[cfg(all(test, windows))]
391mod windows_tests {
392 use std::assert_matches;
393 use std::os::windows::ffi::OsStrExt;
394
395 use super::*;
396
397 #[test]
398 fn fs_err_read_link_reads_created_directory_link() -> std::io::Result<()> {
399 let tempdir = tempfile::tempdir()?;
400 let target = tempdir.path().join("target");
401 fs_err::create_dir(&target)?;
402 let link = tempdir.path().join("link");
403
404 create_symlink(&target, &link)?;
405
406 assert_eq!(
407 verbatim_path(&fs_err::read_link(&link)?),
408 verbatim_path(&target)
409 );
410 Ok(())
411 }
412
413 #[test]
414 fn fs_err_read_link_reads_long_junction_target() -> std::io::Result<()> {
415 let tempdir = tempfile::tempdir()?;
416 let mut target = tempdir.path().join("target");
417 while target.as_os_str().encode_wide().count() < 257 {
418 target.push("long-path-component");
419 }
420 fs_err::create_dir_all(&target)?;
421 let link = tempdir.path().join("link");
422
423 create_symlink(&target, &link)?;
424
425 let link_target = fs_err::read_link(&link)?;
426 assert_eq!(verbatim_path(&link_target), verbatim_path(&target));
427 Ok(())
428 }
429
430 #[test]
431 fn create_junction_from_smb_failure_removes_directory() -> std::io::Result<()> {
432 #[expect(clippy::print_stderr)]
433 let Some(smb_fs) = std::env::var(uv_static::EnvVars::UV_INTERNAL__TEST_SMB_FS).ok() else {
434 eprintln!("Skipping: UV_INTERNAL__TEST_SMB_FS not set");
435 return Ok(());
436 };
437 fs_err::create_dir_all(&smb_fs)?;
438 let alt_tempdir = tempfile::tempdir_in(smb_fs)?;
439 let tempdir = tempfile::tempdir()?;
440 let link = tempdir.path().join("link");
441 let target = alt_tempdir.path().join("target");
442 fs_err::create_dir(&target)?;
443
444 let err = create_junction(&target, &link).unwrap_err();
445 assert_eq!(err.kind(), std::io::ErrorKind::InvalidFilename);
446 assert_matches!(
447 fs_err::symlink_metadata(&link),
448 Err(err) if err.kind() == std::io::ErrorKind::NotFound
449 );
450 Ok(())
451 }
452}
453
454pub fn symlink_or_copy_file(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
463 cfg_select! {
464 windows => {
465 fs_err::copy(src.as_ref(), dst.as_ref())?;
466 },
467 unix => {
468 fs_err::os::unix::fs::symlink(src.as_ref(), dst.as_ref())?;
469 },
470 }
471
472 Ok(())
473}
474
475#[cfg(unix)]
480pub fn tempfile_in(path: &Path) -> std::io::Result<NamedTempFile> {
481 use std::os::unix::fs::PermissionsExt;
482 tempfile::Builder::new()
483 .permissions(std::fs::Permissions::from_mode(0o666))
484 .tempfile_in(path)
485}
486
487#[cfg(not(unix))]
489pub fn tempfile_in(path: &Path) -> std::io::Result<NamedTempFile> {
490 tempfile::Builder::new().tempfile_in(path)
491}
492
493#[cfg(feature = "tokio")]
495pub async fn write_atomic(path: impl AsRef<Path>, data: impl AsRef<[u8]>) -> std::io::Result<()> {
496 let temp_file = tempfile_in(
497 path.as_ref()
498 .parent()
499 .expect("Write path must have a parent"),
500 )?;
501 fs_err::tokio::write(&temp_file, &data).await?;
502 persist_with_retry(temp_file, path.as_ref()).await
503}
504
505pub fn write_atomic_sync(path: impl AsRef<Path>, data: impl AsRef<[u8]>) -> std::io::Result<()> {
507 let mut temp_file = tempfile_in(
508 path.as_ref()
509 .parent()
510 .expect("Write path must have a parent"),
511 )?;
512 temp_file.write_all(data.as_ref())?;
513 persist_with_retry_sync(temp_file, path.as_ref())
514}
515
516pub fn copy_atomic_sync(from: impl AsRef<Path>, to: impl AsRef<Path>) -> std::io::Result<()> {
518 let temp_file = tempfile_in(to.as_ref().parent().expect("Write path must have a parent"))?;
519 fs_err::copy(from.as_ref(), &temp_file)?;
520 persist_with_retry_sync(temp_file, to.as_ref())
521}
522
523#[cfg(windows)]
524fn backoff_file_move() -> backon::ExponentialBackoff {
525 use backon::BackoffBuilder;
526 backon::ExponentialBuilder::default()
532 .with_min_delay(std::time::Duration::from_millis(10))
533 .with_max_times(10)
534 .build()
535}
536
537#[cfg(feature = "tokio")]
539pub async fn rename_with_retry(
540 from: impl AsRef<Path>,
541 to: impl AsRef<Path>,
542) -> Result<(), std::io::Error> {
543 #[cfg(windows)]
544 {
545 use backon::Retryable;
546 let from = from.as_ref();
552 let to = to.as_ref();
553
554 let rename = async || fs_err::rename(from, to);
555
556 rename
557 .retry(backoff_file_move())
558 .sleep(tokio::time::sleep)
559 .when(|e| e.kind() == std::io::ErrorKind::PermissionDenied)
560 .notify(|err, _dur| {
561 warn!(
562 "Retrying rename from {} to {} due to transient error: {}",
563 from.display(),
564 to.display(),
565 err
566 );
567 })
568 .await
569 }
570 #[cfg(not(windows))]
571 {
572 fs_err::tokio::rename(from, to).await
573 }
574}
575
576#[cfg_attr(not(windows), allow(unused_variables))]
580pub fn with_retry_sync(
581 from: impl AsRef<Path>,
582 to: impl AsRef<Path>,
583 operation_name: &str,
584 operation: impl Fn() -> Result<(), std::io::Error>,
585) -> Result<(), std::io::Error> {
586 #[cfg(windows)]
587 {
588 use backon::BlockingRetryable;
589 let from = from.as_ref();
595 let to = to.as_ref();
596
597 operation
598 .retry(backoff_file_move())
599 .sleep(std::thread::sleep)
600 .when(|err| err.kind() == std::io::ErrorKind::PermissionDenied)
601 .notify(|err, _dur| {
602 warn!(
603 "Retrying {} from {} to {} due to transient error: {}",
604 operation_name,
605 from.display(),
606 to.display(),
607 err
608 );
609 })
610 .call()
611 .map_err(|err| {
612 std::io::Error::other(format!(
613 "Failed {} {} to {}: {}",
614 operation_name,
615 from.display(),
616 to.display(),
617 err
618 ))
619 })
620 }
621 #[cfg(not(windows))]
622 {
623 operation()
624 }
625}
626
627#[cfg(windows)]
629enum PersistRetryError {
630 Persist(String),
632 LostState,
634}
635
636#[cfg(feature = "tokio")]
639async fn persist_with_retry(
640 from: NamedTempFile,
641 to: impl AsRef<Path>,
642) -> Result<(), std::io::Error> {
643 #[cfg(windows)]
644 {
645 use backon::Retryable;
646 let to = to.as_ref();
652
653 let from = std::sync::Arc::new(std::sync::Mutex::new(Some(from)));
673 let persist = || {
674 let from2 = from.clone();
676
677 async move {
678 let maybe_file: Option<NamedTempFile> = from2
679 .lock()
680 .map_err(|_| PersistRetryError::LostState)?
681 .take();
682 if let Some(file) = maybe_file {
683 file.persist(to).map_err(|err| {
684 let error_message: String = err.to_string();
685 if let Ok(mut guard) = from2.lock() {
687 *guard = Some(err.file);
688 PersistRetryError::Persist(error_message)
689 } else {
690 PersistRetryError::LostState
691 }
692 })
693 } else {
694 Err(PersistRetryError::LostState)
695 }
696 }
697 };
698
699 let persisted = persist
700 .retry(backoff_file_move())
701 .sleep(tokio::time::sleep)
702 .when(|err| matches!(err, PersistRetryError::Persist(_)))
703 .notify(|err, _dur| {
704 if let PersistRetryError::Persist(error_message) = err {
705 warn!(
706 "Retrying to persist temporary file to {}: {}",
707 to.display(),
708 error_message,
709 );
710 }
711 })
712 .await;
713
714 match persisted {
715 Ok(_) => Ok(()),
716 Err(PersistRetryError::Persist(error_message)) => Err(std::io::Error::other(format!(
717 "Failed to persist temporary file to {}: {}",
718 to.display(),
719 error_message,
720 ))),
721 Err(PersistRetryError::LostState) => Err(std::io::Error::other(format!(
722 "Failed to retrieve temporary file while trying to persist to {}",
723 to.display()
724 ))),
725 }
726 }
727 #[cfg(not(windows))]
728 {
729 async { fs_err::rename(from, to) }.await
730 }
731}
732
733pub fn persist_with_retry_sync(
738 from: NamedTempFile,
739 to: impl AsRef<Path>,
740) -> Result<(), std::io::Error> {
741 #[cfg(windows)]
742 {
743 use backon::BlockingRetryable;
744 let to = to.as_ref();
750
751 let mut from = Some(from);
756 let persist = || {
757 if let Some(file) = from.take() {
759 file.persist(to).map_err(|err| {
760 let error_message = err.to_string();
761 from = Some(err.file);
763 PersistRetryError::Persist(error_message)
764 })
765 } else {
766 Err(PersistRetryError::LostState)
767 }
768 };
769
770 let persisted = persist
771 .retry(backoff_file_move())
772 .sleep(std::thread::sleep)
773 .when(|err| matches!(err, PersistRetryError::Persist(_)))
774 .notify(|err, _dur| {
775 if let PersistRetryError::Persist(error_message) = err {
776 warn!(
777 "Retrying to persist temporary file to {}: {}",
778 to.display(),
779 error_message,
780 );
781 }
782 })
783 .call();
784
785 match persisted {
786 Ok(_) => Ok(()),
787 Err(PersistRetryError::Persist(error_message)) => Err(std::io::Error::other(format!(
788 "Failed to persist temporary file to {}: {}",
789 to.display(),
790 error_message,
791 ))),
792 Err(PersistRetryError::LostState) => Err(std::io::Error::other(format!(
793 "Failed to retrieve temporary file while trying to persist to {}",
794 to.display()
795 ))),
796 }
797 }
798 #[cfg(not(windows))]
799 {
800 fs_err::rename(from, to)
801 }
802}
803
804pub fn directories(
808 path: impl AsRef<Path>,
809) -> Result<impl Iterator<Item = PathBuf>, std::io::Error> {
810 let entries = match path.as_ref().read_dir() {
811 Ok(entries) => Some(entries),
812 Err(err) if err.kind() == std::io::ErrorKind::NotFound => None,
813 Err(err) => return Err(err),
814 };
815 Ok(entries
816 .into_iter()
817 .flatten()
818 .filter_map(|entry| match entry {
819 Ok(entry) => Some(entry),
820 Err(err) => {
821 warn!("Failed to read entry: {err}");
822 None
823 }
824 })
825 .filter(|entry| entry.file_type().is_ok_and(|file_type| file_type.is_dir()))
826 .map(|entry| entry.path()))
827}
828
829pub fn entries(path: impl AsRef<Path>) -> Result<impl Iterator<Item = PathBuf>, std::io::Error> {
833 let entries = match path.as_ref().read_dir() {
834 Ok(entries) => Some(entries),
835 Err(err) if err.kind() == std::io::ErrorKind::NotFound => None,
836 Err(err) => return Err(err),
837 };
838 Ok(entries
839 .into_iter()
840 .flatten()
841 .filter_map(|entry| match entry {
842 Ok(entry) => Some(entry),
843 Err(err) => {
844 warn!("Failed to read entry: {err}");
845 None
846 }
847 })
848 .map(|entry| entry.path()))
849}
850
851pub fn files(path: impl AsRef<Path>) -> Result<impl Iterator<Item = PathBuf>, std::io::Error> {
855 let entries = match path.as_ref().read_dir() {
856 Ok(entries) => Some(entries),
857 Err(err) if err.kind() == std::io::ErrorKind::NotFound => None,
858 Err(err) => return Err(err),
859 };
860 Ok(entries
861 .into_iter()
862 .flatten()
863 .filter_map(|entry| match entry {
864 Ok(entry) => Some(entry),
865 Err(err) => {
866 warn!("Failed to read entry: {err}");
867 None
868 }
869 })
870 .filter(|entry| entry.file_type().is_ok_and(|file_type| file_type.is_file()))
871 .map(|entry| entry.path()))
872}
873
874pub fn is_temporary(path: impl AsRef<Path>) -> bool {
876 path.as_ref()
877 .file_name()
878 .and_then(|name| name.to_str())
879 .is_some_and(|name| name.starts_with(".tmp"))
880}
881
882pub fn is_virtualenv_executable(executable: impl AsRef<Path>) -> bool {
889 executable
890 .as_ref()
891 .parent()
892 .and_then(Path::parent)
893 .is_some_and(is_virtualenv_base)
894}
895
896pub fn is_virtualenv_base(path: impl AsRef<Path>) -> bool {
903 path.as_ref().join("pyvenv.cfg").is_file()
904}
905
906fn is_known_already_locked_error(err: &std::fs::TryLockError) -> bool {
908 match err {
909 std::fs::TryLockError::WouldBlock => true,
910 std::fs::TryLockError::Error(err) => {
911 if cfg!(windows) && err.raw_os_error() == Some(33) {
913 return true;
914 }
915 false
916 }
917 }
918}
919
920#[cfg(feature = "tokio")]
922pub struct ProgressReader<Reader: tokio::io::AsyncRead + Unpin, Callback: Fn(usize) + Unpin> {
923 reader: Reader,
924 callback: Callback,
925}
926
927#[cfg(feature = "tokio")]
928impl<Reader: tokio::io::AsyncRead + Unpin, Callback: Fn(usize) + Unpin>
929 ProgressReader<Reader, Callback>
930{
931 pub fn new(reader: Reader, callback: Callback) -> Self {
933 Self { reader, callback }
934 }
935}
936
937#[cfg(feature = "tokio")]
938impl<Reader: tokio::io::AsyncRead + Unpin, Callback: Fn(usize) + Unpin> tokio::io::AsyncRead
939 for ProgressReader<Reader, Callback>
940{
941 fn poll_read(
942 mut self: std::pin::Pin<&mut Self>,
943 cx: &mut std::task::Context<'_>,
944 buf: &mut tokio::io::ReadBuf<'_>,
945 ) -> std::task::Poll<std::io::Result<()>> {
946 std::pin::Pin::new(&mut self.as_mut().reader)
947 .poll_read(cx, buf)
948 .map_ok(|()| {
949 (self.callback)(buf.filled().len());
950 })
951 }
952}
953
954pub fn copy_dir_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
956 fs_err::create_dir_all(&dst)?;
957 for entry in fs_err::read_dir(src.as_ref())? {
958 let entry = entry?;
959 let ty = entry.file_type()?;
960 if ty.is_dir() {
961 copy_dir_all(entry.path(), dst.as_ref().join(entry.file_name()))?;
962 } else {
963 fs_err::copy(entry.path(), dst.as_ref().join(entry.file_name()))?;
964 }
965 }
966 Ok(())
967}
968
969pub fn remove_virtualenv(location: &Path) -> io::Result<()> {
973 if !fs_err::symlink_metadata(location)?.is_dir() {
974 return remove_symlink(location);
975 }
976
977 #[cfg(windows)]
980 if let Ok(itself) = std::env::current_exe() {
981 let target = std::path::absolute(location)?;
982 if itself.starts_with(&target) {
983 debug!("Detected self-delete of executable: {}", itself.display());
984 self_replace::self_delete_outside_path(location)?;
985 }
986 }
987
988 for entry in fs_err::read_dir(location)? {
991 let entry = entry?;
992 let path = entry.path();
993 if path == location.join("pyvenv.cfg") {
994 continue;
995 }
996 if path.is_dir() {
997 fs_err::remove_dir_all(&path)?;
998 } else {
999 fs_err::remove_file(&path)?;
1000 }
1001 }
1002
1003 match fs_err::remove_file(location.join("pyvenv.cfg")) {
1004 Ok(()) => {}
1005 Err(err) if err.kind() == io::ErrorKind::NotFound => {}
1006 Err(err) => return Err(err),
1007 }
1008
1009 match fs_err::remove_dir_all(location) {
1011 Ok(()) => {}
1012 Err(err) if err.kind() == io::ErrorKind::NotFound => {}
1013 Err(err) if err.kind() == io::ErrorKind::ResourceBusy => {
1016 debug!(
1017 "Skipping removal of `{}` directory due to {err}",
1018 location.display(),
1019 );
1020 }
1021 Err(err) => return Err(err),
1022 }
1023
1024 Ok(())
1025}
1026
1027pub fn clear_virtualenv(location: &Path) -> io::Result<bool> {
1031 let location = location
1032 .canonicalize()
1033 .unwrap_or_else(|_| location.to_path_buf());
1034 let cleared = match remove_virtualenv(&location) {
1035 Ok(()) => true,
1036 Err(err) if err.kind() == io::ErrorKind::NotFound => false,
1037 Err(err) => return Err(err),
1038 };
1039 fs_err::create_dir_all(location)?;
1040 Ok(cleared)
1041}
1042
1043#[cfg(test)]
1044mod tests {
1045 use std::assert_matches;
1046
1047 use super::*;
1048
1049 #[test]
1050 fn remove_symlink_removes_directory_link_without_removing_target() -> io::Result<()> {
1051 let tempdir = tempfile::tempdir()?;
1052 let target = tempdir.path().join("target");
1053 fs_err::create_dir(&target)?;
1054 fs_err::write(target.join("file"), "content")?;
1055 let link = tempdir.path().join("link");
1056
1057 create_symlink(&target, &link)?;
1058 remove_symlink(&link)?;
1059
1060 assert_matches!(
1061 fs_err::symlink_metadata(&link),
1062 Err(err) if err.kind() == io::ErrorKind::NotFound
1063 );
1064 assert_eq!(fs_err::read_to_string(target.join("file"))?, "content");
1065 Ok(())
1066 }
1067
1068 #[test]
1069 fn remove_virtualenv_removes_directory_link_without_removing_target() -> io::Result<()> {
1070 let tempdir = tempfile::tempdir()?;
1071 let target = tempdir.path().join("target");
1072 fs_err::create_dir(&target)?;
1073 let marker = target.join("marker");
1074 fs_err::write(&marker, "")?;
1075 let environment = tempdir.path().join("environment");
1076 create_symlink(&target, &environment)?;
1077
1078 remove_virtualenv(&environment)?;
1079
1080 assert_matches!(
1081 fs_err::symlink_metadata(environment),
1082 Err(err) if err.kind() == io::ErrorKind::NotFound
1083 );
1084 assert!(marker.is_file());
1085 Ok(())
1086 }
1087
1088 #[test]
1089 fn clear_virtualenv_recreates_missing_directory() -> io::Result<()> {
1090 let tempdir = tempfile::tempdir()?;
1091 let environment = tempdir.path().join("environment");
1092
1093 assert!(!clear_virtualenv(&environment)?);
1094 assert!(environment.is_dir());
1095 Ok(())
1096 }
1097}