feat: add timezone support for backup file naming and extraction

This commit is contained in:
shuaiplus
2026-04-07 20:24:28 +08:00
parent 76623d7201
commit c9e7417825
3 changed files with 34 additions and 29 deletions
+1
View File
@@ -192,6 +192,7 @@ async function executeConfiguredBackup(
}); });
const archive = await buildBackupArchive(env, now, { const archive = await buildBackupArchive(env, now, {
includeAttachments: destination.includeAttachments, includeAttachments: destination.includeAttachments,
timeZone: destination.schedule.timezone,
progress: progress progress: progress
? async (event) => { ? async (event) => {
if (event.step === 'archive_ready') { if (event.step === 'archive_ready') {
+26 -11
View File
@@ -71,6 +71,7 @@ export interface BackupFileIntegrityCheckResult {
export interface BuildBackupArchiveOptions { export interface BuildBackupArchiveOptions {
includeAttachments?: boolean; includeAttachments?: boolean;
progress?: BackupArchiveBuildProgressReporter; progress?: BackupArchiveBuildProgressReporter;
timeZone?: string;
} }
export interface BackupArchiveBuildProgressEvent { export interface BackupArchiveBuildProgressEvent {
@@ -93,17 +94,30 @@ async function sha256Hex(bytes: Uint8Array): Promise<string> {
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('');
} }
function buildBackupFileName(date: Date = new Date(), checksumPrefix: string | null = null): string { function getDateParts(date: Date, timeZone: string): string {
const parts = [ const formatter = new Intl.DateTimeFormat('en-CA', {
date.getUTCFullYear().toString().padStart(4, '0'), timeZone,
(date.getUTCMonth() + 1).toString().padStart(2, '0'), year: 'numeric',
date.getUTCDate().toString().padStart(2, '0'), month: '2-digit',
date.getUTCHours().toString().padStart(2, '0'), day: '2-digit',
date.getUTCMinutes().toString().padStart(2, '0'), hour: '2-digit',
date.getUTCSeconds().toString().padStart(2, '0'), 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}` : ''; 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 { export function extractBackupFileChecksumPrefix(fileName: string): string | null {
@@ -398,7 +412,8 @@ export async function buildBackupArchive(
}); });
const bytes = zipSync(createZipEntries(files)); const bytes = zipSync(createZipEntries(files));
const fileHashPrefix = (await sha256Hex(bytes)).slice(0, BACKUP_FILE_HASH_PREFIX_LENGTH); 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?.({ await options.progress?.({
step: 'archive_ready', step: 'archive_ready',
fileName, fileName,
+7 -18
View File
@@ -148,32 +148,21 @@ interface BackupExportManifest {
const BACKUP_FILE_HASH_PREFIX_LENGTH = 5; 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); const match = String(fileName || '').match(/nodewarden_backup_(\d{8})_(\d{6})(?:_[0-9a-f]{5})?\.zip$/i);
if (!match) return null; if (!match) return null;
const datePart = match[1]; return `${match[1]}_${match[2]}`;
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 { function buildBackupFileName(timestamp: string, checksumPrefix: string): string {
const parts = [ return `nodewarden_backup_${timestamp}_${checksumPrefix}.zip`;
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> { async function applyBackupFileIntegrityName(fileName: string, bytes: Uint8Array): Promise<string> {
const integrity = await verifyBackupFileIntegrity(bytes, fileName); const integrity = await verifyBackupFileIntegrity(bytes, fileName);
const effectiveDate = parseBackupTimestampFromFileName(fileName) || new Date(); const timestamp = extractBackupTimestampFromFileName(fileName);
return buildBackupFileName(effectiveDate, integrity.actualPrefix); if (!timestamp) return fileName;
return buildBackupFileName(timestamp, integrity.actualPrefix);
} }
export async function exportAdminBackup( export async function exportAdminBackup(