fix: exclude runtime db config from backup and migration (#260)
This commit is contained in:
@@ -290,6 +290,52 @@ describe('backupService', () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not export runtime database config in preferences backups', async () => {
|
||||||
|
await db.insert(schema.settings).values([
|
||||||
|
{ key: 'db_type', value: JSON.stringify('postgres') },
|
||||||
|
{ key: 'db_url', value: JSON.stringify('postgres://metapi:secret@db.example.com:5432/metapi') },
|
||||||
|
{ key: 'db_ssl', value: JSON.stringify(true) },
|
||||||
|
{ key: 'routing_fallback_unit_cost', value: JSON.stringify(0.25) },
|
||||||
|
]).run();
|
||||||
|
|
||||||
|
const exported = await backupService.exportBackup('preferences') as any;
|
||||||
|
const exportedSettingKeys = exported.preferences.settings.map((row: { key: string }) => row.key);
|
||||||
|
|
||||||
|
expect(exportedSettingKeys).toContain('routing_fallback_unit_cost');
|
||||||
|
expect(exportedSettingKeys).not.toContain('db_type');
|
||||||
|
expect(exportedSettingKeys).not.toContain('db_url');
|
||||||
|
expect(exportedSettingKeys).not.toContain('db_ssl');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores imported runtime database config settings', async () => {
|
||||||
|
const result = await backupService.importBackup({
|
||||||
|
version: '2.1',
|
||||||
|
timestamp: Date.now(),
|
||||||
|
type: 'preferences',
|
||||||
|
preferences: {
|
||||||
|
settings: [
|
||||||
|
{ key: 'db_type', value: 'postgres' },
|
||||||
|
{ key: 'db_url', value: 'postgres://metapi:secret@db.example.com:5432/metapi' },
|
||||||
|
{ key: 'db_ssl', value: true },
|
||||||
|
{ key: 'routing_fallback_unit_cost', value: 0.25 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.sections.preferences).toBe(true);
|
||||||
|
expect(result.appliedSettings).toEqual([
|
||||||
|
{ key: 'routing_fallback_unit_cost', value: 0.25 },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const settingsRows = await db.select().from(schema.settings).all();
|
||||||
|
const savedKeys = settingsRows.map((row) => row.key);
|
||||||
|
|
||||||
|
expect(savedKeys).toContain('routing_fallback_unit_cost');
|
||||||
|
expect(savedKeys).not.toContain('db_type');
|
||||||
|
expect(savedKeys).not.toContain('db_url');
|
||||||
|
expect(savedKeys).not.toContain('db_ssl');
|
||||||
|
});
|
||||||
|
|
||||||
it('preserves local logs and runtime stats when importing account backups', async () => {
|
it('preserves local logs and runtime stats when importing account backups', async () => {
|
||||||
const exportedAt = '2026-03-20T09:00:00.000Z';
|
const exportedAt = '2026-03-20T09:00:00.000Z';
|
||||||
const localRuntimeAt = '2026-03-21T10:30:00.000Z';
|
const localRuntimeAt = '2026-03-21T10:30:00.000Z';
|
||||||
|
|||||||
@@ -228,6 +228,10 @@ interface BackupImportResult {
|
|||||||
const EXCLUDED_SETTING_KEYS = new Set<string>([
|
const EXCLUDED_SETTING_KEYS = new Set<string>([
|
||||||
// Keep current admin login credential unchanged to avoid accidental lock-out.
|
// Keep current admin login credential unchanged to avoid accidental lock-out.
|
||||||
'auth_token',
|
'auth_token',
|
||||||
|
// Runtime database selection is environment-bound and must not be propagated by backups.
|
||||||
|
'db_type',
|
||||||
|
'db_url',
|
||||||
|
'db_ssl',
|
||||||
]);
|
]);
|
||||||
const BACKUP_WEBDAV_CONFIG_SETTING_KEY = 'backup_webdav_config_v1';
|
const BACKUP_WEBDAV_CONFIG_SETTING_KEY = 'backup_webdav_config_v1';
|
||||||
const BACKUP_WEBDAV_STATE_SETTING_KEY = 'backup_webdav_state_v1';
|
const BACKUP_WEBDAV_STATE_SETTING_KEY = 'backup_webdav_state_v1';
|
||||||
|
|||||||
@@ -465,4 +465,46 @@ describe('databaseMigrationService', () => {
|
|||||||
vi.resetModules();
|
vi.resetModules();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('excludes runtime database config settings from migration statements', () => {
|
||||||
|
const statements = __databaseMigrationServiceTestUtils.buildStatements({
|
||||||
|
version: 'test',
|
||||||
|
timestamp: Date.now(),
|
||||||
|
accounts: {
|
||||||
|
sites: [],
|
||||||
|
siteAnnouncements: [],
|
||||||
|
siteDisabledModels: [],
|
||||||
|
accounts: [],
|
||||||
|
accountTokens: [],
|
||||||
|
checkinLogs: [],
|
||||||
|
modelAvailability: [],
|
||||||
|
tokenModelAvailability: [],
|
||||||
|
tokenRoutes: [],
|
||||||
|
routeChannels: [],
|
||||||
|
routeGroupSources: [],
|
||||||
|
proxyLogs: [],
|
||||||
|
proxyVideoTasks: [],
|
||||||
|
proxyFiles: [],
|
||||||
|
downstreamApiKeys: [],
|
||||||
|
events: [],
|
||||||
|
},
|
||||||
|
preferences: {
|
||||||
|
settings: [
|
||||||
|
{ key: 'db_type', value: 'sqlite' },
|
||||||
|
{ key: 'db_url', value: '/app/data/hub.db' },
|
||||||
|
{ key: 'db_ssl', value: false },
|
||||||
|
{ key: 'routing_fallback_unit_cost', value: 0.25 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
const migratedSettingKeys = statements
|
||||||
|
.filter((statement) => statement.table === 'settings')
|
||||||
|
.map((statement) => statement.values[0]);
|
||||||
|
|
||||||
|
expect(migratedSettingKeys).toContain('routing_fallback_unit_cost');
|
||||||
|
expect(migratedSettingKeys).not.toContain('db_type');
|
||||||
|
expect(migratedSettingKeys).not.toContain('db_url');
|
||||||
|
expect(migratedSettingKeys).not.toContain('db_ssl');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ interface InsertStatement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const DIALECTS: MigrationDialect[] = ['sqlite', 'mysql', 'postgres'];
|
const DIALECTS: MigrationDialect[] = ['sqlite', 'mysql', 'postgres'];
|
||||||
|
const RUNTIME_DATABASE_SETTING_KEYS = new Set(['db_type', 'db_url', 'db_ssl']);
|
||||||
|
|
||||||
function asString(value: unknown): string {
|
function asString(value: unknown): string {
|
||||||
return typeof value === 'string' ? value.trim() : '';
|
return typeof value === 'string' ? value.trim() : '';
|
||||||
@@ -628,6 +629,9 @@ function buildStatements(snapshot: BackupSnapshot): InsertStatement[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const row of snapshot.preferences.settings) {
|
for (const row of snapshot.preferences.settings) {
|
||||||
|
if (RUNTIME_DATABASE_SETTING_KEYS.has(row.key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
statements.push({
|
statements.push({
|
||||||
table: 'settings',
|
table: 'settings',
|
||||||
columns: ['key', 'value'],
|
columns: ['key', 'value'],
|
||||||
|
|||||||
Reference in New Issue
Block a user