| FROM diegosouzapw/omniroute:latest |
|
|
| |
| USER root |
|
|
| |
| RUN apt-get update && \ |
| apt-get install -y --no-install-recommends python3 python3-pip tar openssl && \ |
| pip3 install --break-system-packages --no-cache-dir huggingface_hub && \ |
| apt-get clean && \ |
| rm -rf /var/lib/apt/lists/* |
|
|
| WORKDIR /app |
|
|
| |
| |
| RUN cat > /app/sync.py <<'PY' |
| import os |
| import shutil |
| import tarfile |
| import tempfile |
| import time |
| import sys |
| from pathlib import Path |
| from huggingface_hub import HfApi, hf_hub_download |
|
|
| REPO_ID = os.environ.get("DATASET_REPO") |
| TOKEN = os.environ.get("HF_TOKEN") |
| BACKUP_FILE = "/tmp/omniroute_backup.tar.gz" |
| DATA_DIR = Path("/app/data") |
| CONFIG_DIR = Path("/root/.omniroute") |
|
|
|
|
| def safe_extract(tar, path): |
| """Extract `tar` into `path`, rejecting members that would escape it.""" |
| base = Path(path).resolve() |
| for member in tar.getmembers(): |
| target = (base / member.name).resolve() |
| if not str(target).startswith(str(base)): |
| raise RuntimeError(f"Unsafe tar member: {member.name}") |
| |
| |
| if sys.version_info >= (3, 12): |
| tar.extractall(path, filter="data") |
| else: |
| tar.extractall(path) |
|
|
|
|
| def heal_legacy_layout(): |
| """ |
| Older backups were packed as `./data/...` and `./.omniroute/...`, which the |
| old code extracted to `/data` and `/.omniroute` instead of `/app/data` and |
| `/root/.omniroute`. |
| IMPORTANT: run.sh `mkdir -p`s both target dirs BEFORE we run, so we cannot |
| just check `dst.exists()` — the target is always there (empty). We must |
| check the actual *file* the app needs: |
| - /app/data/storage.sqlite (target) vs /data/storage.sqlite (misplaced) |
| - /root/.omniroute/server.env (target) vs /.omniroute/server.env (misplaced) |
| If the misplaced copy exists and the target marker is missing, we swap. |
| If the target is already populated, we REFUSE to clobber it. |
| """ |
| plans = [ |
| (Path("/data"), "storage.sqlite", Path("/app/data")), |
| (Path("/.omniroute"), "server.env", Path("/root/.omniroute")), |
| ] |
| for src, marker_name, dst in plans: |
| src_marker = src / marker_name |
| dst_marker = dst / marker_name |
|
|
| if not src_marker.exists(): |
| continue |
|
|
| if dst_marker.exists(): |
| print( |
| f">>> [Sync] Healer: target {dst_marker} already present, " |
| f"leaving {src} alone (would not clobber real data)", |
| flush=True, |
| ) |
| continue |
|
|
| print(f">>> [Sync] Healer: misplaced legacy content at {src}", flush=True) |
|
|
| |
| |
| |
| if dst.exists(): |
| try: |
| dst.rmdir() |
| print(f">>> [Sync] Healer: removed empty target {dst}", flush=True) |
| except OSError: |
| print( |
| f">>> [Sync] Healer: ABORT — {dst} is not empty, " |
| f"refusing to clobber", |
| flush=True, |
| ) |
| continue |
|
|
| dst.parent.mkdir(parents=True, exist_ok=True) |
| shutil.move(str(src), str(dst)) |
| print(f">>> [Sync] Healer: relocated {src} -> {dst}", flush=True) |
|
|
|
|
| def upload(): |
| if not REPO_ID or not TOKEN: |
| print(">>> [Sync] DATASET_REPO or HF_TOKEN not configured. Skipping backup.") |
| return |
|
|
| with tempfile.TemporaryDirectory() as tmpdir: |
| tmp_path = Path(tmpdir) |
|
|
| |
| |
| |
| |
| if DATA_DIR.exists(): |
| target = tmp_path / "app" / "data" |
| target.parent.mkdir(parents=True, exist_ok=True) |
| shutil.copytree(DATA_DIR, target) |
| print(f">>> [Sync] Packed {DATA_DIR} -> app/data", flush=True) |
|
|
| if CONFIG_DIR.exists(): |
| target = tmp_path / "root" / ".omniroute" |
| target.parent.mkdir(parents=True, exist_ok=True) |
| shutil.copytree(CONFIG_DIR, target) |
| print(f">>> [Sync] Packed {CONFIG_DIR} -> root/.omniroute", flush=True) |
|
|
| try: |
| with tarfile.open(BACKUP_FILE, "w:gz") as tar: |
| tar.add(str(tmp_path), arcname=".") |
| HfApi(token=TOKEN).upload_file( |
| path_or_fileobj=BACKUP_FILE, |
| path_in_repo="omniroute_backup.tar.gz", |
| repo_id=REPO_ID, |
| repo_type="dataset", |
| ) |
| print(f">>> [Sync] Backup successful: {time.strftime('%Y-%m-%d %H:%M:%S')}", flush=True) |
| except Exception as e: |
| print(f">>> [Sync] Backup failed: {e}", flush=True) |
|
|
|
|
| def download(): |
| if not REPO_ID or not TOKEN: |
| print(">>> [Sync] DATASET_REPO or HF_TOKEN not configured. Skipping restore.") |
| return |
| try: |
| print(">>> [Sync] Downloading data from Dataset...", flush=True) |
| path = hf_hub_download( |
| repo_id=REPO_ID, |
| filename="omniroute_backup.tar.gz", |
| repo_type="dataset", |
| token=TOKEN, |
| ) |
| print(f">>> [Sync] Downloaded archive to {path}", flush=True) |
|
|
| with tarfile.open(path, "r:gz") as tar: |
| members = tar.getmembers() |
| print(f">>> [Sync] Extracting {len(members)} members to / ...", flush=True) |
| safe_extract(tar, "/") |
|
|
| |
| heal_legacy_layout() |
|
|
| |
| |
| critical = [ |
| (DATA_DIR, "storage.sqlite"), |
| (CONFIG_DIR, "server.env"), |
| ] |
| for d, fname in critical: |
| marker = d / fname |
| status = "OK" if marker.exists() else "MISSING" |
| print(f">>> [Sync] Verify {marker}: {status}", flush=True) |
|
|
| print(">>> [Sync] Data restore successful.", flush=True) |
| except Exception as e: |
| print(f">>> [Sync] Skipping restore (might be first run): {e}", flush=True) |
|
|
|
|
| if __name__ == "__main__": |
| if len(sys.argv) > 1 and sys.argv[1] == "upload": |
| upload() |
| elif len(sys.argv) > 1 and sys.argv[1] == "download": |
| download() |
| PY |
|
|
| |
| RUN cat > /app/run.sh <<'SH' |
| |
| set -e |
|
|
| mkdir -p /app/data |
| mkdir -p /root/.omniroute |
|
|
| echo '=== 1. Restoring Data & Config ===' |
| python3 /app/sync.py download |
|
|
| echo '=== 2. Starting Scheduled Backup (Every 10 minutes) ===' |
| (while true; do sleep 600; python3 /app/sync.py upload; done) & |
|
|
| echo '=== 3. Generating & Persisting Secrets ===' |
| |
| |
| |
| if [ ! -f /root/.omniroute/server.env ]; then |
| echo '>>> No restored server.env found, generating fresh secrets' |
| export JWT_SECRET="${JWT_SECRET:-$(openssl rand -base64 48 | tr -d '\n')}" |
| export API_KEY_SECRET="${API_KEY_SECRET:-$(openssl rand -hex 32 | tr -d '\n')}" |
| export STORAGE_ENCRYPTION_KEY="${STORAGE_ENCRYPTION_KEY:-$(openssl rand -hex 32 | tr -d '\n')}" |
| export OMNIROUTE_WS_BRIDGE_SECRET="${OMNIROUTE_WS_BRIDGE_SECRET:-$(openssl rand -base64 32 | tr -d '\n')}" |
| export INITIAL_PASSWORD="${INITIAL_PASSWORD:-$(openssl rand -hex 16 | tr -d '\n')}" |
|
|
| cat > /root/.omniroute/server.env <<EOF |
| JWT_SECRET=${JWT_SECRET} |
| API_KEY_SECRET=${API_KEY_SECRET} |
| STORAGE_ENCRYPTION_KEY=${STORAGE_ENCRYPTION_KEY} |
| OMNIROUTE_WS_BRIDGE_SECRET=${OMNIROUTE_WS_BRIDGE_SECRET} |
| INITIAL_PASSWORD=${INITIAL_PASSWORD} |
| DATA_DIR=/app/data |
| PORT=7860 |
| NODE_ENV=production |
| NEXT_PUBLIC_BASE_URL=https://${SPACE_HOST:-localhost:7860} |
| BASE_URL=http://localhost:7860 |
| AUTH_COOKIE_SECURE=true |
| REQUIRE_API_KEY=true |
| CORS_ORIGIN=* |
| OMNIROUTE_MEMORY_MB=14745 |
| PRICING_SYNC_ENABLED=true |
| OMNIROUTE_AUTO_FREE_FALLBACK_TO_FULL_POOL=true |
| OMNIROUTE_SHOW_LOG=0 |
| APP_LOG_LEVEL=error |
| EOF |
| else |
| echo '>>> Restored server.env found, keeping restored secrets' |
| fi |
|
|
| |
| set -a |
| . "/root/.omniroute/server.env" |
| set +a |
|
|
| export NODE_OPTIONS="--max-old-space-size=${OMNIROUTE_MEMORY_MB}" |
|
|
| echo '=== 4. Starting OmniRoute ===' |
| exec node server.js |
| SH |
|
|
| RUN chmod +x /app/run.sh |
|
|
| EXPOSE 7860 |
| CMD ["/bin/sh", "/app/run.sh"] |