修复系统接口会话丢失与SPA回退
This commit is contained in:
1 parent
805a2c1050
commit
df97f71cc5
2 files changed
+335
-334
No files matched your search
+334
-333
@@ -1,19 +1,26 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import re
|
||||
import tempfile
|
||||
import os
|
||||
import tarfile
|
||||
from contextlib import asynccontextmanager
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
from typing import List, AsyncIterator
|
||||
from urllib.parse import quote
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
|
||||
from fastapi.responses import StreamingResponse
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.api.dependencies import AppState, get_current_user, get_state
|
||||
from sqlalchemy import select
|
||||
|
||||
from app.api.dependencies import AppState, get_current_user, get_state, get_db
|
||||
from app.models.connection import Connection
|
||||
from app.models.session import SessionRecord
|
||||
from app.services.crypto import CryptoService, EncryptedPayload
|
||||
|
||||
router = APIRouter(prefix="/system", tags=["system"])
|
||||
|
||||
@@ -153,11 +160,48 @@ def build_upload_result(
|
||||
return UploadResult(uploaded=uploaded, skipped=skipped, failed=failed, errors=errors or []).model_dump()
|
||||
|
||||
|
||||
async def run_ssh_command(session, command: str, timeout: float = 5.0) -> str:
|
||||
@asynccontextmanager
|
||||
async def resolve_ssh_client(
|
||||
session_id: str,
|
||||
state: AppState,
|
||||
user,
|
||||
db,
|
||||
) -> AsyncIterator:
|
||||
session = state.session_manager.get_session(session_id)
|
||||
if session:
|
||||
yield session.client
|
||||
return
|
||||
|
||||
record = db.execute(
|
||||
select(SessionRecord).where(SessionRecord.id == session_id, SessionRecord.user_id == user.id)
|
||||
).scalar_one_or_none()
|
||||
if not record or record.status != "active":
|
||||
raise HTTPException(status_code=404, detail="会话不存在")
|
||||
|
||||
conn = db.execute(
|
||||
select(Connection).where(Connection.id == record.connection_id, Connection.user_id == user.id)
|
||||
).scalar_one_or_none()
|
||||
if not conn:
|
||||
raise HTTPException(status_code=404, detail="连接不存在")
|
||||
|
||||
crypto = CryptoService(state.config.secret_keys)
|
||||
auth_data = json.loads(conn.auth_data)
|
||||
decrypted = crypto.decrypt(EncryptedPayload(nonce=auth_data["nonce"], ciphertext=auth_data["ciphertext"]))
|
||||
auth_payload = json.loads(decrypted)
|
||||
|
||||
client = await state.session_manager.connect_client(conn, auth_payload)
|
||||
try:
|
||||
yield client
|
||||
finally:
|
||||
client.close()
|
||||
|
||||
|
||||
async def run_ssh_command(session_or_client, command: str, timeout: float = 5.0) -> str:
|
||||
"""在 SSH 会话中执行命令并返回输出"""
|
||||
try:
|
||||
client = getattr(session_or_client, "client", session_or_client)
|
||||
result = await asyncio.wait_for(
|
||||
session.client.run(command, check=False),
|
||||
client.run(command, check=False),
|
||||
timeout=timeout
|
||||
)
|
||||
return result.stdout or ""
|
||||
@@ -401,74 +445,62 @@ async def collect_system_overview(session) -> SystemOverview:
|
||||
async def get_system_stats(
|
||||
session_id: str,
|
||||
state: AppState = Depends(get_state),
|
||||
_user: dict = Depends(get_current_user)
|
||||
user=Depends(get_current_user),
|
||||
db=Depends(get_db),
|
||||
) -> SystemStats:
|
||||
"""获取远程服务器系统状态信息(CPU、内存、交换区)"""
|
||||
session = state.session_manager.get_session(session_id)
|
||||
if not session:
|
||||
raise HTTPException(status_code=404, detail="会话不存在")
|
||||
async with resolve_ssh_client(session_id, state, user, db) as client:
|
||||
stat1, meminfo, cpuinfo = await asyncio.gather(
|
||||
run_ssh_command(client, "cat /proc/stat"),
|
||||
run_ssh_command(client, "cat /proc/meminfo"),
|
||||
run_ssh_command(client, "cat /proc/cpuinfo"),
|
||||
)
|
||||
|
||||
# 骞惰鑾峰彇绗竴娆℃暟鎹?
|
||||
stat1, meminfo, cpuinfo = await asyncio.gather(
|
||||
run_ssh_command(session, "cat /proc/stat"),
|
||||
run_ssh_command(session, "cat /proc/meminfo"),
|
||||
run_ssh_command(session, "cat /proc/cpuinfo"),
|
||||
)
|
||||
interval = 0.3
|
||||
await asyncio.sleep(interval)
|
||||
stat2 = await run_ssh_command(client, "cat /proc/stat")
|
||||
|
||||
# 等待一小段时间再次获取 CPU 统计
|
||||
interval = 0.3
|
||||
await asyncio.sleep(interval)
|
||||
stat2 = await run_ssh_command(session, "cat /proc/stat")
|
||||
memory, swap = parse_memory_info(meminfo)
|
||||
cpu = parse_cpu_info(stat1, stat2, cpuinfo)
|
||||
|
||||
# 解析数据
|
||||
memory, swap = parse_memory_info(meminfo)
|
||||
cpu = parse_cpu_info(stat1, stat2, cpuinfo)
|
||||
|
||||
return SystemStats(cpu=cpu, memory=memory, swap=swap)
|
||||
return SystemStats(cpu=cpu, memory=memory, swap=swap)
|
||||
|
||||
|
||||
@router.get("/network/{session_id}", response_model=NetworkInfo)
|
||||
async def get_network_stats(
|
||||
session_id: str,
|
||||
state: AppState = Depends(get_state),
|
||||
_user: dict = Depends(get_current_user)
|
||||
user=Depends(get_current_user),
|
||||
db=Depends(get_db),
|
||||
) -> NetworkInfo:
|
||||
"""获取远程服务器网络速度"""
|
||||
session = state.session_manager.get_session(session_id)
|
||||
if not session:
|
||||
raise HTTPException(status_code=404, detail="会话不存在")
|
||||
async with resolve_ssh_client(session_id, state, user, db) as client:
|
||||
net1 = await run_ssh_command(client, "cat /proc/net/dev")
|
||||
interval = 0.5
|
||||
await asyncio.sleep(interval)
|
||||
net2 = await run_ssh_command(client, "cat /proc/net/dev")
|
||||
|
||||
# 获取两次网络统计
|
||||
net1 = await run_ssh_command(session, "cat /proc/net/dev")
|
||||
interval = 0.5
|
||||
await asyncio.sleep(interval)
|
||||
net2 = await run_ssh_command(session, "cat /proc/net/dev")
|
||||
|
||||
return parse_network_stats(net1, net2, interval)
|
||||
return parse_network_stats(net1, net2, interval)
|
||||
|
||||
|
||||
@router.get("/processes/{session_id}", response_model=ProcessList)
|
||||
async def get_processes(
|
||||
session_id: str,
|
||||
state: AppState = Depends(get_state),
|
||||
_user: dict = Depends(get_current_user)
|
||||
user=Depends(get_current_user),
|
||||
db=Depends(get_db),
|
||||
) -> ProcessList:
|
||||
"""获取远程服务器进程列表"""
|
||||
session = state.session_manager.get_session(session_id)
|
||||
if not session:
|
||||
raise HTTPException(status_code=404, detail="会话不存在")
|
||||
async with resolve_ssh_client(session_id, state, user, db) as client:
|
||||
ps_output = await run_ssh_command(
|
||||
client,
|
||||
"ps aux 2>/dev/null || ps -ef 2>/dev/null",
|
||||
timeout=5.0
|
||||
)
|
||||
|
||||
# 使用 ps 命令获取进程信息
|
||||
ps_output = await run_ssh_command(
|
||||
session,
|
||||
"ps aux 2>/dev/null || ps -ef 2>/dev/null",
|
||||
timeout=5.0
|
||||
)
|
||||
|
||||
processes = parse_processes(ps_output)
|
||||
# 鎸夊唴瀛樻帓搴忓彇鍓?20
|
||||
processes.sort(key=lambda x: x.memory_bytes, reverse=True)
|
||||
return ProcessList(processes=processes[:20])
|
||||
processes = parse_processes(ps_output)
|
||||
processes.sort(key=lambda x: x.memory_bytes, reverse=True)
|
||||
return ProcessList(processes=processes[:20])
|
||||
|
||||
|
||||
def parse_ls_output(ls_output: str, base_path: str) -> List[FileInfo]:
|
||||
@@ -557,59 +589,52 @@ async def list_directory(
|
||||
session_id: str,
|
||||
path: str = "/",
|
||||
state: AppState = Depends(get_state),
|
||||
_user: dict = Depends(get_current_user)
|
||||
user=Depends(get_current_user),
|
||||
db=Depends(get_db),
|
||||
) -> DirectoryListing:
|
||||
"""列出远程服务器目录内容"""
|
||||
session = state.session_manager.get_session(session_id)
|
||||
if not session:
|
||||
raise HTTPException(status_code=404, detail="会话不存在")
|
||||
async with resolve_ssh_client(session_id, state, user, db) as client:
|
||||
clean_path = sanitize_shell_path(path)
|
||||
if not clean_path.startswith("/"):
|
||||
clean_path = "/" + clean_path
|
||||
|
||||
# 规范化路径,防止路径遍历攻击
|
||||
clean_path = sanitize_shell_path(path)
|
||||
if not clean_path.startswith("/"):
|
||||
clean_path = "/" + clean_path
|
||||
ls_output = await run_ssh_command(
|
||||
client,
|
||||
f"ls -la '{clean_path}' 2>/dev/null",
|
||||
timeout=10.0
|
||||
)
|
||||
|
||||
# 使用 ls -la 获取目录内容
|
||||
ls_output = await run_ssh_command(
|
||||
session,
|
||||
f"ls -la '{clean_path}' 2>/dev/null",
|
||||
timeout=10.0
|
||||
)
|
||||
if not ls_output:
|
||||
raise HTTPException(status_code=404, detail="目录不存在或无权访问")
|
||||
|
||||
if not ls_output:
|
||||
raise HTTPException(status_code=404, detail="目录不存在或无权访问")
|
||||
|
||||
files = parse_ls_output(ls_output, clean_path)
|
||||
return DirectoryListing(path=clean_path, files=files)
|
||||
files = parse_ls_output(ls_output, clean_path)
|
||||
return DirectoryListing(path=clean_path, files=files)
|
||||
|
||||
|
||||
@router.get("/disks/{session_id}", response_model=DiskList)
|
||||
async def get_disks(
|
||||
session_id: str,
|
||||
state: AppState = Depends(get_state),
|
||||
_user: dict = Depends(get_current_user)
|
||||
user=Depends(get_current_user),
|
||||
db=Depends(get_db),
|
||||
) -> DiskList:
|
||||
"""获取磁盘挂载信息"""
|
||||
session = state.session_manager.get_session(session_id)
|
||||
if not session:
|
||||
raise HTTPException(status_code=404, detail="会话不存在")
|
||||
async with resolve_ssh_client(session_id, state, user, db) as client:
|
||||
df_output = await run_ssh_command(client, "df -B1 | grep '^/'", timeout=5.0)
|
||||
|
||||
df_output = await run_ssh_command(session, "df -B1 | grep '^/'", timeout=5.0)
|
||||
|
||||
return DiskList(disks=parse_disks(df_output))
|
||||
return DiskList(disks=parse_disks(df_output))
|
||||
|
||||
|
||||
@router.get("/overview/{session_id}", response_model=SystemOverview)
|
||||
async def get_system_overview(
|
||||
session_id: str,
|
||||
state: AppState = Depends(get_state),
|
||||
_user: dict = Depends(get_current_user)
|
||||
user=Depends(get_current_user),
|
||||
db=Depends(get_db),
|
||||
) -> SystemOverview:
|
||||
"""一次返回系统状态、网络、进程和磁盘信息。"""
|
||||
session = state.session_manager.get_session(session_id)
|
||||
if not session:
|
||||
raise HTTPException(status_code=404, detail="会话不存在")
|
||||
return await collect_system_overview(session)
|
||||
async with resolve_ssh_client(session_id, state, user, db) as client:
|
||||
return await collect_system_overview(client)
|
||||
|
||||
|
||||
@router.get("/file/{session_id}")
|
||||
@@ -618,24 +643,21 @@ async def read_file(
|
||||
path: str,
|
||||
force: bool = False,
|
||||
state: AppState = Depends(get_state),
|
||||
_user: dict = Depends(get_current_user)
|
||||
user=Depends(get_current_user),
|
||||
db=Depends(get_db),
|
||||
) -> dict:
|
||||
"""读取文件内容"""
|
||||
session = state.session_manager.get_session(session_id)
|
||||
if not session:
|
||||
raise HTTPException(status_code=404, detail="会话不存在")
|
||||
async with resolve_ssh_client(session_id, state, user, db) as client:
|
||||
clean_path = sanitize_shell_path(path)
|
||||
|
||||
clean_path = sanitize_shell_path(path)
|
||||
size_check = await run_ssh_command(client, f"stat -c %s '{clean_path}' 2>/dev/null || echo 0")
|
||||
size = int(size_check.strip() or 0)
|
||||
|
||||
# 检查文件大小
|
||||
size_check = await run_ssh_command(session, f"stat -c %s '{clean_path}' 2>/dev/null || echo 0")
|
||||
size = int(size_check.strip() or 0)
|
||||
if size > 1024 * 1024 and not force:
|
||||
return {"size": size, "too_large": True}
|
||||
|
||||
if size > 1024 * 1024 and not force:
|
||||
return {"size": size, "too_large": True}
|
||||
|
||||
content = await run_ssh_command(session, f"cat '{clean_path}' 2>/dev/null", timeout=30.0)
|
||||
return {"content": content, "size": size}
|
||||
content = await run_ssh_command(client, f"cat '{clean_path}' 2>/dev/null", timeout=30.0)
|
||||
return {"content": content, "size": size}
|
||||
|
||||
|
||||
@router.post("/file/{session_id}")
|
||||
@@ -644,26 +666,24 @@ async def write_file(
|
||||
path: str,
|
||||
payload: FileContent,
|
||||
state: AppState = Depends(get_state),
|
||||
_user: dict = Depends(get_current_user)
|
||||
user=Depends(get_current_user),
|
||||
db=Depends(get_db),
|
||||
):
|
||||
"""写入文件内容"""
|
||||
session = state.session_manager.get_session(session_id)
|
||||
if not session:
|
||||
raise HTTPException(status_code=404, detail="会话不存在")
|
||||
async with resolve_ssh_client(session_id, state, user, db) as client:
|
||||
clean_path = sanitize_shell_path(path)
|
||||
|
||||
clean_path = sanitize_shell_path(path)
|
||||
with tempfile.NamedTemporaryFile(mode='w', delete=False) as tmp:
|
||||
tmp.write(payload.content)
|
||||
tmp_path = tmp.name
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode='w', delete=False) as tmp:
|
||||
tmp.write(payload.content)
|
||||
tmp_path = tmp.name
|
||||
try:
|
||||
async with client.start_sftp_client() as sftp:
|
||||
await sftp.put(tmp_path, clean_path)
|
||||
finally:
|
||||
os.unlink(tmp_path)
|
||||
|
||||
try:
|
||||
async with session.client.start_sftp_client() as sftp:
|
||||
await sftp.put(tmp_path, clean_path)
|
||||
finally:
|
||||
os.unlink(tmp_path)
|
||||
|
||||
return {"status": "ok"}
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@router.post("/upload/{session_id}")
|
||||
@@ -673,46 +693,44 @@ async def upload_file(
|
||||
compress: bool = False,
|
||||
file: UploadFile = File(...),
|
||||
state: AppState = Depends(get_state),
|
||||
_user: dict = Depends(get_current_user)
|
||||
user=Depends(get_current_user),
|
||||
db=Depends(get_db),
|
||||
):
|
||||
"""上传文件到远程服务器"""
|
||||
session = state.session_manager.get_session(session_id)
|
||||
if not session:
|
||||
raise HTTPException(status_code=404, detail="会话不存在")
|
||||
async with resolve_ssh_client(session_id, state, user, db) as client:
|
||||
clean_path = sanitize_shell_path(path)
|
||||
|
||||
clean_path = sanitize_shell_path(path)
|
||||
safe_filename = sanitize_upload_filename(file.filename)
|
||||
|
||||
safe_filename = sanitize_upload_filename(file.filename)
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
local_path = ensure_within_directory(tmpdir, os.path.join(tmpdir, safe_filename))
|
||||
content = await file.read()
|
||||
with open(local_path, "wb") as f:
|
||||
f.write(content)
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
local_path = ensure_within_directory(tmpdir, os.path.join(tmpdir, safe_filename))
|
||||
content = await file.read()
|
||||
with open(local_path, "wb") as f:
|
||||
f.write(content)
|
||||
errors: List[str] = []
|
||||
uploaded = 0
|
||||
|
||||
errors: List[str] = []
|
||||
uploaded = 0
|
||||
try:
|
||||
if compress:
|
||||
tar_path = os.path.join(tmpdir, f"{safe_filename}.tar.gz")
|
||||
create_tar_gz(tar_path, local_path, safe_filename)
|
||||
|
||||
try:
|
||||
if compress:
|
||||
tar_path = os.path.join(tmpdir, f"{safe_filename}.tar.gz")
|
||||
create_tar_gz(tar_path, local_path, safe_filename)
|
||||
async with client.start_sftp_client() as sftp:
|
||||
await sftp.put(tar_path, f"{clean_path}/{safe_filename}.tar.gz")
|
||||
|
||||
async with session.client.start_sftp_client() as sftp:
|
||||
await sftp.put(tar_path, f"{clean_path}/{safe_filename}.tar.gz")
|
||||
|
||||
await run_ssh_command(
|
||||
session,
|
||||
f"cd '{clean_path}' && tar -xzf '{safe_filename}.tar.gz' && rm -f '{safe_filename}.tar.gz'",
|
||||
timeout=30.0,
|
||||
)
|
||||
uploaded = 1
|
||||
else:
|
||||
async with session.client.start_sftp_client() as sftp:
|
||||
await sftp.put(local_path, f"{clean_path}/{safe_filename}")
|
||||
uploaded = 1
|
||||
except Exception as exc:
|
||||
errors.append(f"{safe_filename}: {exc}")
|
||||
await run_ssh_command(
|
||||
client,
|
||||
f"cd '{clean_path}' && tar -xzf '{safe_filename}.tar.gz' && rm -f '{safe_filename}.tar.gz'",
|
||||
timeout=30.0,
|
||||
)
|
||||
uploaded = 1
|
||||
else:
|
||||
async with client.start_sftp_client() as sftp:
|
||||
await sftp.put(local_path, f"{clean_path}/{safe_filename}")
|
||||
uploaded = 1
|
||||
except Exception as exc:
|
||||
errors.append(f"{safe_filename}: {exc}")
|
||||
|
||||
if errors:
|
||||
return build_upload_result(uploaded=uploaded, failed=len(errors), errors=errors)
|
||||
@@ -726,37 +744,35 @@ async def upload_targz(
|
||||
path: str,
|
||||
file: UploadFile = File(...),
|
||||
state: AppState = Depends(get_state),
|
||||
_user: dict = Depends(get_current_user)
|
||||
user=Depends(get_current_user),
|
||||
db=Depends(get_db),
|
||||
):
|
||||
"""上传 tar.gz 压缩包并解压"""
|
||||
session = state.session_manager.get_session(session_id)
|
||||
if not session:
|
||||
raise HTTPException(status_code=404, detail="会话不存在")
|
||||
async with resolve_ssh_client(session_id, state, user, db) as client:
|
||||
clean_path = sanitize_shell_path(path)
|
||||
|
||||
clean_path = sanitize_shell_path(path)
|
||||
with tempfile.NamedTemporaryFile(suffix=".tar.gz", delete=False) as tmp:
|
||||
content = await file.read()
|
||||
tmp.write(content)
|
||||
tmp_path = tmp.name
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix=".tar.gz", delete=False) as tmp:
|
||||
content = await file.read()
|
||||
tmp.write(content)
|
||||
tmp_path = tmp.name
|
||||
errors: List[str] = []
|
||||
uploaded = 0
|
||||
|
||||
errors: List[str] = []
|
||||
uploaded = 0
|
||||
try:
|
||||
async with client.start_sftp_client() as sftp:
|
||||
await sftp.put(tmp_path, f"{clean_path}/upload.tar.gz")
|
||||
await run_ssh_command(client, f"cd '{clean_path}' && tar -xzf upload.tar.gz && rm -f upload.tar.gz", timeout=120.0)
|
||||
uploaded = 1
|
||||
except Exception as exc:
|
||||
errors.append(f"upload.tar.gz: {exc}")
|
||||
finally:
|
||||
os.unlink(tmp_path)
|
||||
|
||||
try:
|
||||
async with session.client.start_sftp_client() as sftp:
|
||||
await sftp.put(tmp_path, f"{clean_path}/upload.tar.gz")
|
||||
await run_ssh_command(session, f"cd '{clean_path}' && tar -xzf upload.tar.gz && rm -f upload.tar.gz", timeout=120.0)
|
||||
uploaded = 1
|
||||
except Exception as exc:
|
||||
errors.append(f"upload.tar.gz: {exc}")
|
||||
finally:
|
||||
os.unlink(tmp_path)
|
||||
if errors:
|
||||
return build_upload_result(uploaded=uploaded, failed=len(errors), errors=errors)
|
||||
|
||||
if errors:
|
||||
return build_upload_result(uploaded=uploaded, failed=len(errors), errors=errors)
|
||||
|
||||
return build_upload_result(uploaded=uploaded)
|
||||
return build_upload_result(uploaded=uploaded)
|
||||
|
||||
|
||||
@router.post("/upload-zip/{session_id}")
|
||||
@@ -765,37 +781,35 @@ async def upload_zip(
|
||||
path: str,
|
||||
file: UploadFile = File(...),
|
||||
state: AppState = Depends(get_state),
|
||||
_user: dict = Depends(get_current_user)
|
||||
user=Depends(get_current_user),
|
||||
db=Depends(get_db),
|
||||
):
|
||||
"""上传 zip 压缩包并解压"""
|
||||
session = state.session_manager.get_session(session_id)
|
||||
if not session:
|
||||
raise HTTPException(status_code=404, detail="会话不存在")
|
||||
async with resolve_ssh_client(session_id, state, user, db) as client:
|
||||
clean_path = sanitize_shell_path(path)
|
||||
|
||||
clean_path = sanitize_shell_path(path)
|
||||
with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmp:
|
||||
content = await file.read()
|
||||
tmp.write(content)
|
||||
tmp_path = tmp.name
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmp:
|
||||
content = await file.read()
|
||||
tmp.write(content)
|
||||
tmp_path = tmp.name
|
||||
errors: List[str] = []
|
||||
uploaded = 0
|
||||
|
||||
errors: List[str] = []
|
||||
uploaded = 0
|
||||
try:
|
||||
async with client.start_sftp_client() as sftp:
|
||||
await sftp.put(tmp_path, f"{clean_path}/upload.zip")
|
||||
await run_ssh_command(client, f"cd '{clean_path}' && unzip -q upload.zip && rm -f upload.zip", timeout=120.0)
|
||||
uploaded = 1
|
||||
except Exception as exc:
|
||||
errors.append(f"upload.zip: {exc}")
|
||||
finally:
|
||||
os.unlink(tmp_path)
|
||||
|
||||
try:
|
||||
async with session.client.start_sftp_client() as sftp:
|
||||
await sftp.put(tmp_path, f"{clean_path}/upload.zip")
|
||||
await run_ssh_command(session, f"cd '{clean_path}' && unzip -q upload.zip && rm -f upload.zip", timeout=120.0)
|
||||
uploaded = 1
|
||||
except Exception as exc:
|
||||
errors.append(f"upload.zip: {exc}")
|
||||
finally:
|
||||
os.unlink(tmp_path)
|
||||
if errors:
|
||||
return build_upload_result(uploaded=uploaded, failed=len(errors), errors=errors)
|
||||
|
||||
if errors:
|
||||
return build_upload_result(uploaded=uploaded, failed=len(errors), errors=errors)
|
||||
|
||||
return build_upload_result(uploaded=uploaded)
|
||||
return build_upload_result(uploaded=uploaded)
|
||||
|
||||
|
||||
@router.post("/upload-batch/{session_id}")
|
||||
@@ -806,72 +820,70 @@ async def upload_batch(
|
||||
concurrent: int = 3,
|
||||
files: List[UploadFile] = File(...),
|
||||
state: AppState = Depends(get_state),
|
||||
_user: dict = Depends(get_current_user)
|
||||
user=Depends(get_current_user),
|
||||
db=Depends(get_db),
|
||||
):
|
||||
"""批量上传文件(支持文件夹结构)"""
|
||||
session = state.session_manager.get_session(session_id)
|
||||
if not session:
|
||||
raise HTTPException(status_code=404, detail="会话不存在")
|
||||
async with resolve_ssh_client(session_id, state, user, db) as client:
|
||||
clean_path = sanitize_shell_path(path)
|
||||
|
||||
clean_path = sanitize_shell_path(path)
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
written_files = 0
|
||||
staged_files: List[tuple[str, str]] = []
|
||||
errors: List[str] = []
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
written_files = 0
|
||||
staged_files: List[tuple[str, str]] = []
|
||||
errors: List[str] = []
|
||||
for file in files:
|
||||
relative_path = sanitize_upload_relative_path(file.filename)
|
||||
file_path = ensure_within_directory(tmpdir, os.path.join(tmpdir, relative_path))
|
||||
os.makedirs(os.path.dirname(file_path), exist_ok=True)
|
||||
content = await file.read()
|
||||
with open(file_path, "wb") as f:
|
||||
f.write(content)
|
||||
|
||||
for file in files:
|
||||
relative_path = sanitize_upload_relative_path(file.filename)
|
||||
file_path = ensure_within_directory(tmpdir, os.path.join(tmpdir, relative_path))
|
||||
os.makedirs(os.path.dirname(file_path), exist_ok=True)
|
||||
content = await file.read()
|
||||
with open(file_path, "wb") as f:
|
||||
f.write(content)
|
||||
staged_files.append((relative_path, file_path))
|
||||
written_files += 1
|
||||
|
||||
staged_files.append((relative_path, file_path))
|
||||
written_files += 1
|
||||
if compress:
|
||||
tar_path = os.path.join(tmpdir, "upload.tar.gz")
|
||||
with tarfile.open(tar_path, "w:gz") as tar:
|
||||
for relative_path, file_path in staged_files:
|
||||
tar.add(file_path, arcname=relative_path)
|
||||
|
||||
if compress:
|
||||
tar_path = os.path.join(tmpdir, "upload.tar.gz")
|
||||
with tarfile.open(tar_path, "w:gz") as tar:
|
||||
for relative_path, file_path in staged_files:
|
||||
tar.add(file_path, arcname=relative_path)
|
||||
try:
|
||||
async with client.start_sftp_client() as sftp:
|
||||
await sftp.put(tar_path, f"{clean_path}/upload.tar.gz")
|
||||
|
||||
try:
|
||||
async with session.client.start_sftp_client() as sftp:
|
||||
await sftp.put(tar_path, f"{clean_path}/upload.tar.gz")
|
||||
await run_ssh_command(client, f"cd '{clean_path}' && tar -xzf upload.tar.gz && rm -f upload.tar.gz", timeout=60.0)
|
||||
except Exception as exc:
|
||||
errors.append(f"upload.tar.gz: {exc}")
|
||||
return build_upload_result(uploaded=0, failed=written_files, errors=errors)
|
||||
|
||||
await run_ssh_command(session, f"cd '{clean_path}' && tar -xzf upload.tar.gz && rm -f upload.tar.gz", timeout=60.0)
|
||||
except Exception as exc:
|
||||
errors.append(f"upload.tar.gz: {exc}")
|
||||
return build_upload_result(uploaded=0, failed=written_files, errors=errors)
|
||||
return build_upload_result(uploaded=written_files)
|
||||
|
||||
return build_upload_result(uploaded=written_files)
|
||||
uploaded = 0
|
||||
queue = list(staged_files)
|
||||
concurrency = max(1, min(concurrent, 8))
|
||||
semaphore = asyncio.Semaphore(concurrency)
|
||||
|
||||
uploaded = 0
|
||||
queue = list(staged_files)
|
||||
concurrency = max(1, min(concurrent, 8))
|
||||
semaphore = asyncio.Semaphore(concurrency)
|
||||
async def upload_one(relative_path: str, file_path: str) -> bool:
|
||||
remote_path = f"{clean_path}/{relative_path}"
|
||||
remote_dir = os.path.dirname(remote_path)
|
||||
|
||||
async def upload_one(relative_path: str, file_path: str) -> bool:
|
||||
remote_path = f"{clean_path}/{relative_path}"
|
||||
remote_dir = os.path.dirname(remote_path)
|
||||
try:
|
||||
async with semaphore:
|
||||
await run_ssh_command(client, f"mkdir -p '{remote_dir}'")
|
||||
async with client.start_sftp_client() as sftp:
|
||||
await sftp.put(file_path, remote_path)
|
||||
return True
|
||||
except Exception as exc:
|
||||
errors.append(f"{relative_path}: {exc}")
|
||||
return False
|
||||
|
||||
try:
|
||||
async with semaphore:
|
||||
await run_ssh_command(session, f"mkdir -p '{remote_dir}'")
|
||||
async with session.client.start_sftp_client() as sftp:
|
||||
await sftp.put(file_path, remote_path)
|
||||
return True
|
||||
except Exception as exc:
|
||||
errors.append(f"{relative_path}: {exc}")
|
||||
return False
|
||||
results = await asyncio.gather(*(upload_one(relative_path, file_path) for relative_path, file_path in queue))
|
||||
uploaded = sum(1 for ok in results if ok)
|
||||
|
||||
results = await asyncio.gather(*(upload_one(relative_path, file_path) for relative_path, file_path in queue))
|
||||
uploaded = sum(1 for ok in results if ok)
|
||||
|
||||
failed = len(errors)
|
||||
return build_upload_result(uploaded=uploaded, failed=failed, errors=errors)
|
||||
failed = len(errors)
|
||||
return build_upload_result(uploaded=uploaded, failed=failed, errors=errors)
|
||||
|
||||
|
||||
@router.post("/mkdir/{session_id}")
|
||||
@@ -879,15 +891,14 @@ async def make_directory(
|
||||
session_id: str,
|
||||
path: str,
|
||||
state: AppState = Depends(get_state),
|
||||
_user: dict = Depends(get_current_user)
|
||||
user=Depends(get_current_user),
|
||||
db=Depends(get_db),
|
||||
):
|
||||
"""创建目录"""
|
||||
session = state.session_manager.get_session(session_id)
|
||||
if not session:
|
||||
raise HTTPException(status_code=404, detail="会话不存在")
|
||||
clean_path = sanitize_shell_path(path)
|
||||
await run_ssh_command(session, f"mkdir -p '{clean_path}'")
|
||||
return {"status": "ok"}
|
||||
async with resolve_ssh_client(session_id, state, user, db) as client:
|
||||
clean_path = sanitize_shell_path(path)
|
||||
await run_ssh_command(client, f"mkdir -p '{clean_path}'")
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@router.post("/touch/{session_id}")
|
||||
@@ -895,15 +906,14 @@ async def touch_file(
|
||||
session_id: str,
|
||||
path: str,
|
||||
state: AppState = Depends(get_state),
|
||||
_user: dict = Depends(get_current_user)
|
||||
user=Depends(get_current_user),
|
||||
db=Depends(get_db),
|
||||
):
|
||||
"""创建文件"""
|
||||
session = state.session_manager.get_session(session_id)
|
||||
if not session:
|
||||
raise HTTPException(status_code=404, detail="会话不存在")
|
||||
clean_path = sanitize_shell_path(path)
|
||||
await run_ssh_command(session, f"touch '{clean_path}'")
|
||||
return {"status": "ok"}
|
||||
async with resolve_ssh_client(session_id, state, user, db) as client:
|
||||
clean_path = sanitize_shell_path(path)
|
||||
await run_ssh_command(client, f"touch '{clean_path}'")
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@router.post("/rename/{session_id}")
|
||||
@@ -912,16 +922,15 @@ async def rename_file(
|
||||
old_path: str,
|
||||
new_path: str,
|
||||
state: AppState = Depends(get_state),
|
||||
_user: dict = Depends(get_current_user)
|
||||
user=Depends(get_current_user),
|
||||
db=Depends(get_db),
|
||||
):
|
||||
"""重命名文件或目录"""
|
||||
session = state.session_manager.get_session(session_id)
|
||||
if not session:
|
||||
raise HTTPException(status_code=404, detail="会话不存在")
|
||||
clean_old = sanitize_shell_path(old_path)
|
||||
clean_new = sanitize_shell_path(new_path)
|
||||
await run_ssh_command(session, f"mv '{clean_old}' '{clean_new}'")
|
||||
return {"status": "ok"}
|
||||
async with resolve_ssh_client(session_id, state, user, db) as client:
|
||||
clean_old = sanitize_shell_path(old_path)
|
||||
clean_new = sanitize_shell_path(new_path)
|
||||
await run_ssh_command(client, f"mv '{clean_old}' '{clean_new}'")
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@router.post("/chmod/{session_id}")
|
||||
@@ -931,17 +940,16 @@ async def change_mode(
|
||||
mode: str,
|
||||
recursive: bool = False,
|
||||
state: AppState = Depends(get_state),
|
||||
_user: dict = Depends(get_current_user)
|
||||
user=Depends(get_current_user),
|
||||
db=Depends(get_db),
|
||||
):
|
||||
"""修改文件权限"""
|
||||
session = state.session_manager.get_session(session_id)
|
||||
if not session:
|
||||
raise HTTPException(status_code=404, detail="会话不存在")
|
||||
clean_path = sanitize_shell_path(path)
|
||||
clean_mode = sanitize_shell_path(mode)
|
||||
recursive_flag = "-R " if recursive else ""
|
||||
await run_ssh_command(session, f"chmod {recursive_flag}{clean_mode} '{clean_path}'")
|
||||
return {"status": "ok"}
|
||||
async with resolve_ssh_client(session_id, state, user, db) as client:
|
||||
clean_path = sanitize_shell_path(path)
|
||||
clean_mode = sanitize_shell_path(mode)
|
||||
recursive_flag = "-R " if recursive else ""
|
||||
await run_ssh_command(client, f"chmod {recursive_flag}{clean_mode} '{clean_path}'")
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@router.delete("/delete/{session_id}")
|
||||
@@ -949,15 +957,14 @@ async def delete_file(
|
||||
session_id: str,
|
||||
path: str,
|
||||
state: AppState = Depends(get_state),
|
||||
_user: dict = Depends(get_current_user)
|
||||
user=Depends(get_current_user),
|
||||
db=Depends(get_db),
|
||||
):
|
||||
"""删除文件或目录"""
|
||||
session = state.session_manager.get_session(session_id)
|
||||
if not session:
|
||||
raise HTTPException(status_code=404, detail="会话不存在")
|
||||
clean_path = sanitize_shell_path(path)
|
||||
await run_ssh_command(session, f"rm -rf '{clean_path}'")
|
||||
return {"status": "ok"}
|
||||
async with resolve_ssh_client(session_id, state, user, db) as client:
|
||||
clean_path = sanitize_shell_path(path)
|
||||
await run_ssh_command(client, f"rm -rf '{clean_path}'")
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@router.get("/download/{session_id}")
|
||||
@@ -965,71 +972,65 @@ async def download_file(
|
||||
session_id: str,
|
||||
path: str,
|
||||
state: AppState = Depends(get_state),
|
||||
_user: dict = Depends(get_current_user)
|
||||
user=Depends(get_current_user),
|
||||
db=Depends(get_db),
|
||||
):
|
||||
"""从远程服务器下载文件"""
|
||||
session = state.session_manager.get_session(session_id)
|
||||
if not session:
|
||||
raise HTTPException(status_code=404, detail="会话不存在")
|
||||
async with resolve_ssh_client(session_id, state, user, db) as client:
|
||||
clean_path = sanitize_shell_path(path)
|
||||
|
||||
clean_path = sanitize_shell_path(path)
|
||||
check_result = await run_ssh_command(client, f"[ -d '{clean_path}' ] && echo 'dir' || echo 'file'")
|
||||
is_dir = check_result.strip() == "dir"
|
||||
|
||||
# 检查是否为目录
|
||||
check_result = await run_ssh_command(session, f"[ -d '{clean_path}' ] && echo 'dir' || echo 'file'")
|
||||
is_dir = check_result.strip() == "dir"
|
||||
filename = os.path.basename(clean_path)
|
||||
|
||||
filename = os.path.basename(clean_path)
|
||||
if is_dir:
|
||||
tar_name = f"{filename}.tar.gz"
|
||||
remote_tar = f"/tmp/{tar_name}"
|
||||
await run_ssh_command(client, f"tar -czf {remote_tar} -C {os.path.dirname(clean_path)} {filename}", timeout=60.0)
|
||||
|
||||
if is_dir:
|
||||
# 压缩目录
|
||||
tar_name = f"{filename}.tar.gz"
|
||||
remote_tar = f"/tmp/{tar_name}"
|
||||
await run_ssh_command(session, f"tar -czf {remote_tar} -C {os.path.dirname(clean_path)} {filename}", timeout=60.0)
|
||||
with tempfile.NamedTemporaryFile(delete=False) as tmp:
|
||||
tmp_path = tmp.name
|
||||
|
||||
with tempfile.NamedTemporaryFile(delete=False) as tmp:
|
||||
tmp_path = tmp.name
|
||||
try:
|
||||
async with client.start_sftp_client() as sftp:
|
||||
await sftp.get(remote_tar, tmp_path)
|
||||
|
||||
try:
|
||||
async with session.client.start_sftp_client() as sftp:
|
||||
await sftp.get(remote_tar, tmp_path)
|
||||
await run_ssh_command(client, f"rm {remote_tar}")
|
||||
|
||||
await run_ssh_command(session, f"rm {remote_tar}")
|
||||
with open(tmp_path, "rb") as f:
|
||||
content = f.read()
|
||||
|
||||
with open(tmp_path, "rb") as f:
|
||||
content = f.read()
|
||||
encoded_filename = quote(tar_name)
|
||||
return StreamingResponse(
|
||||
iter([content]),
|
||||
media_type="application/gzip",
|
||||
headers={
|
||||
"Content-Disposition": f"attachment; filename*=UTF-8''{encoded_filename}",
|
||||
"Content-Length": str(len(content)),
|
||||
},
|
||||
)
|
||||
finally:
|
||||
os.unlink(tmp_path)
|
||||
else:
|
||||
with tempfile.NamedTemporaryFile(delete=False) as tmp:
|
||||
tmp_path = tmp.name
|
||||
|
||||
encoded_filename = quote(tar_name)
|
||||
return StreamingResponse(
|
||||
iter([content]),
|
||||
media_type="application/gzip",
|
||||
headers={
|
||||
"Content-Disposition": f"attachment; filename*=UTF-8''{encoded_filename}",
|
||||
"Content-Length": str(len(content)),
|
||||
},
|
||||
)
|
||||
finally:
|
||||
os.unlink(tmp_path)
|
||||
else:
|
||||
# 直接下载文件
|
||||
with tempfile.NamedTemporaryFile(delete=False) as tmp:
|
||||
tmp_path = tmp.name
|
||||
try:
|
||||
async with client.start_sftp_client() as sftp:
|
||||
await sftp.get(clean_path, tmp_path)
|
||||
|
||||
try:
|
||||
async with session.client.start_sftp_client() as sftp:
|
||||
await sftp.get(clean_path, tmp_path)
|
||||
|
||||
with open(tmp_path, "rb") as f:
|
||||
content = f.read()
|
||||
|
||||
encoded_filename = quote(filename)
|
||||
return StreamingResponse(
|
||||
iter([content]),
|
||||
media_type="application/octet-stream",
|
||||
headers={
|
||||
"Content-Disposition": f"attachment; filename*=UTF-8''{encoded_filename}",
|
||||
"Content-Length": str(len(content)),
|
||||
},
|
||||
)
|
||||
finally:
|
||||
os.unlink(tmp_path)
|
||||
with open(tmp_path, "rb") as f:
|
||||
content = f.read()
|
||||
|
||||
encoded_filename = quote(filename)
|
||||
return StreamingResponse(
|
||||
iter([content]),
|
||||
media_type="application/octet-stream",
|
||||
headers={
|
||||
"Content-Disposition": f"attachment; filename*=UTF-8''{encoded_filename}",
|
||||
"Content-Length": str(len(content)),
|
||||
},
|
||||
)
|
||||
finally:
|
||||
os.unlink(tmp_path)
|
||||
+1
-1
@@ -160,7 +160,7 @@ def create_app() -> FastAPI:
|
||||
if response.status_code != 404 or request.method != "GET":
|
||||
return response
|
||||
path = request.url.path
|
||||
if path.startswith("/api") or path.startswith("/auth") or path.startswith("/sessions"):
|
||||
if path.startswith("/api") or path.startswith("/auth") or path.startswith("/sessions") or path.startswith("/system"):
|
||||
return response
|
||||
return index_response
|
||||
|
||||
|
||||
Reference in new issue
Block a user