业务日志: 端点事件记录 (init/chunk/complete/ls/download/delete/quota + 客户端 IP); watchdog 防重复改 flock 锁 (pgrep 会误伤调用链)
This commit is contained in:
@@ -17,12 +17,17 @@ API 层 (api)
|
||||
|
||||
import base64
|
||||
import json
|
||||
import logging
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import Depends, FastAPI, Header, HTTPException, Request
|
||||
from fastapi.responses import FileResponse, JSONResponse
|
||||
|
||||
# 业务日志 (随 uvicorn 输出到 stderr -> /tmp/sz-server.log)
|
||||
log = logging.getLogger("sz")
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s [业务] %(message)s")
|
||||
|
||||
from db import ServerDB
|
||||
from receiver import Receiver, ReceiverError
|
||||
from settings import QUOTA_BYTES, STORAGE_ROOT, TMP_ROOT, TOKEN
|
||||
@@ -45,7 +50,6 @@ def verify_token(authorization: str | None = Header(default=None)) -> None:
|
||||
if authorization != f"Bearer {TOKEN}":
|
||||
raise HTTPException(status_code=401, detail="未授权: token 无效或缺失")
|
||||
|
||||
|
||||
def _enc_headers(rec: dict) -> dict[str, str]:
|
||||
"""下载响应头: 解密参数 + 卷数"""
|
||||
ep = json.dumps(rec["enc_params"], ensure_ascii=False)
|
||||
@@ -58,16 +62,20 @@ def _enc_headers(rec: dict) -> dict[str, str]:
|
||||
# ---------- 文件: ls / 逐卷下载 ----------
|
||||
|
||||
@app.get("/api/files", dependencies=[Depends(verify_token)])
|
||||
async def list_files():
|
||||
async def list_files(request: Request):
|
||||
"""文件列表 (ls)"""
|
||||
return {"files": db.list_files()}
|
||||
files = db.list_files()
|
||||
log.info("ls %s %s 个文件", request.client.host if request.client else "?", len(files))
|
||||
return {"files": files}
|
||||
|
||||
|
||||
@app.get("/api/quota", dependencies=[Depends(verify_token)])
|
||||
async def quota():
|
||||
async def quota(request: Request):
|
||||
"""用户空间配额: 已用 (files size 总和) / 配额 / 剩余"""
|
||||
used = db.sum_files_size()
|
||||
remain = max(QUOTA_BYTES - used, 0)
|
||||
log.info("quota %s 已用 %.1fMB/%.1fMB",
|
||||
request.client.host if request.client else "?", used / 1048576, QUOTA_BYTES / 1048576)
|
||||
return {
|
||||
"used_bytes": used,
|
||||
"quota_bytes": QUOTA_BYTES,
|
||||
@@ -77,7 +85,7 @@ async def quota():
|
||||
|
||||
|
||||
@app.delete("/api/files/{file_id}", dependencies=[Depends(verify_token)])
|
||||
async def delete_file(file_id: str):
|
||||
async def delete_file(file_id: str, request: Request):
|
||||
"""删除文件 (卷目录 + 入库记录)"""
|
||||
rec = db.get_file(file_id)
|
||||
if rec is None:
|
||||
@@ -89,13 +97,14 @@ async def delete_file(file_id: str):
|
||||
elif p.is_file():
|
||||
p.unlink(missing_ok=True)
|
||||
db.delete_file(file_id)
|
||||
log.info("delete %s %s", request.client.host if request.client else "?", file_id)
|
||||
except OSError as e:
|
||||
return JSONResponse(status_code=409, content={"error": f"删除失败: {e}"})
|
||||
return {"ok": True, "file_id": file_id}
|
||||
|
||||
|
||||
@app.get("/api/files/{file_id}/chunk/{idx}", dependencies=[Depends(verify_token)])
|
||||
async def download_chunk(file_id: str, idx: int):
|
||||
async def download_chunk(file_id: str, idx: int, request: Request):
|
||||
"""下载单卷密文 (零合并: 服务端不拼接, 客户端逐卷拉取本地合并)"""
|
||||
rec = db.get_file(file_id)
|
||||
if rec is None:
|
||||
@@ -105,6 +114,8 @@ async def download_chunk(file_id: str, idx: int):
|
||||
chunk_path = Path(rec["path"]) / f"chunk_{idx:04d}"
|
||||
if not chunk_path.exists():
|
||||
return JSONResponse(status_code=404, content={"error": f"卷文件缺失: {chunk_path}"})
|
||||
log.info("download %s %s 卷%d/%d", request.client.host if request.client else "?",
|
||||
file_id, idx, rec["chunk_count"])
|
||||
return FileResponse(
|
||||
chunk_path,
|
||||
filename=rec["file_name"],
|
||||
@@ -124,6 +135,8 @@ async def init_transfer(request: Request):
|
||||
try:
|
||||
init_json = await request.json()
|
||||
transfer_id = tasks.create(init_json)
|
||||
log.info("init %s %s卷 任务=%s", request.client.host if request.client else "?",
|
||||
init_json.get("chunk_count"), transfer_id)
|
||||
except (KeyError, TypeError, ValueError) as e:
|
||||
return JSONResponse(status_code=400, content={"error": f"init json 非法: {e}"})
|
||||
return {"transfer_id": transfer_id}
|
||||
@@ -143,13 +156,15 @@ async def put_chunk(transfer_id: str, idx: int, request: Request):
|
||||
data = await request.body()
|
||||
try:
|
||||
receiver.receive(transfer_id, idx, data)
|
||||
log.info("chunk %s %s 卷%d (%dB)", request.client.host if request.client else "?",
|
||||
transfer_id, idx, len(data))
|
||||
except ReceiverError as e:
|
||||
return JSONResponse(status_code=409, content={"error": str(e)})
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@app.post("/api/transfer/{transfer_id}/complete", dependencies=[Depends(verify_token)])
|
||||
async def complete(transfer_id: str):
|
||||
async def complete(transfer_id: str, request: Request):
|
||||
if tasks.get(transfer_id) is None:
|
||||
return JSONResponse(status_code=404, content={"error": "任务不存在"})
|
||||
# 幂等: 已完成的任务直接返回已有 file_id
|
||||
@@ -177,6 +192,9 @@ async def complete(transfer_id: str):
|
||||
transfer["total_sha256"],
|
||||
)
|
||||
tasks.set_status(transfer_id, "done")
|
||||
log.info("complete %s 任务=%s %s卷 -> file=%s",
|
||||
request.client.host if request.client else "?", transfer_id,
|
||||
transfer["chunk_count"], file_id)
|
||||
return {"status": "done", "file_id": file_id}
|
||||
except OSError as e:
|
||||
tasks.set_status(transfer_id, "failed")
|
||||
|
||||
Reference in New Issue
Block a user