feat: cleanup.py + improved error messages
This commit is contained in:
240
cleanup.py
Normal file
240
cleanup.py
Normal file
@@ -0,0 +1,240 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user