@@ -657,7 +657,22 @@ fn rename_with_fallback(
657657 to : & Path ,
658658 multi_progress : Option < & MultiProgress > ,
659659) -> io:: Result < ( ) > {
660- if fs:: rename ( from, to) . is_err ( ) {
660+ if let Err ( err) = fs:: rename ( from, to) {
661+ #[ cfg( windows) ]
662+ const EXDEV : i32 = windows_sys:: Win32 :: Foundation :: ERROR_NOT_SAME_DEVICE as _ ;
663+ #[ cfg( unix) ]
664+ const EXDEV : i32 = libc:: EXDEV as _ ;
665+
666+ // We will only copy if:
667+ // 1. Files are on different devices (EXDEV error)
668+ // 2. On Windows, if the target file exists and source file is opened by another process
669+ // (MoveFileExW fails with "Access Denied" even if the source file has FILE_SHARE_DELETE permission)
670+ let should_fallback = matches ! ( err. raw_os_error( ) , Some ( EXDEV ) )
671+ || ( from. is_file ( ) && can_delete_file ( from) . unwrap_or ( false ) ) ;
672+ if !should_fallback {
673+ return Err ( err) ;
674+ }
675+
661676 // Get metadata without following symlinks
662677 let metadata = from. symlink_metadata ( ) ?;
663678 let file_type = metadata. file_type ( ) ;
@@ -792,3 +807,55 @@ fn is_empty_dir(path: &Path) -> bool {
792807 Err ( _e) => false ,
793808 }
794809}
810+
811+ /// Checks if a file can be deleted by attempting to open it with delete permissions.
812+ #[ cfg( windows) ]
813+ fn can_delete_file ( path : & Path ) -> Result < bool , io:: Error > {
814+ use std:: {
815+ os:: windows:: ffi:: OsStrExt as _,
816+ ptr:: { null, null_mut} ,
817+ } ;
818+
819+ use windows_sys:: Win32 :: {
820+ Foundation :: { CloseHandle , INVALID_HANDLE_VALUE } ,
821+ Storage :: FileSystem :: {
822+ CreateFileW , DELETE , FILE_ATTRIBUTE_NORMAL , FILE_SHARE_DELETE , FILE_SHARE_READ ,
823+ FILE_SHARE_WRITE , OPEN_EXISTING ,
824+ } ,
825+ } ;
826+
827+ let wide_path = path
828+ . as_os_str ( )
829+ . encode_wide ( )
830+ . chain ( [ 0 ] )
831+ . collect :: < Vec < u16 > > ( ) ;
832+
833+ let handle = unsafe {
834+ CreateFileW (
835+ wide_path. as_ptr ( ) ,
836+ DELETE ,
837+ FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE ,
838+ null ( ) ,
839+ OPEN_EXISTING ,
840+ FILE_ATTRIBUTE_NORMAL ,
841+ null_mut ( ) ,
842+ )
843+ } ;
844+
845+ if handle == INVALID_HANDLE_VALUE {
846+ return Err ( io:: Error :: last_os_error ( ) ) ;
847+ }
848+
849+ unsafe { CloseHandle ( handle) } ;
850+
851+ Ok ( true )
852+ }
853+
854+ #[ cfg( not( windows) ) ]
855+ fn can_delete_file ( _: & Path ) -> Result < bool , io:: Error > {
856+ // On non-Windows platforms, always return false to indicate that we don't need
857+ // to try the copy+delete fallback. This is because on Unix-like systems,
858+ // rename() failing with errors other than EXDEV means the operation cannot
859+ // succeed even with a copy+delete approach (e.g. permission errors).
860+ Ok ( false )
861+ }
0 commit comments