241 lines
6.5 KiB
Python
241 lines
6.5 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
WeChat Decrypt — 数据清理工具
|
||
|
||
安全地查看和解密导出数据占用的磁盘空间,交互式清理。
|
||
|
||
用法:
|
||
python3 cleanup.py # 交互式清理
|
||
python3 cleanup.py status # 仅显示磁盘用量
|
||
python3 cleanup.py --dry-run # 显示将删除的内容但不实际操作
|
||
"""
|
||
|
||
import argparse
|
||
import glob
|
||
import json
|
||
import os
|
||
import shutil
|
||
import sys
|
||
|
||
|
||
def format_size(size_bytes):
|
||
"""格式化文件大小"""
|
||
if size_bytes > 1024 * 1024 * 1024:
|
||
return f"{size_bytes / 1024 / 1024 / 1024:.1f} GB"
|
||
elif size_bytes > 1024 * 1024:
|
||
return f"{size_bytes / 1024 / 1024:.0f} MB"
|
||
elif size_bytes > 1024:
|
||
return f"{size_bytes / 1024:.0f} KB"
|
||
else:
|
||
return f"{size_bytes} B"
|
||
|
||
|
||
class CleanupItem:
|
||
def __init__(self, name, path, is_dir=True, pattern=None, description=""):
|
||
self.name = name
|
||
self.path = path
|
||
self.is_dir = is_dir
|
||
self.pattern = pattern
|
||
self.description = description
|
||
|
||
def size(self):
|
||
if not self.exists():
|
||
return 0
|
||
if self.is_dir:
|
||
if self.pattern:
|
||
files = glob.glob(os.path.join(self.path, self.pattern), recursive=True)
|
||
files = [f for f in files if os.path.isfile(f)]
|
||
else:
|
||
files = []
|
||
for root, dirs, fnames in os.walk(self.path):
|
||
for fname in fnames:
|
||
files.append(os.path.join(root, fname))
|
||
return sum(os.path.getsize(f) for f in files)
|
||
else:
|
||
return os.path.getsize(self.path) if os.path.isfile(self.path) else 0
|
||
|
||
def exists(self):
|
||
if self.is_dir:
|
||
return os.path.isdir(self.path)
|
||
return os.path.isfile(self.path)
|
||
|
||
def delete(self):
|
||
if not self.exists():
|
||
return
|
||
if self.is_dir:
|
||
shutil.rmtree(self.path)
|
||
else:
|
||
os.unlink(self.path)
|
||
|
||
|
||
def get_items():
|
||
"""返回所有可清理项的列表"""
|
||
items = []
|
||
|
||
# 解密数据库
|
||
cfg = {}
|
||
if os.path.exists("config.json"):
|
||
with open("config.json") as f:
|
||
cfg = json.load(f)
|
||
decrypted_dir = cfg.get("decrypted_dir", "decrypted")
|
||
items.append(CleanupItem(
|
||
"解密数据库", decrypted_dir,
|
||
description="解密后的 SQLite 数据库文件(可重新解密恢复)"
|
||
))
|
||
|
||
# WAV 解码缓存
|
||
items.append(CleanupItem(
|
||
"语音 WAV 缓存", "decoded_voices",
|
||
description="SILK 解码后的临时 WAV 文件(可重新解码)"
|
||
))
|
||
|
||
# 图片解码缓存
|
||
items.append(CleanupItem(
|
||
"图片解码缓存", "decoded_images",
|
||
description="解密后的图片缓存"
|
||
))
|
||
|
||
# 导出 JSON
|
||
items.append(CleanupItem(
|
||
"导出聊天记录", "exported_chats",
|
||
description="export_all_chats.py 导出的 JSON 文件(可重新导出)"
|
||
))
|
||
|
||
# 旧格式导出
|
||
items.append(CleanupItem(
|
||
"旧格式导出", "exports",
|
||
description="旧版本导出的数据"
|
||
))
|
||
|
||
# 密钥文件
|
||
for kf in sorted(glob.glob("all_keys*.json")):
|
||
items.append(CleanupItem(
|
||
os.path.basename(kf), kf, is_dir=False,
|
||
description="密钥缓存文件(可重新提取)"
|
||
))
|
||
|
||
return items
|
||
|
||
|
||
def show_status(items):
|
||
"""显示各项目的磁盘用量"""
|
||
total = 0
|
||
rows = []
|
||
for item in items:
|
||
sz = item.size()
|
||
if sz > 0:
|
||
total += sz
|
||
rows.append((item.name, sz, item.description))
|
||
|
||
if not rows:
|
||
print("没有需要清理的数据。")
|
||
return 0
|
||
|
||
# 找最长的名称
|
||
name_width = max(len(r[0]) for r in rows) + 2
|
||
print(f"{'项目':<{name_width}}{'大小':>10} 说明")
|
||
print("-" * (name_width + 45))
|
||
for name, sz, desc in rows:
|
||
print(f"{name:<{name_width}}{format_size(sz):>10} {desc}")
|
||
print("-" * (name_width + 45))
|
||
print(f"{'总计':<{name_width}}{format_size(total):>10}")
|
||
return total
|
||
|
||
|
||
def cleanup(dry_run=False):
|
||
"""交互式清理"""
|
||
items = get_items()
|
||
|
||
print("=" * 60)
|
||
print(" 磁盘用量分析")
|
||
print("=" * 60)
|
||
print()
|
||
total = show_status(items)
|
||
if total == 0:
|
||
print()
|
||
print("没有需要清理的数据。")
|
||
return
|
||
|
||
print()
|
||
print("选择要删除的项目(逗号分隔,如: 1,3,5):")
|
||
print(" 输入 a 选择全部")
|
||
print(" 输入 n 取消")
|
||
choice = input("> ").strip().lower()
|
||
|
||
if choice in ("", "n"):
|
||
print("已取消。")
|
||
return
|
||
|
||
# 解析选择
|
||
indices = []
|
||
if choice == "a":
|
||
indices = list(range(len(items)))
|
||
else:
|
||
for part in choice.split(","):
|
||
part = part.strip()
|
||
try:
|
||
idx = int(part) - 1
|
||
if 0 <= idx < len(items):
|
||
indices.append(idx)
|
||
except ValueError:
|
||
pass
|
||
|
||
if not indices:
|
||
print("未选择任何项目。")
|
||
return
|
||
|
||
# 确认
|
||
total_saved = 0
|
||
print()
|
||
for idx in indices:
|
||
item = items[idx]
|
||
if item.exists():
|
||
sz = item.size()
|
||
total_saved += sz
|
||
print(f" [{idx+1}] {item.name} ({format_size(sz)})")
|
||
|
||
print(f"\n将释放 {format_size(total_saved)} 磁盘空间")
|
||
if dry_run:
|
||
print("(dry-run 模式,未实际删除)")
|
||
return
|
||
|
||
confirm = input("确认删除?(y/N): ").strip().lower()
|
||
if confirm != "y":
|
||
print("已取消。")
|
||
return
|
||
|
||
# 执行删除
|
||
for idx in indices:
|
||
item = items[idx]
|
||
if item.exists():
|
||
sz = item.size()
|
||
item.delete()
|
||
print(f" 已删除: {item.name} ({format_size(sz)})")
|
||
|
||
print()
|
||
# 显示剩余
|
||
remaining = sum(item.size() for item in get_items())
|
||
print(f"剩余: {format_size(remaining)}")
|
||
print("清理完成。")
|
||
|
||
|
||
def main():
|
||
parser = argparse.ArgumentParser(
|
||
description="WeChat Decrypt — 数据清理工具",
|
||
)
|
||
parser.add_argument("mode", nargs="?", default="interactive",
|
||
choices=["interactive", "status"],
|
||
help="interactive(默认)或 status(仅显示)")
|
||
parser.add_argument("--dry-run", action="store_true",
|
||
help="预览模式,不实际删除")
|
||
args = parser.parse_args()
|
||
|
||
if args.mode == "status":
|
||
show_status(get_items())
|
||
else:
|
||
cleanup(dry_run=args.dry_run)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|