Files
thepraandClaude Opus 5.5 fba57318fa A backup is restored at boot, and never undoes a protective act
PrivaPub admin restore <id> (and soon the administrator's page) checks the backup (same host, a format and newest
migration this build reads, every hash) and writes restore.json; the running service sees it within seconds and stops,
and the next start restores it in MaintenanceGate, before migrations, indexes and hosted services: a pre-restore backup
taken once, every collection dropped and imported raw with its indexes, the media the live directory lacks brought
back, then the protective merge from the pre-restore backup. Followers and follows are the live ones; blocks, mutes,
domain blocks, reserved names, tombstones, reports, filters and OAuth applications are the union; deletions win;
accounts made since become tombstones and local posts made since answer 410; every session ends.

Each attempt redoes everything; one refused before any change is abandoned and recorded, one failed midway exits 1 for
systemd to retry, and after three it exits 75, which the unit no longer restarts. Commands wait (exit 75) while a
restore is pending. RestoreRecord tells what happened (admin restore --status).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
2026-10-07 11:55:17 +02:00

91 lines
5.4 KiB
Bash

# Shared by run.sh: the network, Caddy (its CA kept in a volume and copied to .ca/root.crt), Mongo and PrivaPub.
here="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"; repo="$(cd "$here/../.." && pwd)"
port=${PASTURE_PORT:-6971} # PrivaPub straight from the workstation; PASTURE_PORT when 6971 is taken
net=privapub-pasture; publish="$here/.publish"; ca="$here/.ca"
# The network looks public (11.42.0.0/24 is never reached for real from inside the pasture), so peers that refuse private
# addresses with no switch to say otherwise (Pixelfed, Mbin, WordPress, Discourse, Fedify) federate too.
subnet=${PASTURE_SUBNET:-11.42.0.0/24}
site() { curl -k --resolve "$1:6443:127.0.0.1" "${@:2}"; }
wait_http() { # url tries
for _ in $(seq 1 "${2:-60}"); do curl -fsk -o /dev/null "$1" && return 0; sleep 2; done
echo "timed out waiting for $1" >&2; return 1
}
pasture_base_up() {
local dotnet=${DOTNET:-$(command -v dotnet || echo ~/.dotnet/dotnet)}
"$dotnet" publish "$repo/PrivaPub/PrivaPub.csproj" -c Release -r linux-x64 --self-contained true -o "$publish" -v quiet
cp "$here/appsettings.Pasture.json" "$publish/"
podman network exists $net || podman network create --subnet "$subnet" $net >/dev/null
podman volume exists pasture-caddy-data || podman volume create pasture-caddy-data >/dev/null
# (PrivaPub's uploads live in a volume: a container made again must not lose them)
podman volume exists pasture-privapub-media || podman volume create --label pasture=1 pasture-privapub-media >/dev/null
# a one-member replica set, as production's, so backups read at one instant
podman run -d --replace --name pasture-mongo --network $net --network-alias mongo docker.io/library/mongo:8 --quiet --replSet rs0 >/dev/null
for _ in $(seq 1 60); do podman exec pasture-mongo mongosh --quiet --eval 'db.hello()' >/dev/null 2>&1 && break; sleep 1; done
podman exec pasture-mongo mongosh --quiet --eval "rs.initiate({_id: 'rs0', members: [{_id: 0, host: 'mongo:27017'}]})" >/dev/null
for _ in $(seq 1 60); do [ "$(podman exec pasture-mongo mongosh --quiet --eval 'db.hello().isWritablePrimary')" = "true" ] && break; sleep 1; done
caddy_up
local extra=()
for setting in ${PRIVAPUB_ENV:-}; do extra+=(-e "$setting"); done
# --restart=always, as systemd's unit: a restore asked for stops PrivaPub, and the next start carries it out
podman run -d --replace --restart=always --name pasture-privapub --network $net -p "127.0.0.1:$port:80" \
--sysctl net.ipv4.ip_unprivileged_port_start=0 -e ASPNETCORE_ENVIRONMENT=Pasture "${extra[@]}" -w /app -v "$publish:/app:Z,ro" \
-v pasture-privapub-media:/tmp/privapub-media \
mcr.microsoft.com/dotnet/runtime-deps:10.0 /app/PrivaPub >/dev/null
wait_http "http://127.0.0.1:$port/build.json"
rm -rf "$ca"; mkdir -p "$ca"
for _ in $(seq 1 60); do
podman cp pasture-caddy:/data/caddy/pki/authorities/local/root.crt "$ca/root.crt" 2>/dev/null && [ -s "$ca/root.crt" ] && break
curl -sk -o /dev/null --resolve privapub.test:6443:127.0.0.1 https://privapub.test:6443/build.json || true
sleep 1
done
[ -s "$ca/root.crt" ] || { echo "Caddy's root certificate could not be copied" >&2; return 1; }
chmod 644 "$ca/root.crt"
cat /etc/pki/tls/certs/ca-bundle.crt /etc/ssl/certs/ca-certificates.crt 2>/dev/null > "$ca/bundle.pem" || true
cat "$ca/root.crt" >> "$ca/bundle.pem"
chmod 644 "$ca/bundle.pem"
}
# Caddy, with every site the Caddyfile serves as a name on the network, so a peer is added with its Caddyfile block alone.
# Its CA lives in the pasture-caddy-data volume, so recreating it (run.sh add, for a new site) keeps every peer's trust.
caddy_up() {
local aliases=""
for site in $(caddy_sites); do aliases="$aliases --network-alias $site"; done
# shellcheck disable=SC2086
podman run -d --replace --name pasture-caddy --network $net $aliases \
-p 127.0.0.1:6443:443 --sysctl net.ipv4.ip_unprivileged_port_start=0 -v "$here/Caddyfile:/etc/caddy/Caddyfile:Z,ro" \
-v pasture-caddy-data:/data docker.io/library/caddy:2 >/dev/null
}
# caddy_current: whether the running Caddy already answers every site of the Caddyfile on the network
caddy_current() {
local have
have=$(podman inspect pasture-caddy --format '{{range .NetworkSettings.Networks}}{{range .Aliases}}{{.}} {{end}}{{end}}' 2>/dev/null)
for site in $(caddy_sites); do case " $have " in *" $site "*) ;; *) return 1 ;; esac; done
}
# the sites the Caddyfile serves: "a.test {" and "a.test, b.test {" lines
caddy_sites() {
grep -E '^[a-z0-9.-]+\.test(, *[a-z0-9.-]+\.test)* *\{' "$here/Caddyfile" | sed 's/ *{.*//; s/,/ /g'
}
# pasture_down [--purge]: removes every pasture container with its anonymous volumes (images declare volumes, and
# without -v each run left them behind), the named volumes and the network. Caddy's CA is kept unless --purge, so a peer
# that was told to trust it still does after the next up.
pasture_down() {
podman ps -a --format '{{.Names}}' | grep '^pasture-' | xargs -r podman rm -f -v >/dev/null 2>&1 || true
local keep='^pasture-caddy-data$'
[ "${1:-}" = "--purge" ] && keep='^$'
podman volume ls --format '{{.Name}}' | grep '^pasture-' | grep -v "$keep" | xargs -r podman volume rm -f >/dev/null 2>&1 || true
podman network rm $net >/dev/null 2>&1 || true
rm -rf "$here/.state" "$ca"
}
# pasture_stats: memory and CPU of every pasture container, largest first
pasture_stats() {
podman stats --no-stream --format '{{.Name}}\t{{.MemUsage}}\t{{.CPUPerc}}' $(podman ps --format '{{.Names}}' | grep '^pasture-') 2>/dev/null \
| sort -t$'\t' -k2 -h -r
}