feat: add domain settings support in backup import and export processes

This commit is contained in:
shuaiplus
2026-05-07 19:36:32 +08:00
parent 669d7ef242
commit a00279f47d
3 changed files with 39 additions and 3 deletions
+19 -2
View File
@@ -51,6 +51,7 @@ export interface BackupPayload {
db: { db: {
config: SqlRow[]; config: SqlRow[];
users: SqlRow[]; users: SqlRow[];
domain_settings: SqlRow[];
user_revisions: SqlRow[]; user_revisions: SqlRow[];
folders: SqlRow[]; folders: SqlRow[];
ciphers: SqlRow[]; ciphers: SqlRow[];
@@ -284,6 +285,7 @@ export function validateBackupPayloadContents(
const configRows = ensureRowArray(payload.db.config, 'config'); const configRows = ensureRowArray(payload.db.config, 'config');
const userRows = ensureRowArray(payload.db.users, 'users'); const userRows = ensureRowArray(payload.db.users, 'users');
const revisionRows = ensureRowArray(payload.db.user_revisions, 'user_revisions'); const revisionRows = ensureRowArray(payload.db.user_revisions, 'user_revisions');
const domainSettingsRows = ensureRowArray(payload.db.domain_settings || [], 'domain_settings');
const folderRows = ensureRowArray(payload.db.folders, 'folders'); const folderRows = ensureRowArray(payload.db.folders, 'folders');
const cipherRows = ensureRowArray(payload.db.ciphers, 'ciphers'); const cipherRows = ensureRowArray(payload.db.ciphers, 'ciphers');
const attachmentRows = ensureRowArray(payload.db.attachments, 'attachments'); const attachmentRows = ensureRowArray(payload.db.attachments, 'attachments');
@@ -314,6 +316,18 @@ export function validateBackupPayloadContents(
} }
} }
const domainSettingUserIds = new Set<string>();
for (const row of domainSettingsRows) {
const userId = String(row.user_id || '').trim();
if (!userId || !userIds.has(userId)) {
throw new Error(`Backup archive contains domain settings for an unknown user: ${userId || '(empty)'}`);
}
if (domainSettingUserIds.has(userId)) {
throw new Error(`Backup archive contains duplicate domain settings for user: ${userId}`);
}
domainSettingUserIds.add(userId);
}
const folderIds = new Set<string>(); const folderIds = new Set<string>();
for (const row of folderRows) { for (const row of folderRows) {
const id = String(row.id || '').trim(); const id = String(row.id || '').trim();
@@ -365,9 +379,10 @@ export async function buildBackupArchive(
includeAttachments, includeAttachments,
}); });
const encoder = new TextEncoder(); const encoder = new TextEncoder();
const [configRows, userRows, revisionRows, folderRows, cipherRows, attachmentRows] = await Promise.all([ const [configRows, userRows, domainSettingsRows, revisionRows, folderRows, cipherRows, attachmentRows] = await Promise.all([
queryRows(env.DB, 'SELECT key, value FROM config ORDER BY key ASC'), queryRows(env.DB, 'SELECT key, value FROM config ORDER BY key ASC'),
queryRows(env.DB, 'SELECT id, email, name, master_password_hint, master_password_hash, key, private_key, public_key, kdf_type, kdf_iterations, kdf_memory, kdf_parallelism, security_stamp, role, status, verify_devices, totp_secret, totp_recovery_code, api_key, created_at, updated_at FROM users ORDER BY created_at ASC'), queryRows(env.DB, 'SELECT id, email, name, master_password_hint, master_password_hash, key, private_key, public_key, kdf_type, kdf_iterations, kdf_memory, kdf_parallelism, security_stamp, role, status, verify_devices, totp_secret, totp_recovery_code, created_at, updated_at FROM users ORDER BY created_at ASC'),
queryRows(env.DB, 'SELECT user_id, equivalent_domains, custom_equivalent_domains, excluded_global_equivalent_domains, updated_at FROM domain_settings ORDER BY user_id ASC'),
queryRows(env.DB, 'SELECT user_id, revision_date FROM user_revisions ORDER BY user_id ASC'), queryRows(env.DB, 'SELECT user_id, revision_date FROM user_revisions ORDER BY user_id ASC'),
queryRows(env.DB, 'SELECT id, user_id, name, created_at, updated_at FROM folders ORDER BY created_at ASC'), queryRows(env.DB, 'SELECT id, user_id, name, created_at, updated_at FROM folders ORDER BY created_at ASC'),
queryRows(env.DB, 'SELECT id, user_id, type, folder_id, name, notes, favorite, data, reprompt, key, created_at, updated_at, archived_at, deleted_at FROM ciphers ORDER BY created_at ASC'), queryRows(env.DB, 'SELECT id, user_id, type, folder_id, name, notes, favorite, data, reprompt, key, created_at, updated_at, archived_at, deleted_at FROM ciphers ORDER BY created_at ASC'),
@@ -394,6 +409,7 @@ export async function buildBackupArchive(
tableCounts: { tableCounts: {
config: exportedConfigRows.length, config: exportedConfigRows.length,
users: userRows.length, users: userRows.length,
domain_settings: domainSettingsRows.length,
user_revisions: revisionRows.length, user_revisions: revisionRows.length,
folders: folderRows.length, folders: folderRows.length,
ciphers: cipherRows.length, ciphers: cipherRows.length,
@@ -415,6 +431,7 @@ export async function buildBackupArchive(
'db.json': encoder.encode(JSON.stringify({ 'db.json': encoder.encode(JSON.stringify({
config: exportedConfigRows, config: exportedConfigRows,
users: userRows, users: userRows,
domain_settings: domainSettingsRows,
user_revisions: revisionRows, user_revisions: revisionRows,
folders: folderRows, folders: folderRows,
ciphers: cipherRows, ciphers: cipherRows,
+19 -1
View File
@@ -12,6 +12,7 @@ type SqlRow = Record<string, string | number | null>;
type BackupTableName = type BackupTableName =
| 'config' | 'config'
| 'users' | 'users'
| 'domain_settings'
| 'user_revisions' | 'user_revisions'
| 'folders' | 'folders'
| 'ciphers' | 'ciphers'
@@ -20,6 +21,7 @@ type BackupTableName =
const BACKUP_TABLES: BackupTableName[] = [ const BACKUP_TABLES: BackupTableName[] = [
'config', 'config',
'users', 'users',
'domain_settings',
'user_revisions', 'user_revisions',
'folders', 'folders',
'ciphers', 'ciphers',
@@ -35,6 +37,7 @@ export interface BackupImportResultBody {
imported: { imported: {
config: number; config: number;
users: number; users: number;
domainSettings: number;
userRevisions: number; userRevisions: number;
folders: number; folders: number;
ciphers: number; ciphers: number;
@@ -155,6 +158,7 @@ function buildResetImportTargetStatements(db: D1Database): D1PreparedStatement[]
'DELETE FROM attachments', 'DELETE FROM attachments',
'DELETE FROM ciphers', 'DELETE FROM ciphers',
'DELETE FROM folders', 'DELETE FROM folders',
'DELETE FROM domain_settings',
'DELETE FROM user_revisions', 'DELETE FROM user_revisions',
'DELETE FROM users', 'DELETE FROM users',
'DELETE FROM config', 'DELETE FROM config',
@@ -276,6 +280,7 @@ async function importPreparedBackupRows(db: D1Database, payload: BackupPayload['
...row, ...row,
verify_devices: row.verify_devices ?? 1, verify_devices: row.verify_devices ?? 1,
})), })),
domain_settings: cloneRows(payload.domain_settings || []),
user_revisions: cloneRows(payload.user_revisions || []), user_revisions: cloneRows(payload.user_revisions || []),
folders: cloneRows(payload.folders || []), folders: cloneRows(payload.folders || []),
ciphers: cloneRows(payload.ciphers || []).map((row) => ({ ciphers: cloneRows(payload.ciphers || []).map((row) => ({
@@ -594,7 +599,7 @@ async function importBackupRows(db: D1Database, payload: BackupPayload['db'], us
buildInsertStatements( buildInsertStatements(
db, db,
tableName('users'), tableName('users'),
['id', 'email', 'name', 'master_password_hint', 'master_password_hash', 'key', 'private_key', 'public_key', 'kdf_type', 'kdf_iterations', 'kdf_memory', 'kdf_parallelism', 'security_stamp', 'role', 'status', 'verify_devices', 'totp_secret', 'totp_recovery_code', 'api_key', 'created_at', 'updated_at'], ['id', 'email', 'name', 'master_password_hint', 'master_password_hash', 'key', 'private_key', 'public_key', 'kdf_type', 'kdf_iterations', 'kdf_memory', 'kdf_parallelism', 'security_stamp', 'role', 'status', 'verify_devices', 'totp_secret', 'totp_recovery_code', 'created_at', 'updated_at'],
payload.users || [] payload.users || []
) )
); );
@@ -603,6 +608,17 @@ async function importBackupRows(db: D1Database, payload: BackupPayload['db'], us
tableName('user_revisions'), tableName('user_revisions'),
buildInsertStatements(db, tableName('user_revisions'), ['user_id', 'revision_date'], payload.user_revisions || [], true) buildInsertStatements(db, tableName('user_revisions'), ['user_id', 'revision_date'], payload.user_revisions || [], true)
); );
await runInsertBatch(
db,
tableName('domain_settings'),
buildInsertStatements(
db,
tableName('domain_settings'),
['user_id', 'equivalent_domains', 'custom_equivalent_domains', 'excluded_global_equivalent_domains', 'updated_at'],
payload.domain_settings || [],
true
)
);
await runInsertBatch( await runInsertBatch(
db, db,
tableName('folders'), tableName('folders'),
@@ -729,6 +745,7 @@ export async function importBackupArchiveBytes(
imported: { imported: {
config: (db.config || []).length, config: (db.config || []).length,
users: (db.users || []).length, users: (db.users || []).length,
domainSettings: (db.domain_settings || []).length,
userRevisions: (db.user_revisions || []).length, userRevisions: (db.user_revisions || []).length,
folders: (db.folders || []).length, folders: (db.folders || []).length,
ciphers: (db.ciphers || []).length, ciphers: (db.ciphers || []).length,
@@ -870,6 +887,7 @@ export async function importRemoteBackupArchiveBytes(
imported: { imported: {
config: (db.config || []).length, config: (db.config || []).length,
users: (db.users || []).length, users: (db.users || []).length,
domainSettings: (db.domain_settings || []).length,
userRevisions: (db.user_revisions || []).length, userRevisions: (db.user_revisions || []).length,
folders: (db.folders || []).length, folders: (db.folders || []).length,
ciphers: (db.ciphers || []).length, ciphers: (db.ciphers || []).length,
+1
View File
@@ -94,6 +94,7 @@ export interface RemoteBackupBrowserResponse {
export interface AdminBackupImportCounts { export interface AdminBackupImportCounts {
config: number; config: number;
users: number; users: number;
domainSettings?: number;
userRevisions: number; userRevisions: number;
folders: number; folders: number;
ciphers: number; ciphers: number;