backup database to s3
This commit is contained in:
135
db/backup.ts
Normal file
135
db/backup.ts
Normal file
@ -0,0 +1,135 @@
|
||||
import url from "url";
|
||||
import querystring from "querystring";
|
||||
import { spawn } from "child_process";
|
||||
import { PassThrough } from "stream";
|
||||
import { getConfig } from "blitz";
|
||||
import AWS from "aws-sdk";
|
||||
import { sendEmail } from "../integrations/ses";
|
||||
|
||||
const { serverRuntimeConfig } = getConfig();
|
||||
|
||||
const s3 = new AWS.S3({
|
||||
credentials: new AWS.Credentials({
|
||||
accessKeyId: serverRuntimeConfig.awsS3.accessKeyId,
|
||||
secretAccessKey: serverRuntimeConfig.awsS3.secretAccessKey,
|
||||
}),
|
||||
region: serverRuntimeConfig.awsS3.region,
|
||||
});
|
||||
|
||||
export default async function backup(schedule: "daily" | "weekly" | "monthly") {
|
||||
const s3Bucket = `shellphone-${schedule}-backup`;
|
||||
const { database, host, port, user, password } = parseDatabaseUrl(process.env.DATABASE_URL!);
|
||||
const fileName = getFileName(database);
|
||||
|
||||
console.log(`Dumping database ${database}`);
|
||||
const pgDumpChild = spawn("pg_dump", [`-U${user}`, `-d${database}`], {
|
||||
env: {
|
||||
...process.env,
|
||||
PGPASSWORD: password,
|
||||
PGHOST: host,
|
||||
PGPORT: port.toString(),
|
||||
},
|
||||
stdio: ["ignore", "pipe", "inherit"],
|
||||
});
|
||||
|
||||
console.log(`Compressing dump "${fileName}"`);
|
||||
const gzippedDumpStream = new PassThrough();
|
||||
const gzipChild = spawn("gzip", { stdio: ["pipe", "pipe", "inherit"] });
|
||||
gzipChild.on("exit", (code) => {
|
||||
if (code !== 0) {
|
||||
return sendEmail({
|
||||
body: `${schedule} backup failed: gzip: Bad exit code (${code})`,
|
||||
subject: `${schedule} backup failed: gzip: Bad exit code (${code})`,
|
||||
recipients: ["error@shellphone.app"],
|
||||
});
|
||||
}
|
||||
});
|
||||
pgDumpChild.stdout.pipe(gzipChild.stdin);
|
||||
gzipChild.stdout.pipe(gzippedDumpStream);
|
||||
|
||||
pgDumpChild.on("exit", (code) => {
|
||||
if (code !== 0) {
|
||||
console.log("pg_dump failed, upload aborted");
|
||||
return sendEmail({
|
||||
body: `${schedule} backup failed: pg_dump: Bad exit code (${code})`,
|
||||
subject: `${schedule} backup failed: pg_dump: Bad exit code (${code})`,
|
||||
recipients: ["error@shellphone.app"],
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`Uploading "${fileName}" to S3 bucket "${s3Bucket}"`);
|
||||
const uploadPromise = s3
|
||||
.upload({
|
||||
Bucket: s3Bucket,
|
||||
Key: fileName,
|
||||
ACL: "private",
|
||||
ContentType: "text/plain",
|
||||
ContentEncoding: "gzip",
|
||||
Body: gzippedDumpStream,
|
||||
})
|
||||
.promise();
|
||||
|
||||
uploadPromise
|
||||
.then(() => console.log(`Successfully uploaded "${fileName}"`))
|
||||
.catch((error) =>
|
||||
sendEmail({
|
||||
body: `${schedule} backup failed: ${error}`,
|
||||
subject: `${schedule} backup failed: ${error}`,
|
||||
recipients: ["error@shellphone.app"],
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function getFileName(database: string) {
|
||||
const now = new Date();
|
||||
const year = now.getUTCFullYear();
|
||||
const month = (now.getUTCMonth() + 1).toString().padStart(2, "0");
|
||||
const day = now.getUTCDate();
|
||||
const hours = now.getUTCHours();
|
||||
const minutes = now.getUTCMinutes();
|
||||
const seconds = now.getUTCSeconds();
|
||||
|
||||
return `${database}-${year}-${month}-${day}_${hours}-${minutes}-${seconds}.sql.gz`; // 2021-09-15_16-00-02.sql.gz
|
||||
}
|
||||
|
||||
type DatabaseUrl = {
|
||||
readonly user: string;
|
||||
readonly password: string;
|
||||
readonly host: string;
|
||||
readonly port: number;
|
||||
readonly database: string;
|
||||
};
|
||||
|
||||
function parseDatabaseUrl(databaseUrl: string): DatabaseUrl {
|
||||
const parsedUrl = url.parse(databaseUrl, false, true);
|
||||
const config = querystring.parse(parsedUrl.query!);
|
||||
|
||||
if (parsedUrl.auth) {
|
||||
const userPassword = parsedUrl.auth.split(":", 2);
|
||||
config.user = userPassword[0];
|
||||
if (userPassword.length > 1) {
|
||||
config.password = userPassword[1];
|
||||
}
|
||||
}
|
||||
|
||||
if (parsedUrl.pathname) {
|
||||
config.database = parsedUrl.pathname.replace(/^\//, "").replace(/\/$/, "");
|
||||
}
|
||||
|
||||
if (parsedUrl.hostname) {
|
||||
config.host = parsedUrl.hostname;
|
||||
}
|
||||
|
||||
if (parsedUrl.port) {
|
||||
config.port = parsedUrl.port;
|
||||
}
|
||||
|
||||
return {
|
||||
user: config.user as string,
|
||||
password: config.password as string,
|
||||
host: config.host as string,
|
||||
port: Number.parseInt(config.port as string, 10),
|
||||
database: config.database as string,
|
||||
};
|
||||
}
|
Reference in New Issue
Block a user