feat: enhance backup functionality with attachment options

- Added support for including attachments in backup exports.
- Updated backup-related interfaces and functions to handle attachment options.
- Introduced a new UI component for selecting attachment inclusion during backup operations.
- Modified existing components to integrate the new attachment functionality.
- Improved user feedback and error handling during backup processes.
This commit is contained in:
shuaiplus
2026-03-20 04:55:23 +08:00
parent 3d38424d77
commit cbf1e86881
19 changed files with 883 additions and 352 deletions
+63 -6
View File
@@ -16,6 +16,7 @@ import {
type AuthedFetch,
} from './shared';
import { readResponseBytesWithProgress } from '../download';
import { unzipSync, zipSync } from 'fflate';
export type {
BackupDestinationConfig,
@@ -81,13 +82,11 @@ export interface AdminBackupImportCounts {
folders: number;
ciphers: number;
attachments: number;
sends: number;
attachmentFiles: number;
sendFiles: number;
}
export interface AdminBackupImportSkippedItem {
kind: 'attachment' | 'send-file';
kind: 'attachment';
path: string;
sizeBytes: number;
}
@@ -95,7 +94,6 @@ export interface AdminBackupImportSkippedItem {
export interface AdminBackupImportSkipped {
reason: string | null;
attachments: number;
sendFiles: number;
items: AdminBackupImportSkippedItem[];
}
@@ -111,8 +109,25 @@ export interface AdminBackupExportPayload {
bytes: Uint8Array;
}
export async function exportAdminBackup(authedFetch: AuthedFetch): Promise<AdminBackupExportPayload> {
const resp = await authedFetch('/api/admin/backup/export', { method: 'POST' });
interface BackupExportManifestAttachmentBlob {
cipherId: string;
attachmentId: string;
blobName: string;
}
interface BackupExportManifest {
attachmentBlobs?: BackupExportManifestAttachmentBlob[];
}
export async function exportAdminBackup(
authedFetch: AuthedFetch,
includeAttachments: boolean = false
): Promise<AdminBackupExportPayload> {
const resp = await authedFetch('/api/admin/backup/export', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ includeAttachments }),
});
if (!resp.ok) throw new Error(await parseErrorMessage(resp, t('txt_backup_export_failed')));
const mimeType = String(resp.headers.get('Content-Type') || 'application/zip').trim() || 'application/zip';
@@ -121,6 +136,48 @@ export async function exportAdminBackup(authedFetch: AuthedFetch): Promise<Admin
return { fileName, mimeType, bytes };
}
export async function downloadAdminBackupAttachmentBlob(
authedFetch: AuthedFetch,
blobName: string
): Promise<Uint8Array> {
const params = new URLSearchParams();
params.set('blobName', blobName);
const resp = await authedFetch(`/api/admin/backup/blob?${params.toString()}`, { method: 'GET' });
if (!resp.ok) throw new Error(await parseErrorMessage(resp, t('txt_backup_export_failed')));
return new Uint8Array(await resp.arrayBuffer());
}
export async function buildCompleteAdminBackupExport(
authedFetch: AuthedFetch,
includeAttachments: boolean = false
): Promise<AdminBackupExportPayload> {
const payload = await exportAdminBackup(authedFetch, includeAttachments);
if (!includeAttachments) return payload;
const zipped = unzipSync(payload.bytes);
const manifestBytes = zipped['manifest.json'];
if (!manifestBytes) {
throw new Error(t('txt_backup_export_failed'));
}
let manifest: BackupExportManifest;
try {
manifest = JSON.parse(new TextDecoder().decode(manifestBytes)) as BackupExportManifest;
} catch {
throw new Error(t('txt_backup_export_failed'));
}
for (const attachment of manifest.attachmentBlobs || []) {
const bytes = await downloadAdminBackupAttachmentBlob(authedFetch, attachment.blobName);
zipped[`attachments/${attachment.cipherId}/${attachment.attachmentId}.bin`] = bytes;
}
return {
...payload,
bytes: zipSync(zipped, { level: 0 }),
};
}
export async function getAdminBackupSettings(authedFetch: AuthedFetch): Promise<AdminBackupSettings> {
const resp = await authedFetch('/api/admin/backup/settings', { method: 'GET' });
if (!resp.ok) throw new Error(await parseErrorMessage(resp, t('txt_backup_settings_load_failed')));