File size: 9,076 Bytes
2be62d0
a0c86f6
b1363c6
 
 
 
 
 
 
 
 
 
9649929
 
b1363c6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a0c86f6
 
b1363c6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
FROM diegosouzapw/omniroute:latest

# 1. Switch to root to install system packages
USER root

# 2. Install Python for the backup script (Debian-based)
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

# 3. Create the Python sync script (FIXED: tarball mirrors real FS layout
#    + marker-based legacy healer + file-level verify)
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}")
    # Python 3.12+ requires the `filter` argument (and we WANT data filter,
    # which strips leading slashes / .. and refuses absolute paths).
    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  # nothing misplaced for this pair

        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)

        # Target dir exists but is missing its marker -> it must be empty.
        # Try to remove it so shutil.move produces a clean swap; if it's not
        # empty (unexpected), bail out instead of nesting src inside dst.
        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)

        # Mirror the REAL filesystem layout. After upload, the tar contains:
        #   ./app/data/...
        #   ./root/.omniroute/...
        # so extracting to "/" puts files back where the app expects them.
        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, "/")

        # Self-heal: handle backups created before the path fix.
        heal_legacy_layout()

        # Verify the app can actually see the restored files.
        # Check the REAL files, not just the dirs (mkdir -p makes them lie).
        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

# 4. Create the startup shell script (FIXED: don't clobber restored server.env)
RUN cat > /app/run.sh <<'SH'
#!/bin/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 ==='
# Only mint fresh secrets if restore didn't bring a server.env with us.
# Otherwise we'd rotate JWT_SECRET etc. on every container restart and
# invalidate every active session / API key / initial-password.
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

# Source whichever server.env we ended up with (restored or freshly minted).
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"]