mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-01 16:12:02 +00:00
106 lines
4.2 KiB
TypeScript
106 lines
4.2 KiB
TypeScript
import { writeFile } from 'node:fs/promises';
|
|
import { logger } from '@/lib/logger';
|
|
import { ensureStateDir, getStatePath, isConfigReadOnly } from '@/lib/admin/paths';
|
|
import { ADMIN_CONFIG_COLLECTION, ADMIN_STATE_COLLECTION, adminConfigOptions, adminStateOptions } from '@/lib/admin/admin-store';
|
|
import { AUDIT_LOG_OPTIONS } from '@/lib/admin/audit';
|
|
import { GENERATED_SECRET_FILE, SHARED_SECRET_KEY } from '@/lib/auth/session-secret';
|
|
import { SESSION_LIMITS_COLLECTION, sessionLimitsLayout } from '@/lib/auth/session-age';
|
|
import { WOPI_COLLECTION, wopiRevocationsLayout } from '@/lib/wopi/revocation';
|
|
import { SETTINGS_COLLECTION } from '@/lib/settings-sync';
|
|
import { VAULT_COLLECTION } from '@/lib/plugin-secrets/vault';
|
|
import { ARCHIVE_COLLECTION } from '@/lib/account-archive-storage';
|
|
import { TELEMETRY_COLLECTION } from '@/lib/telemetry/state';
|
|
import { VERSION_CHECK_COLLECTION } from '@/lib/version-check/state';
|
|
import { getStore } from './index';
|
|
import { exportSteps as extExportSteps } from './import/ext';
|
|
import { localCollection, localLog } from './local';
|
|
import type { CollectionOptions } from './types';
|
|
|
|
/**
|
|
* The way back from several instances to one (#1066): write everything the
|
|
* shared store holds into this instance's data dirs, in the layout local
|
|
* mode reads, so it can start without REDIS_URL on that volume. Started with
|
|
* SHARED_STORE_EXPORT=1 (see instrumentation.node.ts), which exports and
|
|
* exits instead of serving.
|
|
*
|
|
* Files of the same name are overwritten; other files are left alone.
|
|
* Nothing in the store is changed. Short-lived state (sign-in limits,
|
|
* pairing and hand-off codes, used impersonation links) isn't exported: it
|
|
* lasts minutes and starts empty after any restart.
|
|
*/
|
|
|
|
export interface ExportStep {
|
|
name: string;
|
|
run(): Promise<number>;
|
|
}
|
|
|
|
/** Every document of a collection, from the store to its local files. */
|
|
export function exportCollection(name: string, options: CollectionOptions): ExportStep {
|
|
return {
|
|
name,
|
|
async run() {
|
|
// Read-only config is the mounted files on every replica already.
|
|
if (options.configDir && isConfigReadOnly()) return 0;
|
|
const shared = getStore().collection(name, options);
|
|
const local = localCollection(name, options);
|
|
let written = 0;
|
|
for (const id of await shared.list()) {
|
|
const doc = await shared.get(id);
|
|
if (!doc) continue;
|
|
await local.put(id, doc.data);
|
|
written++;
|
|
}
|
|
return written;
|
|
},
|
|
};
|
|
}
|
|
|
|
const auditStep: ExportStep = {
|
|
name: 'audit',
|
|
async run() {
|
|
const lines = await getStore().log('audit', AUDIT_LOG_OPTIONS).readAll();
|
|
const local = localLog('audit', AUDIT_LOG_OPTIONS);
|
|
await local.clear();
|
|
await local.appendMany(lines);
|
|
return lines.length;
|
|
},
|
|
};
|
|
|
|
/** The generated session secret, so exported settings and credentials still decrypt. */
|
|
const secretStep: ExportStep = {
|
|
name: 'session-secret',
|
|
async run() {
|
|
const secret = await getStore().kv.get(SHARED_SECRET_KEY);
|
|
if (!secret) return 0;
|
|
await ensureStateDir();
|
|
await writeFile(getStatePath(GENERATED_SECRET_FILE), secret, { mode: 0o600 });
|
|
return 1;
|
|
},
|
|
};
|
|
|
|
export const EXPORT_STEPS: ExportStep[] = [
|
|
secretStep,
|
|
exportCollection(ADMIN_CONFIG_COLLECTION, adminConfigOptions),
|
|
exportCollection(ADMIN_STATE_COLLECTION, adminStateOptions),
|
|
// After admin-state: directory-keyrings.json is in that collection too and
|
|
// is written again owner-only here.
|
|
...extExportSteps,
|
|
auditStep,
|
|
exportCollection(SESSION_LIMITS_COLLECTION, sessionLimitsLayout),
|
|
exportCollection(WOPI_COLLECTION, wopiRevocationsLayout),
|
|
exportCollection('settings', SETTINGS_COLLECTION),
|
|
exportCollection('plugin-secrets', VAULT_COLLECTION),
|
|
exportCollection('account-archives', ARCHIVE_COLLECTION),
|
|
exportCollection('telemetry', TELEMETRY_COLLECTION),
|
|
exportCollection('version-check', VERSION_CHECK_COLLECTION),
|
|
];
|
|
|
|
export async function exportSharedData(steps: ExportStep[] = EXPORT_STEPS): Promise<Record<string, number>> {
|
|
const written: Record<string, number> = {};
|
|
for (const step of steps) {
|
|
written[step.name] = await step.run();
|
|
logger.info('Exported from the shared store', { step: step.name, items: written[step.name] });
|
|
}
|
|
return written;
|
|
}
|