97 lines
4.2 KiB
TypeScript
97 lines
4.2 KiB
TypeScript
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<Buffer> {
|
|
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<T extends object>(fn: () => Promise<T>): Promise<T & DatabaseOperationWarnings> {
|
|
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<SystemDatabaseRestoreOutcome & DatabaseOperationWarnings> {
|
|
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,
|
|
}))
|
|
}
|