feat: add function to export portable backup settings envelope

This commit is contained in:
shuaiplus
2026-05-07 19:23:22 +08:00
parent 97d2117e15
commit 669d7ef242
2 changed files with 37 additions and 2 deletions
+23 -2
View File
@@ -1,6 +1,8 @@
import { zipSync, unzipSync } from 'fflate'; import { zipSync, unzipSync } from 'fflate';
import type { Env } from '../types'; import type { Env } from '../types';
import { APP_VERSION } from '../../shared/app-version'; import { APP_VERSION } from '../../shared/app-version';
import { BACKUP_SETTINGS_CONFIG_KEY } from './backup-config';
import { exportPortableBackupSettingsEnvelope } from './backup-settings-crypto';
import { import {
getAttachmentObjectKey, getAttachmentObjectKey,
getBlobStorageKind, getBlobStorageKind,
@@ -9,6 +11,7 @@ import {
type SqlRow = Record<string, string | number | null>; type SqlRow = Record<string, string | number | null>;
const BACKUP_FORMAT_VERSION = 1; const BACKUP_FORMAT_VERSION = 1;
const BACKUP_RUNNER_LOCK_CONFIG_KEY = 'backup.runner.lock.v1';
const BACKUP_FILE_HASH_PREFIX_LENGTH = 5; const BACKUP_FILE_HASH_PREFIX_LENGTH = 5;
// Worker-side backup export must stay well below Cloudflare CPU limits. // Worker-side backup export must stay well below Cloudflare CPU limits.
// Prefer store-only ZIP entries over heavier compression to keep exports reliable. // Prefer store-only ZIP entries over heavier compression to keep exports reliable.
@@ -89,6 +92,23 @@ async function queryRows(db: D1Database, sql: string, ...values: unknown[]): Pro
return (result.results || []).map((row) => ({ ...row })); return (result.results || []).map((row) => ({ ...row }));
} }
function sanitizeConfigRowsForExport(rows: SqlRow[]): SqlRow[] {
const sanitized: SqlRow[] = [];
for (const row of rows) {
const key = String(row.key || '').trim();
if (!key || key === BACKUP_RUNNER_LOCK_CONFIG_KEY) continue;
if (key === BACKUP_SETTINGS_CONFIG_KEY) {
const portableOnly = exportPortableBackupSettingsEnvelope(typeof row.value === 'string' ? row.value : null);
if (portableOnly) sanitized.push({ ...row, value: portableOnly });
continue;
}
sanitized.push({ ...row });
}
return sanitized;
}
async function sha256Hex(bytes: Uint8Array): Promise<string> { async function sha256Hex(bytes: Uint8Array): Promise<string> {
const digest = await crypto.subtle.digest('SHA-256', bytes); const digest = await crypto.subtle.digest('SHA-256', bytes);
return Array.from(new Uint8Array(digest)).map((byte) => byte.toString(16).padStart(2, '0')).join(''); return Array.from(new Uint8Array(digest)).map((byte) => byte.toString(16).padStart(2, '0')).join('');
@@ -353,6 +373,7 @@ export async function buildBackupArchive(
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'),
queryRows(env.DB, 'SELECT id, cipher_id, file_name, size, size_name, key FROM attachments ORDER BY cipher_id ASC, id ASC'), queryRows(env.DB, 'SELECT id, cipher_id, file_name, size, size_name, key FROM attachments ORDER BY cipher_id ASC, id ASC'),
]); ]);
const exportedConfigRows = sanitizeConfigRowsForExport(configRows);
const exportedAttachmentRows = includeAttachments ? attachmentRows : []; const exportedAttachmentRows = includeAttachments ? attachmentRows : [];
const attachmentBlobs: BackupManifestAttachmentBlob[] = exportedAttachmentRows.map((row) => { const attachmentBlobs: BackupManifestAttachmentBlob[] = exportedAttachmentRows.map((row) => {
const cipherId = String(row.cipher_id || '').trim(); const cipherId = String(row.cipher_id || '').trim();
@@ -371,7 +392,7 @@ export async function buildBackupArchive(
appVersion: APP_VERSION, appVersion: APP_VERSION,
storageKind: getBlobStorageKind(env), storageKind: getBlobStorageKind(env),
tableCounts: { tableCounts: {
config: configRows.length, config: exportedConfigRows.length,
users: userRows.length, users: userRows.length,
user_revisions: revisionRows.length, user_revisions: revisionRows.length,
folders: folderRows.length, folders: folderRows.length,
@@ -392,7 +413,7 @@ export async function buildBackupArchive(
const files: Record<string, Uint8Array> = { const files: Record<string, Uint8Array> = {
'manifest.json': encoder.encode(JSON.stringify(manifestBase, null, BACKUP_JSON_INDENT)), 'manifest.json': encoder.encode(JSON.stringify(manifestBase, null, BACKUP_JSON_INDENT)),
'db.json': encoder.encode(JSON.stringify({ 'db.json': encoder.encode(JSON.stringify({
config: configRows, config: exportedConfigRows,
users: userRows, users: userRows,
user_revisions: revisionRows, user_revisions: revisionRows,
folders: folderRows, folders: folderRows,
+14
View File
@@ -155,6 +155,20 @@ export function parseBackupSettingsEnvelope(raw: string | null): BackupSettingsE
} }
} }
export function exportPortableBackupSettingsEnvelope(raw: string | null): string | null {
const envelope = parseBackupSettingsEnvelope(raw);
if (!envelope) return null;
return JSON.stringify({
version: 2,
portableOnly: true,
runtime: {
iv: '',
ciphertext: '',
},
portable: envelope.portable,
});
}
export async function encryptBackupSettingsEnvelope( export async function encryptBackupSettingsEnvelope(
plaintext: string, plaintext: string,
env: Env, env: Env,