diff --git a/transfer.py b/transfer.py index 85c495c..b067d01 100644 --- a/transfer.py +++ b/transfer.py @@ -214,6 +214,21 @@ class TransferClient: if not payload.get("ok"): raise TransferError(f"删除目录失败: {payload}") + def move_file(self, file_id: str, dir_id: int) -> None: + """PUT /api/files/{id}/move: 移动文件到目录 (0=根)""" + code, payload = self._request( + "PUT", f"/api/files/{file_id}/move", json_body={"dir_id": dir_id}) + if code != 200: + raise TransferError(f"移动失败 (HTTP {code}): {payload}") + + def clone_file(self, file_id: str, dir_id: int) -> dict[str, Any]: + """POST /api/files/{id}/clone: 克隆文件到目录 (0=根)""" + code, payload = self._request( + "POST", f"/api/files/{file_id}/clone", json_body={"dir_id": dir_id}) + if code != 200: + raise TransferError(f"克隆失败 (HTTP {code}): {payload}") + return payload + def download(self, file_id: str, dest: Path | None = None) -> tuple[Path, dict[str, Any]]: """逐卷下载密文 (零合并: 服务端只存卷, 客户端本地拼接), 返回 (密文路径, 解密参数)。 @@ -715,6 +730,9 @@ def main() -> int: ap.add_argument("--dir-create", metavar="名称", help="创建目录 (按用户隔离)") ap.add_argument("--dir-delete", metavar="DIR_ID", help="删除目录") ap.add_argument("--dir-list", action="store_true", help="列出目录") + ap.add_argument("--dir", type=int, metavar="DIR_ID", help="上传到指定目录 (默认根目录)") + ap.add_argument("--move", nargs=2, metavar=("文件序号/ID", "DIR_ID|0"), help="移动文件到目录 (0=根)") + ap.add_argument("--clone", nargs=2, metavar=("文件序号/ID", "DIR_ID|0"), help="克隆文件到目录 (0=根)") args = ap.parse_args() server = args.server or _config_server() @@ -796,16 +814,23 @@ def main() -> int: if args.list: try: files = client.list_files() + dirs = client.list_dirs() except TransferError as e: print(f"[错误] {e}") return 1 + if dirs: + print(f"[目录] 共 {len(dirs)} 个:") + for d in dirs: + cnt = sum(1 for f in files if f.get("dir_id") == d["id"]) + print(f" {d['id']:>2}. {d['name']} ({cnt} 个文件)") print(f"[文件] 共 {len(files)} 个:") for i, f in enumerate(files, 1): size_mb = f["size"] / 1048576 name = _decrypt_name(f["file_name"]) or f["file_name"] if name.startswith("enc:"): name = name[:22] + "..." # 加密名截断 (keyring 无对应密钥) - print(f" {i:>2}. {name} ({size_mb:.1f} MB)") + loc = f"[{f.get('dir_name') or '/'}] " + print(f" {i:>2}. {loc}{name} ({size_mb:.1f} MB)") return 0 if args.download: @@ -918,10 +943,34 @@ def main() -> int: print(f"[删除] 已删除: {args.delete}") return 0 + if args.move: + try: + src, dst = args.move + fid = _resolve_id(client, src) + client.move_file(fid, int(dst)) + except (TransferError, ValueError) as e: + print(f"[错误] {e}") + return 1 + print(f"[移动] {src} -> 目录 {dst}") + return 0 + + if args.clone: + try: + src, dst = args.clone + fid = _resolve_id(client, src) + payload = client.clone_file(fid, int(dst)) + except (TransferError, ValueError) as e: + print(f"[错误] {e}") + return 1 + print(f"[克隆] {src} -> 目录 {dst} (新 file_id={payload['file_id']})") + return 0 + if not (args.init and args.manifest): ap.error("需指定 --list / --quota / --download / --delete / (--init + --manifest) 之一") init_json = json.loads(Path(args.init).read_text(encoding="utf-8")) + if args.dir is not None: + init_json["dir_id"] = args.dir # 上传到指定目录 (服务端校验归属) manifest = json.loads(Path(args.manifest).read_text(encoding="utf-8")) chunk_files = { ch["index"]: Path(ch["filename"]).resolve()