filedust 0.2.0__tar.gz → 0.3.1__tar.gz
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.
- {filedust-0.2.0 → filedust-0.3.1}/PKG-INFO +21 -12
- {filedust-0.2.0 → filedust-0.3.1}/README.md +16 -7
- {filedust-0.2.0 → filedust-0.3.1}/pyproject.toml +5 -5
- {filedust-0.2.0 → filedust-0.3.1}/src/filedust/cli.py +18 -2
- filedust-0.3.1/src/filedust/junk.py +267 -0
- filedust-0.2.0/src/filedust/junk.py +0 -106
- {filedust-0.2.0 → filedust-0.3.1}/LICENCE +0 -0
- {filedust-0.2.0 → filedust-0.3.1}/src/filedust/__init__.py +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: filedust
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Opinionated junk cleaner for dev machines (caches, build artifacts, editor backups).
|
|
5
|
-
Home-page: https://github.com/
|
|
5
|
+
Home-page: https://github.com/guardutils/filedust
|
|
6
6
|
License: GPL-3.0-or-later
|
|
7
7
|
Author: Marco D'Aleo
|
|
8
8
|
Author-email: marco@marcodaleo.com
|
|
@@ -13,16 +13,15 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.11
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.12
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
-
Requires-Dist: argcomplete (>=
|
|
17
|
-
Requires-Dist: rich (>=
|
|
18
|
-
Project-URL: Repository, https://github.com/
|
|
16
|
+
Requires-Dist: argcomplete (>=2)
|
|
17
|
+
Requires-Dist: rich (>=12)
|
|
18
|
+
Project-URL: Repository, https://github.com/guardutils/filedust
|
|
19
19
|
Description-Content-Type: text/markdown
|
|
20
20
|
|
|
21
|
-
[
|
|
25
|
-
[](https://github.com/mdaleo404/filedust/actions)
|
|
21
|
+
[](LICENCE)
|
|
22
|
+
[](https://github.com/guardutils/filedust/)
|
|
23
|
+
[](https://github.com/guardutils/filedust/releases)
|
|
24
|
+
[](https://pypi.org/project/filedust/#history)
|
|
26
25
|
[](https://pypi.org/project/filedust/)
|
|
27
26
|
|
|
28
27
|
# filedust
|
|
@@ -55,7 +54,11 @@ One interactive prompt at the end of the run (unless -y is used).
|
|
|
55
54
|
Shows how much disk space can be freed.
|
|
56
55
|
|
|
57
56
|
### Safe by design
|
|
58
|
-
|
|
57
|
+
* It ONLY runs within user's `$HOME`
|
|
58
|
+
|
|
59
|
+
* Put user in control by reading `~/.filedust.conf`
|
|
60
|
+
|
|
61
|
+
* Never touches dotfiles, configs, project files, or anything important unless you want.
|
|
59
62
|
|
|
60
63
|
## Installation
|
|
61
64
|
|
|
@@ -66,11 +69,17 @@ pip install filedust
|
|
|
66
69
|
|
|
67
70
|
### From this repository
|
|
68
71
|
```
|
|
69
|
-
git clone https://github.com/
|
|
72
|
+
git clone https://github.com/guardutils/filedust.git
|
|
70
73
|
cd filedust/
|
|
71
74
|
poetry install
|
|
72
75
|
```
|
|
73
76
|
|
|
77
|
+
### Custom config
|
|
78
|
+
You can download the example and add your custom rule
|
|
79
|
+
```
|
|
80
|
+
wget -O ~/.filedust.conf https://raw.githubusercontent.com/guardutils/filedust/main/.filedust.conf.example
|
|
81
|
+
```
|
|
82
|
+
|
|
74
83
|
### TAB completion
|
|
75
84
|
Add this to your `.bashrc`
|
|
76
85
|
```
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
[
|
|
5
|
-
[](https://github.com/mdaleo404/filedust/actions)
|
|
1
|
+
[](LICENCE)
|
|
2
|
+
[](https://github.com/guardutils/filedust/)
|
|
3
|
+
[](https://github.com/guardutils/filedust/releases)
|
|
4
|
+
[](https://pypi.org/project/filedust/#history)
|
|
6
5
|
[](https://pypi.org/project/filedust/)
|
|
7
6
|
|
|
8
7
|
# filedust
|
|
@@ -35,7 +34,11 @@ One interactive prompt at the end of the run (unless -y is used).
|
|
|
35
34
|
Shows how much disk space can be freed.
|
|
36
35
|
|
|
37
36
|
### Safe by design
|
|
38
|
-
|
|
37
|
+
* It ONLY runs within user's `$HOME`
|
|
38
|
+
|
|
39
|
+
* Put user in control by reading `~/.filedust.conf`
|
|
40
|
+
|
|
41
|
+
* Never touches dotfiles, configs, project files, or anything important unless you want.
|
|
39
42
|
|
|
40
43
|
## Installation
|
|
41
44
|
|
|
@@ -46,11 +49,17 @@ pip install filedust
|
|
|
46
49
|
|
|
47
50
|
### From this repository
|
|
48
51
|
```
|
|
49
|
-
git clone https://github.com/
|
|
52
|
+
git clone https://github.com/guardutils/filedust.git
|
|
50
53
|
cd filedust/
|
|
51
54
|
poetry install
|
|
52
55
|
```
|
|
53
56
|
|
|
57
|
+
### Custom config
|
|
58
|
+
You can download the example and add your custom rule
|
|
59
|
+
```
|
|
60
|
+
wget -O ~/.filedust.conf https://raw.githubusercontent.com/guardutils/filedust/main/.filedust.conf.example
|
|
61
|
+
```
|
|
62
|
+
|
|
54
63
|
### TAB completion
|
|
55
64
|
Add this to your `.bashrc`
|
|
56
65
|
```
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "filedust"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.1"
|
|
4
4
|
description = "Opinionated junk cleaner for dev machines (caches, build artifacts, editor backups)."
|
|
5
5
|
authors = ["Marco D'Aleo <marco@marcodaleo.com>"]
|
|
6
6
|
license = "GPL-3.0-or-later"
|
|
7
7
|
readme = "README.md"
|
|
8
|
-
homepage = "https://github.com/
|
|
9
|
-
repository = "https://github.com/
|
|
8
|
+
homepage = "https://github.com/guardutils/filedust"
|
|
9
|
+
repository = "https://github.com/guardutils/filedust"
|
|
10
10
|
packages = [{ include = "filedust", from = "src" }]
|
|
11
11
|
|
|
12
12
|
[tool.poetry.dependencies]
|
|
13
13
|
python = ">=3.10,<4.0"
|
|
14
|
-
rich = "
|
|
15
|
-
argcomplete = "
|
|
14
|
+
rich = ">=12"
|
|
15
|
+
argcomplete = ">=2"
|
|
16
16
|
|
|
17
17
|
[tool.poetry.scripts]
|
|
18
18
|
filedust = "filedust.cli:main"
|
|
@@ -12,7 +12,7 @@ from rich.table import Table
|
|
|
12
12
|
from rich.prompt import Confirm
|
|
13
13
|
from rich import box
|
|
14
14
|
|
|
15
|
-
from .junk import Finding, iter_junk
|
|
15
|
+
from .junk import Finding, iter_junk, load_user_rules
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
console = Console()
|
|
@@ -172,18 +172,34 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
172
172
|
args = parser.parse_args(argv)
|
|
173
173
|
|
|
174
174
|
root = Path(args.path).expanduser()
|
|
175
|
+
home = Path.home().resolve()
|
|
176
|
+
root_resolved = root.resolve()
|
|
177
|
+
|
|
178
|
+
# Ensure root is inside the user's home directory
|
|
179
|
+
try:
|
|
180
|
+
root_resolved.relative_to(home)
|
|
181
|
+
except ValueError:
|
|
182
|
+
console.print(
|
|
183
|
+
f"[red]Error:[/] Refusing to operate outside the user's home directory.\n"
|
|
184
|
+
f"Requested: {root_resolved}\n"
|
|
185
|
+
f"Allowed: {home}"
|
|
186
|
+
)
|
|
187
|
+
return 1
|
|
175
188
|
|
|
176
189
|
if not root.exists():
|
|
177
190
|
console.print(f"[red]Error:[/] Path not found: {root}")
|
|
178
191
|
return 1
|
|
179
192
|
|
|
193
|
+
print("Looking for junk ...")
|
|
194
|
+
|
|
180
195
|
if root.resolve() == Path("/"):
|
|
181
196
|
console.print(
|
|
182
197
|
"[yellow]Running filedust on the entire filesystem (/). "
|
|
183
198
|
"This may take a while and may require sudo for deletions.[/]"
|
|
184
199
|
)
|
|
185
200
|
|
|
186
|
-
|
|
201
|
+
rules = load_user_rules()
|
|
202
|
+
findings = list(iter_junk(root, rules=rules))
|
|
187
203
|
total_size = compute_total_size(findings)
|
|
188
204
|
|
|
189
205
|
if not findings:
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import configparser
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from fnmatch import fnmatch
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Iterable, List
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class UserRules:
|
|
12
|
+
def __init__(self):
|
|
13
|
+
self.include: list[str] = []
|
|
14
|
+
self.exclude: list[str] = []
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def load_user_rules() -> UserRules:
|
|
18
|
+
rules = UserRules()
|
|
19
|
+
cfg_path = Path.home() / ".filedust.conf"
|
|
20
|
+
|
|
21
|
+
if cfg_path.exists():
|
|
22
|
+
parser = configparser.ConfigParser(allow_no_value=True)
|
|
23
|
+
parser.read(cfg_path)
|
|
24
|
+
|
|
25
|
+
if parser.has_section("include"):
|
|
26
|
+
rules.include = list(parser["include"].keys())
|
|
27
|
+
|
|
28
|
+
if parser.has_section("exclude"):
|
|
29
|
+
rules.exclude = list(parser["exclude"].keys())
|
|
30
|
+
|
|
31
|
+
return rules
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def matches_any(patterns: list[str], relpath: Path) -> bool:
|
|
35
|
+
posix = relpath.as_posix()
|
|
36
|
+
return any(fnmatch(posix, p) for p in patterns)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class Finding:
|
|
41
|
+
path: Path
|
|
42
|
+
kind: str # "file" or "dir"
|
|
43
|
+
reason: str
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# Directories that are *typically* safe to delete completely.
|
|
47
|
+
JUNK_DIR_NAMES = {
|
|
48
|
+
"__pycache__",
|
|
49
|
+
".pytest_cache",
|
|
50
|
+
".mypy_cache",
|
|
51
|
+
".ruff_cache",
|
|
52
|
+
".nox",
|
|
53
|
+
".tox",
|
|
54
|
+
".hypothesis",
|
|
55
|
+
".gradle",
|
|
56
|
+
".parcel-cache",
|
|
57
|
+
".turbo",
|
|
58
|
+
".next",
|
|
59
|
+
".vite",
|
|
60
|
+
".sass-cache",
|
|
61
|
+
".sass-cache",
|
|
62
|
+
"dist",
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
# File name patterns that are almost always junk / temporary.
|
|
66
|
+
JUNK_FILE_PATTERNS = [
|
|
67
|
+
"*~",
|
|
68
|
+
"*.swp",
|
|
69
|
+
"*.swo",
|
|
70
|
+
"*.swpx",
|
|
71
|
+
"*.tmp",
|
|
72
|
+
"*.temp",
|
|
73
|
+
"*.bak",
|
|
74
|
+
"*.orig",
|
|
75
|
+
"*.rej",
|
|
76
|
+
".DS_Store",
|
|
77
|
+
"Thumbs.db",
|
|
78
|
+
"desktop.ini",
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
# VCS / system dirs
|
|
82
|
+
SKIP_DIR_NAMES = {
|
|
83
|
+
".cache",
|
|
84
|
+
"build",
|
|
85
|
+
".gnupg",
|
|
86
|
+
".git",
|
|
87
|
+
".hg",
|
|
88
|
+
".svn",
|
|
89
|
+
".bzr",
|
|
90
|
+
".idea",
|
|
91
|
+
".vscode",
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
HOME = Path.home().resolve()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def safe_exists(path: Path) -> bool | None:
|
|
99
|
+
"""Return True/False if the path exists, or None if permission denied."""
|
|
100
|
+
try:
|
|
101
|
+
return path.exists()
|
|
102
|
+
except Exception:
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def safe_resolve(path: Path, root: Path) -> Path | None:
|
|
107
|
+
"""
|
|
108
|
+
Resolve symlinks only if safe.
|
|
109
|
+
Return resolved path if it stays within root.
|
|
110
|
+
Return None if:
|
|
111
|
+
- resolution escapes the root
|
|
112
|
+
- resolution fails
|
|
113
|
+
- permission denied
|
|
114
|
+
"""
|
|
115
|
+
try:
|
|
116
|
+
resolved = path.resolve(strict=False) # NEVER strict
|
|
117
|
+
resolved.relative_to(root) # ensure containment
|
|
118
|
+
return resolved
|
|
119
|
+
except Exception:
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def is_junk_dir_name(name: str) -> bool:
|
|
124
|
+
return name in JUNK_DIR_NAMES
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def is_junk_file_name(name: str) -> bool:
|
|
128
|
+
return any(fnmatch(name, pattern) for pattern in JUNK_FILE_PATTERNS)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def iter_junk(root: Path, rules: UserRules | None = None) -> Iterable[Finding]:
|
|
132
|
+
"""
|
|
133
|
+
Safe, fast junk scanner:
|
|
134
|
+
- Never follows symlinks.
|
|
135
|
+
- Broken symlinks are not automatically junk — they follow normal rules.
|
|
136
|
+
- User include/exclude overrides all.
|
|
137
|
+
- Built-in junk rules applied only when safe.
|
|
138
|
+
- SKIP_DIR_NAMES protected unless user includes.
|
|
139
|
+
- Fully contained in $HOME.
|
|
140
|
+
- No crashes from PermissionError or unreadable paths.
|
|
141
|
+
"""
|
|
142
|
+
if rules is None:
|
|
143
|
+
rules = UserRules()
|
|
144
|
+
|
|
145
|
+
root = root.resolve()
|
|
146
|
+
root_str = str(root)
|
|
147
|
+
|
|
148
|
+
for dirpath, dirnames, filenames in os.walk(root, followlinks=False):
|
|
149
|
+
dirpath_p = Path(dirpath)
|
|
150
|
+
|
|
151
|
+
# Fast relative path computation
|
|
152
|
+
if dirpath == root_str:
|
|
153
|
+
rel_dir = Path(".")
|
|
154
|
+
else:
|
|
155
|
+
rel_dir = Path(dirpath[len(root_str) :].lstrip("/"))
|
|
156
|
+
|
|
157
|
+
# USER EXCLUDE → skip entire subtree
|
|
158
|
+
if matches_any(rules.exclude, rel_dir):
|
|
159
|
+
dirnames[:] = []
|
|
160
|
+
continue
|
|
161
|
+
|
|
162
|
+
pruned = []
|
|
163
|
+
|
|
164
|
+
# Handling dirs
|
|
165
|
+
for d in dirnames:
|
|
166
|
+
child = dirpath_p / d
|
|
167
|
+
|
|
168
|
+
try:
|
|
169
|
+
st = child.lstat()
|
|
170
|
+
except Exception:
|
|
171
|
+
continue # unreadable
|
|
172
|
+
|
|
173
|
+
is_symlink = (st.st_mode & 0o170000) == 0o120000
|
|
174
|
+
|
|
175
|
+
if is_symlink:
|
|
176
|
+
# If broken symlink dir treat as file later via filenames (skip descent)
|
|
177
|
+
continue
|
|
178
|
+
|
|
179
|
+
rel_child = rel_dir / d
|
|
180
|
+
|
|
181
|
+
# User exclude wins
|
|
182
|
+
if matches_any(rules.exclude, rel_child):
|
|
183
|
+
continue
|
|
184
|
+
|
|
185
|
+
# SKIP_DIR_NAMES unless user includes
|
|
186
|
+
if d in SKIP_DIR_NAMES and not matches_any(
|
|
187
|
+
rules.include, rel_child
|
|
188
|
+
):
|
|
189
|
+
continue
|
|
190
|
+
|
|
191
|
+
pruned.append(d)
|
|
192
|
+
|
|
193
|
+
dirnames[:] = pruned
|
|
194
|
+
|
|
195
|
+
# Detect JUNK dirs
|
|
196
|
+
i = 0
|
|
197
|
+
while i < len(dirnames):
|
|
198
|
+
name = dirnames[i]
|
|
199
|
+
rel_child = rel_dir / name
|
|
200
|
+
|
|
201
|
+
# User include directory
|
|
202
|
+
if matches_any(rules.include, rel_child):
|
|
203
|
+
yield Finding(dirpath_p / name, "dir", "user_include")
|
|
204
|
+
del dirnames[i]
|
|
205
|
+
continue
|
|
206
|
+
|
|
207
|
+
# Built-in safe junk dirs
|
|
208
|
+
if is_junk_dir_name(name):
|
|
209
|
+
yield Finding(dirpath_p / name, "dir", "junk_dir")
|
|
210
|
+
del dirnames[i]
|
|
211
|
+
continue
|
|
212
|
+
|
|
213
|
+
i += 1
|
|
214
|
+
|
|
215
|
+
# Handling files (including symlinks)
|
|
216
|
+
for fname in filenames:
|
|
217
|
+
fpath = dirpath_p / fname
|
|
218
|
+
rel_file = rel_dir / fname
|
|
219
|
+
|
|
220
|
+
try:
|
|
221
|
+
st = fpath.lstat()
|
|
222
|
+
except Exception:
|
|
223
|
+
continue
|
|
224
|
+
|
|
225
|
+
is_symlink = (st.st_mode & 0o170000) == 0o120000
|
|
226
|
+
|
|
227
|
+
# Handling broken symlinks
|
|
228
|
+
if is_symlink:
|
|
229
|
+
exists = safe_exists(fpath)
|
|
230
|
+
|
|
231
|
+
# Permission denied → skip
|
|
232
|
+
if exists is None:
|
|
233
|
+
continue
|
|
234
|
+
|
|
235
|
+
# User exclude wins
|
|
236
|
+
if matches_any(rules.exclude, rel_file):
|
|
237
|
+
continue
|
|
238
|
+
|
|
239
|
+
# User include wins
|
|
240
|
+
if matches_any(rules.include, rel_file):
|
|
241
|
+
yield Finding(fpath, "file", "user_include")
|
|
242
|
+
continue
|
|
243
|
+
|
|
244
|
+
# Broken symlink?
|
|
245
|
+
if exists is False:
|
|
246
|
+
# DO NOT auto-delete — classify like regular file
|
|
247
|
+
# Only built-in junk patterns apply
|
|
248
|
+
if is_junk_file_name(fname):
|
|
249
|
+
yield Finding(fpath, "file", "broken_symlink")
|
|
250
|
+
continue
|
|
251
|
+
|
|
252
|
+
# Valid symlink — NEVER follow; only user-include counts
|
|
253
|
+
continue
|
|
254
|
+
|
|
255
|
+
# Regular files
|
|
256
|
+
# User exclude wins
|
|
257
|
+
if matches_any(rules.exclude, rel_file):
|
|
258
|
+
continue
|
|
259
|
+
|
|
260
|
+
# User include wins
|
|
261
|
+
if matches_any(rules.include, rel_file):
|
|
262
|
+
yield Finding(fpath, "file", "user_include")
|
|
263
|
+
continue
|
|
264
|
+
|
|
265
|
+
# Built-in junk patterns (safe ones)
|
|
266
|
+
if is_junk_file_name(fname):
|
|
267
|
+
yield Finding(fpath, "file", "junk_file")
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import os
|
|
4
|
-
from dataclasses import dataclass
|
|
5
|
-
from fnmatch import fnmatch
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
from typing import Iterable, List
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
@dataclass
|
|
11
|
-
class Finding:
|
|
12
|
-
path: Path
|
|
13
|
-
kind: str # "file" or "dir"
|
|
14
|
-
reason: str
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
# Directories that are *typically* safe to delete completely.
|
|
18
|
-
JUNK_DIR_NAMES = {
|
|
19
|
-
"__pycache__",
|
|
20
|
-
".pytest_cache",
|
|
21
|
-
".mypy_cache",
|
|
22
|
-
".ruff_cache",
|
|
23
|
-
".nox",
|
|
24
|
-
".tox",
|
|
25
|
-
".hypothesis",
|
|
26
|
-
".cache",
|
|
27
|
-
".gradle",
|
|
28
|
-
".parcel-cache",
|
|
29
|
-
".turbo",
|
|
30
|
-
".next",
|
|
31
|
-
".vite",
|
|
32
|
-
".sass-cache",
|
|
33
|
-
".sass-cache",
|
|
34
|
-
"build",
|
|
35
|
-
"dist",
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
# File name patterns that are almost always junk / temporary.
|
|
39
|
-
JUNK_FILE_PATTERNS = [
|
|
40
|
-
"*~",
|
|
41
|
-
"*.swp",
|
|
42
|
-
"*.swo",
|
|
43
|
-
"*.swpx",
|
|
44
|
-
"*.tmp",
|
|
45
|
-
"*.temp",
|
|
46
|
-
"*.bak",
|
|
47
|
-
"*.orig",
|
|
48
|
-
"*.rej",
|
|
49
|
-
".DS_Store",
|
|
50
|
-
"Thumbs.db",
|
|
51
|
-
"desktop.ini",
|
|
52
|
-
]
|
|
53
|
-
|
|
54
|
-
# VCS / system dirs
|
|
55
|
-
SKIP_DIR_NAMES = {
|
|
56
|
-
".git",
|
|
57
|
-
".hg",
|
|
58
|
-
".svn",
|
|
59
|
-
".bzr",
|
|
60
|
-
".idea",
|
|
61
|
-
".vscode",
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def is_junk_dir_name(name: str) -> bool:
|
|
66
|
-
return name in JUNK_DIR_NAMES
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
def is_junk_file_name(name: str) -> bool:
|
|
70
|
-
return any(fnmatch(name, pattern) for pattern in JUNK_FILE_PATTERNS)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
def iter_junk(root: Path) -> Iterable[Finding]:
|
|
74
|
-
"""
|
|
75
|
-
Walk the tree under `root` and yield junk candidates.
|
|
76
|
-
|
|
77
|
-
filedust:
|
|
78
|
-
- Skips known critical / config directories (SKIP_DIR_NAMES).
|
|
79
|
-
- Treats known "junk" directory names as removable as a whole.
|
|
80
|
-
- Treats known junk file patterns as removable.
|
|
81
|
-
"""
|
|
82
|
-
root = root.resolve()
|
|
83
|
-
|
|
84
|
-
for dirpath, dirnames, filenames in os.walk(root):
|
|
85
|
-
dirpath_p = Path(dirpath)
|
|
86
|
-
|
|
87
|
-
# Prune dirs we never touch at all.
|
|
88
|
-
dirnames[:] = [d for d in dirnames if d not in SKIP_DIR_NAMES]
|
|
89
|
-
|
|
90
|
-
# Detect junk directories (and skip walking inside them).
|
|
91
|
-
i = 0
|
|
92
|
-
while i < len(dirnames):
|
|
93
|
-
name = dirnames[i]
|
|
94
|
-
if is_junk_dir_name(name):
|
|
95
|
-
junk_dir = dirpath_p / name
|
|
96
|
-
yield Finding(path=junk_dir, kind="dir", reason="junk_dir")
|
|
97
|
-
# Remove from walk so we don't descend into it.
|
|
98
|
-
del dirnames[i]
|
|
99
|
-
continue
|
|
100
|
-
i += 1
|
|
101
|
-
|
|
102
|
-
# Now process files.
|
|
103
|
-
for fname in filenames:
|
|
104
|
-
if is_junk_file_name(fname):
|
|
105
|
-
fpath = dirpath_p / fname
|
|
106
|
-
yield Finding(path=fpath, kind="file", reason="junk_file")
|
|
File without changes
|
|
File without changes
|