param( [string]$BackupDir = "$PSScriptRoot\..\backups", [int]$RetentionDays = 30 ) $ErrorActionPreference = "Stop" $startTime = Get-Date # Ensure backup directory exists if (-not (Test-Path $BackupDir)) { New-Item -ItemType Directory -Path $BackupDir -Force | Out-Null } # Determine database URL from env or .env.local $dbUrl = $env:DATABASE_URL if (-not $dbUrl -and (Test-Path "$PSScriptRoot\..\.env.local")) { $envContent = Get-Content "$PSScriptRoot\..\.env.local" -Raw if ($envContent -match 'DATABASE_URL=(.+)') { $dbUrl = $Matches[1].Trim() } } if (-not $dbUrl) { Write-Error "DATABASE_URL not found. Set it as an environment variable or in .env.local" exit 1 } # Parse the database URL $uri = [System.Uri]$dbUrl $pgUser = $uri.UserInfo.Split(':')[0] $pgPass = $uri.UserInfo.Split(':')[1] $pgHost = $uri.Host $pgPort = $uri.Port $pgDb = $uri.AbsolutePath.TrimStart('/') # Set PGPASSWORD for pg_dump $env:PGPASSWORD = $pgPass $timestamp = Get-Date -Format "yyyyMMdd_HHmmss" $filename = "crm_backup_$timestamp.sql" $filepath = Join-Path $BackupDir $filename Write-Output "Starting backup of database '$pgDb' to $filepath" Write-Output "Database: $pgHost:$pgPort/$pgDb" try { # Run pg_dump $pgDumpPath = if (Get-Command pg_dump -ErrorAction SilentlyContinue) { "pg_dump" } else { "$env:TEMP\pg\pgsql\bin\pg_dump.exe" } $output = & $pgDumpPath -h $pgHost -p $pgPort -U $pgUser -d $pgDb --format=custom --verbose --file $filename 2>&1 $exitCode = $LASTEXITCODE if ($exitCode -ne 0 -or -not (Test-Path $filename)) { throw "pg_dump failed with exit code $exitCode" } Move-Item -Path $filename -Destination $filepath -Force $fileInfo = Get-Item $filepath $fileSize = $fileInfo.Length # Verify backup by checking if file is valid custom format $verifyOutput = & $pgDumpPath -h $pgHost -p $pgPort -U $pgUser -d $pgDb --format=custom --schema-only --file "$filename.verify" 2>&1 $verifyExitCode = $LASTEXITCODE if (Test-Path "$filename.verify") { Remove-Item "$filename.verify" -Force } $verificationStatus = if ($verifyExitCode -eq 0) { "verified" } else { "failed" } # Calculate duration $endTime = Get-Date $durationSeconds = [math]::Round(($endTime - $startTime).TotalSeconds) # Log the backup $logQuery = @" INSERT INTO backup_logs (backup_type, status, file_name, file_size_bytes, pg_dump_exit_code, verification_status, started_at, completed_at, duration_seconds, retention_days) VALUES ('full', 'completed', '$filename', $fileSize, $exitCode, '$verificationStatus', '$($startTime.ToString("yyyy-MM-dd HH:mm:ss"))', '$($endTime.ToString("yyyy-MM-dd HH:mm:ss"))', $durationSeconds, $RetentionDays); "@ # Try to log to database (may fail if db is not reachable for logging, that's OK) try { $env:PGPASSWORD = $pgPass & $env:TEMP\pg\pgsql\bin\psql.exe -h $pgHost -p $pgPort -U $pgUser -d $pgDb -c $logQuery 2>&1 | Out-Null } catch { Write-Warning "Could not log backup to database: $_" } Write-Output "Backup completed successfully:" Write-Output " File: $filename" Write-Output " Size: $([math]::Round($fileSize / 1MB, 2)) MB" Write-Output " Duration: ${durationSeconds}s" Write-Output " Status: $verificationStatus" # Cleanup old backups (beyond retention period) $cutoff = (Get-Date).AddDays(-$RetentionDays) $oldBackups = Get-ChildItem $BackupDir -Filter "crm_backup_*.sql" | Where-Object { $_.CreationTime -lt $cutoff } foreach ($old in $oldBackups) { Remove-Item $old.FullName -Force Write-Output "Removed old backup: $($old.Name)" } } catch { Write-Error "Backup failed: $_" # Log failure $endTime = Get-Date $durationSeconds = [math]::Round(($endTime - $startTime).TotalSeconds) $errorMsg = $_.ToString().Replace("'", "''") $failQuery = @" INSERT INTO backup_logs (backup_type, status, file_name, error_message, pg_dump_exit_code, verification_status, started_at, completed_at, duration_seconds, retention_days) VALUES ('full', 'failed', '$filename', '$errorMsg', $exitCode, 'failed', '$($startTime.ToString("yyyy-MM-dd HH:mm:ss"))', '$($endTime.ToString("yyyy-MM-dd HH:mm:ss"))', $durationSeconds, $RetentionDays); "@ try { $env:PGPASSWORD = $pgPass & $env:TEMP\pg\pgsql\bin\psql.exe -h $pgHost -p $pgPort -U $pgUser -d $pgDb -c $failQuery 2>&1 | Out-Null } catch {} exit 1 } finally { Remove-Item "Env:PGPASSWORD" -ErrorAction SilentlyContinue }