fix(export): keep MySQL database export snapshots alive
This commit is contained in:
17 files changed
+290
-35
No files matched your search
@@ -4,6 +4,7 @@ import { isTerminalTransferProgress } from "@/lib/backend/transferProgress";
|
||||
|
||||
export type BackgroundTaskKind = "table-export" | "database-export" | "sql-file" | "data-transfer";
|
||||
export type BackgroundTaskStatus = "Running" | "Writing" | "Done" | "Error" | "Cancelled";
|
||||
export type DatabaseExportSource = "manual" | "scheduled";
|
||||
|
||||
export interface DataTransferFailure {
|
||||
table: string;
|
||||
@@ -21,6 +22,9 @@ export interface ExportTask {
|
||||
totalRows: number | null;
|
||||
status: BackgroundTaskStatus;
|
||||
errorMessage: string | null;
|
||||
databaseExportSource?: DatabaseExportSource;
|
||||
currentObject?: string;
|
||||
preparing?: boolean;
|
||||
objectIndex?: number;
|
||||
totalObjects?: number;
|
||||
overallPercent?: number;
|
||||
@@ -254,7 +258,7 @@ export function useExportTracker() {
|
||||
return task;
|
||||
}
|
||||
|
||||
function addDatabaseExportTask(exportId: string, label: string, filePath: string): ExportTask {
|
||||
function addDatabaseExportTask(exportId: string, label: string, filePath: string, databaseExportSource: DatabaseExportSource = "manual"): ExportTask {
|
||||
const task = reactive<ExportTask>({
|
||||
exportId,
|
||||
kind: "database-export",
|
||||
@@ -265,6 +269,9 @@ export function useExportTracker() {
|
||||
totalRows: null,
|
||||
status: "Running",
|
||||
errorMessage: null,
|
||||
databaseExportSource,
|
||||
currentObject: "",
|
||||
preparing: true,
|
||||
objectIndex: 0,
|
||||
totalObjects: 0,
|
||||
startedAt: Date.now(),
|
||||
@@ -392,10 +399,8 @@ export function useExportTracker() {
|
||||
function updateDatabaseExportTask(exportId: string, progress: api.ExportProgress & { overallPercent?: number }) {
|
||||
const task = taskMap.get(exportId);
|
||||
if (!task) return;
|
||||
// Keep the database label during metadata prefetch; only follow object names while writing.
|
||||
if (!progress.preparing && progress.currentObject) {
|
||||
task.tableName = progress.currentObject;
|
||||
}
|
||||
task.currentObject = progress.currentObject;
|
||||
task.preparing = !!progress.preparing;
|
||||
task.rowsExported = progress.rowsExported;
|
||||
task.totalRows = progress.totalRows;
|
||||
task.status = normalizeExportStatus(progress.status);
|
||||
|
||||
@@ -182,7 +182,7 @@ export function useScheduledDatabaseBackups(options: { scheduler?: boolean } = {
|
||||
activeScheduleIds.add(schedule.id);
|
||||
activeRunIds.add(runId);
|
||||
cancellationRequested.delete(runId);
|
||||
addDatabaseExportTask(runId, schedule.name, schedule.destinationDirectory);
|
||||
addDatabaseExportTask(runId, schedule.name, schedule.destinationDirectory, "scheduled");
|
||||
registerTaskCancelHandler(runId, () => cancelRun(runId));
|
||||
|
||||
let finalStatus: Exclude<DatabaseBackupRunStatus, "running"> = "success";
|
||||
@@ -219,27 +219,29 @@ export function useScheduledDatabaseBackups(options: { scheduler?: boolean } = {
|
||||
finalStatus = "cancelled";
|
||||
break;
|
||||
}
|
||||
const schemasByDatabase = connection.db_type === "postgres" ? { [database]: await api.listSchemas(schedule.connectionId, database) } : undefined;
|
||||
const databasePlan = buildAllDatabaseExportPlan({
|
||||
databases: [database],
|
||||
schemaAware: connection.db_type === "postgres",
|
||||
schemasByDatabase,
|
||||
});
|
||||
if (databasePlan.length === 0) throw new Error(`Database ${database} did not resolve to any schemas.`);
|
||||
const scopedDatabasePlan: Array<(typeof databasePlan)[number] & { selectedTables?: string[]; excludedTables?: string[] }> = [];
|
||||
for (const item of databasePlan) {
|
||||
if (schedule.tableFilterMode === "all") {
|
||||
scopedDatabasePlan.push(item);
|
||||
continue;
|
||||
}
|
||||
const availableTables = (await api.listTables(schedule.connectionId, item.database, item.schema)).map((table) => table.name);
|
||||
const scope = resolveScheduledDatabaseBackupTableScope(schedule.tableFilterMode, schedule.tablePatterns, availableTables, item.database, item.schema, tableNamesCaseSensitive);
|
||||
includedTableCount += scope.includedTables.length;
|
||||
if (scope.includedTables.length === 0) continue;
|
||||
scopedDatabasePlan.push({ ...item, selectedTables: scope.selectedTables, excludedTables: scope.excludedTables });
|
||||
}
|
||||
|
||||
const snapshot = await api.beginDatabaseBackupSnapshot(schedule.connectionId, database);
|
||||
let snapshotCompleted = false;
|
||||
try {
|
||||
const databasePlan = buildAllDatabaseExportPlan({
|
||||
databases: [database],
|
||||
schemaAware: connection.db_type === "postgres",
|
||||
schemasByDatabase: { [database]: snapshot.schemas },
|
||||
});
|
||||
if (databasePlan.length === 0) throw new Error(`Database ${database} did not resolve to any schemas.`);
|
||||
const scopedDatabasePlan: Array<(typeof databasePlan)[number] & { selectedTables?: string[]; excludedTables?: string[] }> = [];
|
||||
for (const item of databasePlan) {
|
||||
if (schedule.tableFilterMode === "all") {
|
||||
scopedDatabasePlan.push(item);
|
||||
continue;
|
||||
}
|
||||
const availableTables = (await api.listTables(schedule.connectionId, item.database, item.schema)).map((table) => table.name);
|
||||
const scope = resolveScheduledDatabaseBackupTableScope(schedule.tableFilterMode, schedule.tablePatterns, availableTables, item.database, item.schema, tableNamesCaseSensitive);
|
||||
includedTableCount += scope.includedTables.length;
|
||||
if (scope.includedTables.length === 0) continue;
|
||||
scopedDatabasePlan.push({ ...item, selectedTables: scope.selectedTables, excludedTables: scope.excludedTables });
|
||||
}
|
||||
for (const [planIndex, item] of scopedDatabasePlan.entries()) {
|
||||
if (cancellationRequested.has(runId)) {
|
||||
finalStatus = "cancelled";
|
||||
|
||||
Reference in new issue
Block a user