Spaces:
Paused
Paused
opencode commited on
Commit ·
fd7e22f
1
Parent(s): 6f05fb8
桌面分頁支援上傳到 ~/Desktop
Browse files
README.md
CHANGED
|
@@ -71,7 +71,7 @@ OpenCode 在啟動時安裝,並重設使用者層級設定為預設值。
|
|
| 71 |
bracketed paste,與一般終端機一致。
|
| 72 |
- 檔案瀏覽器可瀏覽/下載伺服器上的**任何路徑**(含 `/tmp`、`/`),不再有白名單限制;
|
| 73 |
選取資料夾後下載會即時串流打包成 `.tar.gz`,不會一次載入記憶體。
|
| 74 |
-
- 桌面分頁
|
| 75 |
- 除錯端點:`GET /api/desktop/status`、`POST /api/desktop/restart`(程序 pid、VNC port、
|
| 76 |
健康狀態);桌面 log 在 `/tmp/webterm-desktop.log`。
|
| 77 |
- 可用 Space 變數覆寫:`DESKTOP_DISPLAY`(預設 `:99`)、`DESKTOP_GEOMETRY`
|
|
|
|
| 71 |
bracketed paste,與一般終端機一致。
|
| 72 |
- 檔案瀏覽器可瀏覽/下載伺服器上的**任何路徑**(含 `/tmp`、`/`),不再有白名單限制;
|
| 73 |
選取資料夾後下載會即時串流打包成 `.tar.gz`,不會一次載入記憶體。
|
| 74 |
+
- 桌面分頁的「上傳」會把檔案存到 `~/Desktop`(同名檔案自動加序號),上傳後可直接在 LXDE 桌面看到。
|
| 75 |
- 除錯端點:`GET /api/desktop/status`、`POST /api/desktop/restart`(程序 pid、VNC port、
|
| 76 |
健康狀態);桌面 log 在 `/tmp/webterm-desktop.log`。
|
| 77 |
- 可用 Space 變數覆寫:`DESKTOP_DISPLAY`(預設 `:99`)、`DESKTOP_GEOMETRY`
|
app.py
CHANGED
|
@@ -20,7 +20,7 @@ from urllib.parse import quote
|
|
| 20 |
|
| 21 |
import spaces
|
| 22 |
import httpx
|
| 23 |
-
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Request
|
| 24 |
from fastapi.responses import HTMLResponse, FileResponse, JSONResponse, StreamingResponse, Response
|
| 25 |
import uvicorn
|
| 26 |
|
|
@@ -488,8 +488,8 @@ function activate(id) {
|
|
| 488 |
activeId = id;
|
| 489 |
saveActive();
|
| 490 |
const current = tabs.find(t => t.id === id);
|
| 491 |
-
uploadBtn.disabled =
|
| 492 |
-
uploadBtn.title =
|
| 493 |
tabs.forEach(t => {
|
| 494 |
const on = t.id === id;
|
| 495 |
t.el.classList.toggle('active', on);
|
|
@@ -598,10 +598,7 @@ addBtn.onclick = async () => {
|
|
| 598 |
|
| 599 |
uploadBtn.onclick = () => {
|
| 600 |
const t = activeTab();
|
| 601 |
-
if (t && t.type === 'desktop')
|
| 602 |
-
setAppStatus('桌面分頁請直接在 LXDE 內使用檔案管理員或瀏覽器操作檔案', 'warning');
|
| 603 |
-
return;
|
| 604 |
-
}
|
| 605 |
fileInput.click();
|
| 606 |
};
|
| 607 |
fileInput.onchange = (e) => {
|
|
@@ -616,6 +613,21 @@ fileInput.onchange = (e) => {
|
|
| 616 |
return;
|
| 617 |
}
|
| 618 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 619 |
const reader = new FileReader();
|
| 620 |
reader.onload = (evt) => {
|
| 621 |
const base64Data = evt.target.result.split(',')[1];
|
|
@@ -930,6 +942,43 @@ async def api_desktop_restart():
|
|
| 930 |
return {"ok": ok, **desktop_manager.status()}
|
| 931 |
|
| 932 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 933 |
@app.get("/novnc/{asset_path:path}")
|
| 934 |
async def novnc_asset(asset_path: str):
|
| 935 |
"""把系統的 noVNC 靜態檔(apt: novnc)提供給瀏覽器;缺少時前端會改用 CDN。"""
|
|
|
|
| 20 |
|
| 21 |
import spaces
|
| 22 |
import httpx
|
| 23 |
+
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Request, File, UploadFile
|
| 24 |
from fastapi.responses import HTMLResponse, FileResponse, JSONResponse, StreamingResponse, Response
|
| 25 |
import uvicorn
|
| 26 |
|
|
|
|
| 488 |
activeId = id;
|
| 489 |
saveActive();
|
| 490 |
const current = tabs.find(t => t.id === id);
|
| 491 |
+
uploadBtn.disabled = false;
|
| 492 |
+
uploadBtn.title = current && current.type === 'desktop' ? '上傳檔案到桌面(~/Desktop)' : 'Upload to current directory';
|
| 493 |
tabs.forEach(t => {
|
| 494 |
const on = t.id === id;
|
| 495 |
t.el.classList.toggle('active', on);
|
|
|
|
| 598 |
|
| 599 |
uploadBtn.onclick = () => {
|
| 600 |
const t = activeTab();
|
| 601 |
+
if (t && t.type === 'desktop') setAppStatus('選擇要上傳到桌面的檔案(~/Desktop)', 'info');
|
|
|
|
|
|
|
|
|
|
| 602 |
fileInput.click();
|
| 603 |
};
|
| 604 |
fileInput.onchange = (e) => {
|
|
|
|
| 613 |
return;
|
| 614 |
}
|
| 615 |
|
| 616 |
+
if (t.type === 'desktop') {
|
| 617 |
+
const body = new FormData();
|
| 618 |
+
body.append('file', file);
|
| 619 |
+
setAppStatus(`正在上傳 ${file.name} 到桌面…`, 'info');
|
| 620 |
+
fetch('/api/desktop/upload', {method: 'POST', body})
|
| 621 |
+
.then(async res => {
|
| 622 |
+
const result = await res.json().catch(() => ({}));
|
| 623 |
+
if (!res.ok) throw new Error(result.error || '上傳失敗');
|
| 624 |
+
setAppStatus(`${file.name} 已上傳到桌面`, 'success');
|
| 625 |
+
})
|
| 626 |
+
.catch(err => setAppStatus('上傳失敗:' + err.message, 'error'));
|
| 627 |
+
fileInput.value = '';
|
| 628 |
+
return;
|
| 629 |
+
}
|
| 630 |
+
|
| 631 |
const reader = new FileReader();
|
| 632 |
reader.onload = (evt) => {
|
| 633 |
const base64Data = evt.target.result.split(',')[1];
|
|
|
|
| 942 |
return {"ok": ok, **desktop_manager.status()}
|
| 943 |
|
| 944 |
|
| 945 |
+
@app.post("/api/desktop/upload")
|
| 946 |
+
async def api_desktop_upload(file: UploadFile = File(...)):
|
| 947 |
+
"""把檔案上傳到共用桌面的 ~/Desktop,讓 LXDE 內可直接看到。"""
|
| 948 |
+
name = os.path.basename(file.filename or "").strip() or "uploaded_file"
|
| 949 |
+
if name in (".", "..") or "\x00" in name:
|
| 950 |
+
return JSONResponse({"error": "invalid filename"}, status_code=400)
|
| 951 |
+
upload_dir = os.path.join(HOME, "Desktop")
|
| 952 |
+
os.makedirs(upload_dir, exist_ok=True)
|
| 953 |
+
safe = os.path.join(upload_dir, name)
|
| 954 |
+
stem, ext = os.path.splitext(name)
|
| 955 |
+
counter = 1
|
| 956 |
+
while os.path.exists(safe):
|
| 957 |
+
safe = os.path.join(upload_dir, f"{stem} ({counter}){ext}")
|
| 958 |
+
counter += 1
|
| 959 |
+
limit = 200 * 1024 * 1024
|
| 960 |
+
written = 0
|
| 961 |
+
try:
|
| 962 |
+
with open(safe, "wb") as out:
|
| 963 |
+
while True:
|
| 964 |
+
chunk = await file.read(1 << 20)
|
| 965 |
+
if not chunk:
|
| 966 |
+
break
|
| 967 |
+
written += len(chunk)
|
| 968 |
+
if written > limit:
|
| 969 |
+
raise ValueError("檔案超過 200MB 上限")
|
| 970 |
+
out.write(chunk)
|
| 971 |
+
except Exception as exc:
|
| 972 |
+
try:
|
| 973 |
+
os.remove(safe)
|
| 974 |
+
except OSError:
|
| 975 |
+
pass
|
| 976 |
+
return JSONResponse({"error": str(exc)}, status_code=413 if written > limit else 500)
|
| 977 |
+
finally:
|
| 978 |
+
await file.close()
|
| 979 |
+
return {"ok": True, "path": safe, "size": written}
|
| 980 |
+
|
| 981 |
+
|
| 982 |
@app.get("/novnc/{asset_path:path}")
|
| 983 |
async def novnc_asset(asset_path: str):
|
| 984 |
"""把系統的 noVNC 靜態檔(apt: novnc)提供給瀏覽器;缺少時前端會改用 CDN。"""
|