From c9e74178251f51c4ee8fba3c7de9ccd2718edda0 Mon Sep 17 00:00:00 2001 From: shuaiplus <2327005759@qq.com> Date: Tue, 7 Apr 2026 20:24:28 +0800 Subject: [PATCH] feat: add timezone support for backup file naming and extraction --- src/handlers/backup.ts | 1 + src/services/backup-archive.ts | 37 ++++++++++++++++++++++++---------- webapp/src/lib/api/backup.ts | 25 +++++++---------------- 3 files changed, 34 insertions(+), 29 deletions(-) diff --git a/src/handlers/backup.ts b/src/handlers/backup.ts index 69dfd0f..1b5584b 100644 --- a/src/handlers/backup.ts +++ b/src/handlers/backup.ts @@ -192,6 +192,7 @@ async function executeConfiguredBackup( }); const archive = await buildBackupArchive(env, now, { includeAttachments: destination.includeAttachments, + timeZone: destination.schedule.timezone, progress: progress ? async (event) => { if (event.step === 'archive_ready') { diff --git a/src/services/backup-archive.ts b/src/services/backup-archive.ts index dca00e5..590befb 100644 --- a/src/services/backup-archive.ts +++ b/src/services/backup-archive.ts @@ -71,6 +71,7 @@ export interface BackupFileIntegrityCheckResult { export interface BuildBackupArchiveOptions { includeAttachments?: boolean; progress?: BackupArchiveBuildProgressReporter; + timeZone?: string; } export interface BackupArchiveBuildProgressEvent { @@ -93,17 +94,30 @@ async function sha256Hex(bytes: Uint8Array): Promise { return Array.from(new Uint8Array(digest)).map((byte) => byte.toString(16).padStart(2, '0')).join(''); } -function buildBackupFileName(date: Date = new Date(), checksumPrefix: string | null = null): 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'), - ]; +function getDateParts(date: Date, timeZone: string): string { + const formatter = new Intl.DateTimeFormat('en-CA', { + timeZone, + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hourCycle: 'h23', + }); + const parts = formatter.formatToParts(date); + const pick = (type: string): string => parts.find((part) => part.type === type)?.value || ''; + return `${pick('year')}${pick('month')}${pick('day')}_${pick('hour')}${pick('minute')}${pick('second')}`; +} + +function buildBackupFileNameInTimeZone( + date: Date = new Date(), + checksumPrefix: string | null = null, + timeZone: string = 'UTC' +): string { + const parts = getDateParts(date, timeZone); const suffix = checksumPrefix ? `_${checksumPrefix}` : ''; - return `nodewarden_backup_${parts[0]}${parts[1]}${parts[2]}_${parts[3]}${parts[4]}${parts[5]}${suffix}.zip`; + return `nodewarden_backup_${parts}${suffix}.zip`; } export function extractBackupFileChecksumPrefix(fileName: string): string | null { @@ -398,7 +412,8 @@ export async function buildBackupArchive( }); const bytes = zipSync(createZipEntries(files)); const fileHashPrefix = (await sha256Hex(bytes)).slice(0, BACKUP_FILE_HASH_PREFIX_LENGTH); - const fileName = buildBackupFileName(date, fileHashPrefix); + const backupTimeZone = options.timeZone || 'UTC'; + const fileName = buildBackupFileNameInTimeZone(date, fileHashPrefix, backupTimeZone); await options.progress?.({ step: 'archive_ready', fileName, diff --git a/webapp/src/lib/api/backup.ts b/webapp/src/lib/api/backup.ts index 5965c15..caa3226 100644 --- a/webapp/src/lib/api/backup.ts +++ b/webapp/src/lib/api/backup.ts @@ -148,32 +148,21 @@ interface BackupExportManifest { const BACKUP_FILE_HASH_PREFIX_LENGTH = 5; -function parseBackupTimestampFromFileName(fileName: string): Date | null { +function extractBackupTimestampFromFileName(fileName: string): string | 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; + return `${match[1]}_${match[2]}`; } -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`; +function buildBackupFileName(timestamp: string, checksumPrefix: string): string { + return `nodewarden_backup_${timestamp}_${checksumPrefix}.zip`; } async function applyBackupFileIntegrityName(fileName: string, bytes: Uint8Array): Promise { const integrity = await verifyBackupFileIntegrity(bytes, fileName); - const effectiveDate = parseBackupTimestampFromFileName(fileName) || new Date(); - return buildBackupFileName(effectiveDate, integrity.actualPrefix); + const timestamp = extractBackupTimestampFromFileName(fileName); + if (!timestamp) return fileName; + return buildBackupFileName(timestamp, integrity.actualPrefix); } export async function exportAdminBackup(