My nix-darwin and NixOS config
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

chore: update scripts

+316 -41
+259
scripts/cleanup-service-data.sh
··· 1 + #!/usr/bin/env bash 2 + # ============================================================================= 3 + # cleanup-service-data.sh 4 + # 5 + # Removes residual on-disk data from services that have been uninstalled 6 + # (i.e., disabled in NixOS config and no longer running). 7 + # 8 + # For each known service the script checks: 9 + # 1. Whether the service unit is currently active or enabled — if so, 10 + # the service is live and its data is NEVER touched. 11 + # 2. Whether the data path(s) exist on disk. 12 + # 3. For PostgreSQL-backed services, whether the DB still exists. 13 + # 14 + # Nothing is deleted without explicit per-service confirmation. 15 + # 16 + # Run as root on the NixOS server. 17 + # ============================================================================= 18 + set -euo pipefail 19 + 20 + RED='\033[0;31m' 21 + GREEN='\033[0;32m' 22 + YELLOW='\033[1;33m' 23 + CYAN='\033[0;36m' 24 + BOLD='\033[1m' 25 + NC='\033[0m' 26 + 27 + info() { echo -e "${GREEN}[INFO]${NC} $*"; } 28 + warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } 29 + skip() { echo -e "${CYAN}[SKIP]${NC} $*"; } 30 + removed() { echo -e "${RED}[DEL]${NC} $*"; } 31 + section() { echo -e "\n${BOLD}── $* ${NC}"; } 32 + confirm() { 33 + read -r -p "$(echo -e "${YELLOW} ${1} [y/N] ${NC}")" r 34 + [[ "${r,,}" == "y" ]] 35 + } 36 + 37 + [[ "$(id -u)" -eq 0 ]] || { 38 + echo -e "${RED}Run as root.${NC}" >&2 39 + exit 1 40 + } 41 + 42 + DRY_RUN=false 43 + [[ "${1:-}" == "--dry-run" ]] && DRY_RUN=true && warn "Dry-run mode — nothing will be deleted." 44 + 45 + # ── Helpers ─────────────────────────────────────────────────────────────────── 46 + 47 + # Returns 0 if the systemd service is active or enabled (i.e., still live). 48 + service_live() { 49 + local unit="$1" 50 + systemctl is-active --quiet "$unit" 2>/dev/null || 51 + systemctl is-enabled --quiet "$unit" 2>/dev/null 52 + } 53 + 54 + # Removes a directory, respecting dry-run. 55 + remove_dir() { 56 + local path="$1" 57 + if [[ ! -d "$path" ]]; then return; fi 58 + local size 59 + size=$(du -sh "$path" 2>/dev/null | cut -f1 || echo "?") 60 + if $DRY_RUN; then 61 + warn " DRY-RUN: would remove $path ($size)" 62 + else 63 + rm -rf "$path" 64 + removed "$path ($size freed)" 65 + fi 66 + } 67 + 68 + # Drops a PostgreSQL database if it exists. 69 + drop_pg_db() { 70 + local db="$1" 71 + local exists 72 + exists=$(sudo -u postgres psql -tA -c \ 73 + "SELECT 1 FROM pg_database WHERE datname='${db}';" 2>/dev/null || echo "") 74 + if [[ "$exists" != "1" ]]; then 75 + skip " PostgreSQL DB '${db}' does not exist — nothing to drop." 76 + return 77 + fi 78 + if $DRY_RUN; then 79 + warn " DRY-RUN: would drop PostgreSQL DB '${db}'" 80 + else 81 + sudo -u postgres dropdb "$db" 82 + removed " PostgreSQL DB '${db}' dropped." 83 + fi 84 + } 85 + 86 + # Drops a PostgreSQL role if it exists and has no remaining owned objects. 87 + drop_pg_role() { 88 + local role="$1" 89 + local exists 90 + exists=$(sudo -u postgres psql -tA -c \ 91 + "SELECT 1 FROM pg_roles WHERE rolname='${role}';" 2>/dev/null || echo "") 92 + if [[ "$exists" != "1" ]]; then return; fi 93 + if $DRY_RUN; then 94 + warn " DRY-RUN: would drop PostgreSQL role '${role}'" 95 + else 96 + if sudo -u postgres dropuser "$role" 2>/dev/null; then 97 + removed " PostgreSQL role '${role}' dropped." 98 + else 99 + warn " Could not drop role '${role}' (may still own objects)." 100 + fi 101 + fi 102 + } 103 + 104 + # ── Service definitions ─────────────────────────────────────────────────────── 105 + # Each entry: check_unit display_name pg_db(or-) dirs... 106 + 107 + declare -A SVC_LABELS=( 108 + [gotosocial]="GoToSocial (ActivityPub)" 109 + [sharkey]="Sharkey (ActivityPub)" 110 + [forgejo]="Forgejo (Git forge)" 111 + [nextcloud]="Nextcloud" 112 + [immich - server]="Immich (photos)" 113 + [jellyfin]="Jellyfin" 114 + [vaultwarden]="Vaultwarden" 115 + [bluesky - pds]="Bluesky PDS (ATProto)" 116 + [samba - smbd]="Time Machine (Samba)" 117 + [grafana]="Grafana" 118 + [prometheus]="Prometheus" 119 + ) 120 + 121 + # unit → "pg_db:pg_role dir1 dir2 ..." (- means no PostgreSQL) 122 + declare -A SVC_DATA=( 123 + [gotosocial]="-:/srv/gotosocial" 124 + [sharkey]="sharkey:sharkey /srv/sharkey" 125 + [forgejo]="-:/srv/forgejo" 126 + [nextcloud]="nextcloud:nextcloud /srv/nextcloud" 127 + [immich - server]="immich:immich /srv/immich" 128 + # Jellyfin: /var/lib/jellyfin is config/metadata — the media dir 129 + # (/srv/nextcloud/data/.../Media) is shared with Nextcloud and is NOT cleaned. 130 + [jellyfin]="-:/var/lib/jellyfin" 131 + [vaultwarden]="-:/srv/vaultwarden" 132 + [bluesky - pds]="-:/srv/bluesky-pds" 133 + [samba - smbd]="-:/srv/timemachine" 134 + [grafana]="-:/var/lib/grafana" 135 + [prometheus]="-:/var/lib/prometheus2" 136 + ) 137 + 138 + # ── Main loop ───────────────────────────────────────────────────────────────── 139 + FOUND=0 140 + 141 + for unit in "${!SVC_DATA[@]}"; do 142 + label="${SVC_LABELS[$unit]}" 143 + entry="${SVC_DATA[$unit]}" 144 + 145 + # Parse "pg_db:pg_role dir1 dir2 ..." 146 + pg_part="${entry%%:*}" 147 + dirs_raw="${entry#*:}" 148 + IFS=' ' read -r -a dirs <<<"$dirs_raw" 149 + 150 + # Determine whether any data actually exists on disk 151 + data_present=false 152 + for d in "${dirs[@]}"; do 153 + [[ -d "$d" ]] && data_present=true && break 154 + done 155 + 156 + # Also check for PostgreSQL DB if applicable 157 + pg_present=false 158 + pg_db="" 159 + pg_role="" 160 + if [[ "$pg_part" != "-" ]]; then 161 + pg_db="${pg_part%%/*}" 162 + pg_role="${pg_part##*/}" 163 + # Only check if psql is available 164 + if command -v psql &>/dev/null; then 165 + exists=$(sudo -u postgres psql -tA -c \ 166 + "SELECT 1 FROM pg_database WHERE datname='${pg_db}';" 2>/dev/null || echo "") 167 + [[ "$exists" == "1" ]] && pg_present=true 168 + fi 169 + fi 170 + 171 + $data_present || $pg_present || continue 172 + 173 + FOUND=$((FOUND + 1)) 174 + section "$label" 175 + 176 + # ── Guard: skip if service is still live ────────────────────────────── 177 + if service_live "$unit"; then 178 + skip "$unit is still active/enabled — skipping." 179 + continue 180 + fi 181 + 182 + # Show what was found 183 + for d in "${dirs[@]}"; do 184 + if [[ -d "$d" ]]; then 185 + size=$(du -sh "$d" 2>/dev/null | cut -f1 || echo "?") 186 + echo -e " ${CYAN}dir${NC} $d ($size)" 187 + fi 188 + done 189 + if $pg_present; then 190 + echo -e " ${CYAN}pg${NC} database: $pg_db role: $pg_role" 191 + fi 192 + 193 + # ── Confirm and remove ──────────────────────────────────────────────── 194 + confirm "Remove all data for $label?" || { 195 + skip "Skipped." 196 + continue 197 + } 198 + 199 + for d in "${dirs[@]}"; do 200 + remove_dir "$d" 201 + done 202 + 203 + if $pg_present; then 204 + drop_pg_db "$pg_db" 205 + drop_pg_role "$pg_role" 206 + fi 207 + 208 + info "Cleaned up $label." 209 + done 210 + 211 + # ── /root key backups from migration ────────────────────────────────────────── 212 + KEYPAIR_BACKUPS=(/root/gts-keypair-*.env) 213 + if [[ -f "${KEYPAIR_BACKUPS[0]}" ]]; then 214 + section "GTS keypair migration backups" 215 + for f in "${KEYPAIR_BACKUPS[@]}"; do 216 + [[ -f "$f" ]] || continue 217 + echo -e " ${CYAN}file${NC} $f" 218 + done 219 + if confirm "Remove GTS keypair backup(s) from /root?"; then 220 + for f in "${KEYPAIR_BACKUPS[@]}"; do 221 + [[ -f "$f" ]] || continue 222 + if $DRY_RUN; then 223 + warn " DRY-RUN: would remove $f" 224 + else 225 + rm -f "$f" 226 + removed "$f" 227 + fi 228 + done 229 + else 230 + skip "Skipped." 231 + fi 232 + fi 233 + 234 + # ── GTS .nix.bak ────────────────────────────────────────────────────────────── 235 + BAK_DIR="$(dirname "$(realpath "$0")")" 236 + GTS_BAK="${BAK_DIR}/../modules/server/gotosocial.nix.bak" 237 + GTS_BAK="$(realpath --canonicalize-missing "$GTS_BAK")" 238 + if [[ -f "$GTS_BAK" ]]; then 239 + section "GTS module backup" 240 + echo -e " ${CYAN}file${NC} $GTS_BAK" 241 + if confirm "Remove gotosocial.nix.bak?"; then 242 + if $DRY_RUN; then 243 + warn " DRY-RUN: would remove $GTS_BAK" 244 + else 245 + rm -f "$GTS_BAK" 246 + removed "$GTS_BAK" 247 + fi 248 + else 249 + skip "Skipped." 250 + fi 251 + fi 252 + 253 + # ── Summary ─────────────────────────────────────────────────────────────────── 254 + echo "" 255 + if [[ "$FOUND" -eq 0 ]]; then 256 + info "No residual service data found." 257 + else 258 + info "Done." 259 + fi
+57 -41
scripts/migrate-gts-to-sharkey.sh
··· 7 7 # 2. Stopping GTS, switching to Sharkey via nixos-rebuild 8 8 # 3. Injecting the old RSA keypair into Sharkey's PostgreSQL 9 9 # 10 + # GTS schema (SQLite): 11 + # table: accounts 12 + # columns: private_key, public_key (PEM strings, local account has domain IS NULL) 13 + # 14 + # Sharkey schema (PostgreSQL, 2025.4.6): 15 + # table: user_keypair 16 + # columns: "userId" (PK, FK → user.id), "publicKey", "privateKey" (varchar 4096) 17 + # 10 18 # Run as root on the NixOS server. 11 - # Prereq: sharkey.nix written + secrets/sharkey.env encrypted + options updated. 19 + # Prereq: sharkey.nix written + secrets/sharkey.env sops-encrypted + nixos-rebuild pending. 12 20 # ============================================================================= 13 21 set -euo pipefail 14 22 ··· 42 50 command -v "$cmd" &>/dev/null || error "Missing required command: $cmd" 43 51 done 44 52 45 - # ── Step 1: Extract RSA keys ────────────────────────────────────────────────── 53 + # ── Step 1: Extract RSA keys from GTS SQLite ───────────────────────────────── 54 + # GTS bun ORM maps PrivateKey/PublicKey (*rsa.PrivateKey/*rsa.PublicKey) to 55 + # snake_case columns private_key/public_key, stored as PEM strings. 46 56 info "Extracting RSA keypair for @${GTS_USERNAME} from SQLite..." 47 57 48 58 PRIVATE_KEY=$(sqlite3 "$GTS_DB" \ ··· 50 60 PUBLIC_KEY=$(sqlite3 "$GTS_DB" \ 51 61 "SELECT public_key FROM accounts WHERE username='${GTS_USERNAME}' AND domain IS NULL LIMIT 1;") 52 62 53 - [[ -n "$PRIVATE_KEY" && -n "$PUBLIC_KEY" ]] || error "Could not extract keys — check username." 63 + [[ -n "$PRIVATE_KEY" ]] || error "private_key is empty — check GTS_USERNAME and that the account is local." 64 + [[ -n "$PUBLIC_KEY" ]] || error "public_key is empty." 54 65 55 - # Persist to a backup file in case we need to re-run step 3 66 + # Sanity-check PEM headers 67 + [[ "$PRIVATE_KEY" == *"BEGIN RSA PRIVATE KEY"* || "$PRIVATE_KEY" == *"BEGIN PRIVATE KEY"* ]] || 68 + error "private_key does not look like a PEM block." 69 + [[ "$PUBLIC_KEY" == *"BEGIN PUBLIC KEY"* ]] || 70 + error "public_key does not look like a PEM block." 71 + 56 72 { 57 73 echo "PRIVATE_KEY<<EOF" 58 74 echo "$PRIVATE_KEY" ··· 65 81 info "Keys backed up to: $KEY_BACKUP" 66 82 67 83 # ── Step 2: Stop GTS, rebuild with Sharkey ──────────────────────────────────── 68 - warn "About to stop GoToSocial. ap.ewancroft.uk will be down until Sharkey starts." 84 + warn "About to stop GoToSocial. ap.ewancroft.uk will be offline until Sharkey starts." 69 85 confirm "Continue?" || { 70 86 info "Aborted." 71 87 exit 0 ··· 75 91 systemctl stop "$GTS_SERVICE" 76 92 77 93 warn "Now run: nixos-rebuild switch --flake .#server" 78 - warn "(services.gotosocial.enable = false, services.sharkey.enable = true)" 94 + warn "(myConfig.services.sharkey.enable = true in your host config)" 79 95 read -r -p "$(echo -e "${YELLOW}Press Enter once nixos-rebuild switch completes...${NC}")" 80 96 81 97 systemctl is-active --quiet sharkey || error "Sharkey is not running. Check: journalctl -u sharkey -n 50" 82 98 systemctl is-active --quiet postgresql || error "PostgreSQL is not running." 83 99 info "Sharkey + PostgreSQL are up." 84 100 85 - # ── Step 3: Create account in Sharkey ───────────────────────────────────────── 86 - warn "Create the @${GTS_USERNAME} account in the Sharkey admin panel now." 87 - warn " https://${AP_HOSTNAME} -> Admin -> Users -> Create" 88 - warn "Username must be: ${GTS_USERNAME}" 101 + # ── Step 3: Create the local account in Sharkey ─────────────────────────────── 102 + warn "Create @${GTS_USERNAME} in the Sharkey setup wizard or admin panel:" 103 + warn " https://${AP_HOSTNAME} (first run triggers the setup wizard)" 104 + warn " Username must be exactly: ${GTS_USERNAME}" 89 105 read -r -p "$(echo -e "${YELLOW}Press Enter once the account exists...${NC}")" 90 106 91 107 SHARKEY_USER_ID=$(sudo -u postgres psql -d "$SHARKEY_DB" -tA \ 92 108 -c "SELECT id FROM \"user\" WHERE username='${GTS_USERNAME}' AND host IS NULL LIMIT 1;" 2>/dev/null || true) 109 + SHARKEY_USER_ID="${SHARKEY_USER_ID// /}" # trim whitespace psql may add 93 110 94 - [[ -n "$SHARKEY_USER_ID" ]] || error "User @${GTS_USERNAME} not found in Sharkey DB. Create the account first." 111 + [[ -n "$SHARKEY_USER_ID" ]] || error "@${GTS_USERNAME} not found in Sharkey DB — create the account first." 95 112 info "Sharkey user ID: ${SHARKEY_USER_ID}" 96 113 97 - # ── Step 4: Inject old RSA keypair ──────────────────────────────────────────── 98 - info "Injecting GTS RSA keypair into Sharkey..." 114 + # ── Step 4: Inject old RSA keypair into user_keypair ───────────────────────── 115 + # Sharkey 2025.4.6 always has the user_keypair table (UserKeypair.ts entity). 116 + # Columns: "userId" (PK), "publicKey" varchar(4096), "privateKey" varchar(4096). 117 + # Dollar-quoting ($pem$...$pem$) handles PEM newlines safely without escaping. 118 + info "Injecting GTS RSA keypair into Sharkey's user_keypair table..." 99 119 100 - HAS_KEYPAIR_TABLE=$(sudo -u postgres psql -d "$SHARKEY_DB" -tA \ 101 - -c "SELECT to_regclass('public.user_keypair');" 2>/dev/null || echo "") 120 + sudo -u postgres psql -d "$SHARKEY_DB" -c \ 121 + "INSERT INTO user_keypair (\"userId\", \"publicKey\", \"privateKey\") 122 + VALUES ( 123 + '${SHARKEY_USER_ID}', 124 + \$pem\$${PUBLIC_KEY}\$pem\$, 125 + \$pem\$${PRIVATE_KEY}\$pem\$ 126 + ) 127 + ON CONFLICT (\"userId\") DO UPDATE 128 + SET \"publicKey\" = EXCLUDED.\"publicKey\", 129 + \"privateKey\" = EXCLUDED.\"privateKey\";" 102 130 103 - if [[ "$HAS_KEYPAIR_TABLE" == "user_keypair" ]]; then 104 - sudo -u postgres psql -d "$SHARKEY_DB" -c \ 105 - "INSERT INTO user_keypair (\"userId\", \"publicKey\", \"privateKey\") 106 - VALUES ('${SHARKEY_USER_ID}', \$pem\$${PUBLIC_KEY}\$pem\$, \$pem\$${PRIVATE_KEY}\$pem\$) 107 - ON CONFLICT (\"userId\") DO UPDATE 108 - SET \"publicKey\" = EXCLUDED.\"publicKey\", 109 - \"privateKey\" = EXCLUDED.\"privateKey\";" 110 - info "Updated user_keypair table." 111 - else 112 - # Older schema — keys inline on user table 113 - sudo -u postgres psql -d "$SHARKEY_DB" -c \ 114 - "UPDATE \"user\" 115 - SET \"publicKey\" = \$pem\$${PUBLIC_KEY}\$pem\$, 116 - \"privateKey\" = \$pem\$${PRIVATE_KEY}\$pem\$ 117 - WHERE id = '${SHARKEY_USER_ID}';" 118 - info "Updated user table (inline key columns)." 119 - fi 131 + info "user_keypair updated." 120 132 121 - info "Restarting Sharkey..." 133 + info "Restarting Sharkey to pick up the new keypair..." 122 134 systemctl restart sharkey 123 135 sleep 5 124 - systemctl is-active --quiet sharkey || error "Sharkey failed to restart." 136 + systemctl is-active --quiet sharkey || error "Sharkey failed to restart. Check: journalctl -u sharkey -n 50" 125 137 126 138 # ── Step 5: Verify ──────────────────────────────────────────────────────────── 127 139 info "Verifying WebFinger..." 128 - WF=$(curl -fsSL "https://${ACCOUNT_DOMAIN}/.well-known/webfinger?resource=acct:${GTS_USERNAME}@${ACCOUNT_DOMAIN}" 2>/dev/null || true) 140 + WF=$(curl -fsSL \ 141 + "https://${ACCOUNT_DOMAIN}/.well-known/webfinger?resource=acct:${GTS_USERNAME}@${ACCOUNT_DOMAIN}" \ 142 + 2>/dev/null || true) 129 143 if echo "$WF" | jq -e '.subject' &>/dev/null; then 130 144 info "WebFinger OK: $(echo "$WF" | jq -r '.subject')" 131 145 else 132 - warn "WebFinger returned unexpected result — check your Vercel redirect." 146 + warn "WebFinger probe failed — check the Vercel redirect at ewancroft.uk." 133 147 fi 134 148 135 149 info "Verifying actor public key..." 136 - ACTOR=$(curl -fsSL -H 'Accept: application/activity+json' "https://${AP_HOSTNAME}/users/${GTS_USERNAME}" 2>/dev/null || true) 150 + ACTOR=$(curl -fsSL -H 'Accept: application/activity+json' \ 151 + "https://${AP_HOSTNAME}/users/${GTS_USERNAME}" 2>/dev/null || true) 137 152 if echo "$ACTOR" | jq -e '.publicKey.publicKeyPem' &>/dev/null; then 138 153 ACTOR_KEY=$(echo "$ACTOR" | jq -r '.publicKey.publicKeyPem') 139 154 if [[ "$ACTOR_KEY" == "$PUBLIC_KEY" ]]; then 140 - info "Actor public key matches GTS original. Identity preserved." 155 + info "Actor public key matches GTS original. ✓ Identity preserved." 141 156 else 142 - warn "Public key mismatch — Sharkey may not have reloaded yet. Try: systemctl restart sharkey" 157 + warn "Public key mismatch — Sharkey may be caching its generated key." 158 + warn "Try: systemctl restart sharkey and re-run the verify block." 143 159 fi 144 160 else 145 - warn "Could not retrieve actor JSON — Sharkey may still be starting up." 161 + warn "Could not fetch actor JSON — Sharkey may still be starting up." 146 162 fi 147 163 148 164 echo "" 149 - info "Done. Key backup retained at: ${KEY_BACKUP}" 165 + info "Done. Key backup at: ${KEY_BACKUP}"