| import path from "node:path"; |
|
|
| export const CONFIG_BACKUP_COUNT = 5; |
|
|
| export interface BackupRotationFs { |
| unlink: (path: string) => Promise<void>; |
| rename: (from: string, to: string) => Promise<void>; |
| chmod?: (path: string, mode: number) => Promise<void>; |
| readdir?: (path: string) => Promise<string[]>; |
| } |
|
|
| export interface BackupMaintenanceFs extends BackupRotationFs { |
| copyFile: (from: string, to: string) => Promise<void>; |
| } |
|
|
| export async function rotateConfigBackups( |
| configPath: string, |
| ioFs: BackupRotationFs, |
| ): Promise<void> { |
| if (CONFIG_BACKUP_COUNT <= 1) { |
| return; |
| } |
| const backupBase = `${configPath}.bak`; |
| const maxIndex = CONFIG_BACKUP_COUNT - 1; |
| await ioFs.unlink(`${backupBase}.${maxIndex}`).catch(() => { |
| |
| }); |
| for (let index = maxIndex - 1; index >= 1; index -= 1) { |
| await ioFs.rename(`${backupBase}.${index}`, `${backupBase}.${index + 1}`).catch(() => { |
| |
| }); |
| } |
| await ioFs.rename(backupBase, `${backupBase}.1`).catch(() => { |
| |
| }); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export async function hardenBackupPermissions( |
| configPath: string, |
| ioFs: BackupRotationFs, |
| ): Promise<void> { |
| if (!ioFs.chmod) { |
| return; |
| } |
| const backupBase = `${configPath}.bak`; |
| |
| await ioFs.chmod(backupBase, 0o600).catch(() => { |
| |
| }); |
| |
| for (let i = 1; i < CONFIG_BACKUP_COUNT; i++) { |
| await ioFs.chmod(`${backupBase}.${i}`, 0o600).catch(() => { |
| |
| }); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| export async function cleanOrphanBackups( |
| configPath: string, |
| ioFs: BackupRotationFs, |
| ): Promise<void> { |
| if (!ioFs.readdir) { |
| return; |
| } |
| const dir = path.dirname(configPath); |
| const base = path.basename(configPath); |
| const bakPrefix = `${base}.bak.`; |
|
|
| |
| const validSuffixes = new Set<string>(); |
| for (let i = 1; i < CONFIG_BACKUP_COUNT; i++) { |
| validSuffixes.add(String(i)); |
| } |
|
|
| let entries: string[]; |
| try { |
| entries = await ioFs.readdir(dir); |
| } catch { |
| return; |
| } |
|
|
| for (const entry of entries) { |
| if (!entry.startsWith(bakPrefix)) { |
| continue; |
| } |
| const suffix = entry.slice(bakPrefix.length); |
| if (validSuffixes.has(suffix)) { |
| continue; |
| } |
| |
| await ioFs.unlink(path.join(dir, entry)).catch(() => { |
| |
| }); |
| } |
| } |
|
|
| |
| |
| |
| |
| export async function maintainConfigBackups( |
| configPath: string, |
| ioFs: BackupMaintenanceFs, |
| ): Promise<void> { |
| await rotateConfigBackups(configPath, ioFs); |
| await ioFs.copyFile(configPath, `${configPath}.bak`).catch(() => { |
| |
| }); |
| await hardenBackupPermissions(configPath, ioFs); |
| await cleanOrphanBackups(configPath, ioFs); |
| } |
|
|