disk-space-saviour 0.0.0-stage → 0.12.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 +587 -0
- package/LICENSE +24 -0
- package/README.md +538 -2
- package/bin/dss.js +5 -0
- package/package.json +96 -4
- package/src/audit.js +84 -0
- package/src/clean.js +716 -0
- package/src/cli.js +448 -0
- package/src/docker/cli.js +289 -0
- package/src/docker/containers.js +334 -0
- package/src/docker/images.js +122 -0
- package/src/docker/scan.js +531 -0
- package/src/emergency.js +169 -0
- package/src/env/local.js +715 -0
- package/src/env/resolve.js +36 -0
- package/src/env/shell.js +433 -0
- package/src/exec.js +185 -0
- package/src/git.js +338 -0
- package/src/health.js +174 -0
- package/src/index.d.ts +255 -0
- package/src/index.js +52 -0
- package/src/items.js +166 -0
- package/src/liveness.js +190 -0
- package/src/options.js +112 -0
- package/src/paths.js +182 -0
- package/src/report.js +376 -0
- package/src/rules/ecosystems.js +916 -0
- package/src/rules/other.js +375 -0
- package/src/rules/versions.js +256 -0
- package/src/scan.js +384 -0
- package/src/scanners/agents.js +166 -0
- package/src/scanners/common.js +38 -0
- package/src/scanners/global.js +217 -0
- package/src/scanners/projects.js +335 -0
- package/src/scanners/rust.js +404 -0
- package/src/scanners/system.js +171 -0
- package/src/scanners/versions.js +230 -0
- package/src/units.js +130 -0
package/src/emergency.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `emergency({free, until})`: frees space tier by tier (safe, moderate,
|
|
3
|
+
* aggressive), re-reading the filesystem after every removal and stopping
|
|
4
|
+
* as soon as the goal is met.
|
|
5
|
+
*
|
|
6
|
+
* - `free: '20G'`: at least 20 GiB available on the target volume;
|
|
7
|
+
* - `until: '80%'`: at most 80% of the target volume used.
|
|
8
|
+
*
|
|
9
|
+
* Within a tier, caches inside containers and on the host go first, Docker
|
|
10
|
+
* objects next, and stopped containers last, and those only with operator
|
|
11
|
+
* approval (`removeStoppedContainers` or an interactive yes). A container
|
|
12
|
+
* kept for investigation needs its own `removeContainers` entry.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { startAudit, writeAudit } from './audit.js';
|
|
16
|
+
import { Cleaner, cleanOrder, finishAudit, wantedItems } from './clean.js';
|
|
17
|
+
import { LocalEnv } from './env/local.js';
|
|
18
|
+
import { TIERS, selectByTier, tierRank } from './items.js';
|
|
19
|
+
import { resolveGoal, resolveOptions } from './options.js';
|
|
20
|
+
import { revalidateReport, scan } from './scan.js';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Used percentage the way `df` prints it: used / (used + available).
|
|
24
|
+
*/
|
|
25
|
+
export function usedPercent(disk) {
|
|
26
|
+
const total = disk.used + disk.free;
|
|
27
|
+
return total > 0 ? (disk.used / total) * 100 : 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Whether `disk` satisfies `goal`.
|
|
32
|
+
*/
|
|
33
|
+
export function goalMet(goal, disk) {
|
|
34
|
+
return (
|
|
35
|
+
(goal.freeBytes === null || disk.free >= goal.freeBytes) &&
|
|
36
|
+
(goal.untilPercent === null || usedPercent(disk) <= goal.untilPercent)
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Bytes still to free for `goal` on `disk` (0 when met).
|
|
42
|
+
*/
|
|
43
|
+
export function bytesNeeded(goal, disk) {
|
|
44
|
+
const total = disk.used + disk.free;
|
|
45
|
+
const forFree =
|
|
46
|
+
goal.freeBytes === null ? 0 : Math.max(0, goal.freeBytes - disk.free);
|
|
47
|
+
const forPercent =
|
|
48
|
+
goal.untilPercent === null
|
|
49
|
+
? 0
|
|
50
|
+
: Math.max(0, disk.used - (goal.untilPercent / 100) * total);
|
|
51
|
+
return Math.ceil(Math.max(forFree, forPercent));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function readDisk(env, target) {
|
|
55
|
+
const disk = await env.diskUsage(target);
|
|
56
|
+
if (!disk) {
|
|
57
|
+
throw new Error(`cannot read the free space of ${target}`);
|
|
58
|
+
}
|
|
59
|
+
return disk;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function simulated(disk, freed) {
|
|
63
|
+
return { ...disk, free: disk.free + freed, used: disk.used - freed };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @param {object} input options (see ./options.js) plus `free`, `until`,
|
|
68
|
+
* `path` (volume to watch, `/` by default), `report` (reuse a scan),
|
|
69
|
+
* `dryRun`, `confirm`, `env`, `audit: false`
|
|
70
|
+
* @returns {Promise<object>} audit log with `goalMet`, `diskBefore`,
|
|
71
|
+
* `diskAfter`
|
|
72
|
+
*/
|
|
73
|
+
export async function emergency(input = {}) {
|
|
74
|
+
const goal = resolveGoal(input);
|
|
75
|
+
const options = resolveOptions({
|
|
76
|
+
...input,
|
|
77
|
+
tier: input.tier ?? 'aggressive',
|
|
78
|
+
});
|
|
79
|
+
const env = input.env ?? new LocalEnv();
|
|
80
|
+
const target =
|
|
81
|
+
input.path ?? (env.platform === 'win32' ? env.currentHome : '/');
|
|
82
|
+
const diskBefore = await readDisk(env, target);
|
|
83
|
+
const audit = startAudit('emergency', {
|
|
84
|
+
dryRun: Boolean(input.dryRun),
|
|
85
|
+
goal,
|
|
86
|
+
path: target,
|
|
87
|
+
diskBefore,
|
|
88
|
+
bytesNeeded: bytesNeeded(goal, diskBefore),
|
|
89
|
+
});
|
|
90
|
+
const report = input.report
|
|
91
|
+
? await revalidateReport(input.report, { ...input, env })
|
|
92
|
+
: await scan({ ...input, env });
|
|
93
|
+
audit.report = { createdAt: report.createdAt, totals: report.totals };
|
|
94
|
+
if (options.audit !== false) {
|
|
95
|
+
await writeAudit(audit, { ...options, inProgress: true });
|
|
96
|
+
}
|
|
97
|
+
let disk = diskBefore;
|
|
98
|
+
if (!goalMet(goal, disk)) {
|
|
99
|
+
disk = await escalate({ report, options, env, target, goal, audit });
|
|
100
|
+
}
|
|
101
|
+
audit.diskAfter = disk;
|
|
102
|
+
audit.goalMet = goalMet(goal, disk);
|
|
103
|
+
return finishAudit(audit, report, options);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function escalate({ report, options, env, target, goal, audit }) {
|
|
107
|
+
const persistProgress = async (entry) => {
|
|
108
|
+
if (!audit.entries.includes(entry)) {
|
|
109
|
+
audit.entries.push(entry);
|
|
110
|
+
}
|
|
111
|
+
if (options.audit !== false) {
|
|
112
|
+
await writeAudit(audit, { ...options, inProgress: true });
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
const cleaner = new Cleaner(report, {
|
|
116
|
+
...options,
|
|
117
|
+
env,
|
|
118
|
+
onProgress: persistProgress,
|
|
119
|
+
});
|
|
120
|
+
audit.health = cleaner.health.records;
|
|
121
|
+
const done = new Set();
|
|
122
|
+
const targetDevice = await env.deviceId?.(target);
|
|
123
|
+
const scoped = await Promise.all(
|
|
124
|
+
wantedItems(report, options).map(async (item) => {
|
|
125
|
+
if (targetDevice === undefined) {
|
|
126
|
+
return item;
|
|
127
|
+
}
|
|
128
|
+
if (item.env !== env.id) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
const location = item.action?.volume ?? item.paths?.[0];
|
|
132
|
+
if (!location || !env.path.isAbsolute(location)) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
return (await env.deviceId(location)) === targetDevice ? item : null;
|
|
136
|
+
})
|
|
137
|
+
);
|
|
138
|
+
const wanted = scoped.filter(Boolean);
|
|
139
|
+
let disk = audit.diskBefore;
|
|
140
|
+
let freed = 0;
|
|
141
|
+
for (const tier of TIERS.slice(0, tierRank(options.tier) + 1)) {
|
|
142
|
+
audit.reachedTier = tier;
|
|
143
|
+
const items = selectByTier(wanted, tier)
|
|
144
|
+
.filter((item) => !done.has(item.id))
|
|
145
|
+
.sort(cleanOrder);
|
|
146
|
+
for (const item of items) {
|
|
147
|
+
done.add(item.id);
|
|
148
|
+
const entry = await cleaner.process(item);
|
|
149
|
+
if (!audit.entries.includes(entry)) {
|
|
150
|
+
audit.entries.push(entry);
|
|
151
|
+
}
|
|
152
|
+
options.onEntry?.(entry, item);
|
|
153
|
+
if (options.audit !== false) {
|
|
154
|
+
await writeAudit(audit, { ...options, inProgress: true });
|
|
155
|
+
}
|
|
156
|
+
if (entry.status !== 'removed' && entry.status !== 'planned') {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
freed += entry.status === 'planned' ? entry.plannedBytes : 0;
|
|
160
|
+
disk = options.dryRun
|
|
161
|
+
? simulated(audit.diskBefore, freed)
|
|
162
|
+
: await readDisk(env, target);
|
|
163
|
+
if (goalMet(goal, disk)) {
|
|
164
|
+
return disk;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return disk;
|
|
169
|
+
}
|