进度条全面 tqdm: 上传卷条/下载嵌套(卷+字节)/加密分卷/解密; crypto/splitter 加 progress 回调; 依赖 tqdm

This commit is contained in:
lou
2026-08-10 01:53:52 +08:00
parent de642cb5bc
commit e0d7c5fd73
4 changed files with 92 additions and 42 deletions
+24 -8
View File
@@ -16,7 +16,7 @@
import json
import os
from pathlib import Path
from typing import IO, Any
from typing import IO, Any, Callable
from cryptography.cobblestone import Cobblestone256Decryptor, Cobblestone256Encryptor
from cryptography.exceptions import InvalidTag
@@ -82,7 +82,12 @@ class CryptoEngine:
# ---------- 加解密 ----------
def encrypt_stream(
self, src: IO[bytes], dst: IO[bytes], key_id: str, context: bytes = CONTEXT
self,
src: IO[bytes],
dst: IO[bytes],
key_id: str,
context: bytes = CONTEXT,
progress: Callable[[int], Any] | None = None,
) -> dict[str, Any]:
"""步骤 A & F: 流式加密。src 读明文 -> dst 写密文, 返回加密参数
@@ -91,32 +96,43 @@ class CryptoEngine:
dst: 可写二进制流
key_id: 密钥标识
context: 应用绑定上下文 (默认 7z-encrypt:v1)
Returns:
dict: 加密参数 {alg, key_id, context}, 供 init json 传给服务端
progress: 进度回调, 每读块后调 progress(已读字节)
"""
key = self.get_key(key_id)
enc = Cobblestone256Encryptor(key, context)
done = 0
while True:
chunk = src.read(1 << 16) # 64KiB 读块
if not chunk:
break
dst.write(enc.update(chunk))
done += len(chunk)
if progress is not None:
progress(done)
dst.write(enc.finalize())
return {"alg": "cobblestone-aes256gcm", "key_id": key_id, "context": context.decode()}
def decrypt_stream(
self, src: IO[bytes], dst: IO[bytes], key_id: str, context: bytes = CONTEXT
self,
src: IO[bytes],
dst: IO[bytes],
key_id: str,
context: bytes = CONTEXT,
progress: Callable[[int], Any] | None = None,
) -> None:
"""流式解密。src 读密文 -> dst 写明文。完整性验证失败抛 IntegrityError"""
"""解密流 (进度回调: 每读块后调 progress(已读字节))"""
key = self.get_key(key_id)
dec = Cobblestone256Decryptor(key, context)
try:
dec = Cobblestone256Decryptor(key, context)
done = 0
while True:
chunk = src.read(1 << 16)
if not chunk:
break
dst.write(dec.update(chunk)) # 块级认证: 篡改在这里就抛 InvalidTag
done += len(chunk)
if progress is not None:
progress(done)
dst.write(dec.finalize()) # finalize 返回剩余明文
except InvalidTag as e:
raise IntegrityError("解密验证失败: 密钥不匹配或密文被篡改") from e
+13 -2
View File
@@ -19,6 +19,8 @@ import sys
from pathlib import Path
from typing import Any
from tqdm import tqdm
from crypto import CryptoEngine, KeyNotFoundError
from splitter import split_stream
@@ -156,12 +158,21 @@ def main() -> int:
engine.generate_key(args.key_id)
print(f"[密钥库] 已生成新密钥 -> {engine.keyring_path}")
with open(src_path, "rb") as src, open(enc_path, "wb") as dst:
enc_params = engine.encrypt_stream(src, dst, args.key_id)
pbar = tqdm(total=src_path.stat().st_size, desc="[加密]", unit="B",
unit_scale=True, leave=False)
enc_params = engine.encrypt_stream(src, dst, args.key_id,
progress=lambda n: pbar.update(n))
pbar.close()
print(f"[1/4] 加密完成 -> {enc_path.name} ({enc_path.stat().st_size / 1048576:.1f} MB)")
# 2. 分卷 -> 卷清单
with open(enc_path, "rb") as f:
manifest = split_stream(f, chunk_size, str(enc_path), total_size=enc_path.stat().st_size)
pbar = tqdm(total=enc_path.stat().st_size, desc="[分卷]", unit="B",
unit_scale=True, leave=False)
manifest = split_stream(f, chunk_size, str(enc_path),
total_size=enc_path.stat().st_size,
progress=lambda n: pbar.update(n))
pbar.close()
with open(manifest_path, "w", encoding="utf-8") as f:
json.dump(manifest, f, indent=2, ensure_ascii=False)
print(f"[2/4] 分卷完成: {len(manifest)} 卷, 清单 -> {manifest_path.name}")
+12 -5
View File
@@ -15,7 +15,7 @@ import argparse
import hashlib
import json
import sys
from typing import IO, Any
from typing import IO, Any, Callable
def _read_block(stream: IO[bytes], size: int) -> bytes:
@@ -32,7 +32,11 @@ def _read_block(stream: IO[bytes], size: int) -> bytes:
def split_stream(
stream: IO[bytes], chunk_size: int, output_prefix: str, total_size: int | None = None
stream: IO[bytes],
chunk_size: int,
output_prefix: str,
total_size: int | None = None,
progress: Callable[[int], Any] | None = None,
) -> list[dict[str, Any]]:
"""从输入流切分卷文件, 返回清单 [{index, size, sha256, filename}]
@@ -40,7 +44,8 @@ def split_stream(
stream: 二进制输入流 (open(...,'rb') 或 sys.stdin.buffer)
chunk_size: 每卷最大字节数
output_prefix: 输出前缀, 卷文件命名为 <prefix>.partXXXX
total_size: 输入总字节数 (文件模式传入以显示百分比进度, stdin 模式为 None)
total_size: 输入总大小 (进度条用, 可 None)
progress: 进度回调, 每写一卷后调 progress(已写字节)
Returns:
list: 清单
@@ -72,8 +77,10 @@ def split_stream(
part_num += 1
done += size
# 实时进度 (不做黑盒等待)
if total_size:
# 实时进度: 有回调交给调用方 (tqdm), 否则内部打印
if progress is not None:
progress(done)
elif total_size:
pct = done * 100 // total_size
if pct != last_pct:
last_pct = pct
+43 -27
View File
@@ -21,7 +21,9 @@ import subprocess
import sys
import time
from pathlib import Path
from typing import Any
from typing import IO, Any
from tqdm import tqdm
from urllib import error as urlerror
from urllib import parse as urlparse
from urllib import request as urlrequest
@@ -175,25 +177,16 @@ class TransferClient:
elif dest.is_dir():
dest = dest / name
assert dest is not None
outer = tqdm(total=chunk_count, desc="[下载]", unit="",
position=0, leave=False)
with open(dest, "wb") as f:
while True:
chunk = resp.read(1 << 20)
if not chunk:
break
f.write(chunk)
self._show_chunk_progress(1, chunk_count)
for idx in range(2, chunk_count + 1):
with self._open_chunk(file_id, idx) as resp:
with open(dest, "ab") as f:
while True:
chunk = resp.read(1 << 20)
if not chunk:
break
f.write(chunk)
self._show_chunk_progress(idx, chunk_count)
sys.stdout.write("\n")
sys.stdout.flush()
return dest, enc_params
self._download_chunk_to(resp, f, outer, 1, chunk_count)
for idx in range(2, chunk_count + 1):
with self._open_chunk(file_id, idx) as resp:
with open(dest, "ab") as f:
self._download_chunk_to(resp, f, outer, idx, chunk_count)
outer.close()
return dest, enc_params
except urlerror.HTTPError as e:
if e.code == 401:
raise TransferError("未授权: token 无效 (检查 config server.token)") from e
@@ -220,6 +213,25 @@ class TransferClient:
sys.stdout.write(f"\r[下载] 卷 {cur}/{total_chunks} ({pct}%)")
sys.stdout.flush()
def _download_chunk_to(
self, resp, f: IO[bytes], outer: Any, idx: int, total_chunks: int
) -> int:
"""卷内字节流写入 + 进度 (内层字节条), 返回写入字节数"""
size = int(resp.headers.get("Content-Length") or 0)
inner = tqdm(total=size, desc=f"{idx}/{total_chunks}", unit="B",
unit_scale=True, position=1, leave=False)
done = 0
while True:
chunk = resp.read(1 << 20)
if not chunk:
break
f.write(chunk)
done += len(chunk)
inner.update(len(chunk))
inner.close()
outer.update(1)
return done
# ---------- 全流程 ----------
def transfer(
@@ -243,19 +255,15 @@ class TransferClient:
if received:
print(f"[传输] 断点续传: 服务端已收 {len(received)}/{total} 卷, 补传 {len(pending)}")
pbar = tqdm(total=len(pending), desc="[传输] 上传", unit="", leave=False)
for done, index in enumerate(pending, start=1):
try:
self.upload_chunk(transfer_id, index, chunk_files[index])
except TransferError:
raise # 重试耗尽, 由调用方决定 (记录断点状态待补传)
pct = done * 100 // len(pending)
sys.stdout.write(
f"\r[传输] 上传进度: {done}/{len(pending)} 卷 ({pct}%)"
)
sys.stdout.flush()
if pending:
sys.stdout.write("\n")
sys.stdout.flush()
pbar.update(1)
pbar.set_description(f"[传输] 上传 {done}/{len(pending)}")
pbar.close()
receipt = self.complete(transfer_id)
print(f"[传输] 完成: {receipt}")
@@ -333,12 +341,16 @@ def main() -> int:
if enc_params.get("compressed"):
# 解密还原的是 7z 流 -> 解压成原始文件
tmp7z = tmp_enc.with_suffix(".7z")
pbar = tqdm(total=enc_path.stat().st_size, desc="[解密]", unit="B",
unit_scale=True, leave=False)
with open(enc_path, "rb") as src, open(tmp7z, "wb") as dst:
engine.decrypt_stream(
src, dst,
enc_params["key_id"],
enc_params.get("context", "7z-encrypt:v1").encode(),
progress=lambda n: pbar.update(n),
)
pbar.close()
enc_path.unlink()
if dest_arg and not dest_arg.is_dir():
out_dir = dest_arg.parent
@@ -360,12 +372,16 @@ def main() -> int:
final = dest_arg
print(f"[下载] 完成 -> {final} ({final.stat().st_size / 1048576:.1f} MB, 已解密+解压)")
else:
pbar = tqdm(total=enc_path.stat().st_size, desc="[解密]", unit="B",
unit_scale=True, leave=False)
with open(enc_path, "rb") as src, open(final, "wb") as dst:
engine.decrypt_stream(
src, dst,
enc_params["key_id"],
enc_params.get("context", "7z-encrypt:v1").encode(),
progress=lambda n: pbar.update(n),
)
pbar.close()
enc_path.unlink()
print(f"[下载] 完成 -> {final} ({final.stat().st_size / 1048576:.1f} MB, 已解密)")
else: