目录扩展: init 接 dir_id (归属校验), files 挂目录, PUT /move + POST /clone (物理复制卷), list 带 dir_name
This commit is contained in:
@@ -216,6 +216,59 @@ async def delete_file(file_id: str, request: Request, username: str = Depends(ve
|
||||
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}")
|
||||
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 过大"})
|
||||
try:
|
||||
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 "?",
|
||||
init_json.get("chunk_count"), transfer_id)
|
||||
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["total_sha256"],
|
||||
username,
|
||||
transfer.get("dir_id"),
|
||||
)
|
||||
tasks.set_status(transfer_id, "done")
|
||||
log.info("complete %s 任务=%s %s卷 -> file=%s",
|
||||
|
||||
@@ -112,6 +112,14 @@ class ServerDB:
|
||||
conn.execute("ALTER TABLE files ADD COLUMN user_id TEXT")
|
||||
except sqlite3.OperationalError:
|
||||
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 ----------
|
||||
|
||||
@@ -199,11 +207,12 @@ class ServerDB:
|
||||
|
||||
# ---------- 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:
|
||||
conn.execute(
|
||||
"INSERT INTO transfers (transfer_id, file_name, file_size, chunk_count, total_sha256, enc_params, user_id, status) "
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
"INSERT INTO transfers (transfer_id, file_name, file_size, chunk_count, total_sha256, enc_params, user_id, dir_id, status) "
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
(
|
||||
transfer_id,
|
||||
init_json["file_name"],
|
||||
@@ -212,6 +221,7 @@ class ServerDB:
|
||||
init_json["total_sha256"],
|
||||
json.dumps(init_json["enc"], ensure_ascii=False),
|
||||
user_id,
|
||||
dir_id,
|
||||
"uploading",
|
||||
),
|
||||
)
|
||||
@@ -286,13 +296,14 @@ class ServerDB:
|
||||
size: int,
|
||||
sha256: str,
|
||||
user_id: str,
|
||||
dir_id: int | None = None,
|
||||
) -> str:
|
||||
file_id = uuid.uuid4().hex[:12]
|
||||
with self._conn() as conn:
|
||||
conn.execute(
|
||||
"INSERT INTO files (file_id, transfer_id, file_name, path, size, sha256, user_id) "
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||
(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 (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
(file_id, transfer_id, file_name, path, size, sha256, user_id, dir_id),
|
||||
)
|
||||
return file_id
|
||||
|
||||
@@ -306,22 +317,50 @@ class ServerDB:
|
||||
return dict(row) if row else None
|
||||
|
||||
def list_files(self, user_id: str) -> list[dict[str, Any]]:
|
||||
"""文件记录 (ls 端点): 每个账户只看自己的空间 (admin 含无主旧文件)"""
|
||||
"""文件记录 (ls 端点): 每个账户只看自己的空间 (admin 含无主旧文件), 带目录名"""
|
||||
with self._conn() as conn:
|
||||
if user_id == "admin":
|
||||
rows = conn.execute(
|
||||
"SELECT file_id, file_name, size, sha256, created_at FROM files "
|
||||
"WHERE user_id = ? OR user_id IS NULL ORDER BY created_at DESC",
|
||||
"SELECT f.file_id, f.file_name, f.size, f.sha256, f.created_at, f.dir_id, d.name AS dir_name "
|
||||
"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",),
|
||||
).fetchall()
|
||||
else:
|
||||
rows = conn.execute(
|
||||
"SELECT file_id, file_name, size, sha256, created_at FROM files "
|
||||
"WHERE user_id = ? ORDER BY created_at DESC",
|
||||
"SELECT f.file_id, f.file_name, f.size, f.sha256, f.created_at, f.dir_id, d.name AS dir_name "
|
||||
"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,),
|
||||
).fetchall()
|
||||
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:
|
||||
"""删除文件记录, 返回是否删除 (del 端点)"""
|
||||
with self._conn() as conn:
|
||||
|
||||
+3
-2
@@ -26,7 +26,8 @@ class TaskManager:
|
||||
def __init__(self, db: ServerDB) -> None:
|
||||
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 (输入清洗: 文件名/卷数上限)
|
||||
|
||||
文件名只做可打印字符过滤 + 长度截断。不删 / \\ 等路径分隔符:
|
||||
@@ -42,7 +43,7 @@ class TaskManager:
|
||||
raise ValueError(f"卷数超上限: {len(chunks)} > 10000")
|
||||
init_json["chunk_count"] = len(chunks)
|
||||
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
|
||||
|
||||
def get(self, transfer_id: str) -> dict[str, Any] | None:
|
||||
|
||||
Reference in New Issue
Block a user