devmate 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,223 @@
1
+ """
2
+ toolkit — 文件瑞士军刀
3
+
4
+ 包含文件整理(organize)、目录大小排行(size)、缓存清理(cleanup)等实用功能。
5
+ """
6
+
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from devmate.bus.command import CommandResult
11
+
12
+ # ── 文件类型分类规则 ────────────────────────────────
13
+
14
+ CATEGORY_RULES: dict[str, list[str]] = {
15
+ "图片": [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".svg", ".webp", ".ico"],
16
+ "文档": [".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx", ".txt", ".md", ".csv"],
17
+ "代码": [".py", ".js", ".ts", ".java", ".go", ".rs", ".c", ".cpp", ".h", ".hpp",
18
+ ".css", ".scss", ".html", ".json", ".xml", ".yaml", ".yml", ".toml"],
19
+ "压缩包": [".zip", ".tar", ".gz", ".bz2", ".7z", ".rar"],
20
+ "视频": [".mp4", ".avi", ".mkv", ".mov", ".flv", ".wmv"],
21
+ "音频": [".mp3", ".wav", ".flac", ".aac", ".ogg"],
22
+ "安装包": [".dmg", ".pkg", ".exe", ".msi", ".deb", ".rpm", ".AppImage"],
23
+ }
24
+
25
+
26
+ def _get_category(suffix: str) -> str:
27
+ """根据文件后缀返回分类名"""
28
+ for category, extensions in CATEGORY_RULES.items():
29
+ if suffix.lower() in extensions:
30
+ return category
31
+ return "其他"
32
+
33
+
34
+ def _size_str(bytes_val: int) -> str:
35
+ """字节数转人类可读大小"""
36
+ for unit in ("B", "KB", "MB", "GB", "TB"):
37
+ if bytes_val < 1024:
38
+ return f"{bytes_val:.1f}{unit}"
39
+ bytes_val /= 1024
40
+ return f"{bytes_val:.1f}PB"
41
+
42
+
43
+ # ── organize:文件整理 ──────────────────────────────
44
+
45
+ def organize(source: str, dry_run: bool = False, by: str = "type") -> CommandResult:
46
+ """按类别整理文件
47
+
48
+ 按文件类型(by=type)或按修改日期(by=date)将文件分类到子目录。
49
+ """
50
+ src = Path(source).expanduser().resolve()
51
+ if not src.is_dir():
52
+ return CommandResult(success=False, error=f"目录不存在: {source}")
53
+
54
+ stats = {"已整理": 0, "跳过": 0, "错误": 0}
55
+ details: list[dict[str, Any]] = []
56
+
57
+ for item in src.iterdir():
58
+ if not item.is_file():
59
+ stats["跳过"] += 1
60
+ continue
61
+
62
+ if by == "type":
63
+ category = _get_category(item.suffix)
64
+ target_dir = src / category
65
+ action_label = category
66
+ elif by == "date":
67
+ mtime = item.stat().st_mtime
68
+ from datetime import datetime
69
+ date_str = datetime.fromtimestamp(mtime).strftime("%Y-%m")
70
+ target_dir = src / date_str
71
+ action_label = date_str
72
+ else:
73
+ return CommandResult(success=False, error=f"不支持的整理方式: {by}")
74
+
75
+ target = target_dir / item.name
76
+
77
+ if target.exists():
78
+ stats["跳过"] += 1
79
+ details.append({"file": item.name, "action": "跳过(已存在)"})
80
+ continue
81
+
82
+ if not dry_run:
83
+ target_dir.mkdir(parents=True, exist_ok=True)
84
+ try:
85
+ item.rename(target)
86
+ except OSError:
87
+ stats["错误"] += 1
88
+ continue
89
+
90
+ stats["已整理"] += 1
91
+ details.append({"file": item.name, "action": f"→ {action_label}"})
92
+
93
+ msg = f"整理完成: {stats['已整理']} 个文件已整理"
94
+ if stats["跳过"]:
95
+ msg += f", {stats['跳过']} 个跳过"
96
+ if stats["错误"]:
97
+ msg += f", {stats['错误']} 个错误"
98
+ if dry_run:
99
+ msg = f"[预览] 将整理 {stats['已整理']} 个文件" + (
100
+ f",{stats['跳过']} 个跳过" if stats["跳过"] else ""
101
+ )
102
+
103
+ return CommandResult(success=True, data={
104
+ "stats": stats,
105
+ "details": details,
106
+ "message": msg,
107
+ "source": str(src),
108
+ })
109
+
110
+
111
+ # ── size:目录大小排行 ─────────────────────────────
112
+
113
+ def size(path: str, top: int = 10) -> CommandResult:
114
+ """统计目录大小排行"""
115
+ target = Path(path).expanduser().resolve()
116
+ if not target.is_dir():
117
+ return CommandResult(success=False, error=f"目录不存在: {path}")
118
+
119
+ entries: list[dict[str, Any]] = []
120
+ total_size = 0
121
+ file_count = 0
122
+
123
+ try:
124
+ for item in target.iterdir():
125
+ if item.is_file():
126
+ size_bytes = item.stat().st_size
127
+ total_size += size_bytes
128
+ file_count += 1
129
+ entries.append({
130
+ "name": item.name,
131
+ "size": size_bytes,
132
+ "size_str": _size_str(size_bytes),
133
+ })
134
+ elif item.is_dir():
135
+ dir_size = sum(
136
+ f.stat().st_size for f in item.rglob("*") if f.is_file()
137
+ ) if any(item.iterdir()) else 0
138
+ total_size += dir_size
139
+ file_count += sum(1 for _ in item.rglob("*") if _.is_file())
140
+ entries.append({
141
+ "name": item.name + "/",
142
+ "size": dir_size,
143
+ "size_str": _size_str(dir_size),
144
+ })
145
+ except PermissionError as e:
146
+ return CommandResult(success=False, error=f"权限不足: {e}")
147
+
148
+ entries.sort(key=lambda x: x["size"], reverse=True)
149
+ top_entries = entries[:top]
150
+
151
+ return CommandResult(success=True, data={
152
+ "entries": top_entries,
153
+ "total_size": _size_str(total_size),
154
+ "file_count": file_count,
155
+ "path": str(target),
156
+ })
157
+
158
+
159
+ # ── cleanup:缓存清理 ─────────────────────────────
160
+
161
+ # 常见缓存目录(相对 ~ 或绝对路径)
162
+ CACHE_PATTERNS: list[str] = [
163
+ "*/__pycache__",
164
+ "*/.pytest_cache",
165
+ "*/node_modules/.cache",
166
+ "*/.mypy_cache",
167
+ "*/.ruff_cache",
168
+ "*/*.pyc",
169
+ "*/.DS_Store",
170
+ "*/Thumbs.db",
171
+ ]
172
+
173
+
174
+ def cleanup(path: str, dry_run: bool = False) -> CommandResult:
175
+ """清理缓存文件和临时文件"""
176
+ target = Path(path).expanduser().resolve()
177
+ if not target.is_dir():
178
+ return CommandResult(success=False, error=f"目录不存在: {path}")
179
+
180
+ stats = {"已清理": 0, "已释放": 0, "跳过": 0}
181
+ details: list[dict[str, Any]] = []
182
+
183
+ # 清理 __pycache__
184
+ for pycache in target.rglob("__pycache__"):
185
+ if pycache.is_dir():
186
+ size = sum(f.stat().st_size for f in pycache.rglob("*") if f.is_file())
187
+ if not dry_run:
188
+ import shutil
189
+ shutil.rmtree(pycache, ignore_errors=True)
190
+ stats["已清理"] += 1
191
+ stats["已释放"] += size
192
+ details.append({"name": str(pycache.relative_to(target)), "size": _size_str(size)})
193
+
194
+ # 清理 .pyc 文件
195
+ for pyc in target.rglob("*.pyc"):
196
+ if pyc.is_file():
197
+ size = pyc.stat().st_size
198
+ if not dry_run:
199
+ pyc.unlink()
200
+ stats["已清理"] += 1
201
+ stats["已释放"] += size
202
+ details.append({"name": str(pyc.relative_to(target)), "size": _size_str(size)})
203
+
204
+ # 清理 .DS_Store
205
+ for ds in target.rglob(".DS_Store"):
206
+ if ds.is_file():
207
+ size = ds.stat().st_size
208
+ if not dry_run:
209
+ ds.unlink()
210
+ stats["已清理"] += 1
211
+ stats["已释放"] += size
212
+ details.append({"name": str(ds.relative_to(target)), "size": _size_str(size)})
213
+
214
+ msg = f"清理完成: 清理了 {stats['已清理']} 个文件,释放 {_size_str(stats['已释放'])}"
215
+ if dry_run:
216
+ msg = f"[预览] 将清理 {stats['已清理']} 个文件,释放 {_size_str(stats['已释放'])}"
217
+
218
+ return CommandResult(success=True, data={
219
+ "stats": stats,
220
+ "details": details,
221
+ "message": msg,
222
+ "path": str(target),
223
+ })