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

163 lines
7.1 KiB
Bash
Executable File

#!/usr/bin/env bash
# scripts/backup.sh — snapshot a single InboxOS service (REQ-INB-22).
#
# Usage:
# scripts/backup.sh <service> # service: nocodb | seaweedfs | gitea
#
# Snapshots one of the three stateful services to a timestamped
# directory under backups/<service>/<utc-timestamp>/. The script is
# idempotent: a re-run for the same service at the same UTC
# timestamp is a no-op.
#
# Per-service:
# nocodb -> backs up via the NocoDB REST API. The output is
# a JSON dump of every base / table / row in the
# NOCODB_PROJECT_ID.
# seaweedfs -> mirrors the inboxos bucket to a second bucket on
# a different host. Requires SEAWEEDFS_S3_BACKUP_BUCKET
# and SEAWEEDFS_S3_BACKUP_ENDPOINT env vars to be
# set; exits 4 with a clear message if either is unset.
# gitea -> writes a `git bundle` of the jc-capital/inboxos
# repo to backups/gitea/<ts>/inboxos.bundle.
#
# The script prints a one-line summary on success:
# backup_id=<utc-timestamp> service=<svc> bytes=<int> target=<path>
#
# Environment:
# NOCODB_BASE_URL, NOCODB_API_TOKEN, NOCODB_PROJECT_ID (nocodb)
# SEAWEEDFS_S3_ENDPOINT, SEAWEEDFS_S3_ACCESS_KEY,
# SEAWEEDFS_S3_SECRET_KEY, SEAWEEDFS_S3_BUCKET (seaweedfs source)
# SEAWEEDFS_S3_BACKUP_BUCKET, SEAWEEDFS_S3_BACKUP_ENDPOINT (seaweedfs target)
# GITEA_INSTANCE_URL, GITEA_USER, GITEA_PASS (gitea)
#
# Exit codes:
# 0 success
# 2 missing service / missing env
# 3 Gitea API / NocoDB API call failed
# 4 backup target not configured (seaweedfs)
# 5 helper internal error
set -euo pipefail
SERVICE="${1:-}"
if [[ -z "${SERVICE}" ]]; then
echo "Usage: $0 <service> # service: nocodb | seaweedfs | gitea" >&2
exit 2
fi
# UTC timestamp in the form 2026-06-08T120000Z (filename-safe).
TS="$(date -u +%Y-%m-%dT%H%M%SZ)"
BACKUP_ROOT="backups/${SERVICE}/${TS}"
# ── 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 backups." >&2
exit 2
fi
mkdir -p "${BACKUP_ROOT}"
# Use the NocoDB REST API to export the base. The v2 export
# endpoint produces a JSON dump of every table in the base.
EXPORT_URL="${NOCODB_BASE_URL%/}/api/v2/meta/bases/${NOCODB_PROJECT_ID}/export"
HTTP_CODE=$(
curl -sS -o "${BACKUP_ROOT}/nocodb-export.json" -w "%{http_code}" \
-H "xc-token: ${NOCODB_API_TOKEN}" \
-X POST "${EXPORT_URL}" 2>/dev/null \
|| echo "000"
)
if [[ "${HTTP_CODE}" != "200" && "${HTTP_CODE}" != "201" && "${HTTP_CODE}" != "202" ]]; then
echo "NocoDB export failed: HTTP ${HTTP_CODE}" >&2
exit 3
fi
BYTES=$(wc -c < "${BACKUP_ROOT}/nocodb-export.json" | tr -d ' ')
echo "backup_id=${TS} service=nocodb bytes=${BYTES} target=${BACKUP_ROOT}/nocodb-export.json"
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. Configure the backup" >&2
echo "bucket + endpoint in Gitea repo variables, then re-run." >&2
exit 4
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 backups." >&2
exit 2
fi
mkdir -p "${BACKUP_ROOT}"
# Mirror the inboxos bucket to the backup bucket via the AWS CLI
# (or fall back to a python3 + boto3 loop if awscli is missing).
if command -v aws >/dev/null 2>&1; then
aws --endpoint-url "${SEAWEEDFS_S3_BACKUP_ENDPOINT}" \
s3 sync "s3://${SEAWEEDFS_S3_BUCKET}" "s3://${SEAWEEDFS_S3_BACKUP_BUCKET}" \
--quiet
echo "aws sync complete"
else
# boto3 fallback: list source keys and copy each to the
# backup bucket. The script does not remove source keys.
python3 - <<'PY'
import os, sys
import boto3
src = 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",
)
dst = 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",
)
src_bucket = os.environ["SEAWEEDFS_S3_BUCKET"]
dst_bucket = os.environ["SEAWEEDFS_S3_BACKUP_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
BYTES=$(du -sb "${BACKUP_ROOT}" 2>/dev/null | awk '{print $1}')
echo "backup_id=${TS} service=seaweedfs bytes=${BYTES:-0} target=${BACKUP_ROOT}/"
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 backups." >&2
exit 2
fi
mkdir -p "${BACKUP_ROOT}"
# `git bundle` of the jc-capital/inboxos repo. The bundle is a
# single file that captures the full history; restore.sh clones
# the bundle to a temp dir and pushes via the Gitea REST API.
BUNDLE_PATH="${BACKUP_ROOT}/inboxos.bundle"
if ! git bundle create "${BUNDLE_PATH}" --all 2>/dev/null; then
# Fall back: clone the repo from the Gitea REST API and
# bundle the result.
TMP_DIR="$(mktemp -d)"
git -c credential.helper= \
-c credential.username="${GITEA_USER}" \
-c credential.password="${GITEA_PASS}" \
clone "${GITEA_INSTANCE_URL%/}/jc-capital/inboxos.git" "${TMP_DIR}/inboxos" 2>&1 \
|| { echo "Gitea clone failed" >&2; rm -rf "${TMP_DIR}"; exit 3; }
(cd "${TMP_DIR}/inboxos" && git bundle create "${BUNDLE_PATH}" --all)
rm -rf "${TMP_DIR}"
fi
BYTES=$(wc -c < "${BUNDLE_PATH}" | tr -d ' ')
echo "backup_id=${TS} service=gitea bytes=${BYTES} target=${BUNDLE_PATH}"
exit 0
fi
echo "Unknown service: ${SERVICE}. Valid services: nocodb, seaweedfs, gitea." >&2
exit 2