"use strict"; /* * 設定ファイルアーカイブ(tar.gz)を展開し、CONFIG_BACKUP_PATHS の各パスへ配置する。 * 復元先に既存のファイル/ディレクトリがある場合は、必ず ".pre-restore-" に * リネームして保持してから配置する(復元前の状態を失わないため)。 */ const fs = require("fs"); const path = require("path"); const { spawn } = require("child_process"); function runTar(args) { return new Promise((resolve, reject) => { const child = spawn("tar", args, { stdio: ["ignore", "pipe", "pipe"] }); let stderr = ""; child.stderr.on("data", (d) => (stderr += d.toString("utf8"))); child.on("error", (err) => reject(new Error(`tar起動失敗: ${err.message}`))); child.on("close", (code) => { if (code !== 0) { reject(new Error(`tar展開失敗 (code=${code}): ${stderr.trim()}`)); return; } resolve({ stderr: stderr.trim() }); }); }); } async function extractArchive(archivePath, extractDir) { fs.mkdirSync(extractDir, { recursive: true }); await runTar(["-xzf", archivePath, "-C", extractDir]); return extractDir; } // tar展開時、絶対パスの先頭"/"は除去されて格納される(例: /etc/nginx → etc/nginx)。 function extractedPathFor(extractDir, absolutePath) { return path.join(extractDir, absolutePath.replace(/^\/+/, "")); } // シンボリックリンク(例: letsencryptの live/*.pem → ../../archive/*.pem という相対リンク)を // 配置する際、fs.cpSync({recursive:true}) は相対リンクを「コピー元ディレクトリを基準とした絶対パス」に // 書き換えてしまう(実機検証で確認済みの既知の挙動)。そのためNodeのfs APIではなく、 // シンボリックリンクをそのまま複製する `cp -a` を使う。 function copyPreservingSymlinks(source, dest) { return new Promise((resolve, reject) => { const child = spawn("cp", ["-a", "--", source, dest], { stdio: ["ignore", "pipe", "pipe"] }); let stderr = ""; child.stderr.on("data", (d) => (stderr += d.toString("utf8"))); child.on("error", (err) => reject(new Error(`cp起動失敗: ${err.message}`))); child.on("close", (code) => { if (code !== 0) { reject(new Error(`cp -a 失敗 (code=${code}): ${stderr.trim()}`)); return; } resolve(); }); }); } // targetPaths(CONFIG_BACKUP_PATHSと同じ絶対パスの配列)それぞれについて、 // 展開済みディレクトリに対応する中身があれば「既存を退避 → 配置」を行う。 // 展開ファイル側に無いパスはスキップ(そのアーカイブに含まれていなかった=バックアップ時に存在しなかった扱い)。 async function restoreConfigPaths({ extractDir, targetPaths, timestamp, logger }) { const results = []; for (const targetPath of targetPaths) { const source = extractedPathFor(extractDir, targetPath); if (!fs.existsSync(source)) { logger.warn(`アーカイブ内に対象が見つからないためスキップ: ${targetPath}`); results.push({ targetPath, status: "skipped" }); continue; } let renamedTo = null; if (fs.existsSync(targetPath)) { renamedTo = `${targetPath}.pre-restore-${timestamp}`; fs.renameSync(targetPath, renamedTo); logger.info(`既存を退避: ${targetPath} → ${renamedTo}`); } fs.mkdirSync(path.dirname(targetPath), { recursive: true }); await copyPreservingSymlinks(source, targetPath); logger.info(`配置完了: ${targetPath}`); results.push({ targetPath, status: "restored", renamedTo }); } return results; } module.exports = { extractArchive, restoreConfigPaths };