• Registers IPC event listeners for backup-related actions in the Electron main process. Handles all backup operations: config management, file operations, scheduler control, and history tracking.

    Parameters

    • mainWindow: BrowserWindow

      The Electron main window.

    Returns void

    export function setupBackupIPC(mainWindow: BrowserWindow): void {
    // Note: We no longer use ipcMain.removeHandler since we're using secureHandle
    // which registers handlers directly without prior cleanup needed

    console.log("[BackupIPC] Setting up backup IPC handlers...");

    secureHandle(
    BACKUP_CHANNELS.GET_SCHEDULE_CONFIG,
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    (_event: Electron.IpcMainInvokeEvent) => {
    try {
    const config = getStoredBackupScheduleConfig();
    console.debug("[BackupIPC] Returning backup schedule config");
    return config;
    } catch (error) {
    console.error(
    "[BackupIPC] Error getting backup schedule config:",
    error,
    );
    return DEFAULT_BACKUP_SCHEDULE_CONFIG;
    }
    },
    mainWindow,
    );

    secureHandle(
    BACKUP_CHANNELS.SET_SCHEDULE_CONFIG,
    (_event: Electron.IpcMainInvokeEvent, config: unknown) => {
    try {
    const validation = validateScheduleConfig(config);
    if (!validation.valid) {
    console.warn(
    "[BackupIPC] Invalid schedule config rejected:",
    validation.error,
    );
    return { success: false, error: validation.error };
    }

    let validatedConfig = validation.validated!;
    // Ensure backup location is initialized
    validatedConfig = ensureBackupLocationInitialized(validatedConfig);

    console.debug("[BackupIPC] Setting backup schedule config");

    saveStoredBackupScheduleConfig(validatedConfig);
    updateScheduler(mainWindow, validatedConfig);
    emitStatusChanged(mainWindow);

    return { success: true };
    } catch (error) {
    const errorMessage =
    error instanceof Error ? error.message : "Unknown error";
    console.error(
    "[BackupIPC] Error setting backup schedule config:",
    error,
    );
    return { success: false, error: errorMessage };
    }
    },
    mainWindow,
    );

    secureHandle(
    BACKUP_CHANNELS.GET_BACKUP_LOCATION,
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    (_event: Electron.IpcMainInvokeEvent) => {
    try {
    const config = getStoredBackupScheduleConfig();
    const location = ensureBackupLocationInitialized(config).backupLocation;

    console.debug("[BackupIPC] Returning backup location");

    return { success: true, data: location };
    } catch (error) {
    const errorMessage =
    error instanceof Error ? error.message : "Unknown error";
    console.error("[BackupIPC] Error getting backup location:", error);
    return { success: false, error: errorMessage };
    }
    },
    mainWindow,
    );

    secureHandle(
    BACKUP_CHANNELS.SET_BACKUP_LOCATION,
    async (_event: Electron.IpcMainInvokeEvent, location: unknown) => {
    try {
    // Input validation: ensure location is a string
    if (typeof location !== "string") {
    console.warn(
    "[BackupIPC] SET_BACKUP_LOCATION: Invalid location type:",
    typeof location,
    );
    return {
    success: false,
    error: "Backup location must be a string",
    code: "INVALID_TYPE",
    };
    }

    const [isValid, errorMsg] = validateBackupLocationPath(location);
    if (!isValid) {
    console.warn(
    "[BackupIPC] Invalid backup location rejected:",
    errorMsg,
    );
    // Extract error code from validation
    let code = "INVALID_PATH";
    if (errorMsg?.includes("does not exist")) code = "ENOENT";
    if (errorMsg?.includes("permission")) code = "EACCES";
    return { success: false, error: errorMsg, code };
    }

    // Check accessibility and create directory if needed
    const normalizedPath = path.normalize(location.trim());
    const [isAccessible, accessError] =
    await checkBackupLocationAccessibility(normalizedPath);
    if (!isAccessible) {
    console.warn(
    "[BackupIPC] Backup location is not accessible:",
    accessError,
    );
    // Extract error code from accessibility check
    let code = "INVALID_PATH";
    if (accessError?.includes("does not exist")) code = "ENOENT";
    if (accessError?.includes("Permission denied")) code = "EACCES";
    return { success: false, error: accessError, code };
    }

    const config = getStoredBackupScheduleConfig();
    const updatedConfig: BackupScheduleConfig = {
    ...config,
    backupLocation: normalizedPath,
    };

    console.debug("[BackupIPC] Updating backup location");

    saveStoredBackupScheduleConfig(updatedConfig);

    return { success: true };
    } catch (error) {
    const errorMessage =
    error instanceof Error ? error.message : "Unknown error";
    console.error("[BackupIPC] Error setting backup location:", error);
    return { success: false, error: errorMessage, code: "UNKNOWN_ERROR" };
    }
    },
    mainWindow,
    );

    secureHandle(
    BACKUP_CHANNELS.OPEN_BACKUP_LOCATION,
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    async (_event: Electron.IpcMainInvokeEvent) => {
    try {
    const config = getStoredBackupScheduleConfig();
    const location = ensureBackupLocationInitialized(config).backupLocation;

    console.debug("[BackupIPC] Opening backup location");

    await fs.mkdir(location, { recursive: true });
    await shell.openPath(location);

    return { success: true };
    } catch (error) {
    const errorMessage =
    error instanceof Error ? error.message : "Unknown error";
    console.error("[BackupIPC] Error opening backup location:", error);
    return { success: false, error: errorMessage };
    }
    },
    mainWindow,
    );

    secureHandle(
    BACKUP_CHANNELS.LIST_LOCAL_BACKUPS,
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    async (_event: Electron.IpcMainInvokeEvent) => {
    try {
    const config = getStoredBackupScheduleConfig();
    const location = ensureBackupLocationInitialized(config).backupLocation;

    // Defensive check: ensure location is initialized and not empty
    if (
    !location ||
    typeof location !== "string" ||
    location.trim().length === 0
    ) {
    const errorMsg =
    "Backup location has not been configured. Please select a backup location in settings.";
    console.warn(
    "[BackupIPC] LIST_LOCAL_BACKUPS: Backup location not initialized",
    );
    return { success: false, error: errorMsg };
    }

    // Input validation: ensure location is valid after initialization
    const [isValid, errorMsg] = validateBackupLocationPath(location);
    if (!isValid) {
    const sanitizedLocation = location.substring(0, 50); // Limit for security
    console.warn(
    "[BackupIPC] LIST_LOCAL_BACKUPS: Invalid backup location:",
    errorMsg,
    );
    return {
    success: false,
    error: `Invalid backup location: ${sanitizedLocation}. Reason: ${errorMsg}`,
    };
    }

    // Check accessibility and surface permission issues early
    const [isAccessible, accessError] =
    await checkBackupLocationAccessibility(location);
    if (!isAccessible) {
    console.warn(
    "[BackupIPC] LIST_LOCAL_BACKUPS: Backup location is not accessible:",
    accessError,
    );
    return { success: false, error: accessError };
    }

    console.debug("[BackupIPC] Listing backups");

    const backups = await listBackupsInLocation(location);

    return { success: true, data: backups };
    } catch (error) {
    const errorMessage =
    error instanceof Error ? error.message : "Unknown error";
    console.error("[BackupIPC] Error listing local backups:", error);
    return { success: false, error: errorMessage };
    }
    },
    mainWindow,
    );

    secureHandle(
    BACKUP_CHANNELS.DELETE_BACKUP,
    async (_event: Electron.IpcMainInvokeEvent, filename: unknown) => {
    try {
    // Ensure filename is a string before validation
    if (typeof filename !== "string") {
    console.warn(
    "[BackupIPC] DELETE_BACKUP: Invalid filename type:",
    typeof filename,
    );
    return { success: false, error: "Filename must be a string" };
    }

    // Input validation: validate filename
    const [isFilenameValid, filenameError] =
    validateBackupFilename(filename);
    if (!isFilenameValid) {
    console.warn(
    "[BackupIPC] DELETE_BACKUP: Invalid filename:",
    filenameError,
    );
    return { success: false, error: filenameError };
    }

    const config = getStoredBackupScheduleConfig();
    const location = ensureBackupLocationInitialized(config).backupLocation;

    // Validate backup location
    const [isValid, errorMsg] = validateBackupLocationPath(location);
    if (!isValid) {
    console.warn(
    "[BackupIPC] DELETE_BACKUP: Invalid backup location:",
    errorMsg,
    );
    return { success: false, error: errorMsg };
    }

    console.log("[BackupIPC] Deleting backup:", filename);
    const result = await deleteBackupFileWithMutex(location, filename);

    // Reconcile history and notify renderer after successful deletion
    if (result.success) {
    // List remaining backup files to reconcile history
    const remainingFiles = await listBackupFiles(location);
    const remainingFilenames = remainingFiles.map((f) => f.name);
    reconcileStoredHistory(remainingFilenames);

    if (mainWindow && !mainWindow.isDestroyed()) {
    mainWindow.webContents.send(BACKUP_CHANNELS.ON_HISTORY_UPDATED);
    console.debug(
    "[BackupIPC] Sent ON_HISTORY_UPDATED notification to renderer after deletion",
    );
    }
    }

    return result;
    } catch (error) {
    const errorMessage =
    error instanceof Error ? error.message : "Unknown error";
    console.error("[BackupIPC] Error deleting backup:", error);
    return { success: false, error: errorMessage };
    }
    },
    mainWindow,
    );

    secureHandle(
    BACKUP_CHANNELS.TRIGGER_BACKUP,
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    async (_event: Electron.IpcMainInvokeEvent) => {
    console.log("[BackupIPC] Manual backup trigger requested");
    return createImmediateBackup(mainWindow);
    },
    mainWindow,
    );

    // CREATE_NOW is semantically equivalent to TRIGGER_BACKUP - both create an immediate backup
    secureHandle(
    BACKUP_CHANNELS.CREATE_NOW,
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    async (_event: Electron.IpcMainInvokeEvent) => {
    console.log("[BackupIPC] Immediate backup creation requested");
    return createImmediateBackup(mainWindow);
    },
    mainWindow,
    );

    secureHandle(
    BACKUP_CHANNELS.GET_BACKUP_STATUS,
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    (_event: Electron.IpcMainInvokeEvent) => {
    try {
    const config = getStoredBackupScheduleConfig();
    return {
    isRunning: schedulerState.isRunning,
    lastBackup: config.lastBackupTimestamp,
    nextBackup: config.nextBackupTimestamp,
    };
    } catch (error) {
    console.error("[BackupIPC] Error getting backup status:", error);
    return {
    isRunning: false,
    lastBackup: null,
    nextBackup: null,
    };
    }
    },
    mainWindow,
    );

    secureHandle(
    BACKUP_CHANNELS.GET_BACKUP_HISTORY,
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    (_event: Electron.IpcMainInvokeEvent) => {
    try {
    const history = getStoredBackupHistory();
    console.debug(
    "[BackupIPC] Returning backup history with",
    history.length,
    "entries",
    );
    return history;
    } catch (error) {
    console.error("[BackupIPC] Error getting backup history:", error);
    return [];
    }
    },
    mainWindow,
    );

    secureHandle(
    BACKUP_CHANNELS.CLEAR_HISTORY,
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    (_event: Electron.IpcMainInvokeEvent) => {
    try {
    console.log("[BackupIPC] Clearing backup history...");
    store.delete(MAIN_PROCESS_STORAGE_KEYS.BACKUP_HISTORY);
    console.log("[BackupIPC] Backup history cleared");

    // Notify renderer that history was updated
    if (mainWindow && !mainWindow.isDestroyed()) {
    mainWindow.webContents.send(BACKUP_CHANNELS.ON_HISTORY_UPDATED);
    console.debug(
    "[BackupIPC] Sent ON_HISTORY_UPDATED notification to renderer",
    );
    }

    return { success: true };
    } catch (error) {
    console.error("[BackupIPC] Error clearing backup history:", error);
    return {
    success: false,
    error: error instanceof Error ? error.message : "Unknown error",
    };
    }
    },
    mainWindow,
    );

    // Read a specific backup file's contents and return as text
    secureHandle(
    BACKUP_CHANNELS.READ_LOCAL_BACKUP,
    async (_event: Electron.IpcMainInvokeEvent, filename: unknown) => {
    try {
    // Ensure filename is a string before validation
    if (typeof filename !== "string") {
    console.warn(
    "[BackupIPC] READ_LOCAL_BACKUP: Invalid filename type:",
    typeof filename,
    );
    return { success: false, error: "Filename must be a string" };
    }

    // Input validation: validate filename
    const [isFilenameValid, filenameError] =
    validateBackupFilename(filename);
    if (!isFilenameValid) {
    console.warn(
    "[BackupIPC] READ_LOCAL_BACKUP: Invalid filename:",
    filenameError,
    );
    return { success: false, error: filenameError };
    }

    const config = getStoredBackupScheduleConfig();
    const location = ensureBackupLocationInitialized(config).backupLocation;

    // Validate backup location
    const [isValid, errorMsg] = validateBackupLocationPath(location);
    if (!isValid) {
    console.warn(
    "[BackupIPC] READ_LOCAL_BACKUP: Invalid backup location:",
    errorMsg,
    );
    return { success: false, error: errorMsg };
    }

    const filePath = path.join(location, filename);

    console.debug("[BackupIPC] Reading backup file");

    const contents = await fs.readFile(filePath, { encoding: "utf-8" });

    const maxSizeBytes = 100 * 1024 * 1024;
    if (contents.length > maxSizeBytes) {
    console.warn(
    "[BackupIPC] READ_LOCAL_BACKUP: File exceeds maximum size:",
    contents.length,
    );
    return { success: false, error: "Backup file exceeds maximum size" };
    }

    return { success: true, data: contents };
    } catch (error) {
    const errorMessage =
    error instanceof Error ? error.message : "Unknown error";
    console.error("[BackupIPC] Error reading local backup file:", error);
    return { success: false, error: errorMessage };
    }
    },
    mainWindow,
    );

    // Restore from a local backup file
    secureHandle(
    BACKUP_CHANNELS.RESTORE_LOCAL_BACKUP,
    async (
    _event: Electron.IpcMainInvokeEvent,
    filename: unknown,
    options: unknown,
    ) => {
    try {
    // Ensure filename is a string before validation
    if (typeof filename !== "string") {
    console.warn(
    "[BackupIPC] RESTORE_LOCAL_BACKUP: Invalid filename type:",
    typeof filename,
    );
    return { success: false, errors: ["Filename must be a string"] };
    }

    // Input validation: validate filename
    const [isFilenameValid, filenameError] =
    validateBackupFilename(filename);
    if (!isFilenameValid) {
    console.warn(
    "[BackupIPC] RESTORE_LOCAL_BACKUP: Invalid filename:",
    filenameError,
    );
    return {
    success: false,
    errors: [filenameError || "Invalid filename"],
    };
    }

    const config = getStoredBackupScheduleConfig();
    const location = ensureBackupLocationInitialized(config).backupLocation;

    // Validate backup location
    const [isValid, errorMsg] = validateBackupLocationPath(location);
    if (!isValid) {
    console.warn(
    "[BackupIPC] RESTORE_LOCAL_BACKUP: Invalid backup location:",
    errorMsg,
    );
    return {
    success: false,
    errors: [errorMsg || "Invalid backup location"],
    };
    }

    const filePath = path.join(location, filename);

    console.info("[BackupIPC] Restoring from backup file:", filePath);

    // Read the backup file
    const contents = await fs.readFile(filePath, { encoding: "utf-8" });

    // Parse and validate the backup
    const backupData = JSON.parse(contents);

    // Parse options (merge mode)
    const restoreOptions =
    options && typeof options === "object" && "merge" in options
    ? { merge: (options as { merge?: boolean }).merge }
    : {};

    // Restore the backup
    const result = await restoreBackup(backupData, restoreOptions);

    if (result.success) {
    console.info(
    "[BackupIPC] Backup restored successfully from:",
    filePath,
    );
    } else {
    console.error(
    "[BackupIPC] Restore failed with errors:",
    result.errors,
    );
    }

    return result;
    } catch (error) {
    const errorMessage =
    error instanceof Error ? error.message : "Unknown error";
    console.error("[BackupIPC] Error restoring backup:", error);
    return { success: false, errors: [errorMessage] };
    }
    },
    mainWindow,
    );

    try {
    const config = getStoredBackupScheduleConfig();
    if (config.enabled) {
    console.log("[BackupIPC] Initializing backup scheduler on app start");
    updateScheduler(mainWindow, config);
    }
    } catch (error) {
    console.error("[BackupIPC] Error initializing backup scheduler:", error);
    }

    console.log("[BackupIPC] ✅ Backup IPC handlers registered");
    }