veodl 1.8.1
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/AGENTS.md +11 -0
- package/CHANGELOG.md +264 -0
- package/LICENSE +21 -0
- package/README.md +725 -0
- package/bin/veo.js +4 -0
- package/docs/AGENT_GUIDE.md +66 -0
- package/package.json +59 -0
- package/src/backend-update.js +191 -0
- package/src/backend.js +520 -0
- package/src/cli.js +454 -0
- package/src/compatibility.js +39 -0
- package/src/config-clipboard.js +67 -0
- package/src/config-diagnostics.js +144 -0
- package/src/config-editor.js +281 -0
- package/src/config-errors.js +64 -0
- package/src/config-reset.js +39 -0
- package/src/config-template.js +171 -0
- package/src/config.js +213 -0
- package/src/disk-space.js +68 -0
- package/src/doctor.js +302 -0
- package/src/download-cache.js +31 -0
- package/src/downloader.js +600 -0
- package/src/execution.js +68 -0
- package/src/flush.js +66 -0
- package/src/history.js +173 -0
- package/src/inspect-media.js +168 -0
- package/src/interactive.js +82 -0
- package/src/jobs.js +172 -0
- package/src/legacy-config-comments.js +106 -0
- package/src/naming.js +50 -0
- package/src/open-file.js +15 -0
- package/src/output.js +61 -0
- package/src/paths.js +26 -0
- package/src/playlist.js +31 -0
- package/src/progress.js +152 -0
- package/src/run-archive.js +148 -0
- package/src/runs.js +333 -0
- package/src/state.js +27 -0
- package/src/stats.js +63 -0
- package/src/terminal-title.js +19 -0
- package/src/tool-setup.js +151 -0
- package/src/updater.js +227 -0
- package/src/utils.js +208 -0
- package/src/version.js +38 -0
package/src/doctor.js
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { commandOutput } from './output.js';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { mkdir, mkdtemp, readdir, readFile, rm, stat, writeFile } from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { inspectBackend, findOnPath, resolveBackend, TERMUX_SETUP } from './backend.js';
|
|
6
|
+
import { loadConfig } from './config.js';
|
|
7
|
+
import { cacheBase } from './paths.js';
|
|
8
|
+
import { fetchLatestVersion, compareVersions, packageVersion, defaultRegistry, veoCacheBase } from './updater.js';
|
|
9
|
+
import { cleanText, readableError } from './utils.js';
|
|
10
|
+
import { hasTermuxEjs } from './tool-setup.js';
|
|
11
|
+
|
|
12
|
+
export const DOCTOR_HELP = `veo doctor - diagnose the local setup
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
veo doctor [options]
|
|
16
|
+
veo doctor fix [options]
|
|
17
|
+
|
|
18
|
+
Options:
|
|
19
|
+
-o, --output <path> Output directory to check (default: current directory)
|
|
20
|
+
--offline Skip network checks
|
|
21
|
+
-h, --help Show help
|
|
22
|
+
|
|
23
|
+
Checks Node.js, the output directory, the backend cache, yt-dlp, FFmpeg/FFprobe,
|
|
24
|
+
reachability of the registry and the yt-dlp release host, leftover partial
|
|
25
|
+
downloads, and stale duplicate-detection records. Downloads no backend and
|
|
26
|
+
changes nothing but its own probe files and
|
|
27
|
+
the backend cache directory. The fix command restores managed tools (downloads
|
|
28
|
+
yt-dlp if needed unless --offline), creates the requested output directory, and
|
|
29
|
+
checks again. It does not change PATH, overrides or config values.
|
|
30
|
+
Missing desktop media tools are downloaded into veo's cache automatically.
|
|
31
|
+
On Android/Termux, fix installs missing tools with: ${TERMUX_SETUP} -y
|
|
32
|
+
With --offline no tools are downloaded and no package manager is run.
|
|
33
|
+
Exit status is 0 when no check fails, 1 otherwise.
|
|
34
|
+
`;
|
|
35
|
+
|
|
36
|
+
const LABEL_WIDTH = 18;
|
|
37
|
+
|
|
38
|
+
export function findProbeVersion(text, pattern = /\d{4}\.\d{2}\.\d{2}(?:\.\d+)?/) {
|
|
39
|
+
return cleanText(String(text)).match(pattern)?.[0];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Runs a trusted backend binary with fixed arguments; never uses a shell.
|
|
43
|
+
export function probe(executable, args, { timeoutMs = 15_000 } = {}) {
|
|
44
|
+
return new Promise((resolve, reject) => {
|
|
45
|
+
let child;
|
|
46
|
+
try {
|
|
47
|
+
child = spawn(executable, args, { shell: false, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
48
|
+
} catch (error) {
|
|
49
|
+
reject(error);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
let output = '';
|
|
53
|
+
let errors = '';
|
|
54
|
+
const timer = setTimeout(() => { child.kill(); reject(new Error('timed out')); }, timeoutMs);
|
|
55
|
+
timer.unref?.();
|
|
56
|
+
child.stdout.on('data', data => { if (output.length < 4096) output += data; });
|
|
57
|
+
child.stderr.on('data', data => { if (errors.length < 4096) errors += data; });
|
|
58
|
+
child.on('error', error => { clearTimeout(timer); reject(error); });
|
|
59
|
+
child.on('close', code => {
|
|
60
|
+
clearTimeout(timer);
|
|
61
|
+
if (code === 0) resolve(output || errors);
|
|
62
|
+
else reject(new Error(errors.trim() || `${path.basename(executable)} exited with code ${code}`));
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function writable(directory) {
|
|
68
|
+
try {
|
|
69
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
70
|
+
const probeDirectory = await mkdtemp(path.join(directory, '.veo-write-'));
|
|
71
|
+
await writeFile(path.join(probeDirectory, 'probe'), 'veo');
|
|
72
|
+
await rm(probeDirectory, { recursive: true, force: true });
|
|
73
|
+
return { ok: true };
|
|
74
|
+
} catch (error) {
|
|
75
|
+
return { ok: false, reason: readableError(error) };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// The nearest existing ancestor decides whether a not-yet-created output
|
|
80
|
+
// directory will be creatable, without creating it as a side effect.
|
|
81
|
+
async function nearestExisting(directory) {
|
|
82
|
+
let current = path.resolve(directory);
|
|
83
|
+
for (;;) {
|
|
84
|
+
try {
|
|
85
|
+
if ((await stat(current)).isDirectory()) return current;
|
|
86
|
+
} catch { /* keep walking up */ }
|
|
87
|
+
const parent = path.dirname(current);
|
|
88
|
+
if (parent === current) return undefined;
|
|
89
|
+
current = parent;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function summarize(checks) {
|
|
94
|
+
const failed = checks.filter(check => check.level === 'fail').length;
|
|
95
|
+
const warned = checks.filter(check => check.level === 'warn').length;
|
|
96
|
+
return { failed, warned, exitCode: failed ? 1 : 0 };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function formatReport({ version, checks, summary }) {
|
|
100
|
+
const lines = [`veo ${version} doctor`, ''];
|
|
101
|
+
for (const check of checks) lines.push(` ${check.level.padEnd(4)} ${check.label.padEnd(LABEL_WIDTH)}${check.detail}`);
|
|
102
|
+
lines.push('');
|
|
103
|
+
lines.push(summary.failed
|
|
104
|
+
? `${summary.failed} problem${summary.failed === 1 ? '' : 's'} found${summary.warned ? `, ${summary.warned} warning${summary.warned === 1 ? '' : 's'}` : ''}.`
|
|
105
|
+
: summary.warned ? `No problems found, ${summary.warned} warning${summary.warned === 1 ? '' : 's'}.` : 'No problems found.');
|
|
106
|
+
return `${lines.join('\n')}\n`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export async function collectChecks({
|
|
110
|
+
version,
|
|
111
|
+
platform = process.platform,
|
|
112
|
+
arch = process.arch,
|
|
113
|
+
nodeVersion = process.versions.node,
|
|
114
|
+
engines = '>=22',
|
|
115
|
+
output,
|
|
116
|
+
cwd = process.cwd(),
|
|
117
|
+
env = process.env,
|
|
118
|
+
offline = false,
|
|
119
|
+
inspect = inspectBackend,
|
|
120
|
+
find = findOnPath,
|
|
121
|
+
loadConfigImpl,
|
|
122
|
+
fetchImpl = fetch,
|
|
123
|
+
runProbe = probe,
|
|
124
|
+
hasEjs = hasTermuxEjs,
|
|
125
|
+
registry = defaultRegistry(env),
|
|
126
|
+
fetchLatest = fetchLatestVersion,
|
|
127
|
+
} = {}) {
|
|
128
|
+
const checks = [];
|
|
129
|
+
const push = (level, label, detail) => checks.push({ level, label, detail });
|
|
130
|
+
|
|
131
|
+
const major = Number(String(nodeVersion).split('.')[0]);
|
|
132
|
+
push(major >= 22 ? 'ok' : 'fail', 'Node.js', `v${nodeVersion} (veo requires ${engines.replace(/^>=\s*/, 'Node.js ')}${major >= 22 ? '' : ' — please upgrade'})`);
|
|
133
|
+
|
|
134
|
+
const report = await inspect().catch(error => ({ errors: [readableError(error)], ytDlp: {}, ffmpeg: {}, ffprobe: {} }));
|
|
135
|
+
const supported = platform === 'android' || Boolean(report.asset) || report.ytDlp?.source === 'override';
|
|
136
|
+
push(supported ? 'ok' : 'fail', 'Platform', `${platform}/${arch}${platform === 'android' ? ' (Termux system tools)' : report.asset ? ` (yt-dlp ${report.asset})` : report.ytDlp?.source === 'override' ? ' (VEO_YT_DLP_PATH)' : ' — set VEO_YT_DLP_PATH to a trusted executable'}`);
|
|
137
|
+
|
|
138
|
+
for (const message of report.errors || []) push('fail', 'Environment', message);
|
|
139
|
+
|
|
140
|
+
const target = path.resolve(cwd, output || cwd);
|
|
141
|
+
const existing = await nearestExisting(target);
|
|
142
|
+
const outputAccess = existing ? await writable(existing) : { ok: false, reason: 'no existing parent directory' };
|
|
143
|
+
const outputNote = existing === target ? '' : existing ? ` (will be created under ${existing})` : '';
|
|
144
|
+
push(outputAccess.ok ? 'ok' : 'fail', 'Output directory', `${target}${outputNote}${outputAccess.ok ? '' : ` — ${outputAccess.reason}`}`);
|
|
145
|
+
|
|
146
|
+
const cacheAccess = await writable(report.directory || veoCacheBase({ platform, env }));
|
|
147
|
+
push(cacheAccess.ok ? 'ok' : 'fail', 'Backend cache', `${report.directory || veoCacheBase({ platform, env })}${cacheAccess.ok ? '' : ` — ${cacheAccess.reason}`}`);
|
|
148
|
+
|
|
149
|
+
const yt = report.ytDlp || {};
|
|
150
|
+
if (yt.present && !['override', 'system'].includes(yt.source) && !yt.verified) {
|
|
151
|
+
push('fail', 'yt-dlp', `${yt.path} — SHA-256 verification failed; run veo doctor fix`);
|
|
152
|
+
} else if (yt.present) {
|
|
153
|
+
let detail = `${yt.path} (${yt.source === 'override' ? 'VEO_YT_DLP_PATH' : yt.source === 'system' ? 'system installation, not pinned by veo' : `release ${report.release}${yt.verified ? ', SHA-256 verified' : ''}`})`;
|
|
154
|
+
try {
|
|
155
|
+
const out = await runProbe(yt.path, ['--version']);
|
|
156
|
+
const probed = findProbeVersion(out);
|
|
157
|
+
detail = `${probed ? `version ${probed}, ` : ''}${detail}`;
|
|
158
|
+
push('ok', 'yt-dlp', detail);
|
|
159
|
+
} catch (error) {
|
|
160
|
+
push('fail', 'yt-dlp', `${detail} — could not run it: ${readableError(error)}`);
|
|
161
|
+
}
|
|
162
|
+
} else if (platform === 'android') {
|
|
163
|
+
push('fail', 'yt-dlp', `not found — run veo doctor fix to install automatically (Termux: ${TERMUX_SETUP})`);
|
|
164
|
+
} else if (supported) {
|
|
165
|
+
push('warn', 'yt-dlp', `not cached yet; downloaded on first download (release ${report.release ?? 'unknown'})`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (platform === 'android' && yt.source !== 'override') {
|
|
169
|
+
const ejs = await hasEjs({ find, run: runProbe });
|
|
170
|
+
push(ejs ? 'ok' : 'fail', 'YouTube JS', ejs ? 'yt-dlp-ejs installed; veo uses Node.js' : 'yt-dlp-ejs missing — run veo doctor fix to install automatically');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
for (const name of ['ffmpeg', 'ffprobe']) {
|
|
174
|
+
const tool = report[name] || {};
|
|
175
|
+
if (!tool.present) {
|
|
176
|
+
push('fail', name, 'not found — run veo doctor fix to install automatically, or set VEO_FFMPEG_PATH to a directory containing ffmpeg and ffprobe');
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
const source = tool.source === 'override' ? 'VEO_FFMPEG_PATH' : tool.source === 'cache' ? 'managed cache' : 'system installation';
|
|
180
|
+
try {
|
|
181
|
+
await runProbe(tool.path, ['-version']);
|
|
182
|
+
push('ok', name, `${tool.path} (${source})`);
|
|
183
|
+
} catch (error) {
|
|
184
|
+
push('fail', name, `${tool.path} (${source}) — could not run it: ${readableError(error)}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Only relevant when the bundled tools cannot serve this platform (e.g. Windows on ARM).
|
|
189
|
+
const systemFfmpeg = await find(['ffmpeg']);
|
|
190
|
+
const systemFfprobe = await find(['ffprobe']);
|
|
191
|
+
const mediaReady = ['ffmpeg', 'ffprobe'].every(name => checks.some(check => check.label === name && check.level === 'ok'));
|
|
192
|
+
push(systemFfmpeg && systemFfprobe || mediaReady ? 'ok' : 'warn', 'System FFmpeg',
|
|
193
|
+
systemFfmpeg && systemFfprobe ? `${path.dirname(systemFfmpeg)} (fallback)` : mediaReady ? 'not required — the selected FFmpeg and FFprobe work' : platform === 'android' ? 'not on PATH — in Termux run: pkg install ffmpeg' : 'not on PATH — run veo doctor fix to prepare the bundled tools');
|
|
194
|
+
|
|
195
|
+
const partials = await readdir(target, { withFileTypes: true }).catch(() => []);
|
|
196
|
+
const leftover = partials.filter(entry => entry.isDirectory() && entry.name.startsWith('.veo-') && entry.name !== '.veo-history').map(entry => entry.name);
|
|
197
|
+
if (leftover.length) push('warn', 'Partial data', `${leftover.length} legacy folder${leftover.length === 1 ? '' : 's'} in the output directory (${leftover.slice(0, 3).join(', ')}${leftover.length > 3 ? ', …' : ''}). These are not migrated to the local cache; inspect them before removing them.`);
|
|
198
|
+
else push('ok', 'Partial data', 'no leftover download folders');
|
|
199
|
+
const historyDir = path.join(target, '.veo-history');
|
|
200
|
+
const historyInfo = await stat(historyDir).catch(() => null);
|
|
201
|
+
if (!historyInfo) push('ok', 'Download history', 'no duplicate-detection records in the output directory');
|
|
202
|
+
else if (!historyInfo.isDirectory()) push('fail', 'Download history', `${historyDir} is not a directory — remove or rename it; downloads cannot save their duplicate-detection records otherwise`);
|
|
203
|
+
else {
|
|
204
|
+
const records = (await readdir(historyDir).catch(() => [])).filter(name => name.endsWith('.json'));
|
|
205
|
+
let stale = 0;
|
|
206
|
+
for (const name of records) {
|
|
207
|
+
let usable = false;
|
|
208
|
+
try {
|
|
209
|
+
const record = JSON.parse(await readFile(path.join(historyDir, name), 'utf8'));
|
|
210
|
+
usable = Boolean(record?.files?.length && record.files.every(file => typeof file === 'string' && path.dirname(file) === target)
|
|
211
|
+
&& (await Promise.all(record.files.map(file => stat(file).then(info => info.isFile(), () => false)))).every(Boolean));
|
|
212
|
+
} catch { usable = false; }
|
|
213
|
+
if (!usable) stale++;
|
|
214
|
+
}
|
|
215
|
+
if (!records.length) push('ok', 'Download history', 'empty record folder; removed automatically on the next download into this folder');
|
|
216
|
+
else if (stale) push('warn', 'Download history', `${stale} of ${records.length} record${records.length === 1 ? '' : 's'} reference${records.length === 1 ? 's' : ''} missing files; removed automatically on the next download into this folder`);
|
|
217
|
+
else push('ok', 'Download history', `${records.length} record${records.length === 1 ? '' : 's'}, all files present`);
|
|
218
|
+
}
|
|
219
|
+
const localDownloads = path.join(cacheBase({ env }), 'downloads');
|
|
220
|
+
const cachedDownloads = await readdir(localDownloads, { withFileTypes: true }).catch(() => []);
|
|
221
|
+
const count = cachedDownloads.filter(entry => entry.isDirectory() && /^\.veo-part-[a-f0-9]{24}$/.test(entry.name)).length;
|
|
222
|
+
push(count ? 'warn' : 'ok', 'Local downloads', `${localDownloads}: ${count} retained download folder(s). Failed transfers expire after 15 minutes and are cleaned on the next run; unfinished --resume downloads are kept.`);
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
const loaded = await (loadConfigImpl || loadConfig)({ env });
|
|
226
|
+
const keys = Object.keys(loaded.config || {});
|
|
227
|
+
push('ok', 'Config', loaded.exists ? `${loaded.file} (${keys.length ? keys.join(', ') : 'no defaults set'})` : `${loaded.file} not present (optional)`);
|
|
228
|
+
for (const warning of loaded.warnings || []) push('warn', 'Config', warning);
|
|
229
|
+
} catch (error) {
|
|
230
|
+
push('fail', 'Config', `${readableError(error)}`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (offline) {
|
|
234
|
+
push('warn', 'Network', 'checks skipped (--offline)');
|
|
235
|
+
} else {
|
|
236
|
+
let latest = null;
|
|
237
|
+
try {
|
|
238
|
+
latest = await fetchLatest({ registry, fetchImpl, timeoutMs: 6000 });
|
|
239
|
+
push('ok', 'npm registry', `${registry} reachable`);
|
|
240
|
+
} catch (error) {
|
|
241
|
+
push('warn', 'npm registry', `${registry} unreachable — ${readableError(error)}`);
|
|
242
|
+
}
|
|
243
|
+
try {
|
|
244
|
+
const response = await fetchImpl('https://github.com/yt-dlp/yt-dlp/releases', { method: 'HEAD', signal: AbortSignal.timeout(6000) });
|
|
245
|
+
await response.body?.cancel();
|
|
246
|
+
push(response.ok || response.status < 500 ? 'ok' : 'warn', 'GitHub', `yt-dlp release host reachable (HTTP ${response.status})`);
|
|
247
|
+
} catch (error) {
|
|
248
|
+
push('warn', 'GitHub', `yt-dlp release host unreachable — ${readableError(error)} (set VEO_YT_DLP_PATH to use a local backend)`);
|
|
249
|
+
}
|
|
250
|
+
if (latest) {
|
|
251
|
+
const newer = compareVersions(latest, version) > 0;
|
|
252
|
+
push(newer ? 'warn' : 'ok', 'veo version', newer ? `${version} — ${latest} is available (run: veo update)` : `${version} is the latest release`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return checks;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export async function doctorMain(args = [], {
|
|
259
|
+
stdout = process.stdout,
|
|
260
|
+
stderr = process.stderr,
|
|
261
|
+
env = process.env,
|
|
262
|
+
cwd = process.cwd(),
|
|
263
|
+
registry = defaultRegistry(env),
|
|
264
|
+
...deps
|
|
265
|
+
} = {}) {
|
|
266
|
+
[args, stdout, stderr] = commandOutput(args, stdout, stderr);
|
|
267
|
+
let offline = false;
|
|
268
|
+
let output;
|
|
269
|
+
const fix = args[0] === 'fix';
|
|
270
|
+
if (fix) args = args.slice(1);
|
|
271
|
+
for (let index = 0; index < args.length; index++) {
|
|
272
|
+
const token = args[index];
|
|
273
|
+
if (token === '--offline') offline = true;
|
|
274
|
+
else if (token === '-h' || token === '--help') { stdout.write(DOCTOR_HELP); return 0; }
|
|
275
|
+
else if (token === '-o' || token === '--output') {
|
|
276
|
+
output = args[++index];
|
|
277
|
+
if (output === undefined) throw new Error('--output requires a directory. Run veo doctor --help for usage.');
|
|
278
|
+
} else throw new Error(`Unknown option for veo doctor: ${cleanText(token)}. Run veo doctor --help for usage.`);
|
|
279
|
+
}
|
|
280
|
+
const version = deps.version ?? await packageVersion();
|
|
281
|
+
const repairs = [];
|
|
282
|
+
if (fix) {
|
|
283
|
+
stdout.write('Repairing local setup…\n');
|
|
284
|
+
try {
|
|
285
|
+
await (deps.repairBackend || resolveBackend)({ offline, onStatus: message => stdout.write(`${cleanText(message)}\n`) });
|
|
286
|
+
stdout.write('Backend tools are ready.\n');
|
|
287
|
+
} catch (error) {
|
|
288
|
+
repairs.push({ level: 'fail', label: 'Repair tools', detail: readableError(error) });
|
|
289
|
+
}
|
|
290
|
+
if (output) {
|
|
291
|
+
try { await mkdir(path.resolve(cwd, output), { recursive: true }); }
|
|
292
|
+
catch (error) { repairs.push({ level: 'fail', label: 'Repair output', detail: readableError(error) }); }
|
|
293
|
+
}
|
|
294
|
+
stdout.write('Checking setup after repairs…\n\n');
|
|
295
|
+
}
|
|
296
|
+
const checks = await collectChecks({ version, cwd, env, offline, output, registry, ...deps });
|
|
297
|
+
checks.push(...repairs);
|
|
298
|
+
const summary = summarize(checks);
|
|
299
|
+
stdout.write(formatReport({ version, checks, summary }));
|
|
300
|
+
if (summary.failed) stderr.write('veo: some checks failed. See the report above.\n');
|
|
301
|
+
return summary.exitCode;
|
|
302
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { lstat, mkdir, open, readdir, rm } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { cacheBase } from './paths.js';
|
|
4
|
+
import { readJson } from './state.js';
|
|
5
|
+
|
|
6
|
+
export const TRANSFER_RETENTION_MS = 15 * 60 * 1000;
|
|
7
|
+
export const downloadCacheRoot = () => path.join(cacheBase(), 'downloads');
|
|
8
|
+
|
|
9
|
+
export async function cleanupDownloadCache(root = downloadCacheRoot(), now = Date.now()) {
|
|
10
|
+
await mkdir(root, { recursive: true });
|
|
11
|
+
for (const entry of await readdir(root, { withFileTypes: true })) {
|
|
12
|
+
if (!entry.isDirectory() || !/^\.veo-part-[a-f0-9]{24}$/.test(entry.name)) continue;
|
|
13
|
+
const directory = path.resolve(root, entry.name);
|
|
14
|
+
const info = await lstat(directory).catch(error => { if (error.code === 'ENOENT') return null; throw error; });
|
|
15
|
+
if (path.dirname(directory) !== path.resolve(root) || !info?.isDirectory() || info.isSymbolicLink()) continue;
|
|
16
|
+
const lockPath = path.join(directory, '.lock');
|
|
17
|
+
let lock;
|
|
18
|
+
try { lock = await open(lockPath, 'wx'); }
|
|
19
|
+
catch (error) { if (error.code === 'EEXIST' || error.code === 'ENOENT') continue; throw error; }
|
|
20
|
+
let expired = false;
|
|
21
|
+
try {
|
|
22
|
+
const manifest = await readJson(path.join(directory, 'job.json'), null);
|
|
23
|
+
expired = manifest?.version === 1 && Number.isFinite(manifest.expiresAt) && manifest.expiresAt <= now;
|
|
24
|
+
} catch { /* Unknown state is left untouched. */ }
|
|
25
|
+
finally { await lock.close(); }
|
|
26
|
+
if (expired) {
|
|
27
|
+
// Lock remains present until our private cache directory is removed.
|
|
28
|
+
await rm(directory, { recursive: true, force: true });
|
|
29
|
+
} else await rm(lockPath, { force: true });
|
|
30
|
+
}
|
|
31
|
+
}
|