371 lines
11 KiB
PowerShell
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ============================================
# 数据库备份脚本 (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