mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-01 16:12:02 +00:00
feat: move plugins, themes, branding and the signing key with the shared-store import and export #1066
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// @vitest-environment node
|
||||
import { createHash, randomBytes } from 'node:crypto';
|
||||
import { createHash, generateKeyPairSync, randomBytes } from 'node:crypto';
|
||||
import { mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import path from 'node:path';
|
||||
@@ -76,6 +76,13 @@ describe('moving an install onto the shared store and back', () => {
|
||||
writeFileSync(path.join(a.PLUGIN_SECRETS_DIR, vaultFile), randomBytes(120).toString('base64'));
|
||||
writeFileSync(path.join(a.TELEMETRY_DATA_DIR, 'state.json'), JSON.stringify({ consent: 'on' }, null, 2));
|
||||
writeFileSync(path.join(a.VERSION_CHECK_DATA_DIR, 'state.json'), JSON.stringify({ lastCheckedAt: '2026-11-01' }, null, 2));
|
||||
mkdirSync(path.join(a.ADMIN_CONFIG_DIR, 'plugins'));
|
||||
writeFileSync(path.join(a.ADMIN_CONFIG_DIR, 'plugins', 'registry.json'), JSON.stringify({ plugins: [{ id: 'demo' }] }, null, 2));
|
||||
writeFileSync(path.join(a.ADMIN_CONFIG_DIR, 'plugins', 'demo.js'), 'module.exports = {}');
|
||||
mkdirSync(path.join(a.ADMIN_CONFIG_DIR, 'branding'));
|
||||
writeFileSync(path.join(a.ADMIN_CONFIG_DIR, 'branding', 'faviconUrl.png'), randomBytes(64));
|
||||
const signingKey = generateKeyPairSync('ed25519').privateKey.export({ type: 'pkcs8', format: 'pem' }) as string;
|
||||
writeFileSync(path.join(a.ADMIN_CONFIG_DIR, 'plugin-signing.key'), signingKey);
|
||||
const before = snapshot(a.base);
|
||||
|
||||
const first = await importLocalData();
|
||||
@@ -87,6 +94,9 @@ describe('moving an install onto the shared store and back', () => {
|
||||
'plugin-secrets': 1,
|
||||
telemetry: 1,
|
||||
'version-check': 1,
|
||||
plugins: 2,
|
||||
branding: 1,
|
||||
'plugin-signing-key': 1,
|
||||
});
|
||||
const second = await importLocalData();
|
||||
expect(Object.values(second.imported).every((n) => n === 0), JSON.stringify(second.imported)).toBe(true);
|
||||
@@ -98,6 +108,7 @@ describe('moving an install onto the shared store and back', () => {
|
||||
const b = install('b');
|
||||
const written = await exportSharedData();
|
||||
expect(written['settings']).toBe(1);
|
||||
expect(written).toMatchObject({ plugins: 2, branding: 1, 'plugin-signing-key': 1 });
|
||||
const after = snapshot(b.base);
|
||||
for (const [file, digest] of Object.entries(before)) {
|
||||
expect(after[file], `${file} exported`).toBe(digest);
|
||||
|
||||
@@ -12,6 +12,7 @@ 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';
|
||||
|
||||
@@ -81,6 +82,9 @@ 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),
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { ImportSummary } from './boot';
|
||||
import { steps as adminSteps } from './import/admin';
|
||||
import { steps as authSteps } from './import/auth';
|
||||
import { steps as dataSteps } from './import/data';
|
||||
import { steps as extSteps } from './import/ext';
|
||||
|
||||
/**
|
||||
* Copy an existing single-instance install's local files into an
|
||||
@@ -16,8 +17,8 @@ export interface ImportStep {
|
||||
run(): Promise<number>;
|
||||
}
|
||||
|
||||
/** In this order: config first, then state, then user data. */
|
||||
const STEPS: ImportStep[] = [...adminSteps, ...authSteps, ...dataSteps];
|
||||
/** In this order: config first (admin, then plugins, themes, branding), then state, then user data. */
|
||||
const STEPS: ImportStep[] = [...adminSteps, ...extSteps, ...authSteps, ...dataSteps];
|
||||
|
||||
export async function importLocalData(steps: ImportStep[] = STEPS): Promise<ImportSummary> {
|
||||
const imported: Record<string, number> = {};
|
||||
|
||||
Reference in New Issue
Block a user