filedust 0.2.0__tar.gz → 0.3.0__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.0}/PKG-INFO +18 -9
- {filedust-0.2.0 → filedust-0.3.0}/README.md +15 -6
- {filedust-0.2.0 → filedust-0.3.0}/pyproject.toml +3 -3
- {filedust-0.2.0 → filedust-0.3.0}/src/filedust/cli.py +17 -2
- filedust-0.3.0/src/filedust/junk.py +267 -0
- filedust-0.2.0/src/filedust/junk.py +0 -106
- {filedust-0.2.0 → filedust-0.3.0}/LICENCE +0 -0
- {filedust-0.2.0 → filedust-0.3.0}/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.0
|
|
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
|
|
@@ -15,14 +15,13 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.13
|
|
16
16
|
Requires-Dist: argcomplete (>=3.3.0,<4.0.0)
|
|
17
17
|
Requires-Dist: rich (>=13.0.0,<14.0.0)
|
|
18
|
-
Project-URL: Repository, https://github.com/
|
|
18
|
+
Project-URL: Repository, https://github.com/guardutils/filedust
|
|
19
19
|
Description-Content-Type: text/markdown
|
|
20
20
|
|
|
21
|
-
[](LICENCE)
|
|
22
|
+
[](https://github.com/guardutils/filedust/)
|
|
23
|
+

|
|
24
24
|

|
|
25
|
-
[](https://github.com/mdaleo404/filedust/actions)
|
|
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
|
-
[](LICENCE)
|
|
2
|
+
[](https://github.com/guardutils/filedust/)
|
|
3
|
+

|
|
4
4
|

|
|
5
|
-
[](https://github.com/mdaleo404/filedust/actions)
|
|
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,12 +1,12 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "filedust"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.0"
|
|
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]
|
|
@@ -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()
|
|
@@ -168,10 +168,24 @@ def delete_all(findings: List[Finding]) -> int:
|
|
|
168
168
|
|
|
169
169
|
|
|
170
170
|
def main(argv: list[str] | None = None) -> int:
|
|
171
|
+
print("Looking for junk ...")
|
|
171
172
|
parser = build_parser()
|
|
172
173
|
args = parser.parse_args(argv)
|
|
173
174
|
|
|
174
175
|
root = Path(args.path).expanduser()
|
|
176
|
+
home = Path.home().resolve()
|
|
177
|
+
root_resolved = root.resolve()
|
|
178
|
+
|
|
179
|
+
# Ensure root is inside the user's home directory
|
|
180
|
+
try:
|
|
181
|
+
root_resolved.relative_to(home)
|
|
182
|
+
except ValueError:
|
|
183
|
+
console.print(
|
|
184
|
+
f"[red]Error:[/] Refusing to operate outside the user's home directory.\n"
|
|
185
|
+
f"Requested: {root_resolved}\n"
|
|
186
|
+
f"Allowed: {home}"
|
|
187
|
+
)
|
|
188
|
+
return 1
|
|
175
189
|
|
|
176
190
|
if not root.exists():
|
|
177
191
|
console.print(f"[red]Error:[/] Path not found: {root}")
|
|
@@ -183,7 +197,8 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
183
197
|
"This may take a while and may require sudo for deletions.[/]"
|
|
184
198
|
)
|
|
185
199
|
|
|
186
|
-
|
|
200
|
+
rules = load_user_rules()
|
|
201
|
+
findings = list(iter_junk(root, rules=rules))
|
|
187
202
|
total_size = compute_total_size(findings)
|
|
188
203
|
|
|
189
204
|
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
|