Skip to main content

uv_fs/
lib.rs

1use std::path::{Path, PathBuf};
2
3use tempfile::NamedTempFile;
4use tracing::warn;
5
6pub use crate::locked_file::*;
7pub use crate::path::*;
8
9pub mod cachedir;
10mod locked_file;
11mod path;
12pub mod which;
13
14/// Attempt to check if the two paths refer to the same file.
15///
16/// Returns `Some(true)` if the files are missing, but would be the same if they existed.
17pub fn is_same_file_allow_missing(left: &Path, right: &Path) -> Option<bool> {
18    // First, check an exact path comparison.
19    if left == right {
20        return Some(true);
21    }
22
23    // Second, check the files directly.
24    if let Ok(value) = same_file::is_same_file(left, right) {
25        return Some(value);
26    }
27
28    // Often, one of the directories won't exist yet so perform the comparison up a level.
29    if let (Some(left_parent), Some(right_parent), Some(left_name), Some(right_name)) = (
30        left.parent(),
31        right.parent(),
32        left.file_name(),
33        right.file_name(),
34    ) {
35        match same_file::is_same_file(left_parent, right_parent) {
36            Ok(true) => return Some(left_name == right_name),
37            Ok(false) => return Some(false),
38            _ => (),
39        }
40    }
41
42    // We couldn't determine if they're the same.
43    None
44}
45
46/// Reads data from the path and requires that it be valid UTF-8 or UTF-16.
47///
48/// This uses BOM sniffing to determine if the data should be transcoded
49/// from UTF-16 to Rust's `String` type (which uses UTF-8).
50///
51/// This should generally only be used when one specifically wants to support
52/// reading UTF-16 transparently.
53///
54/// If the file path is `-`, then contents are read from stdin instead.
55#[cfg(feature = "tokio")]
56pub async fn read_to_string_transcode(path: impl AsRef<Path>) -> std::io::Result<String> {
57    use std::io::Read;
58
59    use encoding_rs_io::DecodeReaderBytes;
60
61    let path = path.as_ref();
62    let raw = if path == Path::new("-") {
63        let mut buf = Vec::with_capacity(1024);
64        std::io::stdin().read_to_end(&mut buf)?;
65        buf
66    } else {
67        fs_err::tokio::read(path).await?
68    };
69    let mut buf = String::with_capacity(1024);
70    DecodeReaderBytes::new(&*raw)
71        .read_to_string(&mut buf)
72        .map_err(|err| {
73            let path = path.display();
74            std::io::Error::other(format!("failed to decode file {path}: {err}"))
75        })?;
76    Ok(buf)
77}
78
79/// Create a symlink at `dst` pointing to `src`, replacing any existing symlink.
80///
81/// On Windows, this uses the `junction` crate to create a junction point. The
82/// operation is _not_ atomic, as we first delete the junction, then create a
83/// junction at the same path.
84///
85/// Note that because junctions are used, the source must be a directory.
86///
87/// Changes to this function should be reflected in [`create_symlink`].
88#[cfg(windows)]
89pub fn replace_symlink(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
90    // If the source is a file, we can't create a junction
91    if src.as_ref().is_file() {
92        return Err(std::io::Error::new(
93            std::io::ErrorKind::InvalidInput,
94            format!(
95                "Cannot create a junction for {}: is not a directory",
96                src.as_ref().display()
97            ),
98        ));
99    }
100
101    // Remove the existing symlink, if any.
102    match junction::delete(dunce::simplified(dst.as_ref())) {
103        Ok(()) => match fs_err::remove_dir_all(dst.as_ref()) {
104            Ok(()) => {}
105            Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
106            Err(err) => return Err(err),
107        },
108        Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
109        Err(err) => return Err(err),
110    }
111
112    // Replace it with a new symlink.
113    junction::create(
114        dunce::simplified(src.as_ref()),
115        dunce::simplified(dst.as_ref()),
116    )
117}
118
119/// Create a symlink at `dst` pointing to `src`, replacing any existing symlink if necessary.
120///
121/// On Unix, this method creates a temporary file, then moves it into place.
122#[cfg(unix)]
123pub fn replace_symlink(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
124    // Attempt to create the symlink directly.
125    match fs_err::os::unix::fs::symlink(src.as_ref(), dst.as_ref()) {
126        Ok(()) => Ok(()),
127        Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
128            // Create a symlink, using a temporary file to ensure atomicity.
129            let temp_dir = tempfile::tempdir_in(dst.as_ref().parent().unwrap())?;
130            let temp_file = temp_dir.path().join("link");
131            fs_err::os::unix::fs::symlink(src, &temp_file)?;
132
133            // Move the symlink into the target location.
134            fs_err::rename(&temp_file, dst.as_ref())?;
135
136            Ok(())
137        }
138        Err(err) => Err(err),
139    }
140}
141
142/// Create a symlink at `dst` pointing to `src`.
143///
144/// On Windows, this uses the `junction` crate to create a junction point.
145///
146/// Note that because junctions are used, the source must be a directory.
147///
148/// Changes to this function should be reflected in [`replace_symlink`].
149#[cfg(windows)]
150pub fn create_symlink(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
151    // If the source is a file, we can't create a junction
152    if src.as_ref().is_file() {
153        return Err(std::io::Error::new(
154            std::io::ErrorKind::InvalidInput,
155            format!(
156                "Cannot create a junction for {}: is not a directory",
157                src.as_ref().display()
158            ),
159        ));
160    }
161
162    junction::create(
163        dunce::simplified(src.as_ref()),
164        dunce::simplified(dst.as_ref()),
165    )
166}
167
168/// Create a symlink at `dst` pointing to `src`.
169#[cfg(unix)]
170pub fn create_symlink(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
171    fs_err::os::unix::fs::symlink(src.as_ref(), dst.as_ref())
172}
173
174#[cfg(unix)]
175pub fn remove_symlink(path: impl AsRef<Path>) -> std::io::Result<()> {
176    fs_err::remove_file(path.as_ref())
177}
178
179/// Create a symlink at `dst` pointing to `src` on Unix or copy `src` to `dst` on Windows
180///
181/// This does not replace an existing symlink or file at `dst`.
182///
183/// This does not fallback to copying on Unix.
184///
185/// This function should only be used for files. If targeting a directory, use [`replace_symlink`]
186/// instead; it will use a junction on Windows, which is more performant.
187pub fn symlink_or_copy_file(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
188    #[cfg(windows)]
189    {
190        fs_err::copy(src.as_ref(), dst.as_ref())?;
191    }
192    #[cfg(unix)]
193    {
194        fs_err::os::unix::fs::symlink(src.as_ref(), dst.as_ref())?;
195    }
196
197    Ok(())
198}
199
200#[cfg(windows)]
201pub fn remove_symlink(path: impl AsRef<Path>) -> std::io::Result<()> {
202    match junction::delete(dunce::simplified(path.as_ref())) {
203        Ok(()) => match fs_err::remove_dir_all(path.as_ref()) {
204            Ok(()) => Ok(()),
205            Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
206            Err(err) => Err(err),
207        },
208        Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
209        Err(err) => Err(err),
210    }
211}
212
213/// Return a [`NamedTempFile`] in the specified directory.
214///
215/// Sets the permissions of the temporary file to `0o666`, to match the non-temporary file default.
216/// ([`NamedTempfile`] defaults to `0o600`.)
217#[cfg(unix)]
218pub fn tempfile_in(path: &Path) -> std::io::Result<NamedTempFile> {
219    use std::os::unix::fs::PermissionsExt;
220    tempfile::Builder::new()
221        .permissions(std::fs::Permissions::from_mode(0o666))
222        .tempfile_in(path)
223}
224
225/// Return a [`NamedTempFile`] in the specified directory.
226#[cfg(not(unix))]
227pub fn tempfile_in(path: &Path) -> std::io::Result<NamedTempFile> {
228    tempfile::Builder::new().tempfile_in(path)
229}
230
231/// Write `data` to `path` atomically using a temporary file and atomic rename.
232#[cfg(feature = "tokio")]
233pub async fn write_atomic(path: impl AsRef<Path>, data: impl AsRef<[u8]>) -> std::io::Result<()> {
234    let temp_file = tempfile_in(
235        path.as_ref()
236            .parent()
237            .expect("Write path must have a parent"),
238    )?;
239    fs_err::tokio::write(&temp_file, &data).await?;
240    persist_with_retry(temp_file, path.as_ref()).await
241}
242
243/// Write `data` to `path` atomically using a temporary file and atomic rename.
244pub fn write_atomic_sync(path: impl AsRef<Path>, data: impl AsRef<[u8]>) -> std::io::Result<()> {
245    let temp_file = tempfile_in(
246        path.as_ref()
247            .parent()
248            .expect("Write path must have a parent"),
249    )?;
250    fs_err::write(&temp_file, &data)?;
251    persist_with_retry_sync(temp_file, path.as_ref())
252}
253
254/// Copy `from` to `to` atomically using a temporary file and atomic rename.
255pub fn copy_atomic_sync(from: impl AsRef<Path>, to: impl AsRef<Path>) -> std::io::Result<()> {
256    let temp_file = tempfile_in(to.as_ref().parent().expect("Write path must have a parent"))?;
257    fs_err::copy(from.as_ref(), &temp_file)?;
258    persist_with_retry_sync(temp_file, to.as_ref())
259}
260
261#[cfg(windows)]
262fn backoff_file_move() -> backon::ExponentialBackoff {
263    use backon::BackoffBuilder;
264    // This amounts to 10 total seconds of trying the operation.
265    // We retry 10 times, starting at 10*(2^0) milliseconds for the first retry, doubling with each
266    // retry, so the last (10th) one will take about 10*(2^9) milliseconds ~= 5 seconds. All other
267    // attempts combined should equal the length of the last attempt (because it's a sum of powers
268    // of 2), so 10 seconds overall.
269    backon::ExponentialBuilder::default()
270        .with_min_delay(std::time::Duration::from_millis(10))
271        .with_max_times(10)
272        .build()
273}
274
275/// Rename a file, retrying (on Windows) if it fails due to transient operating system errors.
276#[cfg(feature = "tokio")]
277pub async fn rename_with_retry(
278    from: impl AsRef<Path>,
279    to: impl AsRef<Path>,
280) -> Result<(), std::io::Error> {
281    #[cfg(windows)]
282    {
283        use backon::Retryable;
284        // On Windows, antivirus software can lock files temporarily, making them inaccessible.
285        // This is most common for DLLs, and the common suggestion is to retry the operation with
286        // some backoff.
287        //
288        // See: <https://github.com/astral-sh/uv/issues/1491> & <https://github.com/astral-sh/uv/issues/9531>
289        let from = from.as_ref();
290        let to = to.as_ref();
291
292        let rename = async || fs_err::rename(from, to);
293
294        rename
295            .retry(backoff_file_move())
296            .sleep(tokio::time::sleep)
297            .when(|e| e.kind() == std::io::ErrorKind::PermissionDenied)
298            .notify(|err, _dur| {
299                warn!(
300                    "Retrying rename from {} to {} due to transient error: {}",
301                    from.display(),
302                    to.display(),
303                    err
304                );
305            })
306            .await
307    }
308    #[cfg(not(windows))]
309    {
310        fs_err::tokio::rename(from, to).await
311    }
312}
313
314/// Rename or copy a file, retrying (on Windows) if it fails due to transient operating system
315/// errors, in a synchronous context.
316#[cfg_attr(not(windows), allow(unused_variables))]
317pub fn with_retry_sync(
318    from: impl AsRef<Path>,
319    to: impl AsRef<Path>,
320    operation_name: &str,
321    operation: impl Fn() -> Result<(), std::io::Error>,
322) -> Result<(), std::io::Error> {
323    #[cfg(windows)]
324    {
325        use backon::BlockingRetryable;
326        // On Windows, antivirus software can lock files temporarily, making them inaccessible.
327        // This is most common for DLLs, and the common suggestion is to retry the operation with
328        // some backoff.
329        //
330        // See: <https://github.com/astral-sh/uv/issues/1491> & <https://github.com/astral-sh/uv/issues/9531>
331        let from = from.as_ref();
332        let to = to.as_ref();
333
334        operation
335            .retry(backoff_file_move())
336            .sleep(std::thread::sleep)
337            .when(|err| err.kind() == std::io::ErrorKind::PermissionDenied)
338            .notify(|err, _dur| {
339                warn!(
340                    "Retrying {} from {} to {} due to transient error: {}",
341                    operation_name,
342                    from.display(),
343                    to.display(),
344                    err
345                );
346            })
347            .call()
348            .map_err(|err| {
349                std::io::Error::other(format!(
350                    "Failed {} {} to {}: {}",
351                    operation_name,
352                    from.display(),
353                    to.display(),
354                    err
355                ))
356            })
357    }
358    #[cfg(not(windows))]
359    {
360        operation()
361    }
362}
363
364/// Why a file persist failed
365#[cfg(windows)]
366enum PersistRetryError {
367    /// Something went wrong while persisting, maybe retry (contains error message)
368    Persist(String),
369    /// Something went wrong trying to retrieve the file to persist, we must bail
370    LostState,
371}
372
373/// Persist a `NamedTempFile`, retrying (on Windows) if it fails due to transient operating system errors, in a synchronous context.
374pub async fn persist_with_retry(
375    from: NamedTempFile,
376    to: impl AsRef<Path>,
377) -> Result<(), std::io::Error> {
378    #[cfg(windows)]
379    {
380        use backon::Retryable;
381        // On Windows, antivirus software can lock files temporarily, making them inaccessible.
382        // This is most common for DLLs, and the common suggestion is to retry the operation with
383        // some backoff.
384        //
385        // See: <https://github.com/astral-sh/uv/issues/1491> & <https://github.com/astral-sh/uv/issues/9531>
386        let to = to.as_ref();
387
388        // Ok there's a lot of complex ownership stuff going on here.
389        //
390        // the `NamedTempFile` `persist` method consumes `self`, and returns it back inside
391        // the Error in case of `PersistError`:
392        // https://docs.rs/tempfile/latest/tempfile/struct.NamedTempFile.html#method.persist
393        // So every time we fail, we need to reset the `NamedTempFile` to try again.
394        //
395        // Every time we (re)try we call this outer closure (`let persist = ...`), so it needs to
396        // be at least a `FnMut` (as opposed to `Fnonce`). However the closure needs to return a
397        // totally owned `Future` (so effectively it returns a `FnOnce`).
398        //
399        // But if the `Future` is totally owned it *necessarily* can't write back the `NamedTempFile`
400        // to somewhere the outer `FnMut` can see using references. So we need to use `Arc`s
401        // with interior mutability (`Mutex`) to have the closure and all the Futures it creates share
402        // a single memory location that the `NamedTempFile` can be shuttled in and out of.
403        //
404        // In spite of the Mutex all of this code will run logically serially, so there shouldn't be a
405        // chance for a race where we try to get the `NamedTempFile` but it's actually None. The code
406        // is just written pedantically/robustly.
407        let from = std::sync::Arc::new(std::sync::Mutex::new(Some(from)));
408        let persist = || {
409            // Turn our by-ref-captured Arc into an owned Arc that the Future can capture by-value
410            let from2 = from.clone();
411
412            async move {
413                let maybe_file: Option<NamedTempFile> = from2
414                    .lock()
415                    .map_err(|_| PersistRetryError::LostState)?
416                    .take();
417                if let Some(file) = maybe_file {
418                    file.persist(to).map_err(|err| {
419                        let error_message: String = err.to_string();
420                        // Set back the `NamedTempFile` returned back by the Error
421                        if let Ok(mut guard) = from2.lock() {
422                            *guard = Some(err.file);
423                            PersistRetryError::Persist(error_message)
424                        } else {
425                            PersistRetryError::LostState
426                        }
427                    })
428                } else {
429                    Err(PersistRetryError::LostState)
430                }
431            }
432        };
433
434        let persisted = persist
435            .retry(backoff_file_move())
436            .sleep(tokio::time::sleep)
437            .when(|err| matches!(err, PersistRetryError::Persist(_)))
438            .notify(|err, _dur| {
439                if let PersistRetryError::Persist(error_message) = err {
440                    warn!(
441                        "Retrying to persist temporary file to {}: {}",
442                        to.display(),
443                        error_message,
444                    );
445                }
446            })
447            .await;
448
449        match persisted {
450            Ok(_) => Ok(()),
451            Err(PersistRetryError::Persist(error_message)) => Err(std::io::Error::other(format!(
452                "Failed to persist temporary file to {}: {}",
453                to.display(),
454                error_message,
455            ))),
456            Err(PersistRetryError::LostState) => Err(std::io::Error::other(format!(
457                "Failed to retrieve temporary file while trying to persist to {}",
458                to.display()
459            ))),
460        }
461    }
462    #[cfg(not(windows))]
463    {
464        async { fs_err::rename(from, to) }.await
465    }
466}
467
468/// Persist a `NamedTempFile`, retrying (on Windows) if it fails due to transient operating system errors, in a synchronous context.
469pub fn persist_with_retry_sync(
470    from: NamedTempFile,
471    to: impl AsRef<Path>,
472) -> Result<(), std::io::Error> {
473    #[cfg(windows)]
474    {
475        use backon::BlockingRetryable;
476        // On Windows, antivirus software can lock files temporarily, making them inaccessible.
477        // This is most common for DLLs, and the common suggestion is to retry the operation with
478        // some backoff.
479        //
480        // See: <https://github.com/astral-sh/uv/issues/1491> & <https://github.com/astral-sh/uv/issues/9531>
481        let to = to.as_ref();
482
483        // the `NamedTempFile` `persist` method consumes `self`, and returns it back inside the Error in case of `PersistError`
484        // https://docs.rs/tempfile/latest/tempfile/struct.NamedTempFile.html#method.persist
485        // So we will update the `from` optional value in safe and borrow-checker friendly way every retry
486        // Allows us to use the NamedTempFile inside a FnMut closure used for backoff::retry
487        let mut from = Some(from);
488        let persist = || {
489            // Needed because we cannot move out of `from`, a captured variable in an `FnMut` closure, and then pass it to the async move block
490            if let Some(file) = from.take() {
491                file.persist(to).map_err(|err| {
492                    let error_message = err.to_string();
493                    // Set back the NamedTempFile returned back by the Error
494                    from = Some(err.file);
495                    PersistRetryError::Persist(error_message)
496                })
497            } else {
498                Err(PersistRetryError::LostState)
499            }
500        };
501
502        let persisted = persist
503            .retry(backoff_file_move())
504            .sleep(std::thread::sleep)
505            .when(|err| matches!(err, PersistRetryError::Persist(_)))
506            .notify(|err, _dur| {
507                if let PersistRetryError::Persist(error_message) = err {
508                    warn!(
509                        "Retrying to persist temporary file to {}: {}",
510                        to.display(),
511                        error_message,
512                    );
513                }
514            })
515            .call();
516
517        match persisted {
518            Ok(_) => Ok(()),
519            Err(PersistRetryError::Persist(error_message)) => Err(std::io::Error::other(format!(
520                "Failed to persist temporary file to {}: {}",
521                to.display(),
522                error_message,
523            ))),
524            Err(PersistRetryError::LostState) => Err(std::io::Error::other(format!(
525                "Failed to retrieve temporary file while trying to persist to {}",
526                to.display()
527            ))),
528        }
529    }
530    #[cfg(not(windows))]
531    {
532        fs_err::rename(from, to)
533    }
534}
535
536/// Iterate over the subdirectories of a directory.
537///
538/// If the directory does not exist, returns an empty iterator.
539pub fn directories(
540    path: impl AsRef<Path>,
541) -> Result<impl Iterator<Item = PathBuf>, std::io::Error> {
542    let entries = match path.as_ref().read_dir() {
543        Ok(entries) => Some(entries),
544        Err(err) if err.kind() == std::io::ErrorKind::NotFound => None,
545        Err(err) => return Err(err),
546    };
547    Ok(entries
548        .into_iter()
549        .flatten()
550        .filter_map(|entry| match entry {
551            Ok(entry) => Some(entry),
552            Err(err) => {
553                warn!("Failed to read entry: {err}");
554                None
555            }
556        })
557        .filter(|entry| entry.file_type().is_ok_and(|file_type| file_type.is_dir()))
558        .map(|entry| entry.path()))
559}
560
561/// Iterate over the entries in a directory.
562///
563/// If the directory does not exist, returns an empty iterator.
564pub fn entries(path: impl AsRef<Path>) -> Result<impl Iterator<Item = PathBuf>, std::io::Error> {
565    let entries = match path.as_ref().read_dir() {
566        Ok(entries) => Some(entries),
567        Err(err) if err.kind() == std::io::ErrorKind::NotFound => None,
568        Err(err) => return Err(err),
569    };
570    Ok(entries
571        .into_iter()
572        .flatten()
573        .filter_map(|entry| match entry {
574            Ok(entry) => Some(entry),
575            Err(err) => {
576                warn!("Failed to read entry: {err}");
577                None
578            }
579        })
580        .map(|entry| entry.path()))
581}
582
583/// Iterate over the files in a directory.
584///
585/// If the directory does not exist, returns an empty iterator.
586pub fn files(path: impl AsRef<Path>) -> Result<impl Iterator<Item = PathBuf>, std::io::Error> {
587    let entries = match path.as_ref().read_dir() {
588        Ok(entries) => Some(entries),
589        Err(err) if err.kind() == std::io::ErrorKind::NotFound => None,
590        Err(err) => return Err(err),
591    };
592    Ok(entries
593        .into_iter()
594        .flatten()
595        .filter_map(|entry| match entry {
596            Ok(entry) => Some(entry),
597            Err(err) => {
598                warn!("Failed to read entry: {err}");
599                None
600            }
601        })
602        .filter(|entry| entry.file_type().is_ok_and(|file_type| file_type.is_file()))
603        .map(|entry| entry.path()))
604}
605
606/// Returns `true` if a path is a temporary file or directory.
607pub fn is_temporary(path: impl AsRef<Path>) -> bool {
608    path.as_ref()
609        .file_name()
610        .and_then(|name| name.to_str())
611        .is_some_and(|name| name.starts_with(".tmp"))
612}
613
614/// Checks if the grandparent directory of the given executable is the base
615/// of a virtual environment.
616///
617/// The procedure described in PEP 405 includes checking both the parent and
618/// grandparent directory of an executable, but in practice we've found this to
619/// be unnecessary.
620pub fn is_virtualenv_executable(executable: impl AsRef<Path>) -> bool {
621    executable
622        .as_ref()
623        .parent()
624        .and_then(Path::parent)
625        .is_some_and(is_virtualenv_base)
626}
627
628/// Returns `true` if a path is the base path of a virtual environment,
629/// indicated by the presence of a `pyvenv.cfg` file.
630///
631/// The procedure described in PEP 405 includes scanning `pyvenv.cfg`
632/// for a `home` key, but in practice we've found this to be
633/// unnecessary.
634pub fn is_virtualenv_base(path: impl AsRef<Path>) -> bool {
635    path.as_ref().join("pyvenv.cfg").is_file()
636}
637
638/// Whether the error is due to a lock being held.
639fn is_known_already_locked_error(err: &std::fs::TryLockError) -> bool {
640    match err {
641        std::fs::TryLockError::WouldBlock => true,
642        std::fs::TryLockError::Error(err) => {
643            // 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." }
644            if cfg!(windows) && err.raw_os_error() == Some(33) {
645                return true;
646            }
647            false
648        }
649    }
650}
651
652/// An asynchronous reader that reports progress as bytes are read.
653#[cfg(feature = "tokio")]
654pub struct ProgressReader<Reader: tokio::io::AsyncRead + Unpin, Callback: Fn(usize) + Unpin> {
655    reader: Reader,
656    callback: Callback,
657}
658
659#[cfg(feature = "tokio")]
660impl<Reader: tokio::io::AsyncRead + Unpin, Callback: Fn(usize) + Unpin>
661    ProgressReader<Reader, Callback>
662{
663    /// Create a new [`ProgressReader`] that wraps another reader.
664    pub fn new(reader: Reader, callback: Callback) -> Self {
665        Self { reader, callback }
666    }
667}
668
669#[cfg(feature = "tokio")]
670impl<Reader: tokio::io::AsyncRead + Unpin, Callback: Fn(usize) + Unpin> tokio::io::AsyncRead
671    for ProgressReader<Reader, Callback>
672{
673    fn poll_read(
674        mut self: std::pin::Pin<&mut Self>,
675        cx: &mut std::task::Context<'_>,
676        buf: &mut tokio::io::ReadBuf<'_>,
677    ) -> std::task::Poll<std::io::Result<()>> {
678        std::pin::Pin::new(&mut self.as_mut().reader)
679            .poll_read(cx, buf)
680            .map_ok(|()| {
681                (self.callback)(buf.filled().len());
682            })
683    }
684}
685
686/// Recursively copy a directory and its contents.
687pub fn copy_dir_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
688    fs_err::create_dir_all(&dst)?;
689    for entry in fs_err::read_dir(src.as_ref())? {
690        let entry = entry?;
691        let ty = entry.file_type()?;
692        if ty.is_dir() {
693            copy_dir_all(entry.path(), dst.as_ref().join(entry.file_name()))?;
694        } else {
695            fs_err::copy(entry.path(), dst.as_ref().join(entry.file_name()))?;
696        }
697    }
698    Ok(())
699}