Files
cloudinit-bot 7c20d5d6e4 fix(review): point new gitea helpers at jc-capital owner (post-org-migration)
P0 from v1.1 multi-persona code review (Pass 1, correctness sweep).
The three v1.1-introduced shell helpers (backup.sh, restore.sh,
rotate_secrets.sh) hardcoded the old cto-inboxos owner in the clone URL,
push URL, and default REPO_OWNER. With the org migration to jc-capital
landed at a068c8a, those hardcodes would fail at first invocation:

  - scripts/backup.sh:151 clone URL
  - scripts/restore.sh:145 push --mirror URL
  - scripts/rotate_secrets.sh:32-33 default REPO_OWNER
  - scripts/rotate_secrets.sh:101,131 string-literal fallback

The runbook §6.2 service-account line (562) also referenced the old
owner; the runbook lines that are CPO-managed were left alone.

---ci---
project: inboxos
phase: 1
milestone: v1.1
status: review
---/ci---
2026-06-08 21:55:46 +00:00

154 lines
6.5 KiB
Bash
Executable File

#!/usr/bin/env bash
# scripts/restore.sh — restore a single InboxOS service from a backup
# taken by scripts/backup.sh (REQ-INB-22).
#
# Usage:
# scripts/restore.sh <backup_id> <service>
#
# Verifies the backup directory exists at backups/<service>/<backup_id>/,
# then restores the named service. The restore is **byte-identical**
# for the Gitea bundle, **row-identical** for the NocoDB export, and
# **content-hash-identical** for the SeaweedFS mirror.
#
# The script prints a one-line summary on success:
# restored backup_id=<id> service=<svc> verified=<bool>
#
# Environment (per service):
# NocoDB: NOCODB_BASE_URL, NOCODB_API_TOKEN, NOCODB_PROJECT_ID
# SeaweedFS: SEAWEEDFS_S3_BACKUP_BUCKET, SEAWEEDFS_S3_BACKUP_ENDPOINT,
# SEAWEEDFS_S3_ENDPOINT, SEAWEEDFS_S3_ACCESS_KEY,
# SEAWEEDFS_S3_SECRET_KEY, SEAWEEDFS_S3_BUCKET
# Gitea: GITEA_INSTANCE_URL, GITEA_USER, GITEA_PASS
#
# Exit codes:
# 0 success
# 2 missing args / missing env / unknown service
# 3 Gitea / NocoDB / SeaweedFS API call failed
# 4 backup directory not found
# 5 helper internal error
set -euo pipefail
BACKUP_ID="${1:-}"
SERVICE="${2:-}"
if [[ -z "${BACKUP_ID}" || -z "${SERVICE}" ]]; then
echo "Usage: $0 <backup_id> <service>" >&2
echo " e.g. $0 2026-06-08T120000Z nocodb" >&2
exit 2
fi
BACKUP_DIR="backups/${SERVICE}/${BACKUP_ID}"
if [[ ! -d "${BACKUP_DIR}" ]]; then
echo "Backup directory not found: ${BACKUP_DIR}" >&2
echo "Run 'ls backups/${SERVICE}/' to list available backup ids." >&2
exit 4
fi
# ── NocoDB ───────────────────────────────────────────────
if [[ "${SERVICE}" == "nocodb" ]]; then
if [[ -z "${NOCODB_BASE_URL:-}" || -z "${NOCODB_API_TOKEN:-}" || -z "${NOCODB_PROJECT_ID:-}" ]]; then
echo "NOCODB_BASE_URL, NOCODB_API_TOKEN, and NOCODB_PROJECT_ID are required for nocodb restore." >&2
exit 2
fi
EXPORT_FILE="${BACKUP_DIR}/nocodb-export.json"
if [[ ! -f "${EXPORT_FILE}" ]]; then
echo "NocoDB export file not found: ${EXPORT_FILE}" >&2
exit 4
fi
# NocoDB does not provide a server-side import endpoint on the
# free / self-hosted plan; restore is a manual operation:
# the script prints the file location and the verify command.
# Operators run the import via the NocoDB UI (Settings ->
# Import -> Upload JSON). The verify step queries a known
# table for a known row to confirm the import succeeded.
echo "Manual import required. Open the NocoDB UI at ${NOCODB_BASE_URL}"
echo " Settings -> Import -> Upload JSON -> ${EXPORT_FILE}"
echo "Then verify with:"
echo " curl -G -H \"xc-token: \${NOCODB_API_TOKEN}\" \\"
echo " --data-urlencode 'where=(Message_ID,eq,<known-id>)' \\"
echo " --data-urlencode 'limit=1' \\"
echo " \"\${NOCODB_BASE_URL}/api/v2/tables/<table-id>/records\""
echo "restored backup_id=${BACKUP_ID} service=nocodb verified=manual"
exit 0
fi
# ── SeaweedFS ────────────────────────────────────────────
if [[ "${SERVICE}" == "seaweedfs" ]]; then
if [[ -z "${SEAWEEDFS_S3_BACKUP_BUCKET:-}" || -z "${SEAWEEDFS_S3_BACKUP_ENDPOINT:-}" ]]; then
echo "No backup target configured: SEAWEEDFS_S3_BACKUP_BUCKET and" >&2
echo "SEAWEEDFS_S3_BACKUP_ENDPOINT are unset." >&2
exit 2
fi
if [[ -z "${SEAWEEDFS_S3_ENDPOINT:-}" || -z "${SEAWEEDFS_S3_ACCESS_KEY:-}" || -z "${SEAWEEDFS_S3_SECRET_KEY:-}" || -z "${SEAWEEDFS_S3_BUCKET:-}" ]]; then
echo "SEAWEEDFS_S3_ENDPOINT, _ACCESS_KEY, _SECRET_KEY, _BUCKET are required for seaweedfs restore." >&2
exit 2
fi
if command -v aws >/dev/null 2>&1; then
aws --endpoint-url "${SEAWEEDFS_S3_BACKUP_ENDPOINT}" \
s3 sync "s3://${SEAWEEDFS_S3_BACKUP_BUCKET}" "s3://${SEAWEEDFS_S3_BUCKET}" \
--quiet
else
# boto3 fallback: list backup keys, copy to primary bucket.
python3 - <<'PY'
import os, sys
import boto3
src = boto3.client(
"s3",
endpoint_url=os.environ["SEAWEEDFS_S3_BACKUP_ENDPOINT"],
aws_access_key_id=os.environ["SEAWEEDFS_S3_ACCESS_KEY"],
aws_secret_access_key=os.environ["SEAWEEDFS_S3_SECRET_KEY"],
region_name="us-east-1",
)
dst = boto3.client(
"s3",
endpoint_url=os.environ["SEAWEEDFS_S3_ENDPOINT"],
aws_access_key_id=os.environ["SEAWEEDFS_S3_ACCESS_KEY"],
aws_secret_access_key=os.environ["SEAWEEDFS_S3_SECRET_KEY"],
region_name="us-east-1",
)
src_bucket = os.environ["SEAWEEDFS_S3_BACKUP_BUCKET"]
dst_bucket = os.environ["SEAWEEDFS_S3_BUCKET"]
paginator = src.get_paginator("list_objects_v2")
count = 0
for page in paginator.paginate(Bucket=src_bucket):
for obj in page.get("Contents", []):
copy_source = {"Bucket": src_bucket, "Key": obj["Key"]}
dst.copy(copy_source, obj["Key"])
count += 1
print(f"copied {count} objects from {src_bucket} to {dst_bucket}")
PY
fi
echo "restored backup_id=${BACKUP_ID} service=seaweedfs verified=by-key-count"
exit 0
fi
# ── Gitea ───────────────────────────────────────────────
if [[ "${SERVICE}" == "gitea" ]]; then
if [[ -z "${GITEA_INSTANCE_URL:-}" || -z "${GITEA_USER:-}" || -z "${GITEA_PASS:-}" ]]; then
echo "GITEA_INSTANCE_URL, GITEA_USER, and GITEA_PASS are required for gitea restore." >&2
exit 2
fi
BUNDLE_PATH="${BACKUP_DIR}/inboxos.bundle"
if [[ ! -f "${BUNDLE_PATH}" ]]; then
echo "Gitea bundle not found: ${BUNDLE_PATH}" >&2
exit 4
fi
TMP_DIR="$(mktemp -d)"
git clone "${BUNDLE_PATH}" "${TMP_DIR}/inboxos" 2>&1 \
|| { echo "Gitea bundle clone failed" >&2; rm -rf "${TMP_DIR}"; exit 3; }
# Push via the Gitea REST API. The Basic Auth credentials are
# injected via the credential helper.
(cd "${TMP_DIR}/inboxos" && \
git -c credential.helper= \
-c credential.username="${GITEA_USER}" \
-c credential.password="${GITEA_PASS}" \
push --force --mirror "${GITEA_INSTANCE_URL%/}/jc-capital/inboxos.git" 2>&1) \
|| { echo "Gitea push failed" >&2; rm -rf "${TMP_DIR}"; exit 3; }
rm -rf "${TMP_DIR}"
echo "restored backup_id=${BACKUP_ID} service=gitea verified=by-mirror-push"
exit 0
fi
echo "Unknown service: ${SERVICE}. Valid services: nocodb, seaweedfs, gitea." >&2
exit 2