Bjo53 commited on
Commit
6124680
·
verified ·
1 Parent(s): f49c236

Upload folder using huggingface_hub

Browse files
Files changed (5) hide show
  1. __pycache__/app.cpython-312.pyc +0 -0
  2. app.py +136 -0
  3. requirements.txt +6 -0
  4. startup.sh +80 -0
  5. static/index.html +425 -0
__pycache__/app.cpython-312.pyc ADDED
Binary file (5.05 kB). View file
 
app.py ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import pty
3
+ import asyncio
4
+ import struct
5
+ import fcntl
6
+ import termios
7
+ import json
8
+
9
+ from fastapi import FastAPI, WebSocket, WebSocketDisconnect
10
+ from fastapi.responses import FileResponse
11
+ from fastapi.staticfiles import StaticFiles
12
+
13
+ app = FastAPI()
14
+
15
+ app.mount("/static", StaticFiles(directory="static"), name="static")
16
+
17
+
18
+ @app.get("/")
19
+ async def index():
20
+ return FileResponse("static/index.html")
21
+
22
+
23
+ @app.websocket("/ws")
24
+ async def terminal(websocket: WebSocket):
25
+
26
+ await websocket.accept()
27
+
28
+ master_fd, slave_fd = pty.openpty()
29
+
30
+ proc = await asyncio.create_subprocess_exec(
31
+ "/bin/bash",
32
+ stdin=slave_fd,
33
+ stdout=slave_fd,
34
+ stderr=slave_fd,
35
+ env={
36
+ **os.environ,
37
+ "TERM": "xterm-256color",
38
+ "HOME": "/home/ubuntu",
39
+ "USER": "ubuntu"
40
+ }
41
+ )
42
+
43
+ os.close(slave_fd)
44
+
45
+ loop = asyncio.get_event_loop()
46
+
47
+ # Read terminal output
48
+ async def read_pty():
49
+
50
+ while True:
51
+
52
+ try:
53
+
54
+ data = await loop.run_in_executor(
55
+ None,
56
+ lambda: os.read(master_fd, 4096)
57
+ )
58
+
59
+ if data:
60
+
61
+ try:
62
+ text = data.decode("utf-8")
63
+
64
+ except UnicodeDecodeError:
65
+ text = data.decode(
66
+ "utf-8",
67
+ errors="ignore"
68
+ )
69
+
70
+ await websocket.send_text(text)
71
+
72
+ except OSError:
73
+ break
74
+
75
+ read_task = asyncio.create_task(read_pty())
76
+
77
+ try:
78
+
79
+ while True:
80
+
81
+ message = await websocket.receive_text()
82
+
83
+ try:
84
+
85
+ msg = json.loads(message)
86
+
87
+ if msg.get("type") == "input":
88
+
89
+ os.write(
90
+ master_fd,
91
+ msg["data"].encode("utf-8")
92
+ )
93
+
94
+ elif msg.get("type") == "resize":
95
+
96
+ cols = msg.get("cols", 80)
97
+ rows = msg.get("rows", 24)
98
+
99
+ fcntl.ioctl(
100
+ master_fd,
101
+ termios.TIOCSWINSZ,
102
+ struct.pack(
103
+ "HHHH",
104
+ rows,
105
+ cols,
106
+ 0,
107
+ 0
108
+ )
109
+ )
110
+
111
+ except (json.JSONDecodeError, OSError):
112
+ break
113
+
114
+ except (WebSocketDisconnect, Exception):
115
+ pass
116
+
117
+ finally:
118
+
119
+ read_task.cancel()
120
+
121
+ try:
122
+ proc.terminate()
123
+
124
+ await asyncio.wait_for(
125
+ proc.wait(),
126
+ timeout=2
127
+ )
128
+
129
+ except Exception:
130
+ proc.kill()
131
+
132
+ try:
133
+ os.close(master_fd)
134
+
135
+ except Exception:
136
+ pass
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ fastapi==0.111.0
2
+ uvicorn[standard]==0.30.0
3
+ websockets==12.0
4
+ python-multipart==0.0.9
5
+ huggingface_hub==0.23.0
6
+ httptools==0.6.1
startup.sh ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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="*"
static/index.html ADDED
@@ -0,0 +1,425 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
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
+ min-height: 500px;
136
+ background: #0d1117;
137
+ }
138
+
139
+ .xterm {
140
+ padding: 8px;
141
+ }
142
+
143
+ #ctx-menu {
144
+ display: none;
145
+ position: fixed;
146
+ background: #1c2128;
147
+ border: 1px solid #30363d;
148
+ border-radius: 8px;
149
+ padding: 4px;
150
+ z-index: 9999;
151
+ min-width: 140px;
152
+ }
153
+
154
+ #ctx-menu button {
155
+ display: block;
156
+ width: 100%;
157
+ background: none;
158
+ border: none;
159
+ color: #e6edf3;
160
+ padding: 8px 12px;
161
+ text-align: left;
162
+ cursor: pointer;
163
+ border-radius: 4px;
164
+ font-family: inherit;
165
+ }
166
+
167
+ #ctx-menu button:hover {
168
+ background: #27c93f22;
169
+ color: #27c93f;
170
+ }
171
+
172
+ #footer {
173
+ margin-top: 14px;
174
+ color: #444c56;
175
+ font-size: 11px;
176
+ text-align: center;
177
+ line-height: 1.8;
178
+ }
179
+ </style>
180
+ </head>
181
+
182
+ <body>
183
+
184
+ <h1>⬛ Ubuntu 24.04 LTS — Web Terminal</h1>
185
+
186
+ <div id="term-wrap">
187
+
188
+ <div id="titlebar">
189
+ <div class="dot r"></div>
190
+ <div class="dot y"></div>
191
+ <div class="dot g"></div>
192
+
193
+ <span id="title-text">ubuntu@space: ~</span>
194
+
195
+ <button id="copy-btn">
196
+ ⎘ Copy Selection
197
+ </button>
198
+ </div>
199
+
200
+ <div id="status-bar">
201
+ <div id="status-dot"></div>
202
+
203
+ <span id="status-text">
204
+ Connecting...
205
+ </span>
206
+
207
+ <button id="reconnect-btn">
208
+ ↺ Reconnect
209
+ </button>
210
+ </div>
211
+
212
+ <div id="terminal-container"></div>
213
+ </div>
214
+
215
+ <div id="ctx-menu">
216
+ <button id="ctx-copy">⎘ Copy</button>
217
+ <button id="ctx-paste">⎙ Paste</button>
218
+ <button id="ctx-clear">✕ Clear</button>
219
+ </div>
220
+
221
+ <div id="footer">
222
+ Real Ubuntu shell running on Hugging Face Space
223
+ </div>
224
+
225
+ <script>
226
+ let ws = null;
227
+ let reconnectTimeout = null;
228
+
229
+ const term = new Terminal({
230
+ cursorBlink: true,
231
+ fontSize: 13,
232
+ fontFamily: "'JetBrains Mono', monospace",
233
+ allowTransparency: false,
234
+ scrollback: 3000,
235
+ disableStdin: false,
236
+ macOptionIsMeta: true,
237
+ theme: {
238
+ background: '#0d1117',
239
+ foreground: '#e6edf3',
240
+ cursor: '#27c93f'
241
+ }
242
+ });
243
+
244
+ const fitAddon = new FitAddon.FitAddon();
245
+
246
+ term.loadAddon(fitAddon);
247
+
248
+ term.open(document.getElementById('terminal-container'));
249
+
250
+ fitAddon.fit();
251
+
252
+ term.focus();
253
+
254
+ const statusDot = document.getElementById('status-dot');
255
+ const statusText = document.getElementById('status-text');
256
+ const reconnectBtn = document.getElementById('reconnect-btn');
257
+
258
+ function setStatus(connected, text) {
259
+ statusDot.className = connected ? 'connected' : '';
260
+ statusText.textContent = text;
261
+
262
+ reconnectBtn.className = connected ? '' : 'visible';
263
+ }
264
+
265
+ function getWsUrl() {
266
+ const protocol = location.protocol === 'https:' ? 'wss' : 'ws';
267
+ return `${protocol}://${location.host}/ws`;
268
+ }
269
+
270
+ function connect() {
271
+
272
+ if (ws) {
273
+ ws.close();
274
+ ws = null;
275
+ }
276
+
277
+ setStatus(false, 'Connecting...');
278
+
279
+ ws = new WebSocket(getWsUrl());
280
+
281
+ ws.onopen = () => {
282
+ setStatus(true, 'Connected');
283
+ sendResize();
284
+ term.focus();
285
+ };
286
+
287
+ ws.onmessage = (event) => {
288
+ term.write(event.data);
289
+ };
290
+
291
+ ws.onerror = () => {
292
+ setStatus(false, 'Connection error');
293
+ };
294
+
295
+ ws.onclose = () => {
296
+ setStatus(false, 'Disconnected');
297
+
298
+ clearTimeout(reconnectTimeout);
299
+
300
+ reconnectTimeout = setTimeout(() => {
301
+ connect();
302
+ }, 2000);
303
+ };
304
+ }
305
+
306
+ function sendResize() {
307
+
308
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
309
+ return;
310
+ }
311
+
312
+ ws.send(JSON.stringify({
313
+ type: 'resize',
314
+ cols: term.cols,
315
+ rows: term.rows
316
+ }));
317
+ }
318
+
319
+ term.onData((data) => {
320
+
321
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
322
+ return;
323
+ }
324
+
325
+ if (typeof data !== 'string') {
326
+ return;
327
+ }
328
+
329
+ ws.send(JSON.stringify({
330
+ type: 'input',
331
+ data: data
332
+ }));
333
+ });
334
+
335
+ term.onResize(() => {
336
+ sendResize();
337
+ });
338
+
339
+ window.addEventListener('resize', () => {
340
+ fitAddon.fit();
341
+ sendResize();
342
+ });
343
+
344
+ reconnectBtn.addEventListener('click', connect);
345
+
346
+ document.getElementById('copy-btn').addEventListener('click', async () => {
347
+
348
+ const selection = term.getSelection();
349
+
350
+ if (!selection) return;
351
+
352
+ await navigator.clipboard.writeText(selection);
353
+
354
+ const btn = document.getElementById('copy-btn');
355
+
356
+ btn.textContent = '✓ Copied';
357
+
358
+ setTimeout(() => {
359
+ btn.textContent = '⎘ Copy Selection';
360
+ }, 1500);
361
+ });
362
+
363
+ const ctxMenu = document.getElementById('ctx-menu');
364
+
365
+ document.getElementById('terminal-container')
366
+ .addEventListener('contextmenu', (e) => {
367
+
368
+ e.preventDefault();
369
+
370
+ ctxMenu.style.display = 'block';
371
+
372
+ ctxMenu.style.left = `${e.pageX}px`;
373
+ ctxMenu.style.top = `${e.pageY}px`;
374
+ });
375
+
376
+ document.addEventListener('click', () => {
377
+ ctxMenu.style.display = 'none';
378
+ });
379
+
380
+ document.getElementById('ctx-copy')
381
+ .addEventListener('click', async () => {
382
+
383
+ const selection = term.getSelection();
384
+
385
+ if (!selection) return;
386
+
387
+ await navigator.clipboard.writeText(selection);
388
+ });
389
+
390
+ document.getElementById('ctx-paste')
391
+ .addEventListener('click', async () => {
392
+
393
+ try {
394
+
395
+ const text = await navigator.clipboard.readText();
396
+
397
+ term.paste(text);
398
+
399
+ } catch (err) {
400
+ console.error(err);
401
+ }
402
+ });
403
+
404
+ document.getElementById('ctx-clear')
405
+ .addEventListener('click', () => {
406
+ term.clear();
407
+ });
408
+
409
+ // Prevent duplicate paste bubbling
410
+ setTimeout(() => {
411
+
412
+ if (term.textarea) {
413
+
414
+ term.textarea.addEventListener('paste', (e) => {
415
+ e.stopPropagation();
416
+ });
417
+ }
418
+
419
+ }, 500);
420
+
421
+ connect();
422
+ </script>
423
+
424
+ </body>
425
+ </html>