The Electron main window.
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");
}
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.