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, {
includeAttachments: destination.includeAttachments,
timeZone: destination.schedule.timezone,
progress: progress
? async (event) => {
if (event.step === 'archive_ready') {
+26 -11
View File
@@ -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<string> {
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,
+7 -18
View File
@@ -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<string> {
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(