371 lines
11 KiB
PowerShell
371 lines
11 KiB
PowerShell
# ============================================
|
||
# 数据库备份脚本 (PowerShell)
|
||
# ============================================
|
||
# 功能:备份 PostgreSQL 数据库(支持本地和 Docker 环境)
|
||
# 使用:.\backup-db.ps1 [-BackupDir <目录>] [-DockerContainer <容器名>]
|
||
# ============================================
|
||
|
||
param(
|
||
[string]$BackupDir = ".\backups\database",
|
||
[string]$DockerContainer = "furniture-postgres"
|
||
)
|
||
|
||
$ErrorActionPreference = "Stop"
|
||
|
||
# 配置
|
||
$Timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
|
||
$BackupFile = "db-backup-$Timestamp.sql.gz"
|
||
$LogFile = "backup-db-$Timestamp.log"
|
||
$global:UseDocker = $false
|
||
|
||
# 确保备份目录存在
|
||
if (-not (Test-Path $BackupDir)) {
|
||
New-Item -ItemType Directory -Path $BackupDir -Force | Out-Null
|
||
}
|
||
|
||
# 日志函数
|
||
function Write-Log {
|
||
param([string]$Message, [string]$Level = "INFO")
|
||
|
||
$TimeStamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
|
||
$LogMessage = "[$TimeStamp] $($Level): $Message"
|
||
|
||
switch ($Level) {
|
||
"ERROR" { Write-Host $LogMessage -ForegroundColor Red }
|
||
"WARN" { Write-Host $LogMessage -ForegroundColor Yellow }
|
||
"SUCCESS" { Write-Host $LogMessage -ForegroundColor Green }
|
||
default { Write-Host $LogMessage }
|
||
}
|
||
|
||
Add-Content -Path "$BackupDir\$LogFile" -Value $LogMessage
|
||
}
|
||
|
||
# 检查依赖
|
||
function Test-Dependencies {
|
||
Write-Log "检查依赖..."
|
||
|
||
# 检查是否有 Docker 容器运行
|
||
if (Get-Command docker -ErrorAction SilentlyContinue) {
|
||
$containerRunning = docker ps --filter "name=$DockerContainer" --format "{{.Names}}" 2>$null
|
||
if ($containerRunning -eq $DockerContainer) {
|
||
$global:UseDocker = $true
|
||
Write-Log "检测到 Docker 容器: $DockerContainer,将使用 Docker 执行备份"
|
||
}
|
||
}
|
||
|
||
# 如果不使用 Docker,检查本地 pg_dump
|
||
if (-not $global:UseDocker) {
|
||
if (-not (Get-Command pg_dump -ErrorAction SilentlyContinue)) {
|
||
Write-Log "pg_dump 未安装且未检测到 Docker 容器,无法执行备份" "ERROR"
|
||
exit 1
|
||
}
|
||
}
|
||
|
||
# 检查 7-Zip 或 gzip(仅本地模式需要)
|
||
$global:CompressTool = $null
|
||
if (Get-Command 7z -ErrorAction SilentlyContinue) {
|
||
$global:CompressTool = "7z"
|
||
} elseif (Get-Command gzip -ErrorAction SilentlyContinue) {
|
||
$global:CompressTool = "gzip"
|
||
}
|
||
|
||
if (-not $global:UseDocker -and -not $global:CompressTool) {
|
||
Write-Log "未找到压缩工具(7z 或 gzip)" "ERROR"
|
||
exit 1
|
||
}
|
||
|
||
Write-Log "✅ 依赖检查通过" "SUCCESS"
|
||
}
|
||
|
||
# 加载环境变量
|
||
function Load-EnvFile {
|
||
if (Test-Path .env) {
|
||
Get-Content .env | ForEach-Object {
|
||
if ($_ -match '^([^#][^=]+)=(.*)$') {
|
||
$name = $matches[1].Trim()
|
||
$value = $matches[2].Trim()
|
||
[Environment]::SetEnvironmentVariable($name, $value, "Process")
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
# 验证数据库连接
|
||
function Test-DatabaseConnection {
|
||
Write-Log "验证数据库连接..."
|
||
|
||
$DatabaseUrl = $env:DATABASE_URL
|
||
if (-not $DatabaseUrl) {
|
||
Write-Log "DATABASE_URL 环境变量未设置" "ERROR"
|
||
exit 1
|
||
}
|
||
|
||
# 解析数据库连接字符串
|
||
if ($DatabaseUrl -match 'postgresql://([^:]+):([^@]+)@([^:]+):([^/]+)/([^\?]+)') {
|
||
$global:DbUser = $matches[1]
|
||
$global:DbPass = $matches[2]
|
||
$global:DbHost = $matches[3]
|
||
$global:DbPort = $matches[4]
|
||
$global:DbName = $matches[5]
|
||
} else {
|
||
Write-Log "DATABASE_URL 格式无效" "ERROR"
|
||
exit 1
|
||
}
|
||
|
||
# 测试连接
|
||
if ($global:UseDocker) {
|
||
$result = docker exec $DockerContainer psql -U $global:DbUser -d $global:DbName -c "SELECT 1" 2>&1
|
||
} else {
|
||
$env:PGPASSWORD = $global:DbPass
|
||
$result = & psql -h $global:DbHost -p $global:DbPort -U $global:DbUser -d $global:DbName -c "SELECT 1" 2>&1
|
||
}
|
||
|
||
if ($LASTEXITCODE -ne 0) {
|
||
Write-Log "无法连接到数据库: $result" "ERROR"
|
||
exit 1
|
||
}
|
||
|
||
Write-Log "✅ 数据库连接成功" "SUCCESS"
|
||
}
|
||
|
||
# 创建备份目录
|
||
function New-BackupDirectory {
|
||
Write-Log "创建备份目录: $BackupDir"
|
||
|
||
if (-not (Test-Path $BackupDir)) {
|
||
New-Item -ItemType Directory -Path $BackupDir -Force | Out-Null
|
||
}
|
||
|
||
if (-not (Test-Path $BackupDir)) {
|
||
Write-Log "无法创建备份目录" "ERROR"
|
||
exit 1
|
||
}
|
||
|
||
Write-Log "✅ 备份目录已创建" "SUCCESS"
|
||
}
|
||
|
||
# 检查磁盘空间
|
||
function Test-DiskSpace {
|
||
Write-Log "检查磁盘空间..."
|
||
|
||
$drive = (Get-Item $BackupDir).PSDrive
|
||
$freeSpace = [math]::Round($drive.Free / 1MB, 2)
|
||
|
||
# 获取数据库大小
|
||
$sizeQuery = "SELECT pg_database_size('$($global:DbName)') / 1024 / 1024"
|
||
if ($global:UseDocker) {
|
||
$dbSizeResult = docker exec $DockerContainer psql -U $global:DbUser -d $global:DbName -t -c $sizeQuery 2>&1
|
||
} else {
|
||
$env:PGPASSWORD = $global:DbPass
|
||
$dbSizeResult = & psql -h $global:DbHost -p $global:DbPort -U $global:DbUser -d $global:DbName -t -c $sizeQuery 2>&1
|
||
}
|
||
|
||
# 解析结果(可能是数组或字符串)
|
||
if ($dbSizeResult -is [array]) {
|
||
$dbSizeStr = ($dbSizeResult | Where-Object { $_ -match '\d+' } | Select-Object -First 1).Trim()
|
||
} else {
|
||
$dbSizeStr = $dbSizeResult.ToString().Trim()
|
||
}
|
||
$dbSize = [math]::Round([double]$dbSizeStr, 0)
|
||
|
||
$requiredSpace = $dbSize * 2
|
||
|
||
Write-Log "数据库大小: $dbSize MB"
|
||
Write-Log "可用空间: $freeSpace MB"
|
||
Write-Log "需要空间: $requiredSpace MB"
|
||
|
||
if ($freeSpace -lt $requiredSpace) {
|
||
Write-Log "磁盘空间不足" "ERROR"
|
||
exit 1
|
||
}
|
||
|
||
Write-Log "✅ 磁盘空间充足" "SUCCESS"
|
||
}
|
||
|
||
# 执行备份
|
||
function Start-Backup {
|
||
Write-Log "开始备份数据库..."
|
||
Write-Log "备份文件: $BackupFile"
|
||
|
||
$startTime = Get-Date
|
||
$backupPath = Join-Path $BackupDir $BackupFile
|
||
|
||
try {
|
||
if ($global:UseDocker) {
|
||
# Docker 模式:在容器内执行备份
|
||
$tempFile = "/tmp/db-backup-$Timestamp.sql"
|
||
$tempGzFile = "/tmp/db-backup-$Timestamp.sql.gz"
|
||
|
||
# 在容器内执行 pg_dump
|
||
docker exec $DockerContainer pg_dump -U $global:DbUser -d $global:DbName `
|
||
--format=plain --no-owner --no-acl --clean --if-exists `
|
||
-f $tempFile
|
||
|
||
if ($LASTEXITCODE -ne 0) {
|
||
throw "pg_dump 执行失败"
|
||
}
|
||
|
||
# 在容器内压缩
|
||
docker exec $DockerContainer gzip -f $tempFile
|
||
|
||
if ($LASTEXITCODE -ne 0) {
|
||
throw "压缩失败"
|
||
}
|
||
|
||
# 复制到本地
|
||
docker cp "${DockerContainer}:${tempGzFile}" $backupPath
|
||
|
||
if ($LASTEXITCODE -ne 0) {
|
||
throw "复制备份文件失败"
|
||
}
|
||
|
||
# 清理容器内临时文件
|
||
docker exec $DockerContainer rm -f $tempGzFile 2>$null
|
||
} else {
|
||
# 本地模式
|
||
$tempSqlFile = Join-Path $BackupDir "temp-$Timestamp.sql"
|
||
|
||
$env:PGPASSWORD = $global:DbPass
|
||
& pg_dump -h $global:DbHost -p $global:DbPort -U $global:DbUser -d $global:DbName `
|
||
--format=plain --no-owner --no-acl --clean --if-exists `
|
||
-f $tempSqlFile
|
||
|
||
if ($LASTEXITCODE -ne 0) {
|
||
throw "pg_dump 执行失败"
|
||
}
|
||
|
||
# 压缩文件
|
||
if ($global:CompressTool -eq "7z") {
|
||
& 7z a -tgzip $backupPath $tempSqlFile | Out-Null
|
||
} else {
|
||
& gzip -c $tempSqlFile > $backupPath
|
||
}
|
||
|
||
if ($LASTEXITCODE -ne 0) {
|
||
throw "压缩失败"
|
||
}
|
||
|
||
Remove-Item $tempSqlFile -Force
|
||
}
|
||
|
||
$endTime = Get-Date
|
||
$duration = ($endTime - $startTime).TotalSeconds
|
||
|
||
Write-Log "✅ 备份完成,耗时: $([math]::Round($duration, 2)) 秒" "SUCCESS"
|
||
}
|
||
catch {
|
||
Write-Log "备份失败: $_" "ERROR"
|
||
exit 1
|
||
}
|
||
}
|
||
|
||
# 验证备份文件
|
||
function Test-BackupFile {
|
||
Write-Log "验证备份文件..."
|
||
|
||
$backupPath = Join-Path $BackupDir $BackupFile
|
||
|
||
if (-not (Test-Path $backupPath)) {
|
||
Write-Log "备份文件不存在" "ERROR"
|
||
exit 1
|
||
}
|
||
|
||
# 检查文件大小
|
||
$fileSize = (Get-Item $backupPath).Length
|
||
$fileSizeMB = [math]::Round($fileSize / 1MB, 2)
|
||
Write-Log "备份文件大小: $fileSizeMB MB"
|
||
|
||
# 生成 checksum
|
||
$hash = Get-FileHash -Path $backupPath -Algorithm SHA256
|
||
$checksum = $hash.Hash
|
||
$checksum | Out-File -FilePath "$backupPath.sha256" -Encoding ASCII
|
||
|
||
Write-Log "✅ 备份文件完整性验证通过" "SUCCESS"
|
||
Write-Log "✅ Checksum: $checksum" "SUCCESS"
|
||
}
|
||
|
||
# 清理旧备份
|
||
function Remove-OldBackups {
|
||
Write-Log "清理旧备份..."
|
||
|
||
$keepCount = 5
|
||
|
||
# 获取所有备份文件
|
||
$backupFiles = Get-ChildItem -Path $BackupDir -Filter "db-backup-*.sql.gz" |
|
||
Sort-Object LastWriteTime -Descending
|
||
|
||
$backupCount = $backupFiles.Count
|
||
|
||
if ($backupCount -gt $keepCount) {
|
||
$deleteCount = $backupCount - $keepCount
|
||
Write-Log "发现 $backupCount 个备份,删除最旧的 $deleteCount 个"
|
||
|
||
$backupFiles | Select-Object -Skip $keepCount | ForEach-Object {
|
||
Write-Log "删除: $($_.Name)"
|
||
Remove-Item $_.FullName -Force
|
||
$checksumFile = "$($_.FullName).sha256"
|
||
if (Test-Path $checksumFile) {
|
||
Remove-Item $checksumFile -Force
|
||
}
|
||
}
|
||
|
||
Write-Log "✅ 清理完成" "SUCCESS"
|
||
} else {
|
||
Write-Log "当前有 $backupCount 个备份,无需清理"
|
||
}
|
||
}
|
||
|
||
# 生成备份摘要
|
||
function New-BackupSummary {
|
||
Write-Log "生成备份摘要..."
|
||
|
||
$backupPath = Join-Path $BackupDir $BackupFile
|
||
$fileSize = (Get-Item $backupPath).Length
|
||
$checksum = Get-Content "$backupPath.sha256"
|
||
|
||
$summary = @{
|
||
timestamp = (Get-Date -Format "o")
|
||
backup_file = $BackupFile
|
||
database = $global:DbName
|
||
host = $global:DbHost
|
||
size = $fileSize
|
||
checksum = $checksum
|
||
status = "success"
|
||
}
|
||
|
||
$summaryPath = Join-Path $BackupDir "backup-summary-$Timestamp.json"
|
||
$summary | ConvertTo-Json | Out-File -FilePath $summaryPath -Encoding UTF8
|
||
|
||
Write-Log "✅ 备份摘要已生成" "SUCCESS"
|
||
}
|
||
|
||
# 主函数
|
||
function Main {
|
||
Write-Host "========================================"
|
||
Write-Host " 数据库备份脚本"
|
||
Write-Host "========================================"
|
||
Write-Host ""
|
||
|
||
Load-EnvFile
|
||
Test-Dependencies
|
||
Test-DatabaseConnection
|
||
New-BackupDirectory
|
||
Test-DiskSpace
|
||
Start-Backup
|
||
Test-BackupFile
|
||
Remove-OldBackups
|
||
New-BackupSummary
|
||
|
||
Write-Host ""
|
||
Write-Host "========================================"
|
||
Write-Log "🎉 备份成功完成!" "SUCCESS"
|
||
Write-Host "========================================"
|
||
Write-Host ""
|
||
Write-Host "备份文件: $BackupDir\$BackupFile"
|
||
Write-Host "日志文件: $BackupDir\$LogFile"
|
||
Write-Host ""
|
||
}
|
||
|
||
# 执行主函数
|
||
Main
|