1- import { renameSync } from "node:fs" ;
2- import { rename } from "node:fs/promises" ;
1+ import { copyFileSync , renameSync } from "node:fs" ;
2+ import { copyFile , rename } from "node:fs/promises" ;
3+ import { removeScratch } from "./removeScratch.js" ;
34
45// Windows reports EPERM/EACCES/EBUSY while antivirus or a just-exited process still holds the
56// file; the hold clears within milliseconds. Total wait before giving up: about 1.3 s.
@@ -10,6 +11,9 @@ interface Options {
1011 platform ?: NodeJS . Platform ;
1112}
1213
14+ // A folder that denies delete (locked-down TEMP) refuses every rename; copying the bytes still works.
15+ // The source is left behind when it cannot be removed.
16+
1317function isRetryable ( error : unknown , platform : NodeJS . Platform ) : boolean {
1418 if ( platform !== "win32" ) return false ;
1519 return RETRYABLE . has ( ( error as NodeJS . ErrnoException | undefined ) ?. code ?? "" ) ;
@@ -23,16 +27,29 @@ function sleepSync(ms: number): void {
2327export function renameWithRetry (
2428 from : string ,
2529 to : string ,
26- options : Options & { rename ?: ( from : string , to : string ) => void } = { } ,
30+ options : Options & {
31+ rename ?: ( from : string , to : string ) => void ;
32+ copy ?: ( from : string , to : string ) => void ;
33+ } = { } ,
2734) : void {
2835 const doRename = options . rename ?? renameSync ;
36+ const doCopy = options . copy ?? copyFileSync ;
2937 const platform = options . platform ?? process . platform ;
3038 for ( let attempt = 0 ; ; attempt ++ ) {
3139 try {
3240 doRename ( from , to ) ;
3341 return ;
3442 } catch ( error ) {
35- if ( attempt >= BACKOFF_MS . length || ! isRetryable ( error , platform ) ) throw error ;
43+ if ( ! isRetryable ( error , platform ) ) throw error ;
44+ if ( attempt >= BACKOFF_MS . length ) {
45+ try {
46+ doCopy ( from , to ) ;
47+ } catch {
48+ throw error ;
49+ }
50+ removeScratch ( from ) ;
51+ return ;
52+ }
3653 sleepSync ( BACKOFF_MS [ attempt ] ! ) ;
3754 }
3855 }
@@ -42,16 +59,29 @@ export function renameWithRetry(
4259export async function renameWithRetryAsync (
4360 from : string ,
4461 to : string ,
45- options : Options & { rename ?: ( from : string , to : string ) => Promise < void > } = { } ,
62+ options : Options & {
63+ rename ?: ( from : string , to : string ) => Promise < void > ;
64+ copy ?: ( from : string , to : string ) => Promise < void > ;
65+ } = { } ,
4666) : Promise < void > {
4767 const doRename = options . rename ?? rename ;
68+ const doCopy = options . copy ?? copyFile ;
4869 const platform = options . platform ?? process . platform ;
4970 for ( let attempt = 0 ; ; attempt ++ ) {
5071 try {
5172 await doRename ( from , to ) ;
5273 return ;
5374 } catch ( error ) {
54- if ( attempt >= BACKOFF_MS . length || ! isRetryable ( error , platform ) ) throw error ;
75+ if ( ! isRetryable ( error , platform ) ) throw error ;
76+ if ( attempt >= BACKOFF_MS . length ) {
77+ try {
78+ await doCopy ( from , to ) ;
79+ } catch {
80+ throw error ;
81+ }
82+ removeScratch ( from ) ;
83+ return ;
84+ }
5585 await new Promise ( ( resolve ) => setTimeout ( resolve , BACKOFF_MS [ attempt ] ) ) ;
5686 }
5787 }
0 commit comments