feat: enhance backup and restore functionality with integrity checks and progress tracking

- Added support for backup integrity verification during export and restore processes.
- Introduced progress dispatching for backup export and restore operations.
- Implemented new API endpoints for inspecting remote backup integrity.
- Enhanced user interface with progress indicators and warning dialogs for integrity issues.
- Updated localization strings for new features and user feedback.
- Refactored backup-related functions for better clarity and maintainability.
This commit is contained in:
shuaiplus
2026-03-28 05:52:47 +08:00
parent bd8e26d2ab
commit 2a7879efaa
18 changed files with 2250 additions and 225 deletions
+144 -6
View File
@@ -57,6 +57,21 @@ export interface AdminBackupRunResponse {
settings: AdminBackupSettings;
}
export interface BackupFileIntegrityCheckResult {
hasChecksumPrefix: boolean;
expectedPrefix: string | null;
actualPrefix: string;
matches: boolean;
}
export interface RemoteBackupIntegrityResponse {
object: 'backup-remote-integrity';
destinationId: string;
path: string;
fileName: string;
integrity: BackupFileIntegrityCheckResult;
}
export interface RemoteBackupItem {
path: string;
name: string;
@@ -109,6 +124,18 @@ export interface AdminBackupExportPayload {
bytes: Uint8Array;
}
export interface BackupExportClientProgressEvent {
operation: 'backup-export';
source: 'local';
step: string;
fileName: string;
stageTitle: string;
stageDetail: string;
done?: boolean;
ok?: boolean;
error?: string | null;
}
interface BackupExportManifestAttachmentBlob {
cipherId: string;
attachmentId: string;
@@ -119,6 +146,36 @@ interface BackupExportManifest {
attachmentBlobs?: BackupExportManifestAttachmentBlob[];
}
const BACKUP_FILE_HASH_PREFIX_LENGTH = 5;
function parseBackupTimestampFromFileName(fileName: string): Date | null {
const match = String(fileName || '').match(/nodewarden_backup_(\d{8})_(\d{6})(?:_[0-9a-f]{5})?\.zip$/i);
if (!match) return null;
const datePart = match[1];
const timePart = match[2];
const iso = `${datePart.slice(0, 4)}-${datePart.slice(4, 6)}-${datePart.slice(6, 8)}T${timePart.slice(0, 2)}:${timePart.slice(2, 4)}:${timePart.slice(4, 6)}.000Z`;
const parsed = new Date(iso);
return Number.isFinite(parsed.getTime()) ? parsed : null;
}
function buildBackupFileName(date: Date, checksumPrefix: string): string {
const parts = [
date.getUTCFullYear().toString().padStart(4, '0'),
(date.getUTCMonth() + 1).toString().padStart(2, '0'),
date.getUTCDate().toString().padStart(2, '0'),
date.getUTCHours().toString().padStart(2, '0'),
date.getUTCMinutes().toString().padStart(2, '0'),
date.getUTCSeconds().toString().padStart(2, '0'),
];
return `nodewarden_backup_${parts[0]}${parts[1]}${parts[2]}_${parts[3]}${parts[4]}${parts[5]}_${checksumPrefix}.zip`;
}
async function applyBackupFileIntegrityName(fileName: string, bytes: Uint8Array): Promise<string> {
const integrity = await verifyBackupFileIntegrity(bytes, fileName);
const effectiveDate = parseBackupTimestampFromFileName(fileName) || new Date();
return buildBackupFileName(effectiveDate, integrity.actualPrefix);
}
export async function exportAdminBackup(
authedFetch: AuthedFetch,
includeAttachments: boolean = false
@@ -149,10 +206,21 @@ export async function downloadAdminBackupAttachmentBlob(
export async function buildCompleteAdminBackupExport(
authedFetch: AuthedFetch,
includeAttachments: boolean = false
includeAttachments: boolean = false,
onProgress?: (event: BackupExportClientProgressEvent) => void | Promise<void>
): Promise<AdminBackupExportPayload> {
const payload = await exportAdminBackup(authedFetch, includeAttachments);
if (!includeAttachments) return payload;
if (!includeAttachments) {
await onProgress?.({
operation: 'backup-export',
source: 'local',
step: 'export_client_save',
fileName: payload.fileName,
stageTitle: 'txt_backup_export_progress_save_title',
stageDetail: 'txt_backup_export_progress_save_detail',
});
return payload;
}
const zipped = unzipSync(payload.bytes);
const manifestBytes = zipped['manifest.json'];
@@ -167,14 +235,41 @@ export async function buildCompleteAdminBackupExport(
throw new Error(t('txt_backup_export_failed'));
}
await onProgress?.({
operation: 'backup-export',
source: 'local',
step: 'export_client_fetch_attachments',
fileName: payload.fileName,
stageTitle: 'txt_backup_export_progress_fetch_attachments_title',
stageDetail: 'txt_backup_export_progress_fetch_attachments_detail',
});
for (const attachment of manifest.attachmentBlobs || []) {
const bytes = await downloadAdminBackupAttachmentBlob(authedFetch, attachment.blobName);
zipped[`attachments/${attachment.cipherId}/${attachment.attachmentId}.bin`] = bytes;
}
await onProgress?.({
operation: 'backup-export',
source: 'local',
step: 'export_client_rebuild',
fileName: payload.fileName,
stageTitle: 'txt_backup_export_progress_rebuild_title',
stageDetail: 'txt_backup_export_progress_rebuild_detail',
});
const rebuiltBytes = zipSync(zipped, { level: 0 });
const rebuiltFileName = await applyBackupFileIntegrityName(payload.fileName, rebuiltBytes);
await onProgress?.({
operation: 'backup-export',
source: 'local',
step: 'export_client_save',
fileName: rebuiltFileName,
stageTitle: 'txt_backup_export_progress_save_title',
stageDetail: 'txt_backup_export_progress_save_detail',
});
return {
...payload,
bytes: zipSync(zipped, { level: 0 }),
bytes: rebuiltBytes,
fileName: rebuiltFileName,
};
}
@@ -276,6 +371,29 @@ export async function downloadRemoteBackup(
return { fileName, mimeType, bytes };
}
export function extractBackupFileChecksumPrefix(fileName: string): string | null {
const normalized = String(fileName || '').trim();
const match = normalized.match(/_([0-9a-f]{5})\.zip$/i);
return match ? match[1].toLowerCase() : null;
}
async function sha256Hex(bytes: Uint8Array): Promise<string> {
const digest = await crypto.subtle.digest('SHA-256', bytes);
return Array.from(new Uint8Array(digest)).map((byte) => byte.toString(16).padStart(2, '0')).join('');
}
export async function verifyBackupFileIntegrity(bytes: Uint8Array, fileName: string): Promise<BackupFileIntegrityCheckResult> {
const expectedPrefix = extractBackupFileChecksumPrefix(fileName);
const actualHash = await sha256Hex(bytes);
const actualPrefix = actualHash.slice(0, BACKUP_FILE_HASH_PREFIX_LENGTH);
return {
hasChecksumPrefix: !!expectedPrefix,
expectedPrefix,
actualPrefix,
matches: !expectedPrefix || expectedPrefix === actualPrefix,
};
}
export async function deleteRemoteBackup(
authedFetch: AuthedFetch,
destinationId: string,
@@ -288,16 +406,32 @@ export async function deleteRemoteBackup(
if (!resp.ok) throw new Error(await parseErrorMessage(resp, t('txt_backup_remote_delete_failed')));
}
export async function inspectRemoteBackupIntegrity(
authedFetch: AuthedFetch,
destinationId: string,
path: string
): Promise<RemoteBackupIntegrityResponse> {
const params = new URLSearchParams();
params.set('destinationId', destinationId);
params.set('path', path);
const resp = await authedFetch(`/api/admin/backup/remote/integrity?${params.toString()}`, { method: 'GET' });
if (!resp.ok) throw new Error(await parseErrorMessage(resp, t('txt_backup_remote_download_failed')));
const body = await parseJson<RemoteBackupIntegrityResponse>(resp);
if (!body?.integrity || !body?.fileName) throw new Error(t('txt_backup_remote_invalid_response'));
return body;
}
export async function restoreRemoteBackup(
authedFetch: AuthedFetch,
destinationId: string,
path: string,
replaceExisting: boolean = false
replaceExisting: boolean = false,
allowChecksumMismatch: boolean = false
): Promise<AdminBackupImportResponse> {
const resp = await authedFetch('/api/admin/backup/remote/restore', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ destinationId, path, replaceExisting }),
body: JSON.stringify({ destinationId, path, replaceExisting, allowChecksumMismatch }),
});
if (!resp.ok) throw new Error(await parseErrorMessage(resp, t('txt_backup_remote_restore_failed')));
const body = await parseJson<AdminBackupImportResponse>(resp);
@@ -308,13 +442,17 @@ export async function restoreRemoteBackup(
export async function importAdminBackup(
authedFetch: AuthedFetch,
file: File,
replaceExisting: boolean = false
replaceExisting: boolean = false,
allowChecksumMismatch: boolean = false
): Promise<AdminBackupImportResponse> {
const formData = new FormData();
formData.set('file', file, file.name || 'nodewarden_backup.zip');
if (replaceExisting) {
formData.set('replaceExisting', '1');
}
if (allowChecksumMismatch) {
formData.set('allowChecksumMismatch', '1');
}
const resp = await authedFetch('/api/admin/backup/import', {
method: 'POST',
+1
View File
@@ -17,6 +17,7 @@ export interface WebVaultSignalRInvocation {
UserId?: string;
Date?: string;
RevisionDate?: string;
[key: string]: unknown;
};
}>;
}
+27
View File
@@ -0,0 +1,27 @@
export type BackupProgressOperation = 'backup-restore' | 'backup-export' | 'backup-remote-run';
export interface BackupProgressDetail {
operation: BackupProgressOperation;
source?: 'local' | 'remote';
step: string;
fileName: string;
stageTitle?: string;
stageDetail?: string;
replaceExisting?: boolean;
done?: boolean;
ok?: boolean;
error?: string | null;
Date?: string;
}
export type BackupRestoreProgressDetail = BackupProgressDetail;
export const BACKUP_PROGRESS_EVENT = 'nodewarden:backup-progress';
export const BACKUP_RESTORE_PROGRESS_EVENT = BACKUP_PROGRESS_EVENT;
export function dispatchBackupProgress(detail: BackupProgressDetail): void {
if (typeof window === 'undefined') return;
window.dispatchEvent(new CustomEvent<BackupProgressDetail>(BACKUP_PROGRESS_EVENT, { detail }));
}
export const dispatchBackupRestoreProgress = dispatchBackupProgress;
+154 -6
View File
@@ -26,11 +26,16 @@ const messages: Record<Locale, Record<string, string>> = {
txt_backup_export_success: "Backup exported",
txt_backup_import_success_relogin: "Backup restored. Please sign in again.",
txt_backup_restore_success_relogin: "Backup restored. Please sign in again.",
txt_backup_restore_completed_verified: "Backup file integrity verification passed.",
txt_backup_restore_completed_without_checksum: "Backup restored. No filename integrity marker was available for verification.",
txt_backup_remote_restore_completed_verified: "Remote backup integrity verification passed.",
txt_backup_remote_restore_completed_without_checksum: "Remote backup restored. No filename integrity marker was available for verification.",
txt_backup_restore_skipped_summary: "{reason}. Skipped {attachments} attachment(s).",
txt_backup_restore_skipped_reason_default: "Some files could not be restored",
txt_backup_export_failed: "Backup export failed",
txt_backup_import_failed: "Backup restore failed",
txt_backup_restore_failed: "Backup restore failed",
txt_backup_integrity_check_failed: "Backup integrity verification failed",
txt_backup_center_title: "Instance Backup",
txt_backup_center_description: "Keep local exports for manual restore, and configure one daily remote backup target for unattended protection.",
txt_backup_restore_note: "Restoring will overwrite the current instance if you choose the replace flow.",
@@ -99,6 +104,7 @@ const messages: Record<Locale, Record<string, string>> = {
txt_backup_run_manual: "Run Manually",
txt_backup_running_now: "Running...",
txt_backup_remote_run_success: "Remote backup completed",
txt_backup_remote_run_success_verified: "Remote backup completed and integrity verification passed.",
txt_backup_remote_run_failed: "Remote backup failed",
txt_backup_remote_title: "Remote Backups",
txt_backup_remote_note: "Browse the saved destination and choose a backup ZIP to download or restore.",
@@ -112,6 +118,68 @@ const messages: Record<Locale, Record<string, string>> = {
txt_backup_remote_restore: "Restore",
txt_backup_remote_restore_stage_prepare: "Preparing remote backup restore...",
txt_backup_remote_restore_stage_replace: "Clearing current data and restoring remote backup...",
txt_backup_progress_kicker: "Backup Task",
txt_backup_progress_subject: "Current item: {name}",
txt_backup_restore_progress_kicker: "Restore Progress",
txt_backup_restore_progress_local_title: "Restoring local backup",
txt_backup_restore_progress_remote_title: "Restoring remote backup",
txt_backup_export_progress_title: "Exporting backup",
txt_backup_remote_run_progress_title: "Running remote backup",
txt_backup_restore_progress_file: "Current file: {name}",
txt_backup_restore_progress_elapsed: "{seconds}s elapsed",
txt_backup_archive_progress_collect_title: "Collecting vault data",
txt_backup_archive_progress_collect_detail: "The server is reading database tables and assembling the backup payload.",
txt_backup_archive_progress_collect_with_attachments_detail: "The server is reading database tables and collecting attachment metadata for the backup payload.",
txt_backup_archive_progress_package_title: "Packaging backup archive",
txt_backup_archive_progress_package_detail: "The server is generating the backup ZIP and computing its checksum prefix.",
txt_backup_archive_progress_package_with_attachments_detail: "The server is generating the backup ZIP metadata and computing its checksum prefix for the attachment-aware export.",
txt_backup_archive_progress_ready_title: "Preparing download",
txt_backup_archive_progress_ready_detail: "The backup archive is ready and is being returned to the browser.",
txt_backup_export_progress_fetch_attachments_title: "Downloading attachment files",
txt_backup_export_progress_fetch_attachments_detail: "The browser is fetching attachment objects and adding them into the export package.",
txt_backup_export_progress_rebuild_title: "Rebuilding export archive",
txt_backup_export_progress_rebuild_detail: "The browser is rebuilding the final ZIP and refreshing its checksum suffix.",
txt_backup_export_progress_save_title: "Saving export file",
txt_backup_export_progress_save_detail: "The browser is preparing the final backup file for download.",
txt_backup_export_progress_complete_title: "Export completed",
txt_backup_export_progress_complete_detail: "The backup export is ready.",
txt_backup_export_progress_failed_title: "Export failed",
txt_backup_export_progress_failed_detail: "The backup export could not be completed.",
txt_backup_remote_run_progress_prepare_title: "Preparing remote backup",
txt_backup_remote_run_progress_prepare_detail: "The server is loading the selected destination and preparing this backup run.",
txt_backup_remote_run_progress_sync_attachments_title: "Checking attachment index",
txt_backup_remote_run_progress_sync_attachments_detail: "The server is comparing attachment metadata so only missing attachment objects are uploaded.",
txt_backup_remote_run_progress_sync_attachments_skipped_detail: "This backup does not include attachments, so attachment synchronization is skipped.",
txt_backup_remote_run_progress_upload_title: "Uploading backup archive",
txt_backup_remote_run_progress_upload_detail: "The server is uploading the backup ZIP to the remote destination.",
txt_backup_remote_run_progress_verify_title: "Verifying uploaded archive",
txt_backup_remote_run_progress_verify_detail: "The server is downloading the uploaded ZIP back and verifying its checksum and size.",
txt_backup_remote_run_progress_cleanup_title: "Cleaning older backups",
txt_backup_remote_run_progress_cleanup_detail: "The server is pruning older backup files according to the retention policy.",
txt_backup_remote_run_progress_complete_title: "Remote backup completed",
txt_backup_remote_run_progress_complete_detail: "The remote backup has been uploaded and verified successfully.",
txt_backup_remote_run_progress_failed_title: "Remote backup failed",
txt_backup_remote_run_progress_failed_detail: "The remote backup could not be completed.",
txt_backup_restore_progress_local_upload_title: "Uploading backup archive",
txt_backup_restore_progress_local_upload_detail: "The selected ZIP is being sent to the server for processing.",
txt_backup_restore_progress_local_shadow_title: "Creating shadow workspace",
txt_backup_restore_progress_local_shadow_detail: "The server is preparing an isolated restore area so the current data remains untouched until validation passes.",
txt_backup_restore_progress_local_data_title: "Writing vault data",
txt_backup_restore_progress_local_data_detail: "The server is importing users, folders, vault items, and related metadata into shadow tables.",
txt_backup_restore_progress_local_files_title: "Restoring attachment files",
txt_backup_restore_progress_local_files_detail: "The server is writing attachment objects back to storage and removing any attachment rows that cannot be restored.",
txt_backup_restore_progress_local_finalize_title: "Validating and switching data",
txt_backup_restore_progress_local_finalize_detail: "The server is performing final validation and then swapping the verified restore data into the live tables.",
txt_backup_restore_progress_remote_fetch_title: "Reading remote backup",
txt_backup_restore_progress_remote_fetch_detail: "The server is downloading the selected backup package from the remote destination.",
txt_backup_restore_progress_remote_shadow_title: "Creating shadow workspace",
txt_backup_restore_progress_remote_shadow_detail: "The server is preparing an isolated restore area so the current data remains untouched until validation passes.",
txt_backup_restore_progress_remote_data_title: "Writing vault data",
txt_backup_restore_progress_remote_data_detail: "The server is importing users, folders, vault items, and related metadata into shadow tables.",
txt_backup_restore_progress_remote_files_title: "Restoring remote attachments",
txt_backup_restore_progress_remote_files_detail: "The server is fetching required attachment objects from remote storage and writing them back into local storage.",
txt_backup_restore_progress_remote_finalize_title: "Validating and switching data",
txt_backup_restore_progress_remote_finalize_detail: "The server is performing final validation and then switching the verified restore data into the live tables.",
txt_backup_remote_loading: "Loading remote backups...",
txt_backup_remote_cached_empty: "Click Refresh to load this destination.",
txt_backup_remote_empty: "No backup files found in this folder.",
@@ -126,6 +194,11 @@ const messages: Record<Locale, Record<string, string>> = {
txt_backup_remote_delete_confirm_message: "Delete backup file \"{name}\"? This cannot be undone.",
txt_backup_remote_deleting: "Deleting...",
txt_backup_remote_restore_failed: "Restoring remote backup failed",
txt_backup_restore_checksum_warning_title: "Backup Integrity Warning",
txt_backup_restore_checksum_warning_message: "The selected backup file \"{name}\" failed filename integrity verification. Expected prefix {expected}, actual prefix {actual}. The file may be incomplete or corrupted. Continuing may restore damaged data.",
txt_backup_remote_restore_checksum_warning_message: "The remote backup file \"{name}\" failed filename integrity verification. Expected prefix {expected}, actual prefix {actual}. The file may be corrupted during upload or storage. Continuing may restore damaged data and may cause serious data loss.",
txt_backup_restore_checksum_warning_message_fallback: "The selected backup file failed integrity verification. Continuing may restore damaged data.",
txt_backup_restore_checksum_warning_confirm: "Continue Restore",
txt_backup_remote_restore_invalid_response: "Invalid remote backup restore response",
txt_backup_remote_run_invalid_response: "Invalid remote backup run response",
txt_backup_settings_invalid_response: "Invalid backup settings response",
@@ -197,9 +270,9 @@ const messages: Record<Locale, Record<string, string>> = {
txt_backup_no_file_selected: "No backup file selected",
txt_backup_selected_file_name: "Selected file: {name}",
txt_backup_replace_confirm_title: "Replace Current Instance Data",
txt_backup_replace_confirm_message: "The current instance already contains data. Clear it and restore the selected backup?",
txt_backup_clear_and_import: "Clear and Import",
txt_backup_clear_and_restore: "Clear and Restore",
txt_backup_replace_confirm_message: "The current instance already contains data. Continue restoring and replace the current instance data with the selected backup after verification succeeds?",
txt_backup_clear_and_import: "Replace and Import",
txt_backup_clear_and_restore: "Replace and Restore",
txt_access_count: "Access Count",
txt_accessed_count_times: "Accessed {count} times",
txt_actions: "Actions",
@@ -643,6 +716,7 @@ const messages: Record<Locale, Record<string, string>> = {
txt_vault_synced: "Vault synced",
txt_verification_code: "Verification Code",
txt_verify: "Verify",
txt_warning: "Warning",
txt_view_recovery_code: "View Recovery Code",
txt_web: "Web",
txt_website: "Website",
@@ -676,11 +750,16 @@ const zhCNOverrides: Record<string, string> = {
txt_backup_export_success: '备份已导出',
txt_backup_import_success_relogin: '备份已还原,请重新登录',
txt_backup_restore_success_relogin: '备份已还原,请重新登录',
txt_backup_restore_completed_verified: '备份文件完整性校验已通过。',
txt_backup_restore_completed_without_checksum: '备份已还原,但文件名中未提供可校验的完整性标记。',
txt_backup_remote_restore_completed_verified: '远程备份完整性校验已通过。',
txt_backup_remote_restore_completed_without_checksum: '远程备份已还原,但文件名中未提供可校验的完整性标记。',
txt_backup_restore_skipped_summary: '{reason},已跳过 {attachments} 个附件',
txt_backup_restore_skipped_reason_default: '部分文件无法还原',
txt_backup_export_failed: '备份导出失败',
txt_backup_import_failed: '备份还原失败',
txt_backup_restore_failed: '备份还原失败',
txt_backup_integrity_check_failed: '备份完整性校验失败',
txt_backup_center_title: '实例备份',
txt_backup_center_description: '把本地导出和远程自动备份放在一起管理,既方便手动恢复,也能每天自动留一份。',
txt_backup_restore_note: '还原会覆盖当前实例;如果当前已有数据,系统会要求你确认“清空后还原”。',
@@ -749,6 +828,7 @@ const zhCNOverrides: Record<string, string> = {
txt_backup_run_manual: '手动执行',
txt_backup_running_now: '执行中...',
txt_backup_remote_run_success: '远程备份已完成',
txt_backup_remote_run_success_verified: '远程备份已完成,且完整性校验已通过。',
txt_backup_remote_run_failed: '远程备份失败',
txt_backup_remote_title: '远端备份',
txt_backup_remote_note: '浏览已保存的备份地点,选择某个备份 ZIP 后可以下载,也可以直接还原。',
@@ -762,6 +842,68 @@ const zhCNOverrides: Record<string, string> = {
txt_backup_remote_restore: '还原',
txt_backup_remote_restore_stage_prepare: '正在读取远端备份并检查可恢复内容...',
txt_backup_remote_restore_stage_replace: '正在清空当前数据并还原远端备份,请稍候...',
txt_backup_progress_kicker: '备份任务',
txt_backup_progress_subject: '当前对象:{name}',
txt_backup_restore_progress_kicker: '还原进度',
txt_backup_restore_progress_local_title: '正在还原本地备份',
txt_backup_restore_progress_remote_title: '正在还原远端备份',
txt_backup_export_progress_title: '正在导出备份',
txt_backup_remote_run_progress_title: '正在执行远程备份',
txt_backup_restore_progress_file: '当前文件:{name}',
txt_backup_restore_progress_elapsed: '已耗时 {seconds} 秒',
txt_backup_archive_progress_collect_title: '正在收集密码库数据',
txt_backup_archive_progress_collect_detail: '服务器正在读取数据库表,并整理备份所需的数据内容。',
txt_backup_archive_progress_collect_with_attachments_detail: '服务器正在读取数据库表,并整理附件元数据与备份内容。',
txt_backup_archive_progress_package_title: '正在打包备份压缩包',
txt_backup_archive_progress_package_detail: '服务器正在生成备份 ZIP,并计算文件名校验前缀。',
txt_backup_archive_progress_package_with_attachments_detail: '服务器正在生成带附件信息的备份 ZIP 元数据,并计算文件名校验前缀。',
txt_backup_archive_progress_ready_title: '正在准备下载',
txt_backup_archive_progress_ready_detail: '备份压缩包已经生成,服务器正在把它返回给浏览器。',
txt_backup_export_progress_fetch_attachments_title: '正在下载附件文件',
txt_backup_export_progress_fetch_attachments_detail: '浏览器正在读取附件对象,并把它们补入导出备份包。',
txt_backup_export_progress_rebuild_title: '正在重建导出压缩包',
txt_backup_export_progress_rebuild_detail: '浏览器正在重建最终 ZIP,并刷新文件名里的校验后缀。',
txt_backup_export_progress_save_title: '正在保存导出文件',
txt_backup_export_progress_save_detail: '浏览器正在准备最终的备份文件下载。',
txt_backup_export_progress_complete_title: '备份导出已完成',
txt_backup_export_progress_complete_detail: '导出备份已经准备完成。',
txt_backup_export_progress_failed_title: '备份导出失败',
txt_backup_export_progress_failed_detail: '导出备份未能完成。',
txt_backup_remote_run_progress_prepare_title: '正在准备远程备份',
txt_backup_remote_run_progress_prepare_detail: '服务器正在读取当前备份目标,并准备执行这次远程备份。',
txt_backup_remote_run_progress_sync_attachments_title: '正在检查附件索引',
txt_backup_remote_run_progress_sync_attachments_detail: '服务器正在比对附件索引,只会上传缺失或不一致的附件对象。',
txt_backup_remote_run_progress_sync_attachments_skipped_detail: '当前备份未包含附件,因此跳过附件同步。',
txt_backup_remote_run_progress_upload_title: '正在上传备份压缩包',
txt_backup_remote_run_progress_upload_detail: '服务器正在把备份 ZIP 上传到远程备份目标。',
txt_backup_remote_run_progress_verify_title: '正在校验已上传压缩包',
txt_backup_remote_run_progress_verify_detail: '服务器正在回读刚上传的 ZIP,并校验它的哈希和大小。',
txt_backup_remote_run_progress_cleanup_title: '正在清理旧备份',
txt_backup_remote_run_progress_cleanup_detail: '服务器正在按保留策略清理旧备份文件。',
txt_backup_remote_run_progress_complete_title: '远程备份已完成',
txt_backup_remote_run_progress_complete_detail: '远程备份已上传完成,并通过完整性校验。',
txt_backup_remote_run_progress_failed_title: '远程备份失败',
txt_backup_remote_run_progress_failed_detail: '远程备份未能完成。',
txt_backup_restore_progress_local_upload_title: '正在上传备份包',
txt_backup_restore_progress_local_upload_detail: '已选 ZIP 正在发送到服务器,服务器收到后会开始执行还原。',
txt_backup_restore_progress_local_shadow_title: '正在创建影子恢复区',
txt_backup_restore_progress_local_shadow_detail: '服务器正在准备独立的影子数据区,只有校验通过后才会替换正式数据。',
txt_backup_restore_progress_local_data_title: '正在写入密码库数据',
txt_backup_restore_progress_local_data_detail: '服务器正在把用户、文件夹、密码条目和相关元数据写入影子表。',
txt_backup_restore_progress_local_files_title: '正在恢复附件文件',
txt_backup_restore_progress_local_files_detail: '服务器正在把附件对象写回存储,并剔除无法恢复的附件记录。',
txt_backup_restore_progress_local_finalize_title: '正在校验并完成切换',
txt_backup_restore_progress_local_finalize_detail: '服务器正在执行最终校验,校验通过后会把已验证的数据切换为正式数据。',
txt_backup_restore_progress_remote_fetch_title: '正在读取远端备份包',
txt_backup_restore_progress_remote_fetch_detail: '服务器正在从远端备份目标下载你选中的备份包。',
txt_backup_restore_progress_remote_shadow_title: '正在创建影子恢复区',
txt_backup_restore_progress_remote_shadow_detail: '服务器正在准备独立的影子数据区,只有校验通过后才会替换正式数据。',
txt_backup_restore_progress_remote_data_title: '正在写入密码库数据',
txt_backup_restore_progress_remote_data_detail: '服务器正在把用户、文件夹、密码条目和相关元数据写入影子表。',
txt_backup_restore_progress_remote_files_title: '正在恢复远端附件',
txt_backup_restore_progress_remote_files_detail: '服务器正在从远端存储读取所需附件,并写回到当前实例的附件存储。',
txt_backup_restore_progress_remote_finalize_title: '正在校验并完成切换',
txt_backup_restore_progress_remote_finalize_detail: '服务器正在执行最终校验,校验通过后会把已验证的数据切换为正式数据。',
txt_backup_remote_loading: '正在读取远端备份...',
txt_backup_remote_cached_empty: '点击“刷新”后读取',
txt_backup_remote_empty: '这个目录下还没有备份文件',
@@ -776,6 +918,11 @@ const zhCNOverrides: Record<string, string> = {
txt_backup_remote_delete_confirm_message: '删除备份文件“{name}”?此操作不可撤销。',
txt_backup_remote_deleting: '删除中...',
txt_backup_remote_restore_failed: '还原远端备份失败',
txt_backup_restore_checksum_warning_title: '备份完整性警告',
txt_backup_restore_checksum_warning_message: '所选备份文件“{name}”未通过文件名完整性校验。期望前缀为 {expected},实际计算结果为 {actual}。该文件可能不完整或已经损坏。继续还原可能会导入受损数据。',
txt_backup_remote_restore_checksum_warning_message: '远程备份文件“{name}”未通过文件名完整性校验。期望前缀为 {expected},实际计算结果为 {actual}。该文件可能在上传或存储过程中损坏。继续还原可能会导入受损数据,并可能造成严重后果。',
txt_backup_restore_checksum_warning_message_fallback: '所选备份文件未通过完整性校验。继续还原可能会导入受损数据。',
txt_backup_restore_checksum_warning_confirm: '继续还原',
txt_backup_remote_restore_invalid_response: '远端备份还原响应无效',
txt_backup_remote_run_invalid_response: '远端备份执行响应无效',
txt_backup_settings_invalid_response: '备份设置响应无效',
@@ -847,9 +994,9 @@ const zhCNOverrides: Record<string, string> = {
txt_backup_no_file_selected: '尚未选择备份文件',
txt_backup_selected_file_name: '已选择文件:{name}',
txt_backup_replace_confirm_title: '替换当前实例数据',
txt_backup_replace_confirm_message: '当前实例里已经有数据。要先清空当前数据库和文件,再还原所选备份吗',
txt_backup_clear_and_import: '清空后导入',
txt_backup_clear_and_restore: '清空后还原',
txt_backup_replace_confirm_message: '当前实例里已经有数据。确认后,系统会先完成校验与恢复准备,只有在恢复成功后才会用所选备份替换当前实例数据。是否继续',
txt_backup_clear_and_import: '替换并导入',
txt_backup_clear_and_restore: '替换并还原',
txt_sign_out: '退出登录',
txt_log_in: '登录',
txt_logging_in: '正在登录...',
@@ -1243,6 +1390,7 @@ const zhCNOverrides: Record<string, string> = {
txt_user_status_updated: '用户状态已更新',
txt_vault_synced: '密码库已同步',
txt_verify: '验证',
txt_warning: '警告',
txt_web: '网页',
txt_windows_desktop: 'Windows 桌面端',
txt_jwt_warning_title: 'JWT_SECRET 配置警告',