import { randomUUID } from "node:crypto" import { spawn } from "node:child_process" import { chmod, mkdir, rm, writeFile } from "node:fs/promises" import os from "node:os" import path from "node:path" import { executeSystemDatabaseRestore, validateSystemDatabaseRestoreArchive, type SystemDatabaseRestoreOutcome } from "./system-database-restore-execution.js" import { env } from "../config.js" import { pool } from "../db/index.js" import { withOperationLock } from "../db/operation-lock.js" import { executeDatabaseOperation, type DatabaseOperationWarnings } from "./system-database-operation.js" import { restartSchedulerAfterDatabaseOperation, stopSchedulerForDatabaseOperation } from "./scheduler.js" import { pauseRipeBackgroundWork, resumeRipeBackgroundWork } from "./traffic-flow-ripe.js" import { invalidateSystemDatabaseCaches } from "./system-database-cache.js" import { restorePartitionedDatabase } from "./system-database-partition-restore.js" import { restartTrafficFlowListenerAfterDatabaseOperation, stopTrafficFlowListenerForDatabaseOperation, } from "./traffic-flow-ingest.js" const BACKUP_LOCK_KEY = 8_723_102 function fmtTimestamp(date = new Date()): string { const pad = (n: number) => String(n).padStart(2, "0") return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}_${pad(date.getHours())}-${pad(date.getMinutes())}-${pad(date.getSeconds())}` } function spawnCollected(command: string, args: string[], opts?: { stdin?: Buffer }): Promise { return new Promise((resolve, reject) => { const child = spawn(command, args, { env: { ...process.env, DATABASE_URL: env.DATABASE_URL }, stdio: ["pipe", "pipe", "pipe"], }) const chunks: Buffer[] = [] let err = "" child.stdout.on("data", (chunk: Buffer) => chunks.push(chunk)) child.stderr.on("data", (chunk: Buffer) => { err += chunk.toString("utf8") }) child.on("error", reject) child.on("close", (code) => { if (code !== 0) { reject(new Error(err.trim().replaceAll(env.DATABASE_URL, "[DATABASE_URL]") || `${command} завершился с кодом ${code}`)) return } resolve(Buffer.concat(chunks)) }) if (opts?.stdin) child.stdin.end(opts.stdin) else child.stdin.end() }) } async function withDatabaseOperation(fn: () => Promise): Promise { return withOperationLock(() => pool.connect(), BACKUP_LOCK_KEY, () => executeDatabaseOperation(fn, { stopListener: stopTrafficFlowListenerForDatabaseOperation, stopScheduler: async () => { await stopSchedulerForDatabaseOperation() await pauseRipeBackgroundWork() }, startListener: restartTrafficFlowListenerAfterDatabaseOperation, startScheduler: async () => { resumeRipeBackgroundWork() await restartSchedulerAfterDatabaseOperation() }, }), "Операция с базой приложения уже выполняется") } export async function exportSystemDatabaseBackup(): Promise<{ filename: string; buffer: Buffer } & DatabaseOperationWarnings> { return await withDatabaseOperation(async () => { const buffer = await spawnCollected("pg_dump", ["-Fc", env.DATABASE_URL]) return { filename: `mikrotik-manager_${fmtTimestamp()}.dump`, buffer, } }) } export async function restoreSystemDatabaseBackup(buffer: Buffer): Promise { validateSystemDatabaseRestoreArchive(buffer) return await withDatabaseOperation(() => executeSystemDatabaseRestore(buffer, env.DATABASE_URL, { allocate: async () => { const tempDir = path.join(os.tmpdir(), "mmapp-db-restore") await mkdir(tempDir, { recursive: true, mode: 0o700 }) await chmod(tempDir, 0o700) return path.join(tempDir, `restore-${randomUUID()}.dump`) }, write: (file, data) => writeFile(file, data, { mode: 0o600 }), run: async args => { await restorePartitionedDatabase(args[args.length - 1], env.DATABASE_URL, { command: spawnCollected, prepareSql: file => writeFile(file, "", { mode: 0o600 }), }) }, remove: async file => { await Promise.all([rm(file, { force: true }), rm(`${file}.sql`, { force: true })]) }, invalidate: invalidateSystemDatabaseCaches, })) }