#!/bin/bash # ============================================================ # Antigravity Manager - HF Spaces Entrypoint # Supabase Storage 数据持久化(变更检测 + 空文件保护) # ============================================================ set -e DATA_DIR="${ABV_DATA_DIR:-/home/user/.antigravity_tools}" SYNC_INTERVAL="${SUPABASE_SYNC_INTERVAL:-300}" SNAPSHOT_DIR="/tmp/.sync_snapshot" EXPORT_FILE="/tmp/.accounts_export.json" EXPORT_REMOTE="config/accounts_export.json" mkdir -p "$DATA_DIR/accounts" "$SNAPSHOT_DIR" # ============================================================ # Supabase Storage API # ============================================================ supabase_enabled() { [ -n "$SUPABASE_URL" ] && [ -n "$SUPABASE_KEY" ] && [ -n "$SUPABASE_BUCKET" ] } sb_download() { local remote="$1" local_path="$2" local code code=$(curl -sf -o "$local_path" -w "%{http_code}" \ "${SUPABASE_URL}/storage/v1/object/${SUPABASE_BUCKET}/${remote}" \ -H "Authorization: Bearer ${SUPABASE_KEY}" \ -H "apikey: ${SUPABASE_KEY}" 2>/dev/null) || true if [ "$code" = "200" ]; then echo "[sync] ✓ downloaded: $remote" return 0 else rm -f "$local_path"; return 1 fi } sb_upload() { local local_path="$1" remote="$2" [ -f "$local_path" ] || return 1 local code code=$(curl -sf -o /dev/null -w "%{http_code}" \ -X POST \ "${SUPABASE_URL}/storage/v1/object/${SUPABASE_BUCKET}/${remote}" \ -H "Authorization: Bearer ${SUPABASE_KEY}" \ -H "apikey: ${SUPABASE_KEY}" \ -H "x-upsert: true" \ -F "file=@${local_path}" 2>/dev/null) || true if [ "$code" = "200" ] || [ "$code" = "201" ]; then echo "[sync] ✓ uploaded: $remote"; return 0 else echo "[sync] ✗ upload failed ($code): $remote"; return 1 fi } sb_list() { local prefix="$1" curl -sf \ "${SUPABASE_URL}/storage/v1/object/list/${SUPABASE_BUCKET}" \ -H "Authorization: Bearer ${SUPABASE_KEY}" \ -H "apikey: ${SUPABASE_KEY}" \ -H "Content-Type: application/json" \ -d "{\"prefix\":\"${prefix}\",\"limit\":1000}" 2>/dev/null } # ============================================================ # 快照:下载后记录哈希,同步时只上传有变化的文件 # ============================================================ file_hash() { md5sum "$1" 2>/dev/null | cut -d' ' -f1; } save_snapshot() { local file="$1" key="$2" if [ -f "$file" ]; then file_hash "$file" > "$SNAPSHOT_DIR/$key" fi } file_changed() { local file="$1" key="$2" [ ! -f "$file" ] && return 1 local old new old=$(cat "$SNAPSHOT_DIR/$key" 2>/dev/null || echo "") new=$(file_hash "$file") [ "$old" != "$new" ] } upload_if_changed() { local file="$1" remote="$2" key="$3" if file_changed "$file" "$key"; then if sb_upload "$file" "$remote"; then save_snapshot "$file" "$key" fi fi } # ============================================================ # 导出文件导入:从 Supabase 下载 accounts_export.json 并通过 API 导入 # ============================================================ import_export_file() { supabase_enabled || return 0 local tmp_export="/tmp/.accounts_export_new.json" sb_download "$EXPORT_REMOTE" "$tmp_export" || { echo "[import] No export file on Supabase"; return 0; } # 检查文件是否有变化 local old_hash new_hash old_hash=$(file_hash "$EXPORT_FILE" 2>/dev/null || echo "none") new_hash=$(file_hash "$tmp_export") if [ "$old_hash" = "$new_hash" ]; then echo "[import] Export file unchanged, skipping" rm -f "$tmp_export" return 0 fi echo "[import] Export file updated, importing accounts..." cp "$tmp_export" "$EXPORT_FILE" rm -f "$tmp_export" # 读取导出文件中的每个账号,通过 API 导入 # 管理接口优先使用 admin_password (WEB_PASSWORD),没有则回退 api_key local port="${PORT:-7860}" local auth_token="${WEB_PASSWORD:-}" [ -z "$auth_token" ] && auth_token="${API_KEY:-}" [ -z "$auth_token" ] && auth_token=$(jq -r '.admin_password // .api_key // empty' "$DATA_DIR/gui_config.json" 2>/dev/null || echo "") local count=0 total total=$(jq 'length' "$EXPORT_FILE" 2>/dev/null || echo "0") for i in $(seq 0 $((total - 1))); do local email rt email=$(jq -r ".[$i].email // empty" "$EXPORT_FILE") rt=$(jq -r ".[$i].refresh_token // empty" "$EXPORT_FILE") [ -z "$rt" ] && continue # 调用 API 添加账号(已存在的会自动更新 token) local resp code code=$(curl -sf -o /tmp/.import_resp.json -w "%{http_code}" \ -X POST "http://127.0.0.1:${port}/api/accounts" \ -H "Authorization: Bearer ${auth_token}" \ -H "Content-Type: application/json" \ -d "{\"refreshToken\":\"${rt}\"}" 2>/dev/null) || true if [ "$code" = "200" ] || [ "$code" = "201" ]; then echo "[import] ✓ imported: ${email:-unknown}" count=$((count + 1)) else local err err=$(jq -r '.error // empty' /tmp/.import_resp.json 2>/dev/null || echo "HTTP $code") echo "[import] ✗ failed: ${email:-unknown} ($err)" fi done echo "[import] Done: $count/$total accounts imported" # 导入后重新保存快照(账号文件已被 app 更新) sleep 2 save_snapshot "$DATA_DIR/accounts.json" "accounts.json" for f in "$DATA_DIR/accounts"/*.json; do if [ -f "$f" ]; then save_snapshot "$f" "acc_$(basename "$f")" fi done } # ============================================================ # 恢复数据 # ============================================================ restore_data() { supabase_enabled || { echo "[sync] Supabase not configured, skipping"; return 0; } echo "[sync] Restoring data from Supabase..." sb_download "config/gui_config.json" "$DATA_DIR/gui_config.json" || true sb_download "config/accounts.json" "$DATA_DIR/accounts.json" || true local file_list file_list=$(sb_list "accounts/") if [ -n "$file_list" ] && [ "$file_list" != "[]" ]; then echo "$file_list" | jq -r '.[].name // empty' 2>/dev/null | while read -r fname; do [ -n "$fname" ] && [ "$fname" != ".emptyFolderPlaceholder" ] && \ sb_download "accounts/${fname}" "$DATA_DIR/accounts/${fname}" || true done fi sb_download "db/token_stats.db" "$DATA_DIR/token_stats.db" || true sb_download "db/security.db" "$DATA_DIR/security.db" || true sb_download "db/user_tokens.db" "$DATA_DIR/user_tokens.db" || true echo "[sync] Restore complete" # 验证 if [ -f "$DATA_DIR/accounts.json" ]; then local count count=$(jq '.accounts | length' "$DATA_DIR/accounts.json" 2>/dev/null || echo "?") echo "[sync] accounts.json contains $count account(s)" fi # 保存快照基准(下载后的状态) save_snapshot "$DATA_DIR/gui_config.json" "gui_config.json" save_snapshot "$DATA_DIR/accounts.json" "accounts.json" save_snapshot "$DATA_DIR/token_stats.db" "token_stats.db" save_snapshot "$DATA_DIR/security.db" "security.db" save_snapshot "$DATA_DIR/user_tokens.db" "user_tokens.db" for f in "$DATA_DIR/accounts"/*.json; do if [ -f "$f" ]; then save_snapshot "$f" "acc_$(basename "$f")" fi done } # ============================================================ # 同步数据(只上传变化的文件 + 空文件保护) # ============================================================ sync_data() { supabase_enabled || return 0 echo "[sync] Checking for changes... ($(date '+%H:%M:%S'))" upload_if_changed "$DATA_DIR/gui_config.json" "config/gui_config.json" "gui_config.json" # accounts.json 保护:0 accounts 绝不上传 if [ -f "$DATA_DIR/accounts.json" ]; then local acc_count acc_count=$(jq '.accounts | length' "$DATA_DIR/accounts.json" 2>/dev/null || echo "0") if [ "$acc_count" -gt 0 ]; then upload_if_changed "$DATA_DIR/accounts.json" "config/accounts.json" "accounts.json" else echo "[sync] ⚠ Skipping accounts.json (0 accounts, protecting remote)" fi fi for f in "$DATA_DIR/accounts"/*.json; do if [ -f "$f" ]; then upload_if_changed "$f" "accounts/$(basename "$f")" "acc_$(basename "$f")" fi done upload_if_changed "$DATA_DIR/token_stats.db" "db/token_stats.db" "token_stats.db" upload_if_changed "$DATA_DIR/security.db" "db/security.db" "security.db" upload_if_changed "$DATA_DIR/user_tokens.db" "db/user_tokens.db" "user_tokens.db" # 检查 Supabase 上的导出文件是否有更新,有则自动导入 import_export_file echo "[sync] Done" } background_sync() { while true; do sleep "$SYNC_INTERVAL"; sync_data; done } graceful_shutdown() { echo "[hf] Shutdown, final sync..." sync_data; exit 0 } trap graceful_shutdown SIGTERM SIGINT # ============================================================ # 主流程 # ============================================================ echo "============================================" echo " Antigravity Manager - HF Spaces" echo "============================================" echo " Data dir : $DATA_DIR" echo " Port : ${PORT:-7860}" echo " Supabase : $(supabase_enabled && echo 'enabled' || echo 'disabled')" echo "============================================" restore_data CONFIG_FILE="$DATA_DIR/gui_config.json" HF_PORT="${PORT:-7860}" if [ -f "$CONFIG_FILE" ]; then # Only patch port in an existing (valid) config downloaded from Supabase if jq --argjson port "$HF_PORT" '.proxy.port = $port' "$CONFIG_FILE" > "${CONFIG_FILE}.tmp" 2>/dev/null; then mv "${CONFIG_FILE}.tmp" "$CONFIG_FILE" echo "[hf] Port set to $HF_PORT in existing config" else rm -f "${CONFIG_FILE}.tmp" echo "[hf] Warning: existing config invalid, removing so app creates fresh default" rm -f "$CONFIG_FILE" fi else echo "[hf] No config file — app will create default on first start" fi if supabase_enabled; then echo "[sync] Background sync every ${SYNC_INTERVAL}s (change-based)" background_sync & fi # 启动应用(后台),等待就绪后导入导出文件 /app/antigravity-tools --headless & APP_PID=$! # 等待服务就绪 echo "[hf] Waiting for app to start..." for i in $(seq 1 30); do if curl -sf "http://127.0.0.1:${HF_PORT}/health" >/dev/null 2>&1; then echo "[hf] App ready" break fi sleep 1 done # 首次导入导出文件 import_export_file # 等待应用进程 wait $APP_PID || true