fix(backup): verify scheduled export destinations before writing

This commit is contained in:
Melody Ruch authored and GitHub committed 2026-08-17 12:41:26 +08:00
1 parent a8f966dcfd
commit 4ce27f8ae5
12 files changed
+652 -7

No files matched your search

@@ -203,6 +203,7 @@ async function submitSchedule() {
if (!canSave.value) return;
saving.value = true;
try {
await api.recordDatabaseExportDestination(draft.value.destinationDirectory);
saveSchedule({
...draft.value,
databases: allDatabases.value ? [] : [...selectedDatabases.value],
@@ -11,6 +11,7 @@ const mocks = vi.hoisted(() => ({
schedules: [] as DatabaseBackupSchedule[],
ensureConnected: vi.fn(async () => {}),
listDatabases: vi.fn(async () => [{ name: "app" }]),
recordDatabaseExportDestination: vi.fn(async (_directory: string) => {}),
toast: vi.fn(),
saveSchedule: vi.fn(),
setScheduleEnabled: vi.fn(),
@@ -53,6 +54,7 @@ vi.mock("@/lib/backend/api", () => ({
listDatabases: mocks.listDatabases,
deleteDatabaseBackupFiles: vi.fn(),
revealPathInFileManager: vi.fn(),
recordDatabaseExportDestination: mocks.recordDatabaseExportDestination,
}));
const mountedApps: App[] = [];
@@ -127,6 +129,8 @@ afterEach(() => {
mocks.schedules.splice(0);
mocks.ensureConnected.mockClear();
mocks.listDatabases.mockClear();
mocks.recordDatabaseExportDestination.mockReset();
mocks.recordDatabaseExportDestination.mockResolvedValue(undefined);
mocks.toast.mockClear();
mocks.saveSchedule.mockClear();
});
@@ -197,4 +201,20 @@ describe("ScheduledDatabaseBackupSettings schedule dialog", () => {
expect(mocks.saveSchedule).toHaveBeenCalledWith(expect.objectContaining({ databases: ["app", "archive"] }));
});
it("does not save when the destination identity cannot be recorded", async () => {
mocks.connections.push({ id: "mysql-1", name: "Local MySQL", db_type: "mysql" });
mocks.schedules.push(schedule());
mocks.recordDatabaseExportDestination.mockRejectedValueOnce(new Error("backup destination unavailable"));
await mountSettings();
buttonWithTitle(String(i18n.global.t("databaseBackup.edit"))).click();
await flush();
saveScheduleButton().click();
await flush();
expect(mocks.recordDatabaseExportDestination).toHaveBeenCalledWith("/backups");
expect(mocks.saveSchedule).not.toHaveBeenCalled();
expect(mocks.toast).toHaveBeenCalledWith("backup destination unavailable", 5000);
});
});
+1
View File
@@ -416,6 +416,7 @@ export const releaseTableImportSource = forward("releaseTableImportSource");
export const beginDatabaseBackupSnapshot = forward("beginDatabaseBackupSnapshot");
export const exportDatabaseSql = forward("exportDatabaseSql");
export const cancelDatabaseExport = forward("cancelDatabaseExport");
export const recordDatabaseExportDestination = forward("recordDatabaseExportDestination");
export const exportQueryResultCsv = forward("exportQueryResultCsv");
export const exportTableDataCsv = forward("exportTableDataCsv");
export const exportQueryResultXlsx = forward("exportQueryResultXlsx");
+4
View File
@@ -2224,6 +2224,10 @@ export async function cancelDatabaseExport(exportId: string): Promise<void> {
await post("/api/export/database/cancel", { exportId });
}
export async function recordDatabaseExportDestination(_directory: string): Promise<void> {
throw new Error("Scheduled database backups are only available in the desktop app.");
}
// --- Table Export ---
export async function startTableExport(request: TableExportRequest, onProgress: (progress: TableExportProgress) => void): Promise<TableExportProgress> {
+4
View File
@@ -4405,6 +4405,10 @@ export async function cancelDatabaseExport(exportId: string): Promise<void> {
await invoke("cancel_database_export", { exportId });
}
export async function recordDatabaseExportDestination(directory: string): Promise<void> {
await invoke("record_database_export_destination", { directory });
}
export async function exportQueryResultCsv(filePath: string, columns: string[], rows: readonly (readonly XlsxCellValue[])[]): Promise<void> {
return invoke("export_query_result_csv", {
request: {