fix(backup): confirm legacy macOS destinations before manual runs
This commit is contained in:
1 parent
889fa4f5d0
commit
0ac2d33188
8 files changed
+123
-2
No files matched your search
@@ -357,9 +357,28 @@ async function requestCancelRun(runId: string) {
|
||||
if (await cancelRun(runId)) toast(t("databaseBackup.cancelRequested"), 2500);
|
||||
}
|
||||
|
||||
async function confirmLegacyDestination(schedule: DatabaseBackupSchedule): Promise<DatabaseBackupSchedule | null> {
|
||||
if (!(await api.databaseExportDestinationNeedsConfirmation(schedule.destinationDirectory))) return schedule;
|
||||
|
||||
const { open } = await import("@tauri-apps/plugin-dialog");
|
||||
const selected = await open({
|
||||
directory: true,
|
||||
multiple: false,
|
||||
defaultPath: schedule.destinationDirectory,
|
||||
title: t("databaseBackup.selectDestination"),
|
||||
});
|
||||
if (typeof selected !== "string") return null;
|
||||
|
||||
await api.recordDatabaseExportDestination(selected);
|
||||
if (selected === schedule.destinationDirectory) return schedule;
|
||||
return saveSchedule({ ...schedule, destinationDirectory: selected });
|
||||
}
|
||||
|
||||
async function runNow(schedule: DatabaseBackupSchedule) {
|
||||
try {
|
||||
const run = await runSchedule(schedule.id, "manual");
|
||||
const confirmedSchedule = await confirmLegacyDestination(schedule);
|
||||
if (!confirmedSchedule) return;
|
||||
const run = await runSchedule(confirmedSchedule.id, "manual");
|
||||
if (!run) return;
|
||||
if (run.status === "success") toast(t("databaseBackup.runSuccess", { count: run.files.length }), 3000);
|
||||
else if (run.status === "cancelled") toast(t("databaseBackup.runCancelled"), 3000);
|
||||
|
||||
+57
-1
@@ -15,7 +15,9 @@ const mocks = vi.hoisted(() => ({
|
||||
activeRuns: [] as DatabaseBackupRun[],
|
||||
ensureConnected: vi.fn(async () => {}),
|
||||
listDatabases: vi.fn(async (_connectionId: string) => [{ name: "app" }]),
|
||||
databaseExportDestinationNeedsConfirmation: vi.fn(async (_directory: string) => false),
|
||||
recordDatabaseExportDestination: vi.fn(async (_directory: string) => {}),
|
||||
openDirectory: vi.fn(async (): Promise<string | null> => "/backups"),
|
||||
toast: vi.fn(),
|
||||
saveSchedule: vi.fn(),
|
||||
setScheduleEnabled: vi.fn(),
|
||||
@@ -58,13 +60,14 @@ vi.mock("@/composables/useToast", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("@tauri-apps/plugin-dialog", () => ({
|
||||
open: vi.fn(async () => "/backups"),
|
||||
open: mocks.openDirectory,
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/backend/api", () => ({
|
||||
listDatabases: mocks.listDatabases,
|
||||
deleteDatabaseBackupFiles: vi.fn(),
|
||||
revealPathInFileManager: vi.fn(),
|
||||
databaseExportDestinationNeedsConfirmation: mocks.databaseExportDestinationNeedsConfirmation,
|
||||
recordDatabaseExportDestination: mocks.recordDatabaseExportDestination,
|
||||
}));
|
||||
|
||||
@@ -230,6 +233,13 @@ function saveScheduleButton(): HTMLButtonElement {
|
||||
return button;
|
||||
}
|
||||
|
||||
function scheduleRunNowButton(): HTMLButtonElement {
|
||||
const title = String(i18n.global.t("databaseBackup.runNow"));
|
||||
const button = Array.from(document.body.querySelectorAll<HTMLButtonElement>("button")).find((item) => item.title === title && !item.textContent?.trim());
|
||||
if (!button) throw new Error("Schedule run-now button not found");
|
||||
return button;
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
for (const app of mountedApps.splice(0)) app.unmount();
|
||||
document.body.innerHTML = "";
|
||||
@@ -242,11 +252,17 @@ afterEach(() => {
|
||||
mocks.ensureConnected.mockClear();
|
||||
mocks.listDatabases.mockReset();
|
||||
mocks.listDatabases.mockResolvedValue([{ name: "app" }]);
|
||||
mocks.databaseExportDestinationNeedsConfirmation.mockReset();
|
||||
mocks.databaseExportDestinationNeedsConfirmation.mockResolvedValue(false);
|
||||
mocks.recordDatabaseExportDestination.mockReset();
|
||||
mocks.recordDatabaseExportDestination.mockResolvedValue(undefined);
|
||||
mocks.openDirectory.mockReset();
|
||||
mocks.openDirectory.mockResolvedValue("/backups");
|
||||
mocks.toast.mockClear();
|
||||
mocks.saveSchedule.mockClear();
|
||||
mocks.cancelRun.mockClear();
|
||||
mocks.runSchedule.mockReset();
|
||||
mocks.runSchedule.mockResolvedValue(null);
|
||||
mocks.runOneShot.mockReset();
|
||||
mocks.runOneShot.mockResolvedValue(null);
|
||||
});
|
||||
@@ -500,6 +516,46 @@ describe("ScheduledDatabaseBackupSettings schedule dialog", () => {
|
||||
expect(document.body.textContent).toContain(String(i18n.global.t("databaseBackup.noSupportedConnections")));
|
||||
});
|
||||
|
||||
it("reconfirms a legacy destination before manually running its schedule", async () => {
|
||||
mocks.schedules.push(schedule());
|
||||
mocks.databaseExportDestinationNeedsConfirmation.mockResolvedValueOnce(true);
|
||||
mocks.runSchedule.mockResolvedValueOnce({
|
||||
id: "run-1",
|
||||
scheduleId: "schedule-1",
|
||||
scheduleName: "Nightly backup",
|
||||
connectionId: "mysql-1",
|
||||
connectionName: "Local MySQL",
|
||||
trigger: "manual",
|
||||
source: "scheduled",
|
||||
status: "success",
|
||||
startedAt: "2026-08-18T00:00:00.000Z",
|
||||
completedAt: "2026-08-18T00:01:00.000Z",
|
||||
files: [],
|
||||
});
|
||||
await mountSettings();
|
||||
|
||||
scheduleRunNowButton().click();
|
||||
await flush();
|
||||
|
||||
expect(mocks.databaseExportDestinationNeedsConfirmation).toHaveBeenCalledWith("/backups");
|
||||
expect(mocks.openDirectory).toHaveBeenCalledWith(expect.objectContaining({ directory: true, defaultPath: "/backups" }));
|
||||
expect(mocks.recordDatabaseExportDestination).toHaveBeenCalledWith("/backups");
|
||||
expect(mocks.runSchedule).toHaveBeenCalledWith("schedule-1", "manual");
|
||||
});
|
||||
|
||||
it("does not run a legacy schedule when destination confirmation is cancelled", async () => {
|
||||
mocks.schedules.push(schedule());
|
||||
mocks.databaseExportDestinationNeedsConfirmation.mockResolvedValueOnce(true);
|
||||
mocks.openDirectory.mockResolvedValueOnce(null);
|
||||
await mountSettings();
|
||||
|
||||
scheduleRunNowButton().click();
|
||||
await flush();
|
||||
|
||||
expect(mocks.recordDatabaseExportDestination).not.toHaveBeenCalled();
|
||||
expect(mocks.runSchedule).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("removes unavailable databases from an edited schedule before saving", async () => {
|
||||
mocks.connections.push({ id: "mysql-1", name: "Local MySQL", db_type: "mysql" });
|
||||
mocks.schedules.push(schedule({ databases: ["app", "deleted"] }));
|
||||
|
||||
@@ -457,6 +457,7 @@ export const beginDatabaseBackupSnapshot = forward("beginDatabaseBackupSnapshot"
|
||||
export const exportDatabaseSql = forward("exportDatabaseSql");
|
||||
export const cancelDatabaseExport = forward("cancelDatabaseExport");
|
||||
export const clearDatabaseExportCancellation = forward("clearDatabaseExportCancellation");
|
||||
export const databaseExportDestinationNeedsConfirmation = forward("databaseExportDestinationNeedsConfirmation");
|
||||
export const recordDatabaseExportDestination = forward("recordDatabaseExportDestination");
|
||||
export const exportQueryResultCsv = forward("exportQueryResultCsv");
|
||||
export const exportTableDataCsv = forward("exportTableDataCsv");
|
||||
|
||||
@@ -2480,6 +2480,10 @@ export async function clearDatabaseExportCancellation(_exportId: string): Promis
|
||||
// The web exporter owns and clears its cancellation marker on completion.
|
||||
}
|
||||
|
||||
export async function databaseExportDestinationNeedsConfirmation(_directory: string): Promise<boolean> {
|
||||
throw new Error("Scheduled database backups are only available in the desktop app.");
|
||||
}
|
||||
|
||||
export async function recordDatabaseExportDestination(_directory: string): Promise<void> {
|
||||
throw new Error("Scheduled database backups are only available in the desktop app.");
|
||||
}
|
||||
|
||||
@@ -5055,6 +5055,10 @@ export async function clearDatabaseExportCancellation(exportId: string): Promise
|
||||
await invoke("clear_database_export_cancellation", { exportId });
|
||||
}
|
||||
|
||||
export async function databaseExportDestinationNeedsConfirmation(directory: string): Promise<boolean> {
|
||||
return invoke("database_export_destination_needs_confirmation", { directory });
|
||||
}
|
||||
|
||||
export async function recordDatabaseExportDestination(directory: string): Promise<void> {
|
||||
await invoke("record_database_export_destination", { directory });
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user