opencode commited on
Commit
fd7e22f
·
1 Parent(s): 6f05fb8

桌面分頁支援上傳到 ~/Desktop

Browse files
Files changed (2) hide show
  1. README.md +1 -1
  2. app.py +56 -7
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 = !!current && current.type === 'desktop';
492
- uploadBtn.title = uploadBtn.disabled ? '桌面分頁不支援上傳' : 'Upload to current directory';
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。"""