feat(backup): add gzip compression and history filtering

This commit is contained in:
二丫讲梵 authored and GitHub committed 2026-08-31 09:02:20 +08:00
1 parent c305325e37
commit b3fcb3a6f1
36 files changed
+1144 -137

No files matched your search

@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { databaseBackupRunsToPrune, databaseBackupTableMatchesPattern, normalizeDatabaseBackupRun, resolveScheduledDatabaseBackupTableScope, type DatabaseBackupRun } from "../../backup/scheduledDatabaseBackup";
import { databaseBackupFilePath, databaseBackupRunsToPrune, databaseBackupTableMatchesPattern, normalizeDatabaseBackupRun, resolveScheduledDatabaseBackupTableScope, type DatabaseBackupRun } from "../../backup/scheduledDatabaseBackup";
const startedAt = "2026-08-12T00:00:00.000Z";
@@ -19,6 +19,10 @@ function run(overrides: Record<string, unknown> = {}) {
}
describe("database backup run persistence", () => {
it("uses a gzip extension when the backup output is compressed", () => {
expect(databaseBackupFilePath("/backups", "Nightly", "app", startedAt, "run-12345678", "gzip")).toMatch(/\.sql\.gz$/);
});
it("accepts a one-shot run without a schedule id", () => {
const normalized = normalizeDatabaseBackupRun(run());
+1
View File
@@ -448,6 +448,7 @@ export const releaseTableImportSource = forward("releaseTableImportSource");
export const beginDatabaseBackupSnapshot = forward("beginDatabaseBackupSnapshot");
export const exportDatabaseSql = forward("exportDatabaseSql");
export const cancelDatabaseExport = forward("cancelDatabaseExport");
export const clearDatabaseExportCancellation = forward("clearDatabaseExportCancellation");
export const recordDatabaseExportDestination = forward("recordDatabaseExportDestination");
export const exportQueryResultCsv = forward("exportQueryResultCsv");
export const exportTableDataCsv = forward("exportTableDataCsv");
+5 -1
View File
@@ -2376,7 +2376,7 @@ export async function releaseTableImportSource(sourceRef: string): Promise<boole
// Database Export
// ---------------------------------------------------------------------------
export async function beginDatabaseBackupSnapshot(_connectionId: string, _database: string): Promise<DatabaseBackupSnapshot> {
export async function beginDatabaseBackupSnapshot(_connectionId: string, _database: string, _exportId?: string): Promise<DatabaseBackupSnapshot> {
throw new Error("Consistent database backup snapshots are only available in the desktop app.");
}
@@ -2422,6 +2422,10 @@ export async function cancelDatabaseExport(exportId: string): Promise<void> {
await post("/api/export/database/cancel", { exportId });
}
export async function clearDatabaseExportCancellation(_exportId: string): Promise<void> {
// The web exporter owns and clears its cancellation marker on completion.
}
export async function recordDatabaseExportDestination(_directory: string): Promise<void> {
throw new Error("Scheduled database backups are only available in the desktop app.");
}
+7 -2
View File
@@ -4675,6 +4675,7 @@ export interface DatabaseExportRequest {
dropTableIfExists?: boolean;
omitAutoIncrement?: boolean;
failOnError?: boolean;
outputCompression?: "none" | "gzip";
snapshotSessionId?: string;
batchSize: number;
}
@@ -4866,8 +4867,8 @@ export async function cancelQueryResultExport(exportId: string, executionId?: st
});
}
export async function beginDatabaseBackupSnapshot(connectionId: string, database: string): Promise<DatabaseBackupSnapshot> {
return invoke("begin_database_backup_snapshot", { connectionId, database });
export async function beginDatabaseBackupSnapshot(connectionId: string, database: string, exportId?: string): Promise<DatabaseBackupSnapshot> {
return invoke("begin_database_backup_snapshot", { connectionId, database, exportId: exportId || null });
}
export async function exportDatabaseSql(request: DatabaseExportRequest, onProgress: (progress: ExportProgress) => void): Promise<void> {
@@ -4891,6 +4892,10 @@ export async function cancelDatabaseExport(exportId: string): Promise<void> {
await invoke("cancel_database_export", { exportId });
}
export async function clearDatabaseExportCancellation(exportId: string): Promise<void> {
await invoke("clear_database_export_cancellation", { exportId });
}
export async function recordDatabaseExportDestination(directory: string): Promise<void> {
await invoke("record_database_export_destination", { directory });
}
@@ -13,6 +13,7 @@ export type DatabaseBackupRunStatus = "running" | "success" | "failed" | "cancel
export type DatabaseBackupRunTrigger = "manual" | "scheduled";
export type DatabaseBackupRunSource = "scheduled" | "one-shot";
export type DatabaseBackupTableFilterMode = "all" | "include" | "exclude";
export type DatabaseBackupOutputCompression = "none" | "gzip";
const CONSISTENT_BACKUP_DATABASE_TYPES = new Set(["mysql", "postgres"]);
@@ -117,6 +118,7 @@ export interface DatabaseBackupExecutionConfig {
includeData: boolean;
includeObjects: boolean;
dropTableIfExists: boolean;
outputCompression: DatabaseBackupOutputCompression;
}
export interface DatabaseBackupSchedule extends DatabaseBackupExecutionConfig {
@@ -146,6 +148,7 @@ export function toDatabaseBackupExecutionConfig(schedule: DatabaseBackupSchedule
includeData: schedule.includeData,
includeObjects: schedule.includeObjects,
dropTableIfExists: schedule.dropTableIfExists,
outputCompression: schedule.outputCompression,
};
}
@@ -244,6 +247,7 @@ export function normalizeDatabaseBackupSchedule(value: unknown, now = new Date()
includeData: booleanValue(input.includeData, true),
includeObjects: booleanValue(input.includeObjects, true),
dropTableIfExists: booleanValue(input.dropTableIfExists, false),
outputCompression: input.outputCompression === "gzip" ? "gzip" : "none",
retentionCount: normalizeDatabaseBackupRetention(input.retentionCount),
createdAt: validIsoDate(input.createdAt, nowIso),
updatedAt: validIsoDate(input.updatedAt, nowIso),
@@ -394,7 +398,8 @@ export function joinDatabaseBackupPath(directory: string, fileName: string): str
return `${directory.replace(/[\\/]+$/, "")}${separator}${fileName}`;
}
export function databaseBackupFilePath(directory: string, scheduleName: string, fileStem: string, startedAt: Date | string, runId: string): string {
const fileName = `dbx-backup__${sanitizeDatabaseBackupFileSegment(scheduleName)}__${databaseBackupTimestamp(startedAt)}__${sanitizeDatabaseBackupFileSegment(fileStem)}__${sanitizeDatabaseBackupFileSegment(runId).slice(0, 8)}.sql`;
export function databaseBackupFilePath(directory: string, scheduleName: string, fileStem: string, startedAt: Date | string, runId: string, outputCompression: DatabaseBackupOutputCompression = "none"): string {
const suffix = outputCompression === "gzip" ? ".sql.gz" : ".sql";
const fileName = `dbx-backup__${sanitizeDatabaseBackupFileSegment(scheduleName)}__${databaseBackupTimestamp(startedAt)}__${sanitizeDatabaseBackupFileSegment(fileStem)}__${sanitizeDatabaseBackupFileSegment(runId).slice(0, 8)}${suffix}`;
return joinDatabaseBackupPath(directory, fileName);
}