Skill update round trip / roundtrip (false, macos-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (false, macos-latest, symlink, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (false, ubuntu-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (false, ubuntu-latest, symlink, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (false, ubuntu-latest, symlink, latest) (push) Canceled after 0s
Skill update round trip / roundtrip (false, windows-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (false, windows-latest, symlink, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, macos-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, macos-latest, symlink, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, ubuntu-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, ubuntu-latest, symlink, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, windows-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, windows-latest, symlink, 1.5.17) (push) Canceled after 0s
28 lines
1.0 KiB
TypeScript
28 lines
1.0 KiB
TypeScript
import { randomUUID } from 'node:crypto'
|
|
import { lstatSync, rmSync } from 'node:fs'
|
|
import { copyFileWithWindowsRetry, renameFileWithWindowsRetry } from './codex-accounts/fs-utils'
|
|
|
|
export function writeRollingFileBackup(sourcePath: string, backupPath: string): void {
|
|
try {
|
|
if (lstatSync(backupPath).isSymbolicLink()) {
|
|
// Why: copyFile follows a destination symlink and can corrupt an
|
|
// unrelated dotfiles target before the primary atomic write begins.
|
|
throw new Error(`Refusing to overwrite symlinked backup: ${backupPath}`)
|
|
}
|
|
} catch (error) {
|
|
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
|
|
throw error
|
|
}
|
|
}
|
|
|
|
const tempPath = `${backupPath}.${process.pid}.${randomUUID()}.tmp`
|
|
try {
|
|
// Why: replacing the backup from a fresh inode cannot mutate another file
|
|
// through a hard link, and a failed rename leaves the prior backup intact.
|
|
copyFileWithWindowsRetry(sourcePath, tempPath)
|
|
renameFileWithWindowsRetry(tempPath, backupPath)
|
|
} finally {
|
|
rmSync(tempPath, { force: true })
|
|
}
|
|
}
|