mirror of
https://github.com/vatesfr/xen-orchestra.git
synced 2026-09-10 22:14:48 -05:00
68 lines
2.8 KiB
TypeScript
68 lines
2.8 KiB
TypeScript
import execa, { type ExecaError } from 'execa'
|
|
|
|
// Recursively set the immutable (`+i`) attribute on a directory and all its contents.
|
|
export async function makeImmutable(dirPath: string): Promise<void> {
|
|
await execa('chattr', ['+i', '-R', dirPath])
|
|
}
|
|
|
|
// chattr processes all paths even when some are missing (it does not abort on the first
|
|
// error), so every existing path is correctly lifted.
|
|
//
|
|
// Per-path "while trying to stat" messages (ENOENT) are silently ignored — these are always
|
|
// expected for optional paths (.xva, .alias.vhd, …) that are absent in delta backups. chattr
|
|
// translates that whole message, so it is run under `LC_ALL=C`: without it, every batch
|
|
// containing an optional missing path fails on a localised system, which means no backup ever
|
|
// gets locked there.
|
|
//
|
|
// Any other error (e.g. EAGAIN on NFS when the file is still open, permission denied) is
|
|
// retried up to 3 times with a 1 s delay before being re-thrown.
|
|
async function execChattrWithMissingFiles(args: string[]): Promise<void> {
|
|
for (let attempt = 0; ; attempt++) {
|
|
try {
|
|
await execa('chattr', args, { env: { LC_ALL: 'C' } })
|
|
return
|
|
} catch (err) {
|
|
const stderr = (err as ExecaError).stderr ?? ''
|
|
const lines = stderr.split('\n').filter(line => line.trim() !== '')
|
|
|
|
// All errors are ENOENT for optional missing paths — silently accept, no retry.
|
|
if (lines.every(line => line.includes('while trying to stat'))) {
|
|
return
|
|
}
|
|
|
|
// At least one non-ENOENT error — retry up to 3 times, then re-throw.
|
|
if (attempt < 3) {
|
|
await new Promise<void>(resolve => setTimeout(resolve, 1000))
|
|
continue
|
|
}
|
|
throw err
|
|
}
|
|
}
|
|
}
|
|
|
|
// Lock multiple paths (files and/or directories) with a single `chattr +i -R` invocation.
|
|
// For regular files `-R` is a no-op (chattr ignores it). Missing paths are silently
|
|
// ignored: chattr processes all remaining paths before exiting non-zero.
|
|
export async function makeImmutableBatch(paths: string[]): Promise<void> {
|
|
if (paths.length === 0) return
|
|
await execChattrWithMissingFiles(['+i', '-R', ...paths])
|
|
}
|
|
|
|
// Recursively remove the immutable (`-i`) attribute from a directory and all its contents.
|
|
export async function liftImmutability(dirPath: string): Promise<void> {
|
|
await execa('chattr', ['-i', '-R', dirPath])
|
|
}
|
|
|
|
// Lift immutability from multiple paths with a single `chattr -i -R` invocation.
|
|
// Works for both flat files and directories.
|
|
export async function liftImmutabilityBatch(paths: string[]): Promise<void> {
|
|
if (paths.length === 0) return
|
|
await execChattrWithMissingFiles(['-i', '-R', ...paths])
|
|
}
|
|
|
|
// Returns whether the immutable (`i`) attribute is set on `path`.
|
|
export async function isImmutable(path: string): Promise<boolean> {
|
|
const { stdout } = await execa('lsattr', ['-d', path])
|
|
return stdout[4] === 'i'
|
|
}
|