"use strict"; /* * pg_dump をspawnし、標準出力を splitSizeMb ごとにローカルファイルへ分割書き出しする。 * `split -d -a 3` 相当の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(); } }); }); } function formatDateFolderName(date = new Date()) { return `${date.getFullYear()}${pad2(date.getMonth() + 1)}${pad2(date.getDate())}`; } module.exports = { runPgDump, formatTimestamp, formatDateFolderName };