mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-03 16:16:59 +00:00
feat: write plugins, themes, approvals and branding back to local files on export #1066
This commit is contained in:
@@ -13,8 +13,9 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
// branding uploads come along; a second run writes nothing, and the local
|
||||
// files stay as they were.
|
||||
|
||||
import { steps } from '../import/ext';
|
||||
import { exportSteps, steps } from '../import/ext';
|
||||
import { importLocalData } from '../import';
|
||||
import { exportSharedData } from '../export';
|
||||
import { getStore, setStoreForTests } from '../index';
|
||||
import { resetLocalStoreForTests } from '../local';
|
||||
import { separateFakeStore } from '@/lib/admin/__tests__/shared-store-fake';
|
||||
@@ -115,6 +116,24 @@ describe('importing plugins, themes and branding into the shared store', () => {
|
||||
expect(snapshot()).toEqual(before);
|
||||
});
|
||||
|
||||
it('writes the same files back into an empty install on export', async () => {
|
||||
const before = snapshot();
|
||||
await importLocalData(steps);
|
||||
// A fresh volume for the instance that goes back to local mode.
|
||||
rmSync(path.join(dir, 'config'), { recursive: true, force: true });
|
||||
rmSync(path.join(dir, 'state'), { recursive: true, force: true });
|
||||
const written = await exportSharedData(exportSteps);
|
||||
expect(written).toMatchObject({ plugins: 5, themes: 3, branding: 1, 'plugin-signing-key': 1, 'directory-keyrings': 1 });
|
||||
|
||||
const bytesOnly = (snap: Record<string, string>) =>
|
||||
Object.fromEntries(Object.entries(snap).filter(([f]) => !f.endsWith('config.json')).map(([f, v]) => [f, v.split(' ')[0]]));
|
||||
expect(bytesOnly(snapshot())).toEqual(bytesOnly(before));
|
||||
if (process.platform !== 'win32') {
|
||||
expect(statSync(path.join(dir, 'state', 'directory-keyrings.json')).mode & 0o777).toBe(0o600);
|
||||
expect(statSync(path.join(dir, 'config', 'plugin-signing.key')).mode & 0o777).toBe(0o600);
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps what the store already holds', async () => {
|
||||
await pluginDocs().put('demo.js', 'already shared()');
|
||||
await getStore().kv.set('plugin-signing-key', 'stored key');
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { chmod, readFile, writeFile } from 'node:fs/promises';
|
||||
import type { ImportStep } from '../import';
|
||||
import { exportCollection, type ExportStep } from '../export';
|
||||
import { getStore, type CollectionOptions } from '../index';
|
||||
import { localCollection } from '../local';
|
||||
import {
|
||||
@@ -18,7 +19,7 @@ import {
|
||||
} from '@/lib/admin/extension-store';
|
||||
import { ADMIN_STATE_COLLECTION } from '@/lib/admin/admin-store';
|
||||
import { SHARED_KEY_NAME } from '@/lib/admin/plugin-signing';
|
||||
import { getConfigPath } from '@/lib/admin/paths';
|
||||
import { ensureConfigDir, getConfigPath, isConfigReadOnly } from '@/lib/admin/paths';
|
||||
|
||||
/**
|
||||
* Part `ext` of the import (#1066): installed plugins and themes (registry,
|
||||
@@ -85,3 +86,41 @@ export const steps: ImportStep[] = [
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* The way back (SHARED_STORE_EXPORT, lib/shared-store/export.ts): the same
|
||||
* items from the store into the local layout. Run after the admin-state
|
||||
* export, so directory-keyrings.json ends up owner-only as local mode
|
||||
* writes it. The stored signing key replaces a key file on the volume, as
|
||||
* every other file of the same name is replaced: the exported signatures
|
||||
* were made with it.
|
||||
*/
|
||||
export const exportSteps: ExportStep[] = [
|
||||
exportCollection(PLUGINS_COLLECTION, pluginsOptions),
|
||||
exportCollection(THEMES_COLLECTION, themesOptions),
|
||||
exportCollection(PLUGIN_CONFIG_COLLECTION, pluginConfigOptions),
|
||||
exportCollection(PLUGIN_APPROVALS_COLLECTION, pluginApprovalsOptions),
|
||||
exportCollection(BRANDING_COLLECTION, brandingOptions),
|
||||
{
|
||||
name: 'directory-keyrings',
|
||||
async run() {
|
||||
const doc = await getStore().collection(ADMIN_STATE_COLLECTION, directoryKeyringsOptions).get(DIRECTORY_KEYRINGS_ID);
|
||||
if (!doc) return 0;
|
||||
await localCollection(ADMIN_STATE_COLLECTION, directoryKeyringsOptions).put(DIRECTORY_KEYRINGS_ID, doc.data);
|
||||
return 1;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'plugin-signing-key',
|
||||
async run() {
|
||||
if (isConfigReadOnly()) return 0;
|
||||
const pem = await getStore().kv.get(SHARED_KEY_NAME);
|
||||
if (!pem) return 0;
|
||||
await ensureConfigDir();
|
||||
const file = getConfigPath('plugin-signing.key');
|
||||
await writeFile(file, pem, { encoding: 'utf-8', mode: 0o600 });
|
||||
await chmod(file, 0o600).catch(() => {});
|
||||
return 1;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user