146 lines
5.3 KiB
Python
146 lines
5.3 KiB
Python
"""
|
|
API 层 (api)
|
|
模块: 服务端 / API 层
|
|
输入: HTTP 请求 (带认证)
|
|
输出: 响应 / 路由到各模块
|
|
|
|
零知识设计: 服务端只传输+存储密文, 不持有密钥, 不解密。
|
|
端点:
|
|
POST /api/transfer/init {init json} -> {transfer_id}
|
|
GET /api/transfer/{id}/chunks -> {received: [n...]}
|
|
PUT /api/transfer/{id}/chunk/{n} 卷密文 -> {ok} / 409
|
|
POST /api/transfer/{id}/complete -> {status, file_id} / 409
|
|
GET /api/files -> {files: [...]}
|
|
GET /api/files/{file_id} -> 密文文件 (头 X-Enc-Params: base64(enc_params))
|
|
全部端点需 Authorization: Bearer <token> (与客户端预共享)
|
|
"""
|
|
|
|
import base64
|
|
import hashlib
|
|
import json
|
|
from pathlib import Path
|
|
|
|
from fastapi import Depends, FastAPI, Header, HTTPException, Request
|
|
from fastapi.responses import FileResponse, JSONResponse
|
|
|
|
from assembler import Assembler, AssemblerError
|
|
from db import ServerDB
|
|
from receiver import Receiver, ReceiverError
|
|
from settings import STORAGE_ROOT, TMP_ROOT, TOKEN
|
|
from storage import Storage
|
|
from task_manager import TaskManager
|
|
|
|
# ---------- 依赖装配 (单例) ----------
|
|
db = ServerDB()
|
|
tasks = TaskManager(db)
|
|
receiver = Receiver(tasks, TMP_ROOT)
|
|
assembler = Assembler(tasks, TMP_ROOT)
|
|
storage = Storage(tasks, STORAGE_ROOT)
|
|
|
|
app = FastAPI(title="7z-encrypt 服务端 (零知识)")
|
|
|
|
|
|
# ---------- 认证 ----------
|
|
|
|
def verify_token(authorization: str | None = Header(default=None)) -> None:
|
|
"""Bearer token 校验, 失败 401"""
|
|
if authorization != f"Bearer {TOKEN}":
|
|
raise HTTPException(status_code=401, detail="未授权: token 无效或缺失")
|
|
|
|
|
|
def _sha256(path: str | Path) -> str:
|
|
h = hashlib.sha256()
|
|
with open(path, "rb") as f:
|
|
for chunk in iter(lambda: f.read(1 << 20), b""):
|
|
h.update(chunk)
|
|
return h.hexdigest()
|
|
|
|
|
|
# ---------- 文件: ls / 下载 (密文 + 解密参数) ----------
|
|
|
|
@app.get("/api/files", dependencies=[Depends(verify_token)])
|
|
async def list_files():
|
|
"""文件列表 (ls)"""
|
|
return {"files": db.list_files()}
|
|
|
|
|
|
@app.get("/api/files/{file_id}", dependencies=[Depends(verify_token)])
|
|
async def download_file(file_id: str):
|
|
"""下载密文文件。enc_params 放响应头 X-Enc-Params (客户端本地解密用)"""
|
|
rec = db.get_file(file_id)
|
|
if rec is None:
|
|
return JSONResponse(status_code=404, content={"error": "文件不存在"})
|
|
path = Path(rec["path"])
|
|
if not path.exists():
|
|
return JSONResponse(status_code=404, content={"error": f"存储文件缺失: {path}"})
|
|
# 解密参数: 任务 init 时存的 enc {alg, key_id, context}
|
|
enc_params = json.dumps(rec["enc_params"], ensure_ascii=False)
|
|
headers = {"X-Enc-Params": base64.b64encode(enc_params.encode()).decode()}
|
|
return FileResponse(
|
|
path,
|
|
filename=rec["file_name"],
|
|
media_type="application/octet-stream",
|
|
headers=headers,
|
|
)
|
|
|
|
|
|
# ---------- 传输 ----------
|
|
|
|
@app.post("/api/transfer/init", dependencies=[Depends(verify_token)])
|
|
async def init_transfer(request: Request):
|
|
try:
|
|
init_json = await request.json()
|
|
transfer_id = tasks.create(init_json)
|
|
except (KeyError, TypeError, ValueError) as e:
|
|
return JSONResponse(status_code=400, content={"error": f"init json 非法: {e}"})
|
|
return {"transfer_id": transfer_id}
|
|
|
|
|
|
@app.get("/api/transfer/{transfer_id}/chunks", dependencies=[Depends(verify_token)])
|
|
async def get_chunks(transfer_id: str):
|
|
if tasks.get(transfer_id) is None:
|
|
return JSONResponse(status_code=404, content={"error": "任务不存在"})
|
|
return {"received": sorted(tasks.received(transfer_id))}
|
|
|
|
|
|
@app.put("/api/transfer/{transfer_id}/chunk/{idx}", dependencies=[Depends(verify_token)])
|
|
async def put_chunk(transfer_id: str, idx: int, request: Request):
|
|
if tasks.get(transfer_id) is None:
|
|
return JSONResponse(status_code=404, content={"error": "任务不存在"})
|
|
data = await request.body()
|
|
try:
|
|
receiver.receive(transfer_id, idx, 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):
|
|
if tasks.get(transfer_id) is None:
|
|
return JSONResponse(status_code=404, content={"error": "任务不存在"})
|
|
# 幂等: 已完成的任务直接返回已有 file_id (客户端超时重试/断线重连场景)
|
|
done_rec = db.get_file_by_transfer(transfer_id)
|
|
if done_rec is not None:
|
|
return {"status": "done", "file_id": done_rec["file_id"]}
|
|
try:
|
|
tasks.set_status(transfer_id, "assembling")
|
|
merged = assembler.assemble(transfer_id)
|
|
|
|
transfer = tasks.get(transfer_id)
|
|
assert transfer is not None
|
|
# 零知识: 合并后的密文直接入库, 服务端不碰解密
|
|
final_path = storage.store(merged, transfer["file_name"])
|
|
file_id = db.insert_file(
|
|
transfer_id,
|
|
transfer["file_name"],
|
|
str(final_path),
|
|
final_path.stat().st_size,
|
|
_sha256(final_path),
|
|
)
|
|
tasks.set_status(transfer_id, "done")
|
|
return {"status": "done", "file_id": file_id}
|
|
except AssemblerError as e:
|
|
tasks.set_status(transfer_id, "failed")
|
|
return JSONResponse(status_code=409, content={"error": str(e), "status": "failed"})
|