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

130 lines
4.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
"""
import hashlib
from pathlib import Path
from fastapi import FastAPI, Request
from fastapi.responses import FileResponse, JSONResponse
from assembler import Assembler, AssemblerError
from settings import KEYRING_PATH, STORAGE_ROOT, TMP_ROOT
from db import ServerDB
from decrypt import DecryptError, Decryptor
from keyring import Keyring
from receiver import Receiver, ReceiverError
from storage import Storage
from task_manager import TaskManager
from unpacker import UnpackError, Unpacker
# ---------- 依赖装配 (单例) ----------
db = ServerDB()
tasks = TaskManager(db)
keyring = Keyring(KEYRING_PATH)
receiver = Receiver(tasks, TMP_ROOT)
assembler = Assembler(tasks, TMP_ROOT)
unpacker = Unpacker(tasks)
decryptor = Decryptor(keyring)
storage = Storage(tasks, STORAGE_ROOT)
app = FastAPI(title="7z-encrypt 服务端")
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()
@app.get("/api/files")
async def list_files():
"""文件列表 (ls)"""
return {"files": db.list_files()}
@app.get("/api/files/{file_id}")
async def download_file(file_id: str):
"""下载文件 (流式返回存储中的明文)"""
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}"})
return FileResponse(
path,
filename=rec["file_name"],
media_type="application/octet-stream",
)
@app.post("/api/transfer/init")
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")
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}")
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")
async def complete(transfer_id: str):
if tasks.get(transfer_id) is None:
return JSONResponse(status_code=404, content={"error": "任务不存在"})
try:
tasks.set_status(transfer_id, "assembling")
merged = assembler.assemble(transfer_id)
tasks.set_status(transfer_id, "decrypting")
merged_path, enc_params = unpacker.unpack(transfer_id, merged)
plain_path = TMP_ROOT / transfer_id / "plain.bin"
decryptor.decrypt(merged_path, enc_params, plain_path)
transfer = tasks.get(transfer_id)
assert transfer is not None
final_path = storage.store(plain_path, 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, UnpackError, DecryptError) as e:
tasks.set_status(transfer_id, "failed")
return JSONResponse(status_code=409, content={"error": str(e), "status": "failed"})