mirror of
https://github.com/ether/etherpad-lite.git
synced 2026-07-17 16:47:05 +00:00
* docs: refresh docs for 3.2.0 — correct stale content, document recent features The hand-maintained VitePress docs under doc/ had drifted behind a lot of recent work. They are authored prose (not generated from the OpenAPI spec), so they need manual upkeep. This pass corrects content that was actively wrong and documents features shipped since they were last touched. Corrections (was wrong / misleading): - cli.md: every command used `node bin/foo.js`, but the scripts are TypeScript run via pnpm — copy-paste failed. Rewrote to `pnpm run --filter bin <script>`, documented ~13 previously-undocumented operator tools, and split running-vs-stopped requirements. Registered the missing `compactStalePads` script in bin/package.json so the documented invocation actually works. - stats.md: described a pre-Prometheus world. Rewrote for the gated `/stats` (JSON) and `/stats/prometheus` endpoints, the live metric set, the opt-in `scalingDiveMetrics` instruments (#7756), and `measured-core`. - admin/updates.md: removed three false "SMTP not yet wired" claims (it is, via nodemailer + the `mail.*` block), documented the `node-engine-mismatch` preflight check and the rollback/preflight failure emails, and stripped obsolete "PR 1 / PR 2" staging language now that all tiers ship. - api/http_api.md: added the undocumented `anonymizeAuthor` (GDPR Art. 17) call, fixed copyPad/movePad version annotations (1.2.8 → 1.2.9), corrected getPadID's param name (readOnlyID → roID), and dropped a reference to a non-existent `getEtherpad` API call. - skins.md: colibris is the current default, not an "experimental" skin for a future 2.0. - localization.md: bare `window._('key')` is unbound and returns undefined; recommend `window.html10n.get(...)` / data-l10n-id instead. - README.md: bumped the v2.2.5 upgrade example to v3.2.0; fixed a docker.adoc link to docker.md. - docker.md: added MAIL_*, ENABLE_METRICS, GDPR_AUTHOR_ERASURE_ENABLED, PRIVACY_BANNER_*, PUBLIC_URL, AUTHENTICATION_METHOD, ENABLE_DARK_MODE, ENABLE_PAD_WIDE_SETTINGS; fixed the SOCKETIO_MAX_HTTP_BUFFER_SIZE default (50000 → 1000000). New documentation: - configuration.md (new): how settings + `${VAR:default}` substitution work, trustProxy, and — the previously-undocumented feature — running under a subpath/ingress via x-proxy-path / X-Forwarded-Prefix / X-Ingress-Path, with the sanitizer rules and Traefik/NGINX examples. Wired into the VitePress sidebar and the index hero. - hooks_server-side.md: ccRegisterBlockElements (the server-side companion plugin authors miss), exportConvert, exportHTMLSend, createServer, restartServer, and clientReady (marked deprecated). - hooks_client-side.md: aceDrop, acePaste, handleClientTimesliderMessage_<name>. VitePress build passes. The legacy .adoc set was intentionally left in place — it still feeds the per-version doc archives published to ether.github.com at release time (bin/release.ts), so it is not dead and is out of scope here. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: address Qodo review — drop .js invocations from configuration.md and CLI help - configuration.md: the settings-override example referenced a nonexistent `node src/node/server.js`. Use the supported launcher instead (`bin/run.sh -s <file>`), and note the runtime is server.ts via tsx. - compactStalePads.ts / compactPad.ts / compactAllPads.ts: their header comments and runtime usage output still printed `node bin/*.js`, which points at files that don't exist. Switched to the documented `pnpm run --filter bin <script>` form so the --help text matches the docs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
236 lines
8.1 KiB
TypeScript
236 lines
8.1 KiB
TypeScript
'use strict';
|
|
|
|
/*
|
|
* Compact every pad on the instance to reclaim database space.
|
|
*
|
|
* Usage:
|
|
* pnpm run --filter bin compactAllPads # collapse all history on every pad
|
|
* pnpm run --filter bin compactAllPads --keep N # keep last N revisions per pad
|
|
* pnpm run --filter bin compactAllPads --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(' pnpm run --filter bin compactAllPads');
|
|
console.error(' pnpm run --filter bin compactAllPads --keep <N>');
|
|
console.error(' pnpm run --filter bin compactAllPads --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);
|
|
})();
|
|
}
|