| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244 |
- import { pgDump, pgRestore } from 'pg-dump-restore'
- import { promises as fs } from 'fs'
- import path from 'path'
- import cron from 'node-cron'
- import { logger } from './logger'
- import process from 'node:process'
- const BACKUP_DIR = process.env.BACKUP_DIR || './backups'
- const RETENTION_DAYS = parseInt(process.env.BACKUP_RETENTION_DAYS || '7')
- const SCHEDULE = process.env.BACKUP_SCHEDULE || '0 2 * * *'
- export interface BackupOptions {
- host?: string
- port?: number
- database?: string
- username?: string
- password?: string
- format?: 'custom' | 'plain' | 'tar'
- outputFile?: string
- }
- export class DatabaseBackup {
- private static instance: DatabaseBackup
- private cronJob: any = null
- private constructor() {}
- static getInstance(): DatabaseBackup {
- if (!DatabaseBackup.instance) {
- DatabaseBackup.instance = new DatabaseBackup()
- }
- return DatabaseBackup.instance
- }
- private getDbConfig() {
- return {
- host: process.env.DB_HOST || 'localhost',
- port: parseInt(process.env.DB_PORT || '5432'),
- database: process.env.DB_DATABASE || 'postgres',
- username: process.env.DB_USERNAME || 'postgres',
- password: process.env.DB_PASSWORD || '',
- }
- }
- async ensureBackupDir(): Promise<void> {
- try {
- await fs.mkdir(BACKUP_DIR, { recursive: true })
- await fs.chmod(BACKUP_DIR, 0o700)
- logger.db(`备份目录已创建: ${BACKUP_DIR}`)
- } catch (error) {
- logger.error(`创建备份目录失败: ${error}`)
- throw error
- }
- }
- async createBackup(options: Partial<BackupOptions> = {}): Promise<string> {
- const config = { ...this.getDbConfig(), ...options }
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
- const backupFile = options.outputFile || path.join(BACKUP_DIR, `backup-${timestamp}.dump`)
- try {
- await this.ensureBackupDir()
- logger.db(`开始创建数据库备份: ${config.database}`)
- await pgDump({
- host: config.host,
- port: config.port,
- database: config.database,
- username: config.username,
- password: config.password,
- }, {
- format: 'custom',
- filePath: backupFile,
- })
- await fs.chmod(backupFile, 0o600)
- const stats = await fs.stat(backupFile)
- logger.db(`备份创建成功: ${backupFile} (${this.formatFileSize(stats.size)})`)
- return backupFile
- } catch (error) {
- logger.error(`备份创建失败: ${error}`)
- throw error
- }
- }
- async restoreBackup(backupFile: string, options: Partial<BackupOptions> = {}): Promise<void> {
- const config = { ...this.getDbConfig(), ...options }
- try {
- if (!await this.backupExists(backupFile)) {
- throw new Error(`备份文件不存在: ${backupFile}`)
- }
- logger.db(`开始恢复数据库备份: ${backupFile}`)
- await pgRestore({
- host: config.host,
- port: config.port,
- database: config.database,
- username: config.username,
- password: config.password,
- }, {
- file: backupFile,
- })
- logger.db(`数据库恢复成功: ${config.database}`)
- } catch (error) {
- logger.error(`数据库恢复失败: ${error}`)
- throw error
- }
- }
- async cleanupOldBackups(): Promise<void> {
- try {
- const files = await fs.readdir(BACKUP_DIR)
- const now = Date.now()
- const retentionTime = RETENTION_DAYS * 24 * 60 * 60 * 1000
- for (const file of files) {
- if (file.endsWith('.dump')) {
- const filePath = path.join(BACKUP_DIR, file)
- const stats = await fs.stat(filePath)
- if (now - stats.mtimeMs > retentionTime) {
- await fs.unlink(filePath)
- logger.db(`删除旧备份文件: ${file}`)
- }
- }
- }
- } catch (error) {
- logger.error(`清理旧备份失败: ${error}`)
- }
- }
- async backupExists(backupFile: string): Promise<boolean> {
- try {
- await fs.access(backupFile)
- return true
- } catch {
- return false
- }
- }
- async getBackupInfo(backupFile: string): Promise<{
- size: number
- mtime: Date
- formattedSize: string
- }> {
- try {
- const stats = await fs.stat(backupFile)
- return {
- size: stats.size,
- mtime: stats.mtime,
- formattedSize: this.formatFileSize(stats.size)
- }
- } catch (error) {
- throw new Error(`获取备份信息失败: ${error}`)
- }
- }
- private formatFileSize(bytes: number): string {
- const sizes = ['B', 'KB', 'MB', 'GB']
- if (bytes === 0) return '0 B'
- const i = Math.floor(Math.log(bytes) / Math.log(1024))
- return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i]
- }
- startScheduledBackups(): void {
- if (this.cronJob) {
- this.cronJob.stop()
- }
- this.cronJob = cron.schedule(SCHEDULE, async () => {
- try {
- logger.db('开始定时备份任务')
- await this.createBackup()
- await this.cleanupOldBackups()
- logger.db('定时备份任务完成')
- } catch (error) {
- logger.error(`定时备份任务失败: ${error}`)
- }
- })
- logger.db(`备份调度已启动: ${SCHEDULE}`)
- }
- stopScheduledBackups(): void {
- if (this.cronJob) {
- this.cronJob.stop()
- logger.db('备份调度已停止')
- }
- }
- getBackupStatus(): {
- scheduled: boolean
- nextRun: Date | null
- lastRun: Date | null
- } {
- return {
- scheduled: this.cronJob !== null,
- nextRun: this.cronJob?.nextDate() || null,
- lastRun: null
- }
- }
- }
- export const databaseBackup = DatabaseBackup.getInstance()
- if (import.meta.url === `file://${process.argv[1]}`) {
- async function main() {
- const command = process.argv[2]
- switch (command) {
- case 'backup':
- await databaseBackup.createBackup()
- break
- case 'restore':
- const backupFile = process.argv[3]
- if (!backupFile) {
- console.error('请指定要恢复的备份文件')
- process.exit(1)
- }
- await databaseBackup.restoreBackup(backupFile)
- break
- case 'cleanup':
- await databaseBackup.cleanupOldBackups()
- break
- case 'list':
- const files = await fs.readdir(BACKUP_DIR)
- const backupFiles = files.filter(f => f.endsWith('.dump'))
- console.log('可用备份文件:')
- backupFiles.forEach(file => console.log(` - ${file}`))
- break
- default:
- console.log('用法: node backup.ts [backup|restore <file>|cleanup|list]')
- process.exit(1)
- }
- }
- main().catch(console.error)
- }
|