From 95ced31222d254b3bb96b3ea79b72db5fdc039b6 Mon Sep 17 00:00:00 2001 From: lou Date: Mon, 10 Aug 2026 01:42:51 +0800 Subject: [PATCH] =?UTF-8?q?quota:=20GET=20/api/quota=20(used=3Dfiles=20siz?= =?UTF-8?q?e=20=E6=80=BB=E5=92=8C=20/=20quota=20=E9=BB=98=E8=AE=A4=2010GB?= =?UTF-8?q?=20SZ=5FQUOTA=20env=20/=20remain=20/=20percent);=20=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=207=20=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api.py | 15 ++++++++++++++- db.py | 6 ++++++ settings.py | 3 +++ tests/test_api.py | 10 ++++++++++ 4 files changed, 33 insertions(+), 1 deletion(-) diff --git a/api.py b/api.py index 9cfd319..3c19447 100644 --- a/api.py +++ b/api.py @@ -25,7 +25,7 @@ from fastapi.responses import FileResponse, JSONResponse from db import ServerDB from receiver import Receiver, ReceiverError -from settings import STORAGE_ROOT, TMP_ROOT, TOKEN +from settings import QUOTA_BYTES, STORAGE_ROOT, TMP_ROOT, TOKEN from storage import Storage from task_manager import TaskManager @@ -63,6 +63,19 @@ async def list_files(): return {"files": db.list_files()} +@app.get("/api/quota", dependencies=[Depends(verify_token)]) +async def quota(): + """用户空间配额: 已用 (files size 总和) / 配额 / 剩余""" + used = db.sum_files_size() + remain = max(QUOTA_BYTES - used, 0) + return { + "used_bytes": used, + "quota_bytes": QUOTA_BYTES, + "remain_bytes": remain, + "percent": round(used * 100 / QUOTA_BYTES, 1) if QUOTA_BYTES else 0, + } + + @app.delete("/api/files/{file_id}", dependencies=[Depends(verify_token)]) async def delete_file(file_id: str): """删除文件 (卷目录 + 入库记录)""" diff --git a/db.py b/db.py index bacd781..ee4ec25 100644 --- a/db.py +++ b/db.py @@ -201,6 +201,12 @@ class ServerDB: cur = conn.execute("DELETE FROM files WHERE file_id = ?", (file_id,)) return cur.rowcount > 0 + def sum_files_size(self) -> int: + """全部文件 size 总和 (配额统计用)""" + with self._conn() as conn: + row = conn.execute("SELECT COALESCE(SUM(size), 0) FROM files").fetchone() + return int(row[0]) + def get_file(self, file_id: str) -> dict[str, Any] | None: """单文件记录 (下载端点, 含解密参数 enc_params + 卷数)""" with self._conn() as conn: diff --git a/settings.py b/settings.py index 15196f5..19c9b5c 100644 --- a/settings.py +++ b/settings.py @@ -17,6 +17,9 @@ STORAGE_ROOT = Path(os.environ.get("SZ_STORAGE_ROOT", "data/storage")) # 临时目录 (分卷/合并中间产物) TMP_ROOT = Path(os.environ.get("SZ_TMP_ROOT", "data/tmp")) +# 用户空间配额 (已用 = files 表 size 总和, 超配额拒绝新上传? 暂只读展示) +QUOTA_BYTES = int(os.environ.get("SZ_QUOTA", str(10 * 1024**3))) # 默认 10GB + # API 服务 HOST = os.environ.get("SZ_HOST", "0.0.0.0") PORT = int(os.environ.get("SZ_PORT", "8000")) diff --git a/tests/test_api.py b/tests/test_api.py index b92c22c..33954f2 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -237,6 +237,16 @@ class ServerPipelineTest(unittest.TestCase): len(self.client.get("/api/files", headers=AUTH).json()["files"]), before ) + # 配额: 返回结构 + 已用 = files size 总和 + r = self.client.get("/api/quota", headers=AUTH) + self.assertEqual(r.status_code, 200) + q = r.json() + self.assertIn("used_bytes", q) + self.assertIn("quota_bytes", q) + self.assertIn("remain_bytes", q) + self.assertEqual(q["used_bytes"], q["quota_bytes"] - q["remain_bytes"]) + self.assertGreater(q["quota_bytes"], 0) + # del: 删除后列表减少, 再删 404 r = self.client.delete(f"/api/files/{file_id}", headers=AUTH) self.assertEqual(r.status_code, 200, r.text)