卷压缩模式: metadata --vol-compress (16MB切卷+逐卷7z+逐卷加密, context=vol:N 派生nonce, 服务端零改动) + 下载端 vol_mode 逐卷解密+解压+拼接
This commit is contained in:
+96
@@ -126,6 +126,96 @@ def sha256_of(path: str | Path) -> str:
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def _build_vol_init(src_path: Path, args: argparse.Namespace) -> int:
|
||||
"""卷压缩模式: 流式切卷 -> 逐卷 7z 压缩 -> 逐卷加密
|
||||
|
||||
每卷独立 context (7z-encrypt:v1:vol:N), nonce 由 context 派生,
|
||||
下载端按卷号重建 context 解密, 服务端零改动。
|
||||
"""
|
||||
import subprocess
|
||||
import tempfile
|
||||
from crypto import CONTEXT
|
||||
|
||||
vol_size = args.vol_compress * 1024 * 1024
|
||||
engine = CryptoEngine()
|
||||
try:
|
||||
engine.get_key(args.key_id)
|
||||
except KeyNotFoundError:
|
||||
engine.generate_key(args.key_id)
|
||||
print(f"[密钥库] 已生成新密钥 -> {engine.keyring_path}")
|
||||
|
||||
# 卷工作目录: 磁盘优先 (/root), Android /tmp 是 tmpfs 但卷小无碍, 统一临时目录
|
||||
workdir = Path(tempfile.gettempdir())
|
||||
init_path = src_path.with_name(src_path.name + ".init.json")
|
||||
manifest_path = src_path.with_name(src_path.name + "_manifest.json")
|
||||
src_st = src_path.stat().st_size
|
||||
print(f"=== 卷压缩模式: {src_path.name} ({src_st / 1048576:.1f} MB), 每卷 {args.vol_compress} MB ===")
|
||||
chunks: list[dict[str, Any]] = []
|
||||
total_sha = hashlib.sha256()
|
||||
idx = 0
|
||||
pbar = tqdm(total=src_st, desc="[压缩+加密]", unit="B", unit_scale=True, leave=False)
|
||||
with open(src_path, "rb") as src:
|
||||
while True:
|
||||
raw = src.read(vol_size)
|
||||
if not raw:
|
||||
break
|
||||
idx += 1
|
||||
raw_path = workdir / f"szvol_{src_path.name}_{idx:04d}.bin"
|
||||
z7_path = raw_path.with_suffix(".7z")
|
||||
chunk_path = src_path.with_name(f"{src_path.name}.chunk{idx:04d}")
|
||||
try:
|
||||
raw_path.write_bytes(raw)
|
||||
r = subprocess.run(
|
||||
["7z", "a", "-y", "-bd", "-mx=9", str(z7_path), str(raw_path)],
|
||||
capture_output=True, text=True,
|
||||
)
|
||||
if r.returncode != 0:
|
||||
raise MetadataError(f"卷 {idx} 7z 压缩失败: {r.stderr[-200:]}")
|
||||
raw_path.unlink()
|
||||
with open(z7_path, "rb") as s, open(chunk_path, "wb") as d:
|
||||
engine.encrypt_stream(
|
||||
s, d, args.key_id, context=f"{CONTEXT.decode()}:vol:{idx}".encode())
|
||||
z7_path.unlink()
|
||||
with open(chunk_path, "rb") as f:
|
||||
while True:
|
||||
b = f.read(1 << 20)
|
||||
if not b:
|
||||
break
|
||||
total_sha.update(b)
|
||||
chunks.append({
|
||||
"index": idx,
|
||||
"size": chunk_path.stat().st_size,
|
||||
"sha256": sha256_of(chunk_path),
|
||||
"filename": str(chunk_path),
|
||||
})
|
||||
finally:
|
||||
raw_path.unlink(missing_ok=True)
|
||||
z7_path.unlink(missing_ok=True)
|
||||
pbar.update(len(raw))
|
||||
pbar.close()
|
||||
|
||||
with open(manifest_path, "w", encoding="utf-8") as f:
|
||||
json.dump(chunks, f, indent=2, ensure_ascii=False)
|
||||
enc_params: dict[str, Any] = {
|
||||
"alg": "cobblestone-aes256gcm", "key_id": args.key_id,
|
||||
"context": CONTEXT.decode(), "compressed": True, "vol_mode": True,
|
||||
}
|
||||
file_name = args.file_name or src_path.name
|
||||
if not args.no_encrypt_name:
|
||||
file_name = engine.encrypt_name(file_name)
|
||||
init_json = build_init_json(
|
||||
file_name=file_name,
|
||||
file_size=src_st,
|
||||
total_sha256=total_sha.hexdigest(),
|
||||
enc=enc_params,
|
||||
chunks=chunks,
|
||||
)
|
||||
with open(init_path, "w", encoding="utf-8") as f:
|
||||
json.dump(init_json, f, indent=2, ensure_ascii=False)
|
||||
print(f"[完成] {idx} 卷 -> {init_path.name}, 清单 -> {manifest_path.name}")
|
||||
return 0
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description="元数据生成: 加密 -> 分卷 -> 组装 init json")
|
||||
ap.add_argument("file", nargs="?", default="index.mp4", help="要处理的文件 (默认 index.mp4)")
|
||||
@@ -135,6 +225,8 @@ def main() -> int:
|
||||
ap.add_argument("--file-name", help="覆盖 init json 的 file_name (压缩场景用原名)")
|
||||
ap.add_argument("--compressed", action="store_true",
|
||||
help="标记 7z 压缩 (enc_params 加 compressed, 下载端自动解压)")
|
||||
ap.add_argument("--vol-compress", type=int, default=0,
|
||||
help="卷压缩模式 MB: 流式切卷(每卷MB) -> 逐卷7z压缩 -> 逐卷加密 (规避大文件OOM)")
|
||||
ap.add_argument("--no-encrypt-name", action="store_true",
|
||||
help="不加密文件名 (默认加密成 enc: 前缀)")
|
||||
args = ap.parse_args()
|
||||
@@ -144,6 +236,10 @@ def main() -> int:
|
||||
print(f"[错误] 文件不存在: {src_path}")
|
||||
return 1
|
||||
|
||||
# 卷压缩模式: 流式切卷 -> 逐卷 7z 压缩 -> 逐卷加密 (每卷内存 16MB 级, 规避大文件 OOM)
|
||||
if args.vol_compress > 0:
|
||||
return _build_vol_init(src_path, args)
|
||||
|
||||
chunk_size = args.chunk_size * 1024 * 1024
|
||||
enc_path = src_path.with_name(src_path.name + ".enc")
|
||||
manifest_path = src_path.with_name(src_path.name + "_manifest.json")
|
||||
|
||||
+55
@@ -212,6 +212,10 @@ class TransferClient:
|
||||
elif dest.is_dir():
|
||||
dest = dest / name
|
||||
assert dest is not None
|
||||
# 卷压缩模式: 每卷独立解密+7z解压+拼接明文 (卷1直接处理, 其余 aria2/逐卷拉取)
|
||||
if enc_params.get("vol_mode"):
|
||||
self._download_vol_mode(file_id, dest, chunk_count, enc_params)
|
||||
return dest, enc_params
|
||||
outer = tqdm(total=chunk_count, desc="[下载]", unit="卷",
|
||||
position=0, leave=False)
|
||||
with open(dest, "wb") as f:
|
||||
@@ -276,6 +280,45 @@ class TransferClient:
|
||||
outer.update(1)
|
||||
return done
|
||||
|
||||
def _download_vol_mode(self, file_id: str, dest: Path, chunk_count: int,
|
||||
enc_params: dict[str, Any]) -> None:
|
||||
"""卷压缩模式下载: 逐卷拉密文 -> 逐卷解密(context=vol:N) -> 7z x -> 拼接明文
|
||||
|
||||
每卷 16MB 级内存, 规避大文件 OOM; 卷 context 与上传端对称派生。
|
||||
"""
|
||||
import subprocess
|
||||
import tempfile
|
||||
from crypto import CryptoEngine
|
||||
base_ctx = enc_params.get("context") or "7z-encrypt:v1"
|
||||
key_id = enc_params.get("key_id") or "default_key"
|
||||
engine = CryptoEngine(_load_keyring_path())
|
||||
with tempfile.TemporaryDirectory(prefix="sz-vol-") as td:
|
||||
td = Path(td)
|
||||
open(dest, "wb").close()
|
||||
pbar = tqdm(total=chunk_count, desc="[下载+解密]", unit="卷", leave=False)
|
||||
for idx in range(1, chunk_count + 1):
|
||||
enc_path = td / f"v{idx}.enc"
|
||||
with self._open_chunk(file_id, idx) as resp:
|
||||
with open(enc_path, "wb") as f:
|
||||
shutil.copyfileobj(resp, f, 1 << 20)
|
||||
z7_path = td / f"v{idx}.7z"
|
||||
with open(enc_path, "rb") as s, open(z7_path, "wb") as d:
|
||||
engine.decrypt_stream(s, d, key_id, f"{base_ctx}:vol:{idx}".encode())
|
||||
out_dir = td / f"out{idx}"
|
||||
r = subprocess.run(
|
||||
["7z", "x", "-y", "-bd", f"-o{out_dir}", str(z7_path)],
|
||||
capture_output=True, text=True,
|
||||
)
|
||||
if r.returncode != 0:
|
||||
raise TransferError(f"卷 {idx} 7z 解压失败: {r.stderr[-200:]}")
|
||||
with open(dest, "ab") as f:
|
||||
for p in sorted(out_dir.iterdir()):
|
||||
if p.is_file():
|
||||
with open(p, "rb") as s:
|
||||
shutil.copyfileobj(s, f, 1 << 20)
|
||||
pbar.update(1)
|
||||
pbar.close()
|
||||
|
||||
def _aria2_pull(self, file_id: str, chunk_range: range, workdir: Path) -> list[Path]:
|
||||
"""aria2 并发拉取卷文件 (多任务打满带宽), 返回按序的卷文件列表
|
||||
|
||||
@@ -662,6 +705,18 @@ def main() -> int:
|
||||
final = dest_arg
|
||||
else:
|
||||
final = Path(name)
|
||||
if enc_params.get("vol_mode"):
|
||||
# 卷压缩模式: download() 内部已逐卷解密+解压+拼接明文到 enc_path
|
||||
if dest_arg and dest_arg.is_dir():
|
||||
final = dest_arg / name
|
||||
elif dest_arg:
|
||||
final = dest_arg
|
||||
else:
|
||||
final = Path(name)
|
||||
final.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.move(enc_path, final)
|
||||
print(f"[下载] 完成 -> {final} ({final.stat().st_size / 1048576:.1f} MB, 卷解密+解压还原)")
|
||||
return 0
|
||||
from crypto import CryptoEngine
|
||||
engine = CryptoEngine(_load_keyring_path())
|
||||
if enc_params.get("compressed"):
|
||||
|
||||
Reference in New Issue
Block a user