feat: implement database backup and restore functionality in settings page
- Added UI components for downloading and restoring the application database, enhancing data management capabilities. - Integrated new API routes for system database operations in the backend. - Implemented state management for backup and restore processes, including loading indicators and user notifications.
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
import { randomUUID } from "node:crypto"
|
||||
import { mkdir, readFile, rm, writeFile } from "node:fs/promises"
|
||||
import os from "node:os"
|
||||
import path from "node:path"
|
||||
import Database from "better-sqlite3"
|
||||
import { env } from "../config.js"
|
||||
import { sqliteDatabase } from "../db/index.js"
|
||||
import { refreshScheduler, stopScheduler } from "./scheduler.js"
|
||||
|
||||
const SQLITE_MAGIC = Buffer.from("SQLite format 3\0")
|
||||
const MAX_RESTORE_BYTES = 512 * 1024 * 1024
|
||||
|
||||
type SqliteHandle = InstanceType<typeof Database>
|
||||
|
||||
let operationInFlight = false
|
||||
|
||||
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 resolveDatabasePath(): string {
|
||||
return path.resolve(process.cwd(), env.DATABASE_PATH)
|
||||
}
|
||||
|
||||
function assertSqliteFile(buffer: Buffer): void {
|
||||
if (buffer.length < SQLITE_MAGIC.length) {
|
||||
throw new Error("Файл слишком маленький для SQLite")
|
||||
}
|
||||
if (!buffer.subarray(0, SQLITE_MAGIC.length).equals(SQLITE_MAGIC)) {
|
||||
throw new Error("Файл не похож на резервную копию SQLite")
|
||||
}
|
||||
}
|
||||
|
||||
async function withDatabaseOperation<T>(fn: () => Promise<T> | T): Promise<T> {
|
||||
if (operationInFlight) {
|
||||
throw new Error("Операция с базой данных уже выполняется")
|
||||
}
|
||||
operationInFlight = true
|
||||
stopScheduler()
|
||||
try {
|
||||
return await fn()
|
||||
} finally {
|
||||
refreshScheduler()
|
||||
operationInFlight = false
|
||||
}
|
||||
}
|
||||
|
||||
export async function exportSystemDatabaseBackup(): Promise<{ filename: string; buffer: Buffer }> {
|
||||
return withDatabaseOperation(async () => {
|
||||
sqliteDatabase.pragma("wal_checkpoint(TRUNCATE)")
|
||||
const tempDir = path.join(os.tmpdir(), "mmapp-db-backup")
|
||||
await mkdir(tempDir, { recursive: true })
|
||||
const tempPath = path.join(tempDir, `manager-${randomUUID()}.db`)
|
||||
try {
|
||||
await sqliteDatabase.backup(tempPath)
|
||||
const buffer = await readFile(tempPath)
|
||||
return {
|
||||
filename: `mikrotik-manager_${fmtTimestamp()}.db`,
|
||||
buffer,
|
||||
}
|
||||
} finally {
|
||||
await rm(tempPath, { force: true })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export async function restoreSystemDatabaseBackup(buffer: Buffer): Promise<void> {
|
||||
if (buffer.length > MAX_RESTORE_BYTES) {
|
||||
throw new Error("Файл бэкапа слишком большой")
|
||||
}
|
||||
assertSqliteFile(buffer)
|
||||
|
||||
await withDatabaseOperation(async () => {
|
||||
const tempDir = path.join(os.tmpdir(), "mmapp-db-restore")
|
||||
await mkdir(tempDir, { recursive: true })
|
||||
const tempPath = path.join(tempDir, `restore-${randomUUID()}.db`)
|
||||
let source: SqliteHandle | null = null
|
||||
try {
|
||||
await writeFile(tempPath, buffer)
|
||||
source = new Database(tempPath, { readonly: true, fileMustExist: true })
|
||||
await source.backup(resolveDatabasePath())
|
||||
sqliteDatabase.pragma("wal_checkpoint(TRUNCATE)")
|
||||
} finally {
|
||||
source?.close()
|
||||
await rm(tempPath, { force: true })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function getSystemDatabasePath(): string {
|
||||
return resolveDatabasePath()
|
||||
}
|
||||
Reference in New Issue
Block a user