Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> git-subtree-dir: maquina-comercial-pessoal git-subtree-mainline:f5af038e19git-subtree-split:a9ef4d776d
77 lines
2.5 KiB
JavaScript
77 lines
2.5 KiB
JavaScript
import fs from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
import { pathToFileURL } from 'node:url';
|
|
import { DatabaseSync } from 'node:sqlite';
|
|
import { paths } from '../src/env.mjs';
|
|
|
|
export async function createBackup({ dataDir = paths.data, now = () => new Date(), retention = 14 } = {}) {
|
|
const stamp = now().toISOString().slice(0, 10);
|
|
const backupsDir = path.join(dataDir, 'backups');
|
|
const target = path.join(backupsDir, `maquina-pessoal-${stamp}`);
|
|
const dbPath = dataDir === paths.data ? paths.db : path.join(dataDir, 'maquina-pessoal.db');
|
|
const authPath = dataDir === paths.data ? paths.whatsappAuth : path.join(dataDir, 'runtime', 'whatsapp-auth');
|
|
|
|
await fs.mkdir(target, { recursive: true });
|
|
await copyDatabaseBackup(dbPath, path.join(target, 'maquina-pessoal.db'));
|
|
await fs.cp(authPath, path.join(target, 'whatsapp-auth'), { recursive: true });
|
|
await pruneBackups(backupsDir, retention);
|
|
|
|
return { file: target };
|
|
}
|
|
|
|
async function copyDatabaseBackup(source, destination) {
|
|
try {
|
|
const db = new DatabaseSync(source, { readOnly: true });
|
|
try {
|
|
db.exec(`VACUUM INTO ${sqlString(destination)};`);
|
|
return;
|
|
} finally {
|
|
db.close();
|
|
}
|
|
} catch {
|
|
await copyDatabaseFiles(source, destination);
|
|
}
|
|
}
|
|
|
|
async function copyDatabaseFiles(source, destination) {
|
|
await fs.copyFile(source, destination);
|
|
|
|
for (const suffix of ['-wal', '-shm']) {
|
|
try {
|
|
await fs.copyFile(`${source}${suffix}`, `${destination}${suffix}`);
|
|
} catch (error) {
|
|
if (error?.code !== 'ENOENT') {
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function sqlString(value) {
|
|
return `'${String(value).replaceAll("'", "''")}'`;
|
|
}
|
|
|
|
async function pruneBackups(backupsDir, retention) {
|
|
const keep = Math.max(0, retention);
|
|
const entries = await fs.readdir(backupsDir, { withFileTypes: true });
|
|
const backupDirs = entries
|
|
.filter((entry) => entry.isDirectory() && entry.name.startsWith('maquina-pessoal-'))
|
|
.map((entry) => entry.name)
|
|
.sort();
|
|
|
|
for (const name of backupDirs.slice(0, Math.max(0, backupDirs.length - keep))) {
|
|
await fs.rm(path.join(backupsDir, name), { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
export function isCliEntryPoint(moduleUrl = import.meta.url, argvPath = process.argv[1]) {
|
|
return Boolean(argvPath) && moduleUrl === pathToFileURL(argvPath).href;
|
|
}
|
|
|
|
if (isCliEntryPoint()) {
|
|
createBackup().then((result) => console.log(`backup criado: ${result.file}`)).catch((error) => {
|
|
console.error(`backup falhou: ${error.message}`);
|
|
process.exit(1);
|
|
});
|
|
}
|