Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 33 additions & 2 deletions src/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,11 +149,15 @@ pub fn remove_all_with_progress<P: AsRef<Path>>(path: P, pr: &dyn SingleReport)
/// Warning: this is the raw `rename(2)`/`fs::rename` behavior. It is atomic on a
/// single filesystem, but it will fail if `from` and `to` are on different
/// mounts. If you need a cross-device-safe move, use [`move_file`] instead.
///
/// On Windows, retries transient failures (`ERROR_ACCESS_DENIED` / `ERROR_SHARING_VIOLATION`)
/// that commonly occur when antivirus or the OS still holds handles to files in the source
/// directory (e.g. after extracting an archive).
pub fn rename<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> Result<()> {
let from = from.as_ref();
let to = to.as_ref();
trace!("mv {} {}", from.display(), to.display());
fs::rename(from, to).wrap_err_with(|| {
do_rename(from, to).wrap_err_with(|| {
format!(
"failed rename: {} -> {}",
display_path(from),
Expand All @@ -162,6 +166,33 @@ pub fn rename<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> Result<()> {
})
}

#[cfg(windows)]
fn do_rename(from: &Path, to: &Path) -> std::io::Result<()> {
const MAX_ATTEMPTS: u32 = 5;
let mut last_err = None;
for attempt in 0..MAX_ATTEMPTS {
match fs::rename(from, to) {
Ok(()) => return Ok(()),
Err(e) if matches!(e.raw_os_error(), Some(5) | Some(32)) => {
// ERROR_ACCESS_DENIED (5) or ERROR_SHARING_VIOLATION (32):
// likely a transient lock from antivirus or the OS.
// Exponential backoff: 50ms, 100ms, 200ms, 400ms, 800ms
last_err = Some(e);
if attempt + 1 < MAX_ATTEMPTS {
std::thread::sleep(std::time::Duration::from_millis(50 * (1 << attempt)));
}
}
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Err(e) => return Err(e),
}
}
Err(last_err.unwrap())
}

#[cfg(not(windows))]
fn do_rename(from: &Path, to: &Path) -> std::io::Result<()> {
fs::rename(from, to)
}

/// Moves a path, falling back to copy+remove when source and destination are on different filesystems.
///
/// This preserves the normal `rename` behavior when possible, but avoids cross-device failures
Expand All @@ -171,7 +202,7 @@ pub fn move_file<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> Result<()> {
let from = from.as_ref();
let to = to.as_ref();

match fs::rename(from, to) {
match do_rename(from, to) {
Ok(()) => Ok(()),
Err(err) if err.kind() == std::io::ErrorKind::CrossesDevices => {
if from.is_dir() {
Expand Down
Loading