zscripts-token-savers/zbackup_ec2.ps1
KellyMichels 502c0e8b29 feat(zversion, zrelease): toolkit versioning + downloadable release zips
Implements the versioning rule (SmartPlant's 5-segment scheme, now the global
standard; currently only sp and zscripts are on it at v1.x):

    v{major}.{rc}.{beta}.{alpha}.{build}

zversion: get / bump / bump-stage / set. A stage bump zeroes every lower
segment including build. 'bump' is one per PR and one per defect fix, not per
file. Any write rewrites three things together, because they are only useful
when they agree: build-version.json (source of truth), a '# Version:' line in
all 42 script headers (a lone copied script still says which release it came
from), and CHECKSUMS.txt (stamping changes every file).

zrelease: packages the current version as releases/zscripts-<version>.zip with
a sibling .sha256, for people who want the toolkit without cloning. One hash
verifies the download; the bundled CHECKSUMS.txt verifies the extracted
contents. Refuses to overwrite an existing version's zip (released = immutable;
bump instead), and refuses to package when zchecksums fails. tests/ excluded
from the zip; releases/ never packages itself.

First release included: releases/zscripts-v1.0.0.0.0.zip (42 scripts + 7
support files) and its .sha256.

.gitattributes: releases/*.sha256 pinned LF (sha256sum treats a trailing CR as
part of the filename), releases/*.zip marked binary.

Verified end-to-end as a downloader would experience it, in WSL: sha256sum -c
on the zip passes, unzip, sha256sum -c CHECKSUMS.txt inside gives 42 OK / 0
FAILED, and the extracted zdeploy.ps1 header and build-version.json both read
v1.0.0.0.0. Double-release guard and -Verify mode exercised. Full Pester suite
219/219 (the checksum tests absorb the new files automatically).
2026-07-28 13:27:50 -05:00

145 lines
5.8 KiB
PowerShell

# Evomedia.net Token Savers — https://github.com/kellymichels/zscripts-token-savers
# Created by Kelly Michels · dev@evomedia.net
# Licensed under the MIT License. See LICENSE.
# Version: v1.0.0.0.0
# zbackup_ec2.ps1 — pull backups down from the server: a Postgres dump for projects
# with a "db" config block, plus a zip of server-side data dirs (uploads/archive/dist).
#
# Usage:
# zbackup_ec2 <project> [<project> ...]
# zbackup_ec2 all # every project with a remote.path
#
# Output:
# <paths.backupsEc2>\<project>\<timestamp>_<project>_db.sql (projects with a db block)
# <paths.backupsEc2>\<project>\<timestamp>_<project>_files.zip
param(
[Parameter(Position = 0, ValueFromRemainingArguments = $true)]
[string[]]$Projects = @()
)
$ErrorActionPreference = "Stop"
. (Join-Path $PSScriptRoot "ZHelpers.ps1")
Start-ZTracking
$cfg = Get-ZConfig
$PEM_KEY = $cfg.ec2.pemKey
$SSH_TARGET = Get-Ec2Target
$RemoteHome = Get-Ec2Home
$Ec2BackupRoot = $cfg.paths.backupsEc2
if ($Projects.Count -eq 0) {
Write-Host ""
Write-Host "Usage: zbackup_ec2 <project> [<project> ...] | all" -ForegroundColor Yellow
Write-Host " Projects in zconfig.json: $((Get-ZProjectKeys) -join ', ')" -ForegroundColor Gray
Write-Host " 'all' pulls a server backup of every project with a remote.path." -ForegroundColor Gray
Stop-ZTracking; exit 1
}
if ($Projects -contains 'all') {
$Projects = @(Get-ZProjectKeys | Where-Object { $cfg.projects.$_.remote -and $cfg.projects.$_.remote.path })
}
function Get-BackupTimestamp { return Get-Date -Format "yyyyMMdd-HHmmss" }
function Ensure-Ec2BackupDir {
param([string]$ProjectKey)
$dir = Join-Path $Ec2BackupRoot $ProjectKey
if (-not (Test-Path -LiteralPath $dir)) {
New-Item -ItemType Directory -Path $dir -Force | Out-Null
}
return $dir
}
function Invoke-ScpFromEc2 {
param([string]$RemotePath, [string]$LocalPath)
scp -o StrictHostKeyChecking=no -i $PEM_KEY "${SSH_TARGET}:$RemotePath" $LocalPath
if ($LASTEXITCODE -ne 0) { throw "SCP download failed: $RemotePath -> $LocalPath (exit $LASTEXITCODE)" }
}
function Invoke-ProjectEc2Backup {
param([string]$Key)
$proj = Get-ZProject -Key $Key
$remotePath = $proj.remote.path
$composeDir = Get-RemoteComposeDir -Key $Key
$ts = Get-BackupTimestamp
$outDir = Ensure-Ec2BackupDir -ProjectKey $Key
$remoteDb = "$RemoteHome/ec2_${Key}_db_$ts.sql"
$remoteZip = "$RemoteHome/ec2_${Key}_files_$ts.zip"
$localDb = Join-Path $outDir "${ts}_${Key}_db.sql"
$localZip = Join-Path $outDir "${ts}_${Key}_files.zip"
$hasDb = ($proj.db -and $proj.db.user -and $proj.db.name)
Write-Host ""
Write-Host "=== [$($Key.ToUpper())] Server backup ($($proj.label)) ===" -ForegroundColor Cyan
if ($hasDb) {
Write-Host " [1/4] PostgreSQL dump on the server..." -ForegroundColor Yellow
$dumpCmd = "cd $composeDir && sudo docker compose exec -T db pg_dump -U $($proj.db.user) -d $($proj.db.name) > $remoteDb"
Invoke-Ec2Step "pg_dump $($proj.db.name)" $dumpCmd
} else {
Write-Host " [1/4] No db config block - skipping database dump." -ForegroundColor DarkGray
}
Write-Host " [2/4] Zipping server-side data (uploads/archive/dist if present)..." -ForegroundColor Yellow
$zipCmd = @(
"sudo apt-get install -y zip >/dev/null 2>&1",
"FILES=''",
"test -d $remotePath/uploads && FILES=`"`$FILES $remotePath/uploads`"",
"test -d $remotePath/archive && FILES=`"`$FILES $remotePath/archive`"",
"test -d $remotePath/dist && FILES=`"`$FILES $remotePath/dist`"",
"test -f $remotePath/build-version.json && FILES=`"`$FILES $remotePath/build-version.json`"",
"if [ -z `"`$FILES`" ]; then echo 'no data dirs found' | sudo zip $remoteZip - >/dev/null; else sudo zip -r $remoteZip `$FILES; fi"
) -join '; '
try {
Invoke-Ec2Step "zip $Key files" $zipCmd
} catch {
Write-Host " WARNING: files zip failed - continuing: $($_.Exception.Message)" -ForegroundColor DarkYellow
Invoke-Ec2Step "placeholder zip" "sudo apt-get install -y zip >/dev/null 2>&1; echo 'zip failed' | sudo zip $remoteZip -"
}
Write-Host " [3/4] Downloading to local..." -ForegroundColor Yellow
if ($hasDb) { Invoke-ScpFromEc2 -RemotePath $remoteDb -LocalPath $localDb }
Invoke-ScpFromEc2 -RemotePath $remoteZip -LocalPath $localZip
Write-Host " [4/4] Cleaning up remote..." -ForegroundColor Yellow
Invoke-Ec2Step "remove remote temp files" "rm -f $remoteDb $remoteZip"
$result = @{ ok = $true; zipPath = $localZip }
$result.zipMb = if (Test-Path $localZip) { [math]::Round((Get-Item $localZip).Length / 1MB, 2) } else { 0 }
if ($hasDb -and (Test-Path $localDb)) {
$result.dbPath = $localDb
$result.dbMb = [math]::Round((Get-Item $localDb).Length / 1MB, 2)
}
return $result
}
$exitCode = 0
$results = @()
foreach ($key in $Projects) {
try {
$results += @{ project = $key; result = Invoke-ProjectEc2Backup -Key $key }
} catch {
Write-Host " FAILED [$key]: $($_.Exception.Message)" -ForegroundColor Red
$results += @{ project = $key; failed = $_.Exception.Message }
if ($exitCode -eq 0) { $exitCode = 1 }
}
}
Write-Host ""
Write-Host "=== Server backup summary ===" -ForegroundColor Cyan
foreach ($r in $results) {
if ($r.result) {
$line = " $($r.project) ->"
if ($r.result.dbPath) { $line += " $($r.result.dbPath) ($($r.result.dbMb) MB) |" }
if ($r.result.zipPath) { $line += " $($r.result.zipPath) ($($r.result.zipMb) MB)" }
Write-Host $line -ForegroundColor Green
} else {
Write-Host (" {0} -> FAILED: {1}" -f $r.project, $r.failed) -ForegroundColor Red
}
}
Write-Host ""
Stop-ZTracking; exit $exitCode