fix(backup): enhance backup configuration and history management

This commit is contained in:
二丫讲梵 authored and GitHub committed 2026-09-09 00:15:12 +08:00
1 parent 789740c9be
commit 69ef418d5f
23 files changed
+623 -92

No files matched your search

@@ -70,6 +70,44 @@ const config: DatabaseBackupExecutionConfig = {
};
describe("useScheduledDatabaseBackups one-shot execution", () => {
it("deletes multiple completed runs and all of their backup files", async () => {
const backup = useScheduledDatabaseBackups();
const firstRun = {
id: "delete-many-first",
scheduleName: "First backup",
connectionId: "mysql-1",
connectionName: "Local MySQL",
trigger: "scheduled",
source: "scheduled",
status: "success",
startedAt: "2026-09-08T01:00:00.000Z",
files: [{ displayName: "first.sql", filePath: "/backups/first.sql" }],
} satisfies DatabaseBackupRun;
const secondRun = {
id: "delete-many-second",
scheduleName: "Second backup",
connectionId: "mysql-1",
connectionName: "Local MySQL",
trigger: "scheduled",
source: "scheduled",
status: "failed",
startedAt: "2026-09-08T02:00:00.000Z",
files: [{ displayName: "second.sql", filePath: "/backups/second.sql" }],
} satisfies DatabaseBackupRun;
backup.runs.value.push(firstRun, secondRun);
mocks.deleteFiles.mockClear();
try {
await backup.deleteRuns([firstRun.id, secondRun.id]);
expect(mocks.deleteFiles).toHaveBeenCalledWith(["/backups/first.sql", "/backups/second.sql"]);
expect(backup.runs.value).not.toContainEqual(expect.objectContaining({ id: firstRun.id }));
expect(backup.runs.value).not.toContainEqual(expect.objectContaining({ id: secondRun.id }));
} finally {
backup.runs.value = backup.runs.value.filter((run) => run.id !== firstRun.id && run.id !== secondRun.id);
}
});
it("executes one-shot backups through the shared exporter without a schedule", async () => {
mocks.runDatabaseExport.mockImplementationOnce(async (_request: unknown, onProgress: (progress: unknown) => void) => {
onProgress({ status: "Done", objectIndex: 1, totalObjects: 1, currentObject: "app" });
@@ -11,6 +11,7 @@ import {
DatabaseBackupConnectionQueue,
databaseBackupAggregateExportStatus,
databaseBackupFilePath,
databaseBackupRunDirectory,
databaseBackupProgressPercent,
databaseBackupRunsToPrune,
databaseBackupScheduleIsDue,
@@ -127,14 +128,22 @@ export function useScheduledDatabaseBackups(options: { scheduler?: boolean } = {
return true;
}
async function deleteRun(runId: string): Promise<void> {
const run = runs.value.find((item) => item.id === runId);
if (!run || activeRunIds.has(runId)) return;
if (run.files.length > 0) await api.deleteDatabaseBackupFiles(run.files.map((file) => file.filePath));
runs.value = runs.value.filter((item) => item.id !== runId);
async function deleteRuns(runIds: readonly string[]): Promise<void> {
const ids = new Set(runIds);
const selectedRuns = runs.value.filter((run) => ids.has(run.id) && !activeRunIds.has(run.id));
if (selectedRuns.length === 0) return;
const paths = selectedRuns.flatMap((run) => run.files.map((file) => file.filePath));
if (paths.length > 0) await api.deleteDatabaseBackupFiles(paths);
const deletedIds = new Set(selectedRuns.map((run) => run.id));
runs.value = runs.value.filter((run) => !deletedIds.has(run.id));
persistRuns();
}
async function deleteRun(runId: string): Promise<void> {
await deleteRuns([runId]);
}
function renameRun(runId: string, displayName: string): boolean {
const run = runs.value.find((item) => item.id === runId);
if (!run || activeRunIds.has(runId)) return false;
@@ -176,12 +185,13 @@ export function useScheduledDatabaseBackups(options: { scheduler?: boolean } = {
return true;
}
async function runBackup(config: DatabaseBackupExecutionConfig, request: { source: DatabaseBackupRunSource; trigger: DatabaseBackupRunTrigger; scheduleId?: string; displayName: string }): Promise<FinishedDatabaseBackupRun | null> {
async function runBackup(config: DatabaseBackupExecutionConfig, request: { source: DatabaseBackupRunSource; trigger: DatabaseBackupRunTrigger; scheduleId?: string; displayName: string; runDirectoryPattern?: string }): Promise<FinishedDatabaseBackupRun | null> {
const backupName = request.displayName;
const connection = connectionStore.getConfig(config.connectionId);
const startedAt = new Date();
const runId = generateDatabaseExportId();
const outputDirectory = request.runDirectoryPattern ? databaseBackupRunDirectory(config.destinationDirectory, request.runDirectoryPattern, backupName, startedAt, runId) : config.destinationDirectory;
const run: DatabaseBackupRun = {
id: runId,
scheduleId: request.scheduleId,
@@ -198,7 +208,7 @@ export function useScheduledDatabaseBackups(options: { scheduler?: boolean } = {
replaceRun(run);
activeRunIds.add(runId);
cancellationRequested.delete(runId);
addDatabaseExportTask(runId, backupName, config.destinationDirectory, request.source === "scheduled" ? "scheduled" : "manual");
addDatabaseExportTask(runId, backupName, outputDirectory, request.source === "scheduled" ? "scheduled" : "manual");
registerTaskCancelHandler(runId, async () => {
await cancelRun(runId);
});
@@ -289,7 +299,7 @@ export function useScheduledDatabaseBackups(options: { scheduler?: boolean } = {
exportIndex += 1;
const childExportId = `${runId}-${exportIndex}`;
activeExportIds.set(runId, childExportId);
const filePath = databaseBackupFilePath(config.destinationDirectory, backupName, item.fileStem, startedAt, runId, config.outputCompression);
const filePath = databaseBackupFilePath(outputDirectory, backupName, item.fileStem, startedAt, runId, config.outputCompression);
generatedPaths.push(filePath);
const terminal = await runDatabaseExportUntilTerminal(
{
@@ -447,7 +457,13 @@ export function useScheduledDatabaseBackups(options: { scheduler?: boolean } = {
if (!schedule || activeScheduleIds.has(scheduleId)) return null;
activeScheduleIds.add(scheduleId);
try {
const run = await runBackup(toDatabaseBackupExecutionConfig(schedule), { source: "scheduled", trigger, scheduleId, displayName: schedule.name });
const run = await runBackup(toDatabaseBackupExecutionConfig(schedule), {
source: "scheduled",
trigger,
scheduleId,
displayName: schedule.name,
runDirectoryPattern: schedule.runDirectoryPattern,
});
if (run) await finalizeScheduledRun(scheduleId, trigger, run);
return run;
} finally {
@@ -521,6 +537,7 @@ export function useScheduledDatabaseBackups(options: { scheduler?: boolean } = {
setScheduleEnabled,
deleteSchedule,
deleteRun,
deleteRuns,
renameRun,
runSchedule,
runOneShot,