Created
May 9, 2026 00:06
-
-
Save motatoes/192624d19d147105d32b457c007b9cfa to your computer and use it in GitHub Desktop.
opencomputer recovery script
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # ---------------------------------------------------------------------------- | |
| # OpenComputer workspace recovery | |
| # | |
| # Restores a recovered /home/sandbox tarball into one of your sandboxes. | |
| # | |
| # Prerequisites (on the machine running this script): | |
| # - oc CLI installed and authenticated (oc config set api-key <your-key>) | |
| # - curl, jq, shasum (or sha256sum) | |
| # | |
| # Usage: | |
| # ./recover-workspace.sh <path-to-tarball.tar.zst> [sandbox-id] | |
| # | |
| # If [sandbox-id] is omitted, a fresh 2 vCPU / 8 GB sandbox is created. | |
| # If provided, recovery extracts INTO that existing sandbox's /home/sandbox. | |
| # | |
| # Optional env vars: | |
| # EXPECTED_SHA256 If set, the script will refuse to upload unless the | |
| # tarball's local SHA matches this value. | |
| # API_URL Override (defaults to value in ~/.oc/config.json). | |
| # API_KEY Override (defaults to value in ~/.oc/config.json). | |
| # FORCE Set to 1 to extract even if /home/sandbox is non-empty. | |
| # | |
| # What it does: | |
| # 1. Verifies the tarball locally (SHA256 if EXPECTED_SHA256 is set). | |
| # 2. Creates a fresh sandbox (or uses the one you passed). | |
| # 3. PUTs the tarball into the sandbox via the SDK files endpoint. | |
| # 4. Re-verifies SHA256 inside the sandbox post-upload. | |
| # 5. Installs zstd via apt (if missing), extracts with --numeric-owner | |
| # preserved, ACLs/xattrs preserved, then chowns to the sandbox user. | |
| # 6. Removes the temp tarball, prints a summary. | |
| # ---------------------------------------------------------------------------- | |
| set -euo pipefail | |
| # ---- args ---- | |
| TARBALL="${1:-}" | |
| TARGET_SB="${2:-}" | |
| if [ -z "$TARBALL" ]; then | |
| echo "usage: $0 <tarball.tar.zst> [sandbox-id]" >&2 | |
| exit 64 | |
| fi | |
| # ---- prereqs ---- | |
| for bin in oc curl jq; do | |
| command -v "$bin" >/dev/null 2>&1 || { echo "ERROR: '$bin' not in PATH" >&2; exit 1; } | |
| done | |
| [ -f "$TARBALL" ] || { echo "ERROR: tarball not found: $TARBALL" >&2; exit 1; } | |
| # ---- pick a sha256 tool ---- | |
| if command -v sha256sum >/dev/null 2>&1; then | |
| sha256() { sha256sum "$1" | awk '{print $1}'; } | |
| elif command -v shasum >/dev/null 2>&1; then | |
| sha256() { shasum -a 256 "$1" | awk '{print $1}'; } | |
| else | |
| echo "ERROR: need sha256sum or shasum in PATH" >&2; exit 1 | |
| fi | |
| # ---- resolve API URL + key ---- | |
| CONFIG="${HOME}/.oc/config.json" | |
| API_URL="${API_URL:-}" | |
| API_KEY="${API_KEY:-}" | |
| if [ -z "$API_URL" ] && [ -f "$CONFIG" ]; then | |
| API_URL=$(jq -r '.api_url // empty' "$CONFIG") | |
| fi | |
| if [ -z "$API_KEY" ] && [ -f "$CONFIG" ]; then | |
| API_KEY=$(jq -r '.api_key // empty' "$CONFIG") | |
| fi | |
| API_URL="${API_URL:-https://app.opencomputer.dev}" | |
| [ -n "$API_KEY" ] || { echo "ERROR: no api_key found. Run 'oc config set api-key <key>' or export API_KEY=..." >&2; exit 1; } | |
| API_BASE="${API_URL%/}/api" | |
| # ---- preflight: tarball SHA ---- | |
| echo "Tarball: $TARBALL ($(ls -la "$TARBALL" | awk '{print $5}') bytes)" | |
| LOCAL_SHA=$(sha256 "$TARBALL") | |
| echo "SHA256: $LOCAL_SHA" | |
| if [ -n "${EXPECTED_SHA256:-}" ]; then | |
| if [ "$LOCAL_SHA" != "$EXPECTED_SHA256" ]; then | |
| echo "ERROR: SHA256 mismatch — refusing to upload." >&2 | |
| echo " expected: $EXPECTED_SHA256" >&2 | |
| echo " got: $LOCAL_SHA" >&2 | |
| exit 2 | |
| fi | |
| echo " ✓ matches EXPECTED_SHA256" | |
| fi | |
| # ---- create or reuse sandbox ---- | |
| if [ -z "$TARGET_SB" ]; then | |
| echo | |
| echo "Creating fresh sandbox (2 vCPU / 8192 MB / timeout 7200s)..." | |
| TARGET_SB=$(oc sandbox create --cpu 2 --memory 8192 --timeout 7200 \ | |
| --metadata purpose=workspace-recovery --json | jq -r '.sandboxID') | |
| echo "Created: $TARGET_SB" | |
| sleep 3 | |
| else | |
| echo "Using existing sandbox: $TARGET_SB" | |
| fi | |
| # ---- safety: refuse to extract over non-empty workspace unless FORCE=1 ---- | |
| if [ "${FORCE:-0}" != "1" ]; then | |
| EXISTING=$(oc exec "$TARGET_SB" --wait --timeout 30 -- bash -c \ | |
| 'find /home/sandbox -mindepth 1 -maxdepth 1 ! -name lost+found 2>/dev/null | head -5' 2>/dev/null || true) | |
| if [ -n "$EXISTING" ]; then | |
| echo "ERROR: /home/sandbox in $TARGET_SB is not empty:" >&2 | |
| echo "$EXISTING" | sed 's/^/ /' >&2 | |
| echo "Re-run with FORCE=1 to extract on top of existing files." >&2 | |
| exit 3 | |
| fi | |
| fi | |
| # ---- upload via SDK files PUT endpoint ---- | |
| REMOTE_TARBALL="/home/sandbox/.recovery.tar.zst" | |
| ENC_PATH=$(printf %s "$REMOTE_TARBALL" | jq -sRr @uri) | |
| echo | |
| echo "Uploading $(basename "$TARBALL") → $TARGET_SB:$REMOTE_TARBALL ..." | |
| START=$(date +%s) | |
| HTTP_STATUS=$(curl -fsS -X PUT \ | |
| "$API_BASE/sandboxes/$TARGET_SB/files?path=$ENC_PATH" \ | |
| -H "X-API-Key: $API_KEY" \ | |
| -H "Content-Type: application/octet-stream" \ | |
| --data-binary @"$TARBALL" \ | |
| -w '%{http_code}' \ | |
| -o /dev/null) || { echo "ERROR: curl failed (network?)" >&2; exit 4; } | |
| ELAPSED=$(( $(date +%s) - START )) | |
| echo " HTTP $HTTP_STATUS in ${ELAPSED}s" | |
| [ "$HTTP_STATUS" = "204" ] || { echo "ERROR: upload failed (HTTP $HTTP_STATUS)" >&2; exit 4; } | |
| # ---- verify SHA inside sandbox ---- | |
| echo "Verifying SHA256 inside the sandbox..." | |
| REMOTE_SHA=$(oc exec "$TARGET_SB" --wait --timeout 120 -- \ | |
| sha256sum "$REMOTE_TARBALL" 2>/dev/null | awk '{print $1}') | |
| echo " remote: $REMOTE_SHA" | |
| if [ "$LOCAL_SHA" != "$REMOTE_SHA" ]; then | |
| echo "ERROR: SHA mismatch after upload (transport corruption?)" >&2 | |
| exit 5 | |
| fi | |
| echo " ✓ matches" | |
| # ---- extract ---- | |
| echo | |
| echo "Extracting in sandbox..." | |
| oc exec "$TARGET_SB" --wait --timeout 1800 -- bash -c ' | |
| set -eu | |
| echo " installing zstd if missing..." | |
| if ! command -v zstd >/dev/null 2>&1; then | |
| sudo apt-get update -qq >/dev/null 2>&1 || sudo apt-get update -qq | |
| sudo apt-get install -y -qq zstd >/dev/null | |
| fi | |
| cd /home/sandbox | |
| echo " extracting (this can take a minute for large tarballs)..." | |
| START=$(date +%s) | |
| sudo tar --numeric-owner --acls --xattrs --zstd -xf .recovery.tar.zst | |
| echo " extracted in $(( $(date +%s) - START ))s" | |
| sudo rm -f .recovery.tar.zst | |
| SBUSER=sandbox | |
| if id "$SBUSER" >/dev/null 2>&1; then | |
| SBUID=$(id -u "$SBUSER"); SBGID=$(id -g "$SBUSER") | |
| else | |
| SBUID=1000; SBGID=1000 | |
| fi | |
| echo " chowning to $SBUSER ($SBUID:$SBGID)..." | |
| sudo chown -R "$SBUID:$SBGID" /home/sandbox | |
| echo | |
| echo " ─── final state ───" | |
| ls -la /home/sandbox | head -25 | |
| echo " files: $(find /home/sandbox -type f 2>/dev/null | wc -l)" | |
| echo " symlinks: $(find /home/sandbox -type l 2>/dev/null | wc -l)" | |
| echo " size: $(du -sh /home/sandbox 2>/dev/null | cut -f1)" | |
| ' | |
| echo | |
| echo "✓ Recovery complete in sandbox $TARGET_SB" | |
| echo | |
| echo "Inspect with:" | |
| echo " oc shell $TARGET_SB" | |
| echo " # then inside: ls -la /home/sandbox" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment