From b1e9eff568948120431128b45bf62ffa4264950b Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Sun, 6 Jul 2025 03:46:28 +0000 Subject: [PATCH] feat: implement secure IPC-based file export for Electron Replace sandboxed Capacitor filesystem with native IPC for reliable file exports: - Add IPC handler in main process for direct Downloads folder access - Expose secure electronAPI via contextBridge in preload script - Update ElectronPlatformService to use native IPC with web fallback - Add TypeScript definitions for electron APIs - Fix file export issues where files were trapped in virtual filesystem - Enable proper date-stamped backup filenames in Downloads folder - Follow Electron security best practices with process isolation Files now export directly to ~/Downloads with exact path feedback. --- electron/src/index.ts | 39 +++- electron/src/preload.ts | 30 ++++ src/components/DataExportSection.vue | 127 +++----------- src/services/PlatformServiceFactory.ts | 54 +----- .../platforms/ElectronPlatformService.ts | 166 ++++++++++++++++++ src/services/platforms/WebPlatformService.ts | 38 +++- src/types/global.d.ts | 33 ++++ 7 files changed, 328 insertions(+), 159 deletions(-) create mode 100644 src/services/platforms/ElectronPlatformService.ts diff --git a/electron/src/index.ts b/electron/src/index.ts index edc8d3b9..a49e3395 100644 --- a/electron/src/index.ts +++ b/electron/src/index.ts @@ -1,10 +1,12 @@ import type { CapacitorElectronConfig } from '@capacitor-community/electron'; import { getCapacitorElectronConfig, setupElectronDeepLinking } from '@capacitor-community/electron'; import type { MenuItemConstructorOptions } from 'electron'; -import { app, MenuItem } from 'electron'; +import { app, MenuItem, ipcMain } from 'electron'; import electronIsDev from 'electron-is-dev'; import unhandled from 'electron-unhandled'; import { autoUpdater } from 'electron-updater'; +import { promises as fs } from 'fs'; +import { join } from 'path'; import { ElectronCapacitorApp, setupContentSecurityPolicy, setupReloadWatcher } from './setup'; @@ -106,3 +108,38 @@ app.on('activate', async function () { }); // Place all ipc or other electron api calls and custom functionality under this line + +/** + * IPC Handler for exporting data to the user's Downloads folder. + * + * This provides a secure, native way to save files directly to the Downloads + * directory using the main process's file system access. + * + * @param fileName - The name of the file to save (including extension) + * @param data - The data to write to the file (string or buffer) + * @returns Promise<{success: boolean, path?: string, error?: string}> + */ +ipcMain.handle('export-data-to-downloads', async (_event, fileName: string, data: string) => { + try { + // Get the user's Downloads directory path + const downloadsDir = app.getPath('downloads'); + const filePath = join(downloadsDir, fileName); + + // Write the file to the Downloads directory + await fs.writeFile(filePath, data, 'utf-8'); + + console.log(`[Electron Main] File exported successfully: ${filePath}`); + + return { + success: true, + path: filePath + }; + } catch (error) { + console.error(`[Electron Main] File export failed:`, error); + + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error occurred' + }; + } +}); diff --git a/electron/src/preload.ts b/electron/src/preload.ts index c817d3b7..6a9e39fe 100644 --- a/electron/src/preload.ts +++ b/electron/src/preload.ts @@ -1,4 +1,34 @@ +import { contextBridge, ipcRenderer } from 'electron'; + require('./rt/electron-rt'); ////////////////////////////// // User Defined Preload scripts below console.log('User Preload!'); + +/** + * Expose secure IPC APIs to the renderer process. + * + * This creates a bridge between the sandboxed renderer and the main process, + * allowing secure file operations while maintaining Electron's security model. + */ +contextBridge.exposeInMainWorld('electronAPI', { + /** + * Export data to the user's Downloads folder. + * + * @param fileName - The name of the file to save (e.g., 'backup-2025-07-06.json') + * @param data - The content to write to the file (string) + * @returns Promise<{success: boolean, path?: string, error?: string}> + * + * @example + * ```typescript + * const result = await window.electronAPI.exportData('my-backup.json', JSON.stringify(data)); + * if (result.success) { + * console.log('File saved to:', result.path); + * } else { + * console.error('Export failed:', result.error); + * } + * ``` + */ + exportData: (fileName: string, data: string) => + ipcRenderer.invoke('export-data-to-downloads', fileName, data) +}); diff --git a/src/components/DataExportSection.vue b/src/components/DataExportSection.vue index a5c31ace..1a9dc167 100644 --- a/src/components/DataExportSection.vue +++ b/src/components/DataExportSection.vue @@ -23,27 +23,16 @@ messages * - Conditional UI based on platform capabilities * * @component * - - - If no download happened yet, click again here to download now. -

- After the download, you can save the file in your preferred storage + After the export, you can save the file in your preferred storage location.