import { spawn } from "child_process"; import fs from "fs"; import path from "path"; import archiver from "archiver"; interface BackupToPathParams { destinationPath: string; filename: string; // should end in .zip } export async function backupDatabaseToPath({ destinationPath, filename, }: BackupToPathParams): Promise { // Verify write access before spawning pg_dump try { fs.accessSync(destinationPath, fs.constants.W_OK); } catch { throw new Error( `No write permission to "${destinationPath}". Try running: sudo chmod a+w "${destinationPath}"` ); } const zipFile = path.join(destinationPath, filename); const sqlName = filename.replace(/\.zip$/, ".sql"); return new Promise((resolve, reject) => { const output = fs.createWriteStream(zipFile); const archive = archiver("zip", { zlib: { level: 6 } }); output.on("close", () => resolve()); archive.on("error", (err) => { try { if (fs.existsSync(zipFile)) fs.unlinkSync(zipFile); } catch {} reject(err); }); archive.pipe(output); const pgDump = spawn( "pg_dump", [ "--no-acl", "--no-owner", "-h", process.env.DB_HOST || "localhost", "-U", process.env.DB_USER || "postgres", process.env.DB_NAME || "dental_db", ], { env: { ...process.env, PGPASSWORD: process.env.DB_PASSWORD }, } ); let pgError = ""; pgDump.stderr.on("data", (d) => (pgError += d.toString())); pgDump.on("error", (err) => { try { if (fs.existsSync(zipFile)) fs.unlinkSync(zipFile); } catch {} reject(new Error(`Failed to start pg_dump: ${err.message}. Make sure postgresql-client is installed.`)); }); pgDump.on("close", (code) => { if (code !== 0) { try { if (fs.existsSync(zipFile)) fs.unlinkSync(zipFile); } catch {} reject(new Error(pgError.trim() || `pg_dump exited with code ${code}`)); return; } archive.finalize(); }); // Stream pg_dump stdout directly into the zip as a .sql entry archive.append(pgDump.stdout, { name: sqlName }); }); }