Bjo53 commited on
Commit
6bf7731
·
verified ·
1 Parent(s): 46f860d

Upload folder using huggingface_hub

Browse files
Files changed (2) hide show
  1. startup.sh +53 -56
  2. static/index.html +68 -385
startup.sh CHANGED
@@ -1,80 +1,77 @@
1
  #!/bin/bash
 
2
 
3
- echo "===== Application Startup at $(date) ====="
 
4
 
5
- # Install huggingface_hub if not present
6
- pip3 install --no-cache-dir -q huggingface_hub==0.23.0 --break-system-packages
 
 
 
 
 
 
 
 
 
 
 
 
7
 
8
- # Pull persistent data from HF Dataset repo if token is set
 
 
 
 
 
 
 
9
  if [ -n "$HF_TOKEN" ] && [ -n "$HF_USERNAME" ]; then
10
- echo "[Storage] Pulling persistent data from HF Dataset..."
11
- python3 -c "
12
  import os
13
- from huggingface_hub import snapshot_download, HfApi
14
- import shutil
15
-
16
  token = os.environ.get('HF_TOKEN')
17
  username = os.environ.get('HF_USERNAME')
18
  repo_id = f'{username}/vps-storage'
19
-
20
  try:
21
- api = HfApi()
22
- # Check if repo exists
23
- api.repo_info(repo_id=repo_id, repo_type='dataset', token=token)
24
-
25
- # Download all files from dataset repo to /persistent
26
- snapshot_download(
27
- repo_id=repo_id,
28
- repo_type='dataset',
29
- local_dir='/persistent',
30
- token=token,
31
- ignore_patterns=['.gitattributes', 'README.md']
32
- )
33
- print('[Storage] Data restored successfully!')
34
- except Exception as e:
35
- print(f'[Storage] No existing data or error: {e}')
36
- print('[Storage] Starting fresh...')
37
- "
38
- else
39
- echo "[Storage] HF_TOKEN or HF_USERNAME not set — skipping restore"
40
  fi
41
 
42
- # Create /persistent directory if it doesn't exist
43
  mkdir -p /persistent
44
 
45
- # Start auto-save in background (every 5 minutes)
46
  python3 -c "
47
- import os, time, subprocess, threading
48
-
49
- def auto_save():
50
  while True:
51
- time.sleep(300) # every 5 minutes
52
  token = os.environ.get('HF_TOKEN')
53
  username = os.environ.get('HF_USERNAME')
54
  if token and username:
55
  try:
56
  from huggingface_hub import HfApi
57
- api = HfApi()
58
- repo_id = f'{username}/vps-storage'
59
- api.upload_folder(
60
- folder_path='/persistent',
61
- repo_id=repo_id,
62
- repo_type='dataset',
63
- token=token,
64
- commit_message='Auto-save from VPS'
65
- )
66
- print(f'[Storage] Auto-saved at {__import__(\"datetime\").datetime.now()}')
67
- except Exception as e:
68
- print(f'[Storage] Auto-save failed: {e}')
69
 
70
- t = threading.Thread(target=auto_save, daemon=True)
71
- t.start()
 
 
 
 
 
 
 
 
 
72
  " &
73
 
74
- # Start the FastAPI app
75
- exec uvicorn app:app \
76
- --host 0.0.0.0 \
77
- --port 7860 \
78
- --ws websockets \
79
- --proxy-headers \
80
- --forwarded-allow-ips="*"
 
1
  #!/bin/bash
2
+ echo "===== Startup $(date) ====="
3
 
4
+ # Install huggingface_hub
5
+ pip3 install --no-cache-dir -q huggingface_hub==0.23.0 --break-system-packages 2>/dev/null
6
 
7
+ # Install Node.js
8
+ if ! command -v node &> /dev/null; then
9
+ echo "[Install] Node.js..."
10
+ curl -fsSL https://deb.nodesource.com/setup_20.x | bash - 2>/dev/null
11
+ apt-get install -y nodejs 2>/dev/null
12
+ fi
13
+
14
+ # Install Python tools
15
+ echo "[Install] Python packages..."
16
+ pip3 install --break-system-packages -q openai anthropic openhands antigravity 2>/dev/null
17
+
18
+ # Install Claude CLI
19
+ echo "[Install] Claude CLI..."
20
+ npm install -g @anthropic-ai/claude-code 2>/dev/null
21
 
22
+ # Install Codex CLI
23
+ echo "[Install] Codex CLI..."
24
+ npm install -g @openai/codex 2>/dev/null
25
+
26
+ # Install pip packages
27
+ pip3 install --break-system-packages -q requests psutil 2>/dev/null
28
+
29
+ # Restore from dataset
30
  if [ -n "$HF_TOKEN" ] && [ -n "$HF_USERNAME" ]; then
31
+ echo "[Storage] Restoring from dataset..."
32
+ python3 -c "
33
  import os
34
+ from huggingface_hub import snapshot_download
 
 
35
  token = os.environ.get('HF_TOKEN')
36
  username = os.environ.get('HF_USERNAME')
37
  repo_id = f'{username}/vps-storage'
 
38
  try:
39
+ snapshot_download(repo_id=repo_id, repo_type='dataset', local_dir='/persistent', token=token, ignore_patterns=['.gitattributes','README.md'])
40
+ print('[Storage] Restored!')
41
+ except: print('[Storage] Fresh start')
42
+ " 2>/dev/null
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  fi
44
 
 
45
  mkdir -p /persistent
46
 
47
+ # Auto-backup every 5 min
48
  python3 -c "
49
+ import os, time, threading
50
+ def backup():
 
51
  while True:
52
+ time.sleep(300)
53
  token = os.environ.get('HF_TOKEN')
54
  username = os.environ.get('HF_USERNAME')
55
  if token and username:
56
  try:
57
  from huggingface_hub import HfApi
58
+ HfApi(token=token).upload_folder(folder_path='/persistent', repo_id=f'{username}/vps-storage', repo_type='dataset', commit_message='auto-backup')
59
+ print(f'[Backup] {__import__(\"datetime\").datetime.now()}')
60
+ except: pass
61
+ threading.Thread(target=backup, daemon=True).start()
62
+ " &
 
 
 
 
 
 
 
63
 
64
+ # Keep-alive ping
65
+ python3 -c "
66
+ import os, time, requests
67
+ def ping():
68
+ while True:
69
+ time.sleep(3600)
70
+ try:
71
+ host = os.environ.get('SPACE_HOST','')
72
+ if host: requests.get(f'https://{host}/', timeout=10)
73
+ except: pass
74
+ threading.Thread(target=ping, daemon=True).start()
75
  " &
76
 
77
+ exec uvicorn app:app --host 0.0.0.0 --port 7860 --ws websockets --proxy-headers --forwarded-allow-ips="*"
 
 
 
 
 
 
static/index.html CHANGED
@@ -3,425 +3,108 @@
3
  <head>
4
  <meta charset="UTF-8"/>
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
6
- <title>Ubuntu 24.04 Web Terminal</title>
7
-
8
  <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet"/>
9
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css"/>
10
-
11
  <script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.js"></script>
12
  <script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.js"></script>
13
-
14
  <style>
15
- *, *::before, *::after {
16
- box-sizing: border-box;
17
- margin: 0;
18
- padding: 0;
19
- }
20
-
21
- body {
22
- background: #090c10;
23
- display: flex;
24
- flex-direction: column;
25
- align-items: center;
26
- min-height: 100vh;
27
- padding: 24px 16px;
28
- font-family: 'JetBrains Mono', monospace;
29
- }
30
-
31
- h1 {
32
- color: #27c93f;
33
- font-size: 15px;
34
- letter-spacing: 2px;
35
- margin-bottom: 16px;
36
- opacity: 0.7;
37
- }
38
-
39
- #term-wrap {
40
- width: 100%;
41
- max-width: 960px;
42
- background: #0d1117;
43
- border-radius: 12px;
44
- overflow: hidden;
45
- border: 1px solid #30363d;
46
- display: flex;
47
- flex-direction: column;
48
- box-shadow: 0 8px 40px rgba(0,0,0,0.6);
49
- }
50
-
51
- #titlebar {
52
- background: #1c2128;
53
- padding: 11px 16px;
54
- display: flex;
55
- align-items: center;
56
- gap: 8px;
57
- border-bottom: 1px solid #30363d;
58
- user-select: none;
59
- }
60
-
61
- .dot {
62
- width: 13px;
63
- height: 13px;
64
- border-radius: 50%;
65
- }
66
-
67
- .dot.r { background: #ff5f56; }
68
- .dot.y { background: #ffbd2e; }
69
- .dot.g { background: #27c93f; }
70
-
71
- #title-text {
72
- color: #768390;
73
- font-size: 12px;
74
- margin-left: 10px;
75
- }
76
-
77
- #copy-btn {
78
- margin-left: auto;
79
- background: #27c93f22;
80
- border: 1px solid #27c93f55;
81
- border-radius: 6px;
82
- color: #27c93f;
83
- font-size: 11px;
84
- padding: 4px 12px;
85
- cursor: pointer;
86
- }
87
-
88
- #copy-btn:hover {
89
- background: #27c93f44;
90
- }
91
-
92
- #status-bar {
93
- background: #161b22;
94
- padding: 6px 16px;
95
- display: flex;
96
- align-items: center;
97
- gap: 10px;
98
- border-bottom: 1px solid #21262d;
99
- }
100
-
101
- #status-dot {
102
- width: 8px;
103
- height: 8px;
104
- border-radius: 50%;
105
- background: #ff5f56;
106
- }
107
-
108
- #status-dot.connected {
109
- background: #27c93f;
110
- }
111
-
112
- #status-text {
113
- color: #768390;
114
- font-size: 11px;
115
- }
116
 
117
- #reconnect-btn {
118
- margin-left: auto;
119
- background: #27c93f22;
120
- border: 1px solid #27c93f55;
121
- border-radius: 6px;
122
- color: #27c93f;
123
- font-size: 11px;
124
- padding: 4px 10px;
125
- cursor: pointer;
126
- display: none;
127
  }
128
-
129
- #reconnect-btn.visible {
130
- display: block;
131
- }
132
-
133
- #terminal-container {
134
- padding: 8px;
135
- height: 500px;
136
- max-height: 500px;
137
- overflow-y: auto;
138
- background: #0d1117;
139
- }
140
-
141
- .xterm {
142
- padding: 8px;
143
- }
144
-
145
- #ctx-menu {
146
- display: none;
147
- position: fixed;
148
- background: #1c2128;
149
- border: 1px solid #30363d;
150
- border-radius: 8px;
151
- padding: 4px;
152
- z-index: 9999;
153
- min-width: 140px;
154
  }
155
-
156
- #ctx-menu button {
157
- display: block;
158
- width: 100%;
159
- background: none;
160
- border: none;
161
- color: #e6edf3;
162
- padding: 8px 12px;
163
- text-align: left;
164
- cursor: pointer;
165
- border-radius: 4px;
166
- font-family: inherit;
167
  }
 
168
 
169
- #ctx-menu button:hover {
170
- background: #27c93f22;
171
- color: #27c93f;
172
- }
173
-
174
- #footer {
175
- margin-top: 14px;
176
- color: #444c56;
177
- font-size: 11px;
178
- text-align: center;
179
- line-height: 1.8;
180
- }
181
  </style>
182
  </head>
183
-
184
  <body>
185
 
186
- <h1>⬛ Ubuntu 24.04 LTS — Web Terminal</h1>
187
-
188
- <div id="term-wrap">
189
-
190
- <div id="titlebar">
191
- <div class="dot r"></div>
192
- <div class="dot y"></div>
193
- <div class="dot g"></div>
194
-
195
- <span id="title-text">ubuntu@space: ~</span>
196
-
197
- <button id="copy-btn">
198
- ⎘ Copy Selection
199
- </button>
200
- </div>
201
-
202
- <div id="status-bar">
203
- <div id="status-dot"></div>
204
-
205
- <span id="status-text">
206
- Connecting...
207
- </span>
208
-
209
- <button id="reconnect-btn">
210
- ↺ Reconnect
211
- </button>
212
- </div>
213
-
214
- <div id="terminal-container"></div>
215
- </div>
216
-
217
- <div id="ctx-menu">
218
- <button id="ctx-copy">⎘ Copy</button>
219
- <button id="ctx-paste">⎙ Paste</button>
220
- <button id="ctx-clear">✕ Clear</button>
221
  </div>
222
 
223
- <div id="footer">
224
- Real Ubuntu shell running on Hugging Face Space
225
- </div>
226
 
227
  <script>
228
- let ws = null;
229
- let reconnectTimeout = null;
230
-
231
- const term = new Terminal({
232
- cursorBlink: true,
233
- fontSize: 13,
234
- fontFamily: "'JetBrains Mono', monospace",
235
- allowTransparency: false,
236
- scrollback: 3000,
237
- disableStdin: false,
238
- macOptionIsMeta: true,
239
- theme: {
240
- background: '#0d1117',
241
- foreground: '#e6edf3',
242
- cursor: '#27c93f'
243
- }
244
- });
245
-
246
- const fitAddon = new FitAddon.FitAddon();
247
-
248
- term.loadAddon(fitAddon);
249
-
250
- term.open(document.getElementById('terminal-container'));
251
 
252
- fitAddon.fit();
 
253
 
254
- term.focus();
255
-
256
- const statusDot = document.getElementById('status-dot');
257
- const statusText = document.getElementById('status-text');
258
- const reconnectBtn = document.getElementById('reconnect-btn');
259
-
260
- function setStatus(connected, text) {
261
- statusDot.className = connected ? 'connected' : '';
262
- statusText.textContent = text;
263
-
264
- reconnectBtn.className = connected ? '' : 'visible';
265
- }
266
-
267
- function getWsUrl() {
268
- const protocol = location.protocol === 'https:' ? 'wss' : 'ws';
269
- return `${protocol}://${location.host}/ws`;
270
- }
271
-
272
- function connect() {
273
-
274
- if (ws) {
275
- ws.close();
276
- ws = null;
277
  }
278
-
279
- setStatus(false, 'Connecting...');
280
-
281
- ws = new WebSocket(getWsUrl());
282
-
283
- ws.onopen = () => {
284
- setStatus(true, 'Connected');
285
- sendResize();
286
- term.focus();
287
- };
288
-
289
- ws.onmessage = (event) => {
290
- term.write(event.data);
291
- };
292
-
293
- ws.onerror = () => {
294
- setStatus(false, 'Connection error');
295
- };
296
-
297
- ws.onclose = () => {
298
- setStatus(false, 'Disconnected');
299
-
300
- clearTimeout(reconnectTimeout);
301
-
302
- reconnectTimeout = setTimeout(() => {
303
- connect();
304
- }, 2000);
305
- };
306
  }
307
 
308
- function sendResize() {
309
-
310
- if (!ws || ws.readyState !== WebSocket.OPEN) {
311
- return;
312
- }
313
 
314
- ws.send(JSON.stringify({
315
- type: 'resize',
316
- cols: term.cols,
317
- rows: term.rows
318
- }));
319
- }
 
 
320
 
321
- term.onData((data) => {
 
 
 
 
322
 
323
- if (!ws || ws.readyState !== WebSocket.OPEN) {
324
- return;
 
325
  }
326
 
327
- if (typeof data !== 'string') {
328
- return;
 
 
 
 
329
  }
330
 
331
- ws.send(JSON.stringify({
332
- type: 'input',
333
- data: data
334
- }));
335
- });
336
-
337
- term.onResize(() => {
338
- sendResize();
339
- });
340
-
341
- window.addEventListener('resize', () => {
342
- fitAddon.fit();
343
- sendResize();
344
- });
345
-
346
- reconnectBtn.addEventListener('click', connect);
347
-
348
- document.getElementById('copy-btn').addEventListener('click', async () => {
349
-
350
- const selection = term.getSelection();
351
-
352
- if (!selection) return;
353
-
354
- await navigator.clipboard.writeText(selection);
355
-
356
- const btn = document.getElementById('copy-btn');
357
-
358
- btn.textContent = '✓ Copied';
359
-
360
- setTimeout(() => {
361
- btn.textContent = '⎘ Copy Selection';
362
- }, 1500);
363
- });
364
-
365
- const ctxMenu = document.getElementById('ctx-menu');
366
-
367
- document.getElementById('terminal-container')
368
- .addEventListener('contextmenu', (e) => {
369
-
370
- e.preventDefault();
371
-
372
- ctxMenu.style.display = 'block';
373
-
374
- ctxMenu.style.left = `${e.pageX}px`;
375
- ctxMenu.style.top = `${e.pageY}px`;
376
- });
377
-
378
- document.addEventListener('click', () => {
379
- ctxMenu.style.display = 'none';
380
- });
381
-
382
- document.getElementById('ctx-copy')
383
- .addEventListener('click', async () => {
384
-
385
- const selection = term.getSelection();
386
-
387
- if (!selection) return;
388
-
389
- await navigator.clipboard.writeText(selection);
390
- });
391
-
392
- document.getElementById('ctx-paste')
393
- .addEventListener('click', async () => {
394
-
395
- try {
396
-
397
- const text = await navigator.clipboard.readText();
398
-
399
- term.paste(text);
400
-
401
- } catch (err) {
402
- console.error(err);
403
  }
404
  });
405
 
406
- document.getElementById('ctx-clear')
407
- .addEventListener('click', () => {
408
- term.clear();
 
409
  });
410
 
411
- // Prevent duplicate paste bubbling
412
- setTimeout(() => {
413
-
414
- if (term.textarea) {
415
-
416
- term.textarea.addEventListener('paste', (e) => {
417
- e.stopPropagation();
418
- });
419
- }
420
-
421
- }, 500);
422
-
423
- connect();
424
  </script>
425
-
426
  </body>
427
- </html>
 
3
  <head>
4
  <meta charset="UTF-8"/>
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
6
+ <title>Terminal</title>
 
7
  <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet"/>
8
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css"/>
 
9
  <script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.js"></script>
10
  <script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.js"></script>
 
11
  <style>
12
+ * { box-sizing: border-box; margin: 0; padding: 0; }
13
+ html, body { height: 100%; overflow: hidden; background: #0d1117; font-family: 'JetBrains Mono', monospace; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
 
15
+ #pass-screen {
16
+ position: fixed; top: 0; left: 0; width: 100%; height: 100%;
17
+ background: #0d1117; display: flex; align-items: center; justify-content: center;
18
+ z-index: 9999; flex-direction: column; gap: 16px;
 
 
 
 
 
 
19
  }
20
+ #pass-screen.hidden { display: none; }
21
+ #pass-screen input {
22
+ background: #161b22; border: 1px solid #30363d; color: #e6edf3;
23
+ padding: 12px 20px; font-size: 16px; border-radius: 8px; width: 300px;
24
+ font-family: 'JetBrains Mono', monospace; text-align: center;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  }
26
+ #pass-screen button {
27
+ background: #27c93f; color: #000; border: none; padding: 10px 30px;
28
+ font-size: 14px; border-radius: 8px; cursor: pointer; font-weight: bold;
 
 
 
 
 
 
 
 
 
29
  }
30
+ #pass-screen .err { color: #ff5f56; font-size: 12px; }
31
 
32
+ #terminal-container { width: 100%; height: 100%; }
33
+ .xterm { padding: 4px; height: 100%; }
 
 
 
 
 
 
 
 
 
 
34
  </style>
35
  </head>
 
36
  <body>
37
 
38
+ <div id="pass-screen">
39
+ <input id="pass-input" type="password" placeholder="Enter password" autofocus/>
40
+ <button id="pass-btn">Unlock</button>
41
+ <div class="err" id="pass-err"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  </div>
43
 
44
+ <div id="terminal-container" style="display:none;"></div>
 
 
45
 
46
  <script>
47
+ const PASS_HASH = "opencode";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
 
49
+ document.getElementById("pass-btn").onclick = () => unlock();
50
+ document.getElementById("pass-input").addEventListener("keydown", e => { if (e.key === "Enter") unlock(); });
51
 
52
+ function unlock() {
53
+ const val = document.getElementById("pass-input").value;
54
+ if (val === PASS_HASH) {
55
+ document.getElementById("pass-screen").classList.add("hidden");
56
+ document.getElementById("terminal-container").style.display = "block";
57
+ initTerminal();
58
+ } else {
59
+ document.getElementById("pass-err").textContent = "Wrong password";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  }
62
 
63
+ let ws = null;
 
 
 
 
64
 
65
+ function initTerminal() {
66
+ const term = new Terminal({
67
+ cursorBlink: true,
68
+ fontSize: 14,
69
+ fontFamily: "'JetBrains Mono', monospace",
70
+ scrollback: 5000,
71
+ theme: { background: '#0d1117', foreground: '#e6edf3', cursor: '#27c93f' }
72
+ });
73
 
74
+ const fitAddon = new FitAddon.FitAddon();
75
+ term.loadAddon(fitAddon);
76
+ term.open(document.getElementById("terminal-container"));
77
+ fitAddon.fit();
78
+ term.focus();
79
 
80
+ function getWsUrl() {
81
+ const p = location.protocol === "https:" ? "wss" : "ws";
82
+ return `${p}://${location.host}/ws`;
83
  }
84
 
85
+ function connect() {
86
+ if (ws) { ws.close(); ws = null; }
87
+ ws = new WebSocket(getWsUrl());
88
+ ws.onopen = () => { ws.send(JSON.stringify({ type: "resize", cols: term.cols, rows: term.rows })); term.focus(); };
89
+ ws.onmessage = (e) => term.write(e.data);
90
+ ws.onclose = () => setTimeout(connect, 2000);
91
  }
92
 
93
+ term.onData((data) => {
94
+ if (ws && ws.readyState === WebSocket.OPEN) {
95
+ ws.send(JSON.stringify({ type: "input", data }));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  }
97
  });
98
 
99
+ term.onResize(() => {
100
+ if (ws && ws.readyState === WebSocket.OPEN) {
101
+ ws.send(JSON.stringify({ type: "resize", cols: term.cols, rows: term.rows }));
102
+ }
103
  });
104
 
105
+ window.addEventListener("resize", () => { fitAddon.fit(); });
106
+ connect();
107
+ }
 
 
 
 
 
 
 
 
 
 
108
  </script>
 
109
  </body>
110
+ </html>