disk-space-saviour 0.15.1 → 0.16.0
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.
- package/CHANGELOG.md +20 -0
- package/README.md +35 -1
- package/bin/dss.js +22 -5
- package/package.json +1 -1
- package/src/audit.js +19 -2
- package/src/clean.js +124 -6
- package/src/cli.js +24 -2
- package/src/docker/cli.js +11 -7
- package/src/docker/scan.js +25 -4
- package/src/docker/upperdir.js +44 -0
- package/src/docker/writable.js +90 -13
- package/src/env/batches.js +20 -0
- package/src/env/cache-roots.js +138 -0
- package/src/env/inspection.js +100 -0
- package/src/env/local.js +69 -32
- package/src/env/proc.js +13 -1
- package/src/env/process-scripts.js +24 -0
- package/src/env/scope.js +95 -0
- package/src/env/shell.js +105 -107
- package/src/exec.js +79 -8
- package/src/health.js +13 -2
- package/src/index.d.ts +25 -1
- package/src/items.js +9 -3
- package/src/liveness.js +112 -25
- package/src/options.js +6 -0
- package/src/report.js +8 -2
- package/src/rules/toolchains.js +66 -0
- package/src/scan.js +112 -7
- package/src/scanners/agents.js +3 -2
- package/src/scanners/common.js +6 -1
- package/src/scanners/global.js +72 -20
- package/src/scanners/projects.js +27 -2
- package/src/scanners/rust.js +7 -1
- package/src/scanners/system.js +7 -6
- package/src/scanners/versions.js +18 -9
package/src/scanners/global.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { scanItems } from '../env/scope.js';
|
|
1
2
|
/**
|
|
2
3
|
* Global per-user caches (ecosystem download and compilation caches, IDE and
|
|
3
4
|
* browser caches), system package caches, and leftovers in temporary
|
|
@@ -9,13 +10,16 @@ import { OTHER_RULES } from '../rules/other.js';
|
|
|
9
10
|
import { APP_RULES, expandChromiumCaches } from '../rules/apps.js';
|
|
10
11
|
import { compareVersions } from '../rules/versions.js';
|
|
11
12
|
import { block, makeItem } from '../items.js';
|
|
12
|
-
import { expandGlobPaths, expandRulePath } from '../paths.js';
|
|
13
|
-
import { olderThan, scanTimeBusy } from './common.js';
|
|
13
|
+
import { expandGlobPaths, expandRulePath, isWithin } from '../paths.js';
|
|
14
|
+
import { olderThan, scanTimeBusy, resolvePaths } from './common.js';
|
|
15
|
+
import { configuredPattern } from '../env/cache-roots.js';
|
|
16
|
+
import { TOOLCHAIN_CACHE_RULES } from '../rules/toolchains.js';
|
|
14
17
|
|
|
15
18
|
export const GLOBAL_RULES = [
|
|
16
19
|
...CACHE_RULES,
|
|
17
20
|
...expandChromiumCaches(OTHER_RULES),
|
|
18
21
|
...APP_RULES,
|
|
22
|
+
...TOOLCHAIN_CACHE_RULES,
|
|
19
23
|
];
|
|
20
24
|
|
|
21
25
|
function scopeBases(rule, homes, tmpDirs) {
|
|
@@ -32,15 +36,30 @@ function scopeBases(rule, homes, tmpDirs) {
|
|
|
32
36
|
function rulePatterns(env, rule, base) {
|
|
33
37
|
const patterns = [];
|
|
34
38
|
for (const pattern of rule.paths) {
|
|
39
|
+
if (pattern.includes('{GEM_PATH}')) {
|
|
40
|
+
if (base.home === env.currentHome) {
|
|
41
|
+
for (const root of (env.vars.GEM_PATH ?? '').split(
|
|
42
|
+
env.platform === 'win32' ? ';' : ':'
|
|
43
|
+
)) {
|
|
44
|
+
if (env.path.isAbsolute(root)) {
|
|
45
|
+
patterns.push(pattern.replace('{GEM_PATH}', root));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
35
51
|
const usesVars = /\{(?!TMP\})[A-Z_]+\}/.test(pattern);
|
|
36
52
|
if (usesVars && base.home !== env.currentHome) {
|
|
37
53
|
continue;
|
|
38
54
|
}
|
|
39
|
-
const expanded = expandRulePath(
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
55
|
+
const expanded = expandRulePath(
|
|
56
|
+
configuredPattern(env, base.home, pattern),
|
|
57
|
+
{
|
|
58
|
+
home: base.home ?? '/',
|
|
59
|
+
vars: { ...env.vars, TMP: base.tmp ?? undefined },
|
|
60
|
+
pathApi: env.path,
|
|
61
|
+
}
|
|
62
|
+
);
|
|
44
63
|
if (expanded) {
|
|
45
64
|
patterns.push(expanded);
|
|
46
65
|
}
|
|
@@ -59,15 +78,26 @@ async function nativeAction(context, rule, base, paths) {
|
|
|
59
78
|
if (!native || options.noNative) {
|
|
60
79
|
return { action: removal, blocker: null };
|
|
61
80
|
}
|
|
62
|
-
const
|
|
63
|
-
(
|
|
64
|
-
|
|
65
|
-
|
|
81
|
+
const init = native.init
|
|
82
|
+
? expandRulePath(configuredPattern(env, base.home, native.init), {
|
|
83
|
+
home: base.home,
|
|
84
|
+
pathApi: env.path,
|
|
85
|
+
})
|
|
86
|
+
: null;
|
|
87
|
+
const nativeRoot = native.cacheRoot
|
|
88
|
+
? expandRulePath(configuredPattern(env, base.home, native.cacheRoot), {
|
|
89
|
+
home: base.home,
|
|
90
|
+
pathApi: env.path,
|
|
91
|
+
})
|
|
92
|
+
: null;
|
|
93
|
+
const usable = await nativeUsable(env, rule, base, paths, nativeRoot, init);
|
|
66
94
|
if (usable) {
|
|
67
95
|
return {
|
|
68
96
|
action: {
|
|
69
97
|
type: 'command',
|
|
70
|
-
argv:
|
|
98
|
+
argv: init
|
|
99
|
+
? [native.tool, '-c', `. "$1" && ${native.command}`, 'dss', init]
|
|
100
|
+
: native.argv,
|
|
71
101
|
measure: paths,
|
|
72
102
|
},
|
|
73
103
|
blocker: null,
|
|
@@ -82,6 +112,16 @@ async function nativeAction(context, rule, base, paths) {
|
|
|
82
112
|
return { action: removal, blocker: null };
|
|
83
113
|
}
|
|
84
114
|
|
|
115
|
+
async function nativeUsable(env, rule, base, paths, root, init) {
|
|
116
|
+
return (
|
|
117
|
+
(rule.scope === 'system' || base.home === env.currentHome) &&
|
|
118
|
+
(await env.which(rule.native.tool)) &&
|
|
119
|
+
(!rule.native.root || (await env.isRoot())) &&
|
|
120
|
+
(!root || paths.every((target) => isWithin(target, root, env.path))) &&
|
|
121
|
+
(!init || (await env.exists(init)))
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
85
125
|
function ageFilter(context, rule) {
|
|
86
126
|
const { options } = context;
|
|
87
127
|
if (rule.minAge === 'stale') {
|
|
@@ -96,13 +136,13 @@ function ageFilter(context, rule) {
|
|
|
96
136
|
}
|
|
97
137
|
|
|
98
138
|
/** Keep the latest installed revision of each browser and platform family. */
|
|
99
|
-
|
|
139
|
+
function olderVersions(env, rule, paths, stats) {
|
|
100
140
|
if (!rule.versionPattern) {
|
|
101
141
|
return paths;
|
|
102
142
|
}
|
|
103
143
|
const groups = new Map();
|
|
104
144
|
for (const target of paths) {
|
|
105
|
-
if (
|
|
145
|
+
if (stats.get(target)?.type !== 'dir') {
|
|
106
146
|
continue;
|
|
107
147
|
}
|
|
108
148
|
const match = rule.versionPattern.exec(env.path.basename(target));
|
|
@@ -201,6 +241,14 @@ export async function scanGlobal(context, { rules = GLOBAL_RULES } = {}) {
|
|
|
201
241
|
env,
|
|
202
242
|
planned.flatMap((entry) => entry.patterns)
|
|
203
243
|
);
|
|
244
|
+
const typedPaths = planned
|
|
245
|
+
.filter(
|
|
246
|
+
({ rule }) => rule.directoryOnly || rule.fileOnly || rule.versionPattern
|
|
247
|
+
)
|
|
248
|
+
.flatMap(({ patterns }) =>
|
|
249
|
+
patterns.flatMap((pattern) => matches.get(pattern) ?? [])
|
|
250
|
+
);
|
|
251
|
+
const stats = await env.statMany([...new Set(typedPaths)]);
|
|
204
252
|
const found = [];
|
|
205
253
|
const seen = new Set();
|
|
206
254
|
for (const { rule, base, patterns } of planned) {
|
|
@@ -210,19 +258,23 @@ export async function scanGlobal(context, { rules = GLOBAL_RULES } = {}) {
|
|
|
210
258
|
|
|
211
259
|
const accepted = [];
|
|
212
260
|
for (const target of candidates) {
|
|
213
|
-
if (!(await acceptedPath(env, rule, target))) {
|
|
261
|
+
if (!(await acceptedPath(env, rule, target, stats))) {
|
|
214
262
|
continue;
|
|
215
263
|
}
|
|
216
264
|
accepted.push(target);
|
|
217
265
|
seen.add(target);
|
|
218
266
|
}
|
|
219
|
-
const paths =
|
|
267
|
+
const paths = olderVersions(env, rule, accepted, stats);
|
|
220
268
|
if (paths.length > 0) {
|
|
221
269
|
found.push({ rule, base, paths });
|
|
222
270
|
}
|
|
223
271
|
}
|
|
224
272
|
const usages = await env.usageMany(found.flatMap((entry) => entry.paths));
|
|
225
|
-
|
|
273
|
+
await resolvePaths(
|
|
274
|
+
context,
|
|
275
|
+
found.flatMap((entry) => entry.paths)
|
|
276
|
+
);
|
|
277
|
+
const items = scanItems(context);
|
|
226
278
|
for (const { rule, base, paths } of found) {
|
|
227
279
|
const accept = ageFilter(context, rule);
|
|
228
280
|
const measured = paths
|
|
@@ -238,15 +290,15 @@ export async function scanGlobal(context, { rules = GLOBAL_RULES } = {}) {
|
|
|
238
290
|
return items;
|
|
239
291
|
}
|
|
240
292
|
|
|
241
|
-
async function acceptedPath(env, rule, target) {
|
|
293
|
+
async function acceptedPath(env, rule, target, stats) {
|
|
242
294
|
const name = env.path.basename(target);
|
|
243
295
|
if (rule.excludeNames?.includes(name)) {
|
|
244
296
|
return false;
|
|
245
297
|
}
|
|
246
|
-
if (rule.directoryOnly &&
|
|
298
|
+
if (rule.directoryOnly && stats.get(target)?.type !== 'dir') {
|
|
247
299
|
return false;
|
|
248
300
|
}
|
|
249
|
-
if (rule.fileOnly &&
|
|
301
|
+
if (rule.fileOnly && stats.get(target)?.type !== 'file') {
|
|
250
302
|
return false;
|
|
251
303
|
}
|
|
252
304
|
if (
|
package/src/scanners/projects.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { scanItems } from '../env/scope.js';
|
|
1
2
|
/**
|
|
2
3
|
* Per-project dependency, build and cache directories (node_modules, target,
|
|
3
4
|
* .venv, ...) found by walking the scan roots.
|
|
@@ -261,7 +262,31 @@ const RUST_PARTS = {
|
|
|
261
262
|
*/
|
|
262
263
|
async function rustItems(context, targetItem, rule) {
|
|
263
264
|
const { env, liveness, options, now } = context;
|
|
264
|
-
const items =
|
|
265
|
+
const items = scanItems(context);
|
|
266
|
+
const semverPath = env.path.join(targetItem.path, 'semver-checks');
|
|
267
|
+
const semverUsage = await env.usage(semverPath);
|
|
268
|
+
if (semverUsage) {
|
|
269
|
+
const semver = makeItem(env, {
|
|
270
|
+
rule: 'cargo-semver-checks-cache',
|
|
271
|
+
kind: 'cache',
|
|
272
|
+
ecosystem: 'rust',
|
|
273
|
+
description: 'cargo-semver-checks baseline builds',
|
|
274
|
+
path: semverPath,
|
|
275
|
+
bytes: semverUsage.bytes,
|
|
276
|
+
newestMtimeMs: semverUsage.newestMtimeMs,
|
|
277
|
+
project: targetItem.project,
|
|
278
|
+
parentId: targetItem.id,
|
|
279
|
+
tier: 'safe',
|
|
280
|
+
reason: 'baseline rustdoc and builds are regenerated on demand',
|
|
281
|
+
checks: {
|
|
282
|
+
busy: ['cargo-semver-checks', ...rule.busy],
|
|
283
|
+
cwd: targetItem.project,
|
|
284
|
+
mtime: true,
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
await addBlockers(context, semver, false);
|
|
288
|
+
items.push(semver);
|
|
289
|
+
}
|
|
265
290
|
for (const profile of await findRustProfiles(env, targetItem.path)) {
|
|
266
291
|
const result = await analyzeRustProfile(env, profile, {
|
|
267
292
|
staleAgeMs: options.staleAgeMs,
|
|
@@ -315,7 +340,7 @@ export async function scanProjects(context, { rules = PROJECT_RULES } = {}) {
|
|
|
315
340
|
});
|
|
316
341
|
const usages = await env.usageMany(matches.map((m) => m.candidate.path));
|
|
317
342
|
const globs = [...new Set(rules.flatMap((rule) => rule.names))];
|
|
318
|
-
const items =
|
|
343
|
+
const items = scanItems(context);
|
|
319
344
|
for (const match of matches) {
|
|
320
345
|
const usage = usages.get(match.candidate.path);
|
|
321
346
|
if (!usage) {
|
package/src/scanners/rust.js
CHANGED
|
@@ -37,7 +37,13 @@ const HASHED = /^(.+)-([0-9a-f]{16})(\..*)?$/;
|
|
|
37
37
|
const INCREMENTAL = /^(.+)-([0-9a-z]{8,20})$/;
|
|
38
38
|
const LIB_PREFIXED = new Set(['.rlib', '.rmeta', '.so', '.dylib', '.a']);
|
|
39
39
|
const KIND_ORDER = ['lib', 'dylib', 'staticlib', 'bin', 'rmeta', 'other'];
|
|
40
|
-
const NON_PROFILES = new Set([
|
|
40
|
+
const NON_PROFILES = new Set([
|
|
41
|
+
'doc',
|
|
42
|
+
'package',
|
|
43
|
+
'tmp',
|
|
44
|
+
'flycheck0',
|
|
45
|
+
'semver-checks',
|
|
46
|
+
]);
|
|
41
47
|
const EXECUTABLE = new Set(['', '.exe']);
|
|
42
48
|
export const DEFAULT_GENERATION_MS = 10 * 60 * 1000;
|
|
43
49
|
export const DEFAULT_LEAF_IDLE_MS = 3 * 60 * 60 * 1000;
|
package/src/scanners/system.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { scanItems } from '../env/scope.js';
|
|
1
2
|
/**
|
|
2
3
|
* System reclaimers that only the owning tool can clean correctly:
|
|
3
4
|
* journald archives (`journalctl --vacuum-size`), disabled snap revisions
|
|
@@ -102,7 +103,7 @@ async function snapItems(context, isRoot) {
|
|
|
102
103
|
return [];
|
|
103
104
|
}
|
|
104
105
|
const result = await env.run(['snap', 'list', '--all']);
|
|
105
|
-
const items =
|
|
106
|
+
const items = scanItems(context);
|
|
106
107
|
for (const { name, revision } of parseDisabledSnaps(result.stdout)) {
|
|
107
108
|
const file = `/var/lib/snapd/snaps/${name}_${revision}.snap`;
|
|
108
109
|
const stat = await env.stat(file);
|
|
@@ -163,9 +164,9 @@ async function brewItems(context, isRoot) {
|
|
|
163
164
|
*/
|
|
164
165
|
export async function scanSystem(context) {
|
|
165
166
|
const isRoot = await context.env.isRoot();
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
...(await
|
|
169
|
-
|
|
170
|
-
|
|
167
|
+
const items = scanItems(context);
|
|
168
|
+
for (const scanner of [journalItems, snapItems, brewItems]) {
|
|
169
|
+
items.push(...(await scanner(context, isRoot)));
|
|
170
|
+
}
|
|
171
|
+
return items;
|
|
171
172
|
}
|
package/src/scanners/versions.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { scanItems } from '../env/scope.js';
|
|
1
2
|
/**
|
|
2
3
|
* Toolchain versions installed by version managers (nvm, pyenv, rbenv,
|
|
3
4
|
* SDKMAN!, rustup, elan, ghcup, opam, swiftly, VS Code Server).
|
|
@@ -15,21 +16,25 @@ import {
|
|
|
15
16
|
} from '../rules/versions.js';
|
|
16
17
|
import { block, makeItem } from '../items.js';
|
|
17
18
|
import { expandGlobPath, expandRulePath } from '../paths.js';
|
|
18
|
-
import { scanTimeBusy } from './common.js';
|
|
19
|
+
import { scanTimeBusy, resolvePaths } from './common.js';
|
|
20
|
+
import { configuredPattern } from '../env/cache-roots.js';
|
|
19
21
|
|
|
20
22
|
async function installDirs(env, rule, home) {
|
|
21
23
|
const dirs = [];
|
|
22
24
|
for (const pattern of rule.dirs) {
|
|
23
|
-
const expanded = expandRulePath(
|
|
24
|
-
|
|
25
|
+
const expanded = expandRulePath(configuredPattern(env, home, pattern), {
|
|
26
|
+
home,
|
|
27
|
+
pathApi: env.path,
|
|
28
|
+
});
|
|
29
|
+
const candidates = await expandGlobPath(env, expanded);
|
|
30
|
+
const stats = await env.statMany(candidates);
|
|
31
|
+
for (const candidate of candidates) {
|
|
25
32
|
const name = env.path.basename(candidate);
|
|
26
33
|
if ((rule.exclude ?? []).includes(name)) {
|
|
27
34
|
continue;
|
|
28
35
|
}
|
|
29
36
|
if (
|
|
30
|
-
!(rule.installTypes ?? ['dir']).includes(
|
|
31
|
-
(await env.stat(candidate))?.type
|
|
32
|
-
)
|
|
37
|
+
!(rule.installTypes ?? ['dir']).includes(stats.get(candidate)?.type)
|
|
33
38
|
) {
|
|
34
39
|
continue;
|
|
35
40
|
}
|
|
@@ -80,7 +85,10 @@ function linkRefs(target) {
|
|
|
80
85
|
*/
|
|
81
86
|
async function managerRefs(env, rule, home, installs) {
|
|
82
87
|
const expand = (pattern) =>
|
|
83
|
-
expandRulePath(
|
|
88
|
+
expandRulePath(configuredPattern(env, home, pattern), {
|
|
89
|
+
home,
|
|
90
|
+
pathApi: env.path,
|
|
91
|
+
});
|
|
84
92
|
const refs = [];
|
|
85
93
|
for (const source of rule.defaults ?? []) {
|
|
86
94
|
const text = await env.readText(expand(source.path), 65536);
|
|
@@ -183,9 +191,10 @@ async function ruleItems(context, rule, home, pins) {
|
|
|
183
191
|
}
|
|
184
192
|
const refs = [...(await managerRefs(env, rule, home, installs)), ...pins];
|
|
185
193
|
const usages = await env.usageMany(installs);
|
|
194
|
+
await resolvePaths(context, installs);
|
|
186
195
|
const newest = newestInstalls(env, rule, installs, usages);
|
|
187
196
|
const versionOf = rule.versionOf ?? ((dir) => env.path.basename(dir));
|
|
188
|
-
const items =
|
|
197
|
+
const items = scanItems(context);
|
|
189
198
|
for (const dir of installs) {
|
|
190
199
|
const version = versionOf(dir);
|
|
191
200
|
if (newest.has(dir) || refs.some((ref) => versionMatches(version, ref))) {
|
|
@@ -232,7 +241,7 @@ export async function scanVersions(context, { rules = VERSION_RULES } = {}) {
|
|
|
232
241
|
rules,
|
|
233
242
|
options.maxDepth ?? 6
|
|
234
243
|
);
|
|
235
|
-
const items =
|
|
244
|
+
const items = scanItems(context);
|
|
236
245
|
for (const rule of rules) {
|
|
237
246
|
for (const home of homes) {
|
|
238
247
|
items.push(...(await ruleItems(context, rule, home, pins.get(rule.id))));
|