119 lines
3.5 KiB
JavaScript
119 lines
3.5 KiB
JavaScript
"use strict";
|
|
|
|
/*
|
|
* pg_dump をspawnし、標準出力を splitSizeMb ごとにローカルファイルへ分割書き出しする。
|
|
* backup.sh の `pg_dump ... | split -d -a 3 -b 3072m - $SAVEPATH$FNAME` と同じ
|
|
* 3桁ゼロ埋め連番(000, 001, ...)をファイル名末尾に付与する。
|
|
*/
|
|
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
const { spawn } = require("child_process");
|
|
|
|
function pad2(n) {
|
|
return String(n).padStart(2, "0");
|
|
}
|
|
|
|
function formatTimestamp(date = new Date()) {
|
|
return (
|
|
`${date.getFullYear()}${pad2(date.getMonth() + 1)}${pad2(date.getDate())}-` +
|
|
`${pad2(date.getHours())}${pad2(date.getMinutes())}${pad2(date.getSeconds())}`
|
|
);
|
|
}
|
|
|
|
function runPgDump({
|
|
pgDumpBin,
|
|
host,
|
|
port,
|
|
user,
|
|
password,
|
|
database,
|
|
outputDir,
|
|
splitSizeMb,
|
|
prefix = "postgres-",
|
|
ext = ".dmp",
|
|
timestamp = formatTimestamp(),
|
|
}) {
|
|
return new Promise((resolve, reject) => {
|
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
|
|
const splitSizeBytes = Number(splitSizeMb) * 1024 * 1024;
|
|
const baseName = `${prefix}${timestamp}${ext}`;
|
|
|
|
const args = ["-h", host, "-p", String(port), "-U", user, "-Fc", database];
|
|
const env = { ...process.env };
|
|
if (password) {
|
|
env.PGPASSWORD = password;
|
|
}
|
|
|
|
const child = spawn(pgDumpBin, args, { env });
|
|
|
|
const files = [];
|
|
let partIndex = 0;
|
|
let currentStream = null;
|
|
let currentBytes = 0;
|
|
let totalBytes = 0;
|
|
let stderrOutput = "";
|
|
let settled = false;
|
|
|
|
function openNextPart() {
|
|
const fileName = `${baseName}${String(partIndex).padStart(3, "0")}`;
|
|
const filePath = path.join(outputDir, fileName);
|
|
files.push(filePath);
|
|
currentStream = fs.createWriteStream(filePath);
|
|
currentBytes = 0;
|
|
partIndex += 1;
|
|
}
|
|
|
|
openNextPart();
|
|
|
|
child.stdout.on("data", (chunk) => {
|
|
let offset = 0;
|
|
while (offset < chunk.length) {
|
|
if (currentBytes >= splitSizeBytes) {
|
|
currentStream.end();
|
|
openNextPart();
|
|
}
|
|
const remaining = splitSizeBytes - currentBytes;
|
|
const writeLen = Math.min(remaining, chunk.length - offset);
|
|
currentStream.write(chunk.subarray(offset, offset + writeLen));
|
|
currentBytes += writeLen;
|
|
totalBytes += writeLen;
|
|
offset += writeLen;
|
|
}
|
|
});
|
|
|
|
child.stderr.on("data", (chunk) => {
|
|
stderrOutput += chunk.toString("utf8");
|
|
});
|
|
|
|
child.on("error", (err) => {
|
|
if (settled) return;
|
|
settled = true;
|
|
if (currentStream) currentStream.end();
|
|
reject(new Error(`pg_dump起動失敗: ${err.message}`));
|
|
});
|
|
|
|
child.on("close", (code) => {
|
|
if (settled) return;
|
|
|
|
const finalize = () => {
|
|
settled = true;
|
|
if (code !== 0) {
|
|
reject(new Error(`pg_dump異常終了 (code=${code}): ${stderrOutput.trim()}`));
|
|
return;
|
|
}
|
|
resolve({ files, totalBytes, stderrOutput: stderrOutput.trim() });
|
|
};
|
|
|
|
if (currentStream && !currentStream.destroyed) {
|
|
currentStream.end(finalize);
|
|
} else {
|
|
finalize();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
module.exports = { runPgDump, formatTimestamp };
|