1#!/usr/bin/env bash
2set -euo pipefail
3
4source_host=${STUDIO_MIGRATION_SOURCE:-zenith}
5root=/mnt/storage1/apps/studio-handoff
6id=$(date -u +%Y%m%dT%H%M%SZ)-$(python3 -c 'import secrets; print(secrets.token_hex(3))')
7pending=$root/.pending-$id
8final=$root/$id
9
10ssh "$source_host" 'test "$(findmnt -n -o SOURCE --mountpoint /mnt/storage1/apps)" = storage1/apps; test "$(findmnt -n -o FSTYPE --mountpoint /mnt/storage1/apps)" = zfs'
11ssh "$source_host" 'sudo -n docker info >/dev/null'
12
13check_stopped() {
14 [[ $(ssh "$source_host" "sudo -n docker inspect -f '{{.State.Running}}' evil-forgejo") == false ]] || {
15 echo 'Stop Zenith evil-forgejo before exporting the final database' >&2
16 exit 1
17 }
18 [[ $(ssh "$source_host" "sudo -n docker ps -a --filter label=com.docker.compose.service=evil-hedgedoc --format '{{.State}}'") == exited ]] || {
19 echo 'Stop Zenith evil-hedgedoc before exporting the final databases' >&2
20 exit 1
21 }
22}
23
24check_stopped
25ssh "$source_host" 'sudo -n docker exec postgres pg_isready -U postgres >/dev/null'
26ssh "$source_host" 'test -r /mnt/storage1/apps/home-infra/.env'
27ssh "$source_host" "set -e; test ! -e '$final'; mkdir -m 700 -p '$root'; mkdir -m 700 '$pending'"
28for database in evil-forgejo evil-hedgedoc; do
29 ssh "$source_host" "set -e; umask 077; sudo -n docker exec postgres pg_dump -U postgres -Fc --no-owner --no-acl '$database' > '$pending/$database.dump'; test -s '$pending/$database.dump'; sudo -n docker exec -i postgres pg_restore -l < '$pending/$database.dump' >/dev/null"
30done
31check_stopped
32ssh "$source_host" python3 - "$pending" "$id" <<'PY'
33import hashlib
34import json
35from pathlib import Path
36import subprocess
37import sys
38
39folder = Path(sys.argv[1])
40manifest = {"id": sys.argv[2], "databases": {}}
41queries = {
42 "evil-forgejo": 'SELECT (SELECT count(*) FROM "user"), (SELECT count(*) FROM repository)',
43 "evil-hedgedoc": 'SELECT (SELECT count(*) FROM "Notes"), (SELECT count(*) FROM "Users"), (SELECT count(*) FROM "Revisions"), (SELECT count(*) FROM "Authors")',
44}
45for name in ("evil-forgejo", "evil-hedgedoc"):
46 source = folder / f"{name}.dump"
47 digest = hashlib.sha256()
48 with source.open("rb") as file:
49 for chunk in iter(lambda: file.read(1024 * 1024), b""):
50 digest.update(chunk)
51 result = subprocess.run(["sudo", "-n", "docker", "exec", "postgres", "psql", "-U", "postgres", "-d", name,
52 "-At", "-c", queries[name]], check=True, capture_output=True, text=True)
53 counts = [int(value) for value in result.stdout.strip().split("|")]
54 if len(counts) != (4 if name == "evil-hedgedoc" else 2):
55 raise ValueError(f"Unexpected count result for {name}")
56 manifest["databases"][name] = {"file": source.name, "bytes": source.stat().st_size,
57 "sha256": digest.hexdigest(), "counts": counts}
58legacy_env = (Path("/mnt/storage1/apps/home-infra/.env")).read_text().splitlines()
59forgejo_secrets = {
60 "lfs_jwt": "EVIL_FORGEJO_SERVER_LFS_JWT_SECRET",
61 "oauth_jwt": "EVIL_FORGEJO_OAUTH2_JWT_SECRET",
62 "security_key": "EVIL_FORGEJO_SECURITY_SECRET_KEY",
63 "internal_token": "EVIL_FORGEJO_SECURITY_INTERNAL_TOKEN",
64 "anubis_key": "ANUBIS_PRIVATE_KEY",
65 "mailer_address": "MAILER_ADDRESS",
66 "mailer_username": "MAILER_USERNAME",
67 "mailer_password": "MAILER_PASSWORD",
68}
69fingerprints = {}
70for target, source in forgejo_secrets.items():
71 values = [line.split("=", 1)[1] for line in legacy_env if line.startswith(source + "=")]
72 if len(values) != 1 or not values[0]:
73 raise ValueError(f"Legacy Forgejo secret is unavailable: {source}")
74 fingerprints[target] = hashlib.sha256(values[0].encode()).hexdigest()
75manifest["databases"]["evil-forgejo"]["secretSha256"] = fingerprints
76(folder / "manifest.json").write_text(json.dumps(manifest, indent=2) + "\n")
77PY
78ssh "$source_host" "set -e; chmod 600 '$pending/manifest.json'; mv '$pending' '$final'; sync"
79check_stopped
80echo "Legacy PostgreSQL handoff: $final"