Files
roonge 1a19f0d984 feat(backup): add gramsrv-backup tool, restore scripts and migration guide
Operators had no supported way to move a deployment or to recover one. This
adds cmd/gramsrv-backup, which captures the PostgreSQL databases, the media and
key tree, Redis and the configuration into a single verifiable bundle, and
restores it on another host.

backup / restore / verify, with MANIFEST.json recording the source commit, the
schema version and exact row counts, and SHA256SUMS in sha256sum(1) format.
Components are captured independently (server, grammystore) since each has its
own database and usually its own container; pg_dump and redis-cli can come from
the host or from docker exec.

Portability is the point of the dump format:

  - plain SQL rather than the custom format, so any psql can replay it and the
    client need not match the server
  - --clean --if-exists so a bundle can be replayed over a populated database
  - --no-owner --no-privileges, and no --create, so the target role and database
    name are not baked into the artifact
  - restored with ON_ERROR_STOP=1 and --single-transaction, so a failure cannot
    leave a half-applied schema
  - \restrict, \unrestrict and SET transaction_timeout are stripped on the way
    in; they are PostgreSQL 17 specific and break older clients and servers
  - the target major version is checked before the restore starts

Freezing (--freeze-units, --freeze-containers) is optional because pg_dump is
already MVCC-consistent; it exists so the media tree is byte consistent, and
everything stopped is restarted from a defer even when the capture fails.

Verification re-hashes every artifact, decompresses it, walks tar members, and
cross-checks file_blobs against the media tree, reporting missing and corrupt
objects. check-media.sh runs the live form of that check.

Also included: shell and PowerShell wrappers, a migration runbook in
docs/backup-restore.md, and internal/doclint, which fails the build when tracked
markdown leaks a key, a token or a credential so documentation stays safe to
publish.
2026-10-04 07:34:54 +00:00

72 lines
2.2 KiB
PowerShell

#!/usr/bin/env pwsh
# Capture a gramsrv backup bundle with sensible defaults for a monolith
# deployment. Every unrecognised argument is forwarded to gramsrv-backup.
#
# ./scripts/backup.ps1 -Out D:\gramsrv-backup\20261004 -Component all
#
# The tool reads .env for database credentials, so nothing secret is passed on
# the command line.
[CmdletBinding()]
param(
[Parameter(ValueFromRemainingArguments = $true)]
[string[]]$ToolArgs
)
$ErrorActionPreference = 'Stop'
$RepoRoot = Split-Path -Parent $PSScriptRoot
$Binary = Join-Path $RepoRoot 'bin/gramsrv-backup.exe'
function Test-HasFlag([string]$Name, [string[]]$Items) {
foreach ($item in $Items) {
if ($item -eq $Name -or $item.StartsWith("$Name=")) { return $true }
}
return $false
}
function Build-Binary {
if (-not (Get-Command go -ErrorAction SilentlyContinue)) {
throw "go is required to build $Binary"
}
Write-Host "building $Binary"
# Serial compilation keeps peak memory low enough for small hosts.
Push-Location $RepoRoot
try {
$env:GOFLAGS = '-p=1'
go build -o $Binary ./cmd/gramsrv-backup
if ($LASTEXITCODE -ne 0) { throw "go build failed" }
} finally {
Pop-Location
}
}
if ($ToolArgs -contains '--help') {
Get-Content -Raw (Join-Path $PSScriptRoot 'backup.sh') | Select-String -Pattern '(?s)usage\(\).*?^}' | ForEach-Object { $_.Matches.Value }
exit 0
}
if (-not (Test-Path $Binary)) { Build-Binary }
$forwarded = @()
if (-not (Test-HasFlag '--out' $ToolArgs)) {
$stamp = (Get-Date).ToUniversalTime().ToString('yyyyMMdd')
$forwarded += @('--out', (Join-Path (Split-Path -Parent $RepoRoot) "gramsrv-backup-$stamp"))
}
if (-not (Test-HasFlag '--repo-dir' $ToolArgs)) {
$forwarded += @('--repo-dir', $RepoRoot)
}
if (-not (Test-HasFlag '--pg-tools' $ToolArgs) -and (Get-Command docker -ErrorAction SilentlyContinue)) {
$forwarded += @('--pg-tools', 'docker')
}
if (-not (Test-HasFlag '--redis-tools' $ToolArgs) -and (Get-Command docker -ErrorAction SilentlyContinue)) {
$forwarded += @('--redis-tools', 'docker')
}
Push-Location $RepoRoot
try {
& $Binary backup @forwarded @ToolArgs
exit $LASTEXITCODE
} finally {
Pop-Location
}