feat: improve backup management, settings UI, and Twilio webhooks
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,54 +1,73 @@
|
||||
import { spawn } from "child_process";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import archiver from "archiver";
|
||||
|
||||
interface BackupToPathParams {
|
||||
destinationPath: string;
|
||||
filename: string;
|
||||
filename: string; // should end in .zip
|
||||
}
|
||||
|
||||
export async function backupDatabaseToPath({
|
||||
destinationPath,
|
||||
filename,
|
||||
}: BackupToPathParams): Promise<void> {
|
||||
const path = await import("path");
|
||||
const fs = await import("fs");
|
||||
// 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 outputFile = path.join(destinationPath, filename);
|
||||
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",
|
||||
"-f",
|
||||
outputFile,
|
||||
"-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,
|
||||
},
|
||||
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) {
|
||||
// clean up partial file if it was created
|
||||
try {
|
||||
if (fs.existsSync(outputFile)) fs.unlinkSync(outputFile);
|
||||
} catch {}
|
||||
return reject(new Error(pgError || "pg_dump failed"));
|
||||
try { if (fs.existsSync(zipFile)) fs.unlinkSync(zipFile); } catch {}
|
||||
reject(new Error(pgError.trim() || `pg_dump exited with code ${code}`));
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
archive.finalize();
|
||||
});
|
||||
|
||||
// Stream pg_dump stdout directly into the zip as a .sql entry
|
||||
archive.append(pgDump.stdout, { name: sqlName });
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user