Skip to main content

uv_fs/
lib.rs

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
20/// Attempt to check if the two paths refer to the same file.
21///
22/// Returns `Some(true)` if the files are missing, but would be the same if they existed.
23pub fn is_same_file_allow_missing(left: &Path, right: &Path) -> Option<bool> {
24    // First, check an exact path comparison.
25    if left == right {
26        return Some(true);
27    }
28
29    // Second, check the files directly.
30    if let Ok(value) = same_file::is_same_file(left, right) {
31        return Some(value);
32    }
33
34    // Often, one of the directories won't exist yet so perform the comparison up a level.
35    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    // We couldn't determine if they're the same.
49    None
50}
51
52/// Reads data from the path and requires that it be valid UTF-8 or UTF-16.
53///
54/// This uses BOM sniffing to determine if the data should be transcoded from UTF-16 to Rust's
55/// `String` type (which uses UTF-8).
56///
57/// This should generally only be used when one specifically wants to support reading UTF-16
58/// transparently.
59///
60/// If the file path is `-`, then contents are read from stdin instead.
61#[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/// Create a symlink at `dst` pointing to `src`, replacing any existing symlink.
82///
83/// On Windows, this uses the `junction` crate to create a junction point. The
84/// operation is _not_ atomic, as we first delete the junction, then create a
85/// junction at the same path.
86///
87/// Note that because junctions are used, the source must be a directory.
88///
89/// Changes to this function should be reflected in [`create_symlink`].
90#[cfg(windows)]
91pub fn replace_symlink(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
92    // If the source is a file, we can't create a junction
93    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    // Remove the existing symlink, if any.
104    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    // Replace it with a new symlink.
115    junction::create(
116        dunce::simplified(src.as_ref()),
117        dunce::simplified(dst.as_ref()),
118    )
119}
120
121/// Create a symlink at `dst` pointing to `src`, replacing any existing symlink if necessary.
122///
123/// On Unix, this method creates a temporary file, then moves it into place.
124#[cfg(unix)]
125pub fn replace_symlink(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
126    // Attempt to create the symlink directly.
127    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            // Create a symlink, using a temporary file to ensure atomicity.
131            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            // Move the symlink into the target location.
136            fs_err::rename(&temp_file, dst.as_ref())?;
137
138            Ok(())
139        }
140        Err(err) => Err(err),
141    }
142}
143
144/// Create a symlink at `dst` pointing to `src`.
145///
146/// On Windows, this uses the `junction` crate to create a junction point.
147///
148/// Note that because junctions are used, the source must be a directory.
149///
150/// Changes to this function should be reflected in [`replace_symlink`].
151#[cfg(windows)]
152pub fn create_symlink(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
153    // If the source is a file, we can't create a junction
154    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/// Create a symlink at `dst` pointing to `src`.
171#[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
181/// Create a symlink at `dst` pointing to `src` on Unix or copy `src` to `dst` on Windows
182///
183/// This does not replace an existing symlink or file at `dst`.
184///
185/// This does not fallback to copying on Unix.
186///
187/// This function should only be used for files. If targeting a directory, use [`replace_symlink`]
188/// instead; it will use a junction on Windows, which is more performant.
189pub 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/// Return a [`NamedTempFile`] in the specified directory.
216///
217/// Sets the permissions of the temporary file to `0o666`, to match the non-temporary file default.
218/// ([`NamedTempfile`] defaults to `0o600`.)
219#[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/// Return a [`NamedTempFile`] in the specified directory.
228#[cfg(not(unix))]
229pub fn tempfile_in(path: &Path) -> std::io::Result<NamedTempFile> {
230    tempfile::Builder::new().tempfile_in(path)
231}
232
233/// Write `data` to `path` atomically using a temporary file and atomic rename.
234#[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
245/// Write `data` to `path` atomically using a temporary file and atomic rename.
246pub 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
256/// Copy `from` to `to` atomically using a temporary file and atomic rename.
257pub 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    // This amounts to 10 total seconds of trying the operation.
267    // We retry 10 times, starting at 10*(2^0) milliseconds for the first retry, doubling with each
268    // retry, so the last (10th) one will take about 10*(2^9) milliseconds ~= 5 seconds. All other
269    // attempts combined should equal the length of the last attempt (because it's a sum of powers
270    // of 2), so 10 seconds overall.
271    backon::ExponentialBuilder::default()
272        .with_min_delay(std::time::Duration::from_millis(10))
273        .with_max_times(10)
274        .build()
275}
276
277/// Rename a file, retrying (on Windows) if it fails due to transient operating system errors.
278#[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        // On Windows, antivirus software can lock files temporarily, making them inaccessible.
287        // This is most common for DLLs, and the common suggestion is to retry the operation with
288        // some backoff.
289        //
290        // See: <https://github.com/astral-sh/uv/issues/1491> & <https://github.com/astral-sh/uv/issues/9531>
291        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// TODO(zanieb): Look into reusing this code?
317/// Wrap an arbitrary operation on two files, e.g., copying, with retries on transient operating
318/// system errors.
319#[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        // On Windows, antivirus software can lock files temporarily, making them inaccessible.
330        // This is most common for DLLs, and the common suggestion is to retry the operation with
331        // some backoff.
332        //
333        // See: <https://github.com/astral-sh/uv/issues/1491> & <https://github.com/astral-sh/uv/issues/9531>
334        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/// Why a file persist failed
368#[cfg(windows)]
369enum PersistRetryError {
370    /// Something went wrong while persisting, maybe retry (contains error message)
371    Persist(String),
372    /// Something went wrong trying to retrieve the file to persist, we must bail
373    LostState,
374}
375
376/// Persist a `NamedTempFile`, retrying (on Windows) if it fails due to transient operating system
377/// errors.
378#[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        // On Windows, antivirus software can lock files temporarily, making them inaccessible.
387        // This is most common for DLLs, and the common suggestion is to retry the operation with
388        // some backoff.
389        //
390        // See: <https://github.com/astral-sh/uv/issues/1491> & <https://github.com/astral-sh/uv/issues/9531>
391        let to = to.as_ref();
392
393        // Ok there's a lot of complex ownership stuff going on here.
394        //
395        // the `NamedTempFile` `persist` method consumes `self`, and returns it back inside
396        // the Error in case of `PersistError`:
397        // https://docs.rs/tempfile/latest/tempfile/struct.NamedTempFile.html#method.persist
398        // So every time we fail, we need to reset the `NamedTempFile` to try again.
399        //
400        // Every time we (re)try we call this outer closure (`let persist = ...`), so it needs to
401        // be at least a `FnMut` (as opposed to `Fnonce`). However the closure needs to return a
402        // totally owned `Future` (so effectively it returns a `FnOnce`).
403        //
404        // But if the `Future` is totally owned it *necessarily* can't write back the `NamedTempFile`
405        // to somewhere the outer `FnMut` can see using references. So we need to use `Arc`s
406        // with interior mutability (`Mutex`) to have the closure and all the Futures it creates share
407        // a single memory location that the `NamedTempFile` can be shuttled in and out of.
408        //
409        // In spite of the Mutex all of this code will run logically serially, so there shouldn't be a
410        // chance for a race where we try to get the `NamedTempFile` but it's actually None. The code
411        // is just written pedantically/robustly.
412        let from = std::sync::Arc::new(std::sync::Mutex::new(Some(from)));
413        let persist = || {
414            // Turn our by-ref-captured Arc into an owned Arc that the Future can capture by-value
415            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                        // Set back the `NamedTempFile` returned back by the Error
426                        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
473/// Persist a `NamedTempFile`, retrying (on Windows) if it fails due to transient operating system
474/// errors.
475///
476/// This is a synchronous implementation of [`persist_with_retry`].
477pub 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        // On Windows, antivirus software can lock files temporarily, making them inaccessible.
485        // This is most common for DLLs, and the common suggestion is to retry the operation with
486        // some backoff.
487        //
488        // See: <https://github.com/astral-sh/uv/issues/1491> & <https://github.com/astral-sh/uv/issues/9531>
489        let to = to.as_ref();
490
491        // the `NamedTempFile` `persist` method consumes `self`, and returns it back inside the Error in case of `PersistError`
492        // https://docs.rs/tempfile/latest/tempfile/struct.NamedTempFile.html#method.persist
493        // So we will update the `from` optional value in safe and borrow-checker friendly way every retry
494        // Allows us to use the NamedTempFile inside a FnMut closure used for backoff::retry
495        let mut from = Some(from);
496        let persist = || {
497            // Needed because we cannot move out of `from`, a captured variable in an `FnMut` closure, and then pass it to the async move block
498            if let Some(file) = from.take() {
499                file.persist(to).map_err(|err| {
500                    let error_message = err.to_string();
501                    // Set back the NamedTempFile returned back by the Error
502                    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
544/// Iterate over the subdirectories of a directory.
545///
546/// If the directory does not exist, returns an empty iterator.
547pub 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
569/// Iterate over the entries in a directory.
570///
571/// If the directory does not exist, returns an empty iterator.
572pub 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
591/// Iterate over the files in a directory.
592///
593/// If the directory does not exist, returns an empty iterator.
594pub 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
614/// Returns `true` if a path is a temporary file or directory.
615pub 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
622/// Checks if the grandparent directory of the given executable is the base
623/// of a virtual environment.
624///
625/// The procedure described in PEP 405 includes checking both the parent and
626/// grandparent directory of an executable, but in practice we've found this to
627/// be unnecessary.
628pub 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
636/// Returns `true` if a path is the base path of a virtual environment,
637/// indicated by the presence of a `pyvenv.cfg` file.
638///
639/// The procedure described in PEP 405 includes scanning `pyvenv.cfg`
640/// for a `home` key, but in practice we've found this to be
641/// unnecessary.
642pub fn is_virtualenv_base(path: impl AsRef<Path>) -> bool {
643    path.as_ref().join("pyvenv.cfg").is_file()
644}
645
646/// Whether the error is due to a lock being held.
647fn 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            // On Windows, we've seen: Os { code: 33, kind: Uncategorized, message: "The process cannot access the file because another process has locked a portion of the file." }
652            if cfg!(windows) && err.raw_os_error() == Some(33) {
653                return true;
654            }
655            false
656        }
657    }
658}
659
660/// An asynchronous reader that reports progress as bytes are read.
661#[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    /// Create a new [`ProgressReader`] that wraps another reader.
672    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
694/// Recursively copy a directory and its contents.
695pub 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}