目录扩展: init 接 dir_id (归属校验), files 挂目录, PUT /move + POST /clone (物理复制卷), list 带 dir_name

This commit is contained in:
lou
2026-08-10 22:04:20 +08:00
parent 7e81373023
commit b54ceff639
3 changed files with 113 additions and 14 deletions
+60 -1
View File
@@ -216,6 +216,59 @@ async def delete_file(file_id: str, request: Request, username: str = Depends(ve
return {"ok": True, "file_id": file_id} return {"ok": True, "file_id": file_id}
def _resolve_dir(dir_id: int | None, username: str) -> int | None:
"""目录归属校验: 0/None=根目录, 否则必须属于该用户 (跨账户 403)"""
if dir_id in (None, 0):
return None
d = db.get_dir(dir_id)
if d is None or d["user_id"] != username:
raise HTTPException(status_code=403, detail="目录不存在或无权使用")
return dir_id
@app.put("/api/files/{file_id}/move")
async def move_file(file_id: str, request: Request, username: str = Depends(verify_token)):
"""移动文件到目录 (body {dir_id}; 0/缺省=根目录)"""
rec = db.get_file(file_id)
if rec is None:
return JSONResponse(status_code=404, content={"error": "文件不存在"})
_check_owner(rec, username)
body = await request.json()
try:
dir_id = _resolve_dir(body.get("dir_id"), username)
except HTTPException as e:
return JSONResponse(status_code=e.status_code, content={"error": e.detail})
db.move_file(file_id, dir_id)
log.info("move %s %s -> dir=%s", request.client.host if request.client else "?", file_id, dir_id)
return {"ok": True, "file_id": file_id, "dir_id": dir_id}
@app.post("/api/files/{file_id}/clone")
async def clone_file(file_id: str, request: Request, username: str = Depends(verify_token)):
"""克隆文件 (物理复制卷目录 + 新记录), body {dir_id} 可选"""
rec = db.get_file(file_id)
if rec is None:
return JSONResponse(status_code=404, content={"error": "文件不存在"})
_check_owner(rec, username)
body = await request.json()
try:
dir_id = _resolve_dir(body.get("dir_id"), username)
except HTTPException as e:
return JSONResponse(status_code=e.status_code, content={"error": e.detail})
src_dir = Path(rec["path"])
new_dir = src_dir.with_name(src_dir.name + "_clone")
try:
shutil.copytree(src_dir, new_dir)
except OSError as e:
return JSONResponse(status_code=409, content={"error": f"克隆失败: {e}"})
new_id = db.clone_file(file_id, dir_id, str(new_dir))
if not new_id:
shutil.rmtree(new_dir, ignore_errors=True)
return JSONResponse(status_code=404, content={"error": "源文件记录丢失"})
log.info("clone %s %s -> %s", request.client.host if request.client else "?", file_id, new_id)
return {"ok": True, "file_id": new_id}
@app.get("/api/files/{file_id}/chunk/{idx}") @app.get("/api/files/{file_id}/chunk/{idx}")
async def download_chunk(file_id: str, idx: int, request: Request, username: str = Depends(verify_token)): async def download_chunk(file_id: str, idx: int, request: Request, username: str = Depends(verify_token)):
"""下载单卷密文 (零合并: 服务端不拼接, 客户端逐卷拉取本地合并)""" """下载单卷密文 (零合并: 服务端不拼接, 客户端逐卷拉取本地合并)"""
@@ -247,7 +300,12 @@ async def init_transfer(request: Request, username: str = Depends(verify_token))
return JSONResponse(status_code=413, content={"error": "init json 过大"}) return JSONResponse(status_code=413, content={"error": "init json 过大"})
try: try:
init_json = await request.json() init_json = await request.json()
transfer_id = tasks.create(init_json, username) dir_id = init_json.pop("dir_id", None)
if dir_id is not None:
d = db.get_dir(int(dir_id))
if d is None or d["user_id"] != username:
return JSONResponse(status_code=403, content={"error": "目录不存在或无权使用"})
transfer_id = tasks.create(init_json, username, dir_id)
log.info("init %s %s卷 任务=%s", request.client.host if request.client else "?", log.info("init %s %s卷 任务=%s", request.client.host if request.client else "?",
init_json.get("chunk_count"), transfer_id) init_json.get("chunk_count"), transfer_id)
except (KeyError, TypeError, ValueError) as e: except (KeyError, TypeError, ValueError) as e:
@@ -382,6 +440,7 @@ async def complete(transfer_id: str, request: Request, username: str = Depends(v
transfer["file_size"], transfer["file_size"],
transfer["total_sha256"], transfer["total_sha256"],
username, username,
transfer.get("dir_id"),
) )
tasks.set_status(transfer_id, "done") tasks.set_status(transfer_id, "done")
log.info("complete %s 任务=%s %s卷 -> file=%s", log.info("complete %s 任务=%s %s卷 -> file=%s",
+50 -11
View File
@@ -112,6 +112,14 @@ class ServerDB:
conn.execute("ALTER TABLE files ADD COLUMN user_id TEXT") conn.execute("ALTER TABLE files ADD COLUMN user_id TEXT")
except sqlite3.OperationalError: except sqlite3.OperationalError:
pass # 列已存在 pass # 列已存在
try:
conn.execute("ALTER TABLE transfers ADD COLUMN dir_id INTEGER")
except sqlite3.OperationalError:
pass # 列已存在
try:
conn.execute("ALTER TABLE files ADD COLUMN dir_id INTEGER")
except sqlite3.OperationalError:
pass # 列已存在
# ---------- users ---------- # ---------- users ----------
@@ -199,11 +207,12 @@ class ServerDB:
# ---------- transfers ---------- # ---------- transfers ----------
def create_transfer(self, transfer_id: str, init_json: dict[str, Any], user_id: str) -> None: def create_transfer(self, transfer_id: str, init_json: dict[str, Any], user_id: str,
dir_id: int | None = None) -> None:
with self._conn() as conn: with self._conn() as conn:
conn.execute( conn.execute(
"INSERT INTO transfers (transfer_id, file_name, file_size, chunk_count, total_sha256, enc_params, user_id, status) " "INSERT INTO transfers (transfer_id, file_name, file_size, chunk_count, total_sha256, enc_params, user_id, dir_id, status) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?)", "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
( (
transfer_id, transfer_id,
init_json["file_name"], init_json["file_name"],
@@ -212,6 +221,7 @@ class ServerDB:
init_json["total_sha256"], init_json["total_sha256"],
json.dumps(init_json["enc"], ensure_ascii=False), json.dumps(init_json["enc"], ensure_ascii=False),
user_id, user_id,
dir_id,
"uploading", "uploading",
), ),
) )
@@ -286,13 +296,14 @@ class ServerDB:
size: int, size: int,
sha256: str, sha256: str,
user_id: str, user_id: str,
dir_id: int | None = None,
) -> str: ) -> str:
file_id = uuid.uuid4().hex[:12] file_id = uuid.uuid4().hex[:12]
with self._conn() as conn: with self._conn() as conn:
conn.execute( conn.execute(
"INSERT INTO files (file_id, transfer_id, file_name, path, size, sha256, user_id) " "INSERT INTO files (file_id, transfer_id, file_name, path, size, sha256, user_id, dir_id) "
"VALUES (?, ?, ?, ?, ?, ?, ?)", "VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
(file_id, transfer_id, file_name, path, size, sha256, user_id), (file_id, transfer_id, file_name, path, size, sha256, user_id, dir_id),
) )
return file_id return file_id
@@ -306,22 +317,50 @@ class ServerDB:
return dict(row) if row else None return dict(row) if row else None
def list_files(self, user_id: str) -> list[dict[str, Any]]: def list_files(self, user_id: str) -> list[dict[str, Any]]:
"""文件记录 (ls 端点): 每个账户只看自己的空间 (admin 含无主旧文件)""" """文件记录 (ls 端点): 每个账户只看自己的空间 (admin 含无主旧文件), 带目录名"""
with self._conn() as conn: with self._conn() as conn:
if user_id == "admin": if user_id == "admin":
rows = conn.execute( rows = conn.execute(
"SELECT file_id, file_name, size, sha256, created_at FROM files " "SELECT f.file_id, f.file_name, f.size, f.sha256, f.created_at, f.dir_id, d.name AS dir_name "
"WHERE user_id = ? OR user_id IS NULL ORDER BY created_at DESC", "FROM files f LEFT JOIN dirs d ON d.id = f.dir_id "
"WHERE f.user_id = ? OR f.user_id IS NULL ORDER BY f.created_at DESC",
("admin",), ("admin",),
).fetchall() ).fetchall()
else: else:
rows = conn.execute( rows = conn.execute(
"SELECT file_id, file_name, size, sha256, created_at FROM files " "SELECT f.file_id, f.file_name, f.size, f.sha256, f.created_at, f.dir_id, d.name AS dir_name "
"WHERE user_id = ? ORDER BY created_at DESC", "FROM files f LEFT JOIN dirs d ON d.id = f.dir_id "
"WHERE f.user_id = ? ORDER BY f.created_at DESC",
(user_id,), (user_id,),
).fetchall() ).fetchall()
return [dict(r) for r in rows] return [dict(r) for r in rows]
def move_file(self, file_id: str, dir_id: int | None) -> bool:
"""移动文件到目录 (dir_id=None=根目录), 返回是否更新"""
with self._conn() as conn:
cur = conn.execute(
"UPDATE files SET dir_id = ? WHERE file_id = ?", (dir_id, file_id)
)
return cur.rowcount > 0
def clone_file(self, file_id: str, dir_id: int | None, new_path: str) -> str:
"""克隆文件记录 (卷目录已物理复制到 new_path), 返回新 file_id"""
new_id = uuid.uuid4().hex[:12]
with self._conn() as conn:
row = conn.execute(
"SELECT transfer_id, file_name, size, sha256, user_id FROM files WHERE file_id = ?",
(file_id,),
).fetchone()
if row is None:
return ""
conn.execute(
"INSERT INTO files (file_id, transfer_id, file_name, path, size, sha256, user_id, dir_id) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
(new_id, row["transfer_id"], row["file_name"], new_path,
row["size"], row["sha256"], row["user_id"], dir_id),
)
return new_id
def delete_file(self, file_id: str) -> bool: def delete_file(self, file_id: str) -> bool:
"""删除文件记录, 返回是否删除 (del 端点)""" """删除文件记录, 返回是否删除 (del 端点)"""
with self._conn() as conn: with self._conn() as conn:
+3 -2
View File
@@ -26,7 +26,8 @@ class TaskManager:
def __init__(self, db: ServerDB) -> None: def __init__(self, db: ServerDB) -> None:
self.db = db self.db = db
def create(self, init_json: dict[str, Any], user_id: str) -> str: def create(self, init_json: dict[str, Any], user_id: str,
dir_id: int | None = None) -> str:
"""POST /init: 建任务, 返回 transfer_id (输入清洗: 文件名/卷数上限) """POST /init: 建任务, 返回 transfer_id (输入清洗: 文件名/卷数上限)
文件名只做可打印字符过滤 + 长度截断。不删 / \\ 等路径分隔符: 文件名只做可打印字符过滤 + 长度截断。不删 / \\ 等路径分隔符:
@@ -42,7 +43,7 @@ class TaskManager:
raise ValueError(f"卷数超上限: {len(chunks)} > 10000") raise ValueError(f"卷数超上限: {len(chunks)} > 10000")
init_json["chunk_count"] = len(chunks) init_json["chunk_count"] = len(chunks)
transfer_id = uuid.uuid4().hex[:12] transfer_id = uuid.uuid4().hex[:12]
self.db.create_transfer(transfer_id, init_json, user_id) self.db.create_transfer(transfer_id, init_json, user_id, dir_id)
return transfer_id return transfer_id
def get(self, transfer_id: str) -> dict[str, Any] | None: def get(self, transfer_id: str) -> dict[str, Any] | None: