# Backup and restore (owner decisions 2026-10-07), on the live pasture and through the administrator's endpoints: the # server is backed up, then life goes on: alice_restore deletes a post and makes another, mastouser follows her, she # blocks bob_restore, and carol_restore is made. The backup is restored: PrivaPub stops, restores it as it starts again, # and nothing protective is undone: the deleted post stays gone, the post made since answers as deleted, mastouser still # follows her, the block holds and carol's name stays taken. Every session ends, so the scenario signs in again, and at # the end the town's PrivaPub accounts too (town.sh renew privapub). Run it alone: it restores the whole server. Needs # the mastodon peer. M=https://mastodon.test:6443 mcurl() { curl -sk --resolve mastodon.test:6443:127.0.0.1 "$@"; } . "$here/peers/mastodon.sh" m_rails() { podman exec pasture-mastodon bin/rails runner "$1" 2>/dev/null | tail -1; } p_mongo() { podman exec pasture-mongo mongosh --quiet PrivaPub --eval "$1"; } echo "restore" podman exec -w /app pasture-privapub /app/PrivaPub admin promote "$ROOT_USER" >/dev/null 2>&1 || true JWT=$(privapub_root) RH="Authorization: Bearer $JWT" AT=$(privapub_token alice_restore) BT=$(privapub_token bob_restore) AH="Authorization: Bearer $AT" [ -n "$AT" ] && [ -n "$BT" ] && ok "PrivaPub tokens for alice_restore and bob_restore" || { ko "PrivaPub tokens for alice_restore and bob_restore"; return 1; } run=$(date +%s) alice=$(curl -s -H "$AH" "$P/api/v1/accounts/verify_credentials") alice_id=$(echo "$alice" | j "print(d['id'])") alice_uri=$(echo "$alice" | j "print(d['url'])" | sed 's|/@|/peasants/|') bob_id=$(curl -s -H "Authorization: Bearer $BT" "$P/api/v1/accounts/verify_credentials" | j "print(d['id'])") regret=$(curl -s -X POST -H "$AH" "$P/api/v1/statuses" -d "status=a post regretted later $run&visibility=public") regret_id=$(echo "$regret" | j "print(d['id'])"); regret_uri=$(echo "$regret" | j "print(d['uri'])") # mastouser follows nobody here yet: the follow comes after the backup m_follows() { m_rails "puts Follow.exists?(account: Account.find_local(\"mastouser\"), target_account: Account.find_by(uri: \"$alice_uri\")) ? \"True\" : \"False\""; } alice_on_m=$(mcurl -H "Authorization: Bearer $(mastodon_token)" "$M/api/v2/search?q=@alice_restore@privapub.test&resolve=true&type=accounts" | j "print(d['accounts'][0]['id'])") if [ "$(m_follows)" = "True" ]; then mcurl -o /dev/null -X POST -H "Authorization: Bearer $(mastodon_token)" "$M/api/v1/accounts/$alice_on_m/unfollow" until_true 30 '[ "$(m_follows)" = "False" ]' fi curl -s -o /dev/null -X POST -H "$AH" "$P/api/v1/accounts/$bob_id/unblock" backups() { curl -s -H "$RH" "$P/clientapi/admin/backups"; } before=$(backups | j "print(' '.join(b['id'] for b in d['backups']))") [ "$(curl -s -o /dev/null -w '%{http_code}' -X POST -H "$RH" "$P/clientapi/admin/backups")" = "202" ] && ok "a backup is asked for" || ko "the backup was refused" backup="" newest() { backups | j " ids=[b['id'] for b in d['backups'] if b['id'] not in '$before'.split()] print(ids[0] if ids and d.get('running') is None else '')"; } until_true 300 '[ -n "$(newest)" ]' && backup=$(newest) [ -n "$backup" ] && ok "backed up as $backup" || { ko "the backup was never made"; return 1; } echo " life goes on" curl -s -o /dev/null -X DELETE -H "$AH" "$P/api/v1/statuses/$regret_id" since=$(curl -s -X POST -H "$AH" "$P/api/v1/statuses" -d "status=made after the backup $run&visibility=public") since_uri=$(echo "$since" | j "print(d['uri'])") mcurl -o /dev/null -X POST -H "Authorization: Bearer $(mastodon_token)" "$M/api/v1/accounts/$alice_on_m/follow" until_true 45 '[ "$(m_follows)" = "True" ]' && ok "mastouser follows alice after the backup" || ko "mastouser never followed alice" until_true 30 '[ "$(curl -s -H "$AH" "$P/api/v1/accounts/$alice_id/followers" | j "print(any(a[\"acct\"]==\"mastouser@mastodon.test\" for a in d))")" = "True" ]' \ && ok "PrivaPub has mastouser following alice" || ko "PrivaPub never had the follower" curl -s -o /dev/null -X POST -H "$AH" "$P/api/v1/accounts/$bob_id/block" carol="carol_restore_$run" curl -s -o /dev/null -X POST -H "$RH" -H 'Content-Type: application/json' "$P/clientapi/avatar/private/insert" \ -d "{\"userName\":\"$carol\",\"name\":\"carol\",\"biography\":\"made after the backup\"}" echo " restored" [ "$(curl -s -o /dev/null -w '%{http_code}' -X POST -H "$RH" -H 'Content-Type: application/json' "$P/clientapi/admin/backups/$backup/restore" \ -d "{\"password\":\"$ROOT_PASS\",\"host\":\"elsewhere.test\"}")" = "422" ] && ok "a restore with the wrong host is refused" || ko "a restore with the wrong host was taken" [ "$(curl -s -o /dev/null -w '%{http_code}' -X POST -H "$RH" -H 'Content-Type: application/json' "$P/clientapi/admin/backups/$backup/restore" \ -d "{\"password\":\"$ROOT_PASS\",\"host\":\"privapub.test\"}")" = "202" ] && ok "the restore is asked for" || { ko "the restore was refused"; return 1; } # it stops within seconds, restores as it starts again, and every session ends: the old token is refused once it is back until_true 300 '[ "$(curl -s -o /dev/null -w "%{http_code}" -H "$RH" "$P/clientapi/admin/backups")" = "401" ]' \ && ok "PrivaPub came back, and the administrator's session ended" || { ko "PrivaPub never came back from the restore"; podman logs --tail 30 pasture-privapub; return 1; } JWT=$(privapub_root); RH="Authorization: Bearer $JWT" [ "$(backups | j "print(d['lastRestore']['backup'])")" = "$backup" ] && ok "the last restore is $backup" || ko "the last restore is not $backup" [ "$(curl -s -o /dev/null -w '%{http_code}' -H "$AH" "$P/api/v1/accounts/verify_credentials")" = "401" ] && ok "alice's old token is refused" || ko "alice's old token still works" AT=$(privapub_token alice_restore); AH="Authorization: Bearer $AT" [ "$(curl -s -o /dev/null -w '%{http_code}' -H "$AH" "$P/api/v1/statuses/$regret_id")" = "404" ] && ok "the post deleted after the backup stays deleted" || ko "the deleted post came back" gone_unsigned "$regret_uri" && ok "its address answers as gone" || ko "its address answers $(pstatus "$regret_uri")" gone_unsigned "$since_uri" && ok "the post made after the backup answers as gone" || ko "the post made after the backup answers $(pstatus "$since_uri")" [ "$(curl -s -H "$AH" "$P/api/v1/accounts/$alice_id/followers" | j "print(any(a['acct']=='mastouser@mastodon.test' for a in d))")" = "True" ] \ && ok "mastouser still follows alice" || ko "the follower gained after the backup was lost" [ "$(m_follows)" = "True" ] && ok "Mastodon still has the follow" || ko "Mastodon lost the follow" [ "$(curl -s -H "$AH" "$P/api/v1/accounts/relationships?id[]=$bob_id" | j "print(d[0]['blocking'])")" = "True" ] \ && ok "alice still blocks bob_restore" || ko "the block made after the backup was undone" [ "$(curl -s -o /dev/null -w '%{http_code}' -X POST -H "$RH" -H 'Content-Type: application/json' "$P/clientapi/avatar/private/insert" \ -d "{\"userName\":\"$carol\",\"name\":\"carol\",\"biography\":\"again\"}")" != "200" ] && ok "carol's name stays taken" || ko "carol's name was free again" status=$(podman exec -w /app pasture-privapub /app/PrivaPub admin restore --status 2>/dev/null | grep -o "personas [^,;]*" | head -1) echo "$status" | grep -q "$carol" && ok "the report names carol among the personas made since" || ko "the report: $status" # the town signs in again; the record goes, so the followers' digests (FEP-8fcf, followsync) are not resting for two weeks "$here/town.sh" renew privapub >/dev/null && ok "the town's PrivaPub accounts signed in again" || ko "the town could not sign in again" p_mongo "db.RestoreRecord.deleteMany({})" >/dev/null