etherpad-lite/bin/compactAllPads.ts
SamTV12345 bfdbd2bb91
chore: removed axios (#7685)
* chore: removed axios

* chore: pnpm
2026-05-06 22:06:04 +02:00

236 lines
8 KiB
TypeScript

'use strict';
/*
* Compact every pad on the instance to reclaim database space.
*
* Usage:
* node bin/compactAllPads.js # collapse all history on every pad
* node bin/compactAllPads.js --keep N # keep last N revisions per pad
* node bin/compactAllPads.js --dry-run # list pads + rev counts, no writes
*
* Composes the existing `listAllPads` and `compactPad` HTTP APIs — there is
* deliberately no instance-wide HTTP endpoint, because doing this over a
* single request would mean one giant response and a long-held connection.
* Per-pad failures don't stop the run; they're logged and counted, and the
* exit code reflects whether anything failed.
*
* Destructive — `getEtherpad`-export anything you can't afford to lose
* before running.
*
* Issue #6194: per-instance bulk compaction. The per-pad `bin/compactPad`
* is the right tool when you know which pad is fat; this is the right tool
* when you want to reclaim space across the whole instance.
*/
import path from 'node:path';
import fs from 'node:fs';
import process from 'node:process';
export type CompactAllOpts = {
keepRevisions: number | null;
dryRun: boolean;
};
// Minimal interface mirroring the API endpoints the script needs. Tests
// substitute their own implementation that goes through supertest+JWT
// instead of fetch+APIKEY, so the loop logic is exercised against a real
// running server without dragging in apikey-file or fetch setup.
export type CompactAllApi = {
listAllPads(): Promise<string[]>;
getRevisionsCount(padId: string): Promise<number>;
compactPad(padId: string, keepRevisions: number | null): Promise<void>;
};
export type CompactAllReport = {
total: number;
ok: number;
failed: number;
totalRevsBefore: number;
totalRevsAfter: number;
};
export type CompactAllLogger = {
info(msg: string): void;
error(msg: string): void;
};
const defaultLogger: CompactAllLogger = {
info: (m) => console.log(m),
error: (m) => console.error(m),
};
// Pure-ish core: composition + per-pad error tolerance + dry-run + tally.
// Returns a structured report so tests can assert on outcomes; the CLI
// shell maps it to an exit code.
export const runCompactAll = async (
api: CompactAllApi, opts: CompactAllOpts,
logger: CompactAllLogger = defaultLogger,
): Promise<CompactAllReport> => {
let padIds: string[];
try {
padIds = await api.listAllPads();
} catch (e: any) {
logger.error(`listAllPads failed: ${e.message ?? e}`);
return {total: 0, ok: 0, failed: 1, totalRevsBefore: 0, totalRevsAfter: 0};
}
if (padIds.length === 0) {
logger.info('No pads on this instance.');
return {total: 0, ok: 0, failed: 0, totalRevsBefore: 0, totalRevsAfter: 0};
}
const strategy = opts.keepRevisions == null
? 'collapse all history'
: `keep last ${opts.keepRevisions} revisions`;
logger.info(`Found ${padIds.length} pad(s). Strategy: ${strategy}` +
`${opts.dryRun ? ' (dry run — no writes)' : ''}.`);
const report: CompactAllReport = {
total: padIds.length, ok: 0, failed: 0,
totalRevsBefore: 0, totalRevsAfter: 0,
};
for (let i = 0; i < padIds.length; i++) {
const padId = padIds[i];
const idx = `[${i + 1}/${padIds.length}]`;
let before: number;
try {
before = await api.getRevisionsCount(padId);
} catch (e: any) {
logger.error(`${idx} ${padId}: getRevisionsCount failed: ${e.message ?? e}`);
report.failed++;
continue;
}
if (opts.dryRun) {
logger.info(`${idx} ${padId}: ${before + 1} revision(s) — would compact`);
report.totalRevsBefore += before + 1;
continue;
}
try {
await api.compactPad(padId, opts.keepRevisions);
} catch (e: any) {
logger.error(`${idx} ${padId}: compactPad failed: ${e.message ?? e}`);
report.failed++;
continue;
}
let after: number | undefined;
try { after = await api.getRevisionsCount(padId); }
catch { /* main op already succeeded; post-count is informational */ }
if (after != null) {
logger.info(`${idx} ${padId}: ${before + 1}${after + 1} revision(s)`);
report.totalRevsBefore += before + 1;
report.totalRevsAfter += after + 1;
} else {
logger.info(`${idx} ${padId}: compacted (post-count unavailable)`);
}
report.ok++;
}
if (opts.dryRun) {
logger.info('');
logger.info(`Dry run complete. ${padIds.length} pad(s), ` +
`${report.totalRevsBefore} total revision(s) — re-run ` +
'without --dry-run to compact.');
} else {
logger.info('');
logger.info(`Done. ${report.ok} pad(s) compacted, ${report.failed} failed. ` +
`Revisions: ${report.totalRevsBefore}${report.totalRevsAfter} ` +
`(reclaimed ${report.totalRevsBefore - report.totalRevsAfter}).`);
}
return report;
};
export const parseArgs = (argv: string[]): CompactAllOpts | null => {
const opts: CompactAllOpts = {keepRevisions: null, dryRun: false};
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (a === '--dry-run') {
opts.dryRun = true;
} else if (a === '--keep') {
const v = argv[++i];
const n = Number(v);
if (!Number.isInteger(n) || n < 0) {
console.error(`--keep expects a non-negative integer; got ${v}`);
return null;
}
opts.keepRevisions = n;
} else {
return null;
}
}
return opts;
};
// CLI entry point. Skipped when this file is imported (e.g. by tests),
// so the test harness can use `runCompactAll` directly without network.
const usage = () => {
console.error('Usage:');
console.error(' node bin/compactAllPads.js');
console.error(' node bin/compactAllPads.js --keep <N>');
console.error(' node bin/compactAllPads.js --dry-run');
process.exit(2);
};
const isMain = require.main === module;
if (isMain) {
process.on('unhandledRejection', (err) => { throw err; });
const settings = require('ep_etherpad-lite/tests/container/loadSettings').loadSettings();
const baseURL = `${settings.ssl ? 'https' : 'http'}://${settings.ip}:${settings.port}`;
const apiGet = async (p: string): Promise<any> => {
const r = await fetch(baseURL + p);
if (!r.ok) throw new Error(`HTTP ${r.status} ${r.statusText}`);
return r.json();
};
const apiPost = async (p: string): Promise<any> => {
const r = await fetch(baseURL + p, {method: 'POST'});
if (!r.ok) throw new Error(`HTTP ${r.status} ${r.statusText}`);
return r.json();
};
const opts = parseArgs(process.argv.slice(2));
if (!opts) usage();
const apikey = fs.readFileSync(
path.join(__dirname, '../APIKEY.txt'), {encoding: 'utf-8'}).trim();
// Bind the abstract API to fetch + APIKEY auth for the CLI shell.
const cliApi: CompactAllApi = {
async listAllPads() {
const apiInfo = await apiGet('/api/');
const apiVersion: string | undefined = apiInfo.currentVersion;
if (!apiVersion) throw new Error('No version set in API');
// Stash on this for subsequent calls. Avoids a per-call /api/ ping.
(cliApi as any)._apiVersion = apiVersion;
const r = await apiGet(`/api/${apiVersion}/listAllPads?apikey=${apikey}`);
if (r.code !== 0) throw new Error(JSON.stringify(r));
return r.data.padIDs ?? [];
},
async getRevisionsCount(padId: string) {
const v = (cliApi as any)._apiVersion;
const r = await apiGet(
`/api/${v}/getRevisionsCount?apikey=${apikey}` +
`&padID=${encodeURIComponent(padId)}`);
if (r.code !== 0) throw new Error(JSON.stringify(r));
return r.data.revisions;
},
async compactPad(padId: string, keepRevisions: number | null) {
const v = (cliApi as any)._apiVersion;
const params = new URLSearchParams({apikey, padID: padId});
if (keepRevisions != null) params.set('keepRevisions', String(keepRevisions));
const r = await apiPost(`/api/${v}/compactPad?${params.toString()}`);
if (r.code !== 0) throw new Error(JSON.stringify(r));
},
};
(async () => {
const report = await runCompactAll(cliApi, opts!);
if (report.failed > 0) process.exit(1);
})();
}