Files
7z-encrypt-server/api.py
T

149 lines
5.4 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(),
"X-Chunk-Count": str(rec["chunk_count"]), # 客户端按卷粒度显示进度
}
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"})