122 lines
3.0 KiB
TypeScript
122 lines
3.0 KiB
TypeScript
import { postgresConfig } from "@core/db/pgConfigNew";
|
|
import logger from "@core/log";
|
|
import { $, S3Client } from "bun";
|
|
import dayjs from "dayjs";
|
|
import { env } from "./env";
|
|
|
|
const AK = env.OSS_ACCESS_KEY_ID;
|
|
const SK = env.OSS_ACCESS_KEY_SECRET;
|
|
const ENDPOINT = env.BACKUP_S3_ENDPOINT;
|
|
const REGION = env.BACKUP_S3_REGION;
|
|
const BUCKET = env.BACKUP_S3_BUCKET;
|
|
const DIR = env.BACKUP_DIR;
|
|
const CONTAINER = env.BACKUP_CONTAINER;
|
|
|
|
const CONFIG = {
|
|
localBackupDir: DIR,
|
|
retentionDaily: 3,
|
|
s3: {
|
|
bucket: BUCKET,
|
|
endpoint: ENDPOINT,
|
|
region: REGION,
|
|
},
|
|
};
|
|
|
|
const { username, password, host, port, database } = postgresConfig;
|
|
const dbUri = `postgresql://${username}:${encodeURIComponent(password)}@${host}:${port}/${database}`;
|
|
|
|
const s3 = new S3Client({
|
|
accessKeyId: AK,
|
|
bucket: CONFIG.s3.bucket,
|
|
endpoint: CONFIG.s3.endpoint,
|
|
region: CONFIG.s3.region,
|
|
secretAccessKey: SK,
|
|
virtualHostedStyle: true,
|
|
});
|
|
|
|
const getDayStr = (): string => {
|
|
return dayjs().format("YYYY-MM-DD");
|
|
};
|
|
|
|
const getMonthStr = (): string => {
|
|
return dayjs().format("YYYY-MM");
|
|
};
|
|
|
|
async function dump(filePath: string) {
|
|
await $`docker exec -u postgres ${CONTAINER} pg_dump -d ${dbUri} -Fc -n public > ${filePath}`;
|
|
}
|
|
|
|
async function runBackup() {
|
|
const dayStr = getDayStr();
|
|
const monthStr = getMonthStr();
|
|
const fileName = `cvsa_${dayStr}.dump`;
|
|
const filePath = `${CONFIG.localBackupDir}/${fileName}`;
|
|
const localDumpfile = Bun.file(filePath);
|
|
|
|
logger.log(`Starting backup...`);
|
|
|
|
if (!(await localDumpfile.exists())) {
|
|
logger.log(`Creating dump ${localDumpfile.name}...`);
|
|
await dump(filePath);
|
|
}
|
|
|
|
const monthlyBackupFile = s3.file(`dump/monthly/${monthStr}`);
|
|
|
|
if (!(await monthlyBackupFile.exists())) {
|
|
const f = Bun.file(filePath);
|
|
logger.log(`Uploading ${filePath} to ${monthlyBackupFile.name}`);
|
|
await monthlyBackupFile.write(f);
|
|
}
|
|
|
|
const dailyBackupFile = s3.file(`dump/daily/${dayStr}`);
|
|
|
|
if (!(await dailyBackupFile.exists())) {
|
|
const f = Bun.file(filePath);
|
|
logger.log(`Uploading ${filePath} to ${dailyBackupFile.name}`);
|
|
await dailyBackupFile.write(f);
|
|
}
|
|
}
|
|
|
|
async function rotateS3Backups() {
|
|
const dailyBackups = await s3.list({
|
|
maxKeys: 1000,
|
|
prefix: "dump/daily/",
|
|
});
|
|
if (!dailyBackups.contents) {
|
|
logger.log("No daily backups found");
|
|
return;
|
|
}
|
|
logger.log(`Found ${dailyBackups.contents.length} daily backups`);
|
|
for (const content of dailyBackups.contents) {
|
|
const key = content.key;
|
|
if (!key) {
|
|
continue;
|
|
}
|
|
const dateStr = key.split("/").at(-1);
|
|
const date = dayjs(dateStr, "YYYY-MM-DD");
|
|
if (date.isBefore(dayjs().subtract(CONFIG.retentionDaily, "day"))) {
|
|
logger.log(`Deleting daily backup ${key}`);
|
|
await s3.delete(key);
|
|
}
|
|
}
|
|
|
|
if (dailyBackups.isTruncated) {
|
|
logger.log("Still more daily backups");
|
|
await rotateS3Backups();
|
|
}
|
|
logger.log("Daily backups rotated");
|
|
}
|
|
|
|
async function main() {
|
|
try {
|
|
await runBackup();
|
|
await rotateS3Backups();
|
|
} catch (err) {
|
|
logger.error(err);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
await main();
|
|
process.exit();
|