Browse Source
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.pull/142/head
7 changed files with 328 additions and 159 deletions
@ -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) |
|||
}); |
|||
|
@ -0,0 +1,166 @@ |
|||
/** |
|||
* @fileoverview Electron Platform Service Implementation |
|||
* @author Matthew Raymer |
|||
* |
|||
* Provides platform-specific functionality for Electron desktop applications. |
|||
* This service extends CapacitorPlatformService to leverage Capacitor's APIs |
|||
* while overriding desktop-specific behaviors. |
|||
* |
|||
* Key Features: |
|||
* - Desktop-specific capabilities configuration |
|||
* - Native IPC-based file operations with proper security |
|||
* - Direct saving to user's Downloads folder via main process |
|||
* - Native desktop integration support |
|||
* |
|||
* Architecture: |
|||
* - Extends CapacitorPlatformService for API compatibility |
|||
* - Overrides methods for desktop-specific implementations |
|||
* - Maintains cross-platform service interface |
|||
* |
|||
* @since 1.0.0 |
|||
*/ |
|||
|
|||
import { CapacitorPlatformService } from "./CapacitorPlatformService"; |
|||
import { logger } from "../../utils/logger"; |
|||
|
|||
/** |
|||
* Electron-specific platform service implementation. |
|||
* |
|||
* This service handles the unique requirements of the Electron platform: |
|||
* - Desktop-specific capabilities and UI patterns |
|||
* - File system operations using Capacitor's Filesystem API |
|||
* - Native desktop integration features |
|||
* - Proper error handling with web fallbacks |
|||
* |
|||
* @extends CapacitorPlatformService |
|||
* @example |
|||
* ```typescript
|
|||
* const electronService = new ElectronPlatformService(); |
|||
* await electronService.writeAndShareFile('backup.json', jsonData); |
|||
* ``` |
|||
*/ |
|||
export class ElectronPlatformService extends CapacitorPlatformService { |
|||
/** |
|||
* Gets the capabilities of the Electron platform. |
|||
* Overrides the mobile-focused capabilities from CapacitorPlatformService |
|||
* to provide desktop-specific feature flags. |
|||
* |
|||
* @returns Platform capabilities object specific to Electron |
|||
*/ |
|||
getCapabilities() { |
|||
return { |
|||
hasFileSystem: true, |
|||
hasCamera: false, // Desktop typically doesn't have integrated cameras for our use case
|
|||
isMobile: false, // Electron is desktop, not mobile
|
|||
isIOS: false, |
|||
hasFileDownload: true, // Desktop supports direct file downloads
|
|||
needsFileHandlingInstructions: false, // Desktop users are familiar with file handling
|
|||
isNativeApp: true, // Electron is a native app
|
|||
}; |
|||
} |
|||
|
|||
/** |
|||
* Handles file export for Electron platform using native IPC. |
|||
* |
|||
* This method provides a secure, native file export mechanism that: |
|||
* 1. Uses Electron's IPC (Inter-Process Communication) for secure file operations |
|||
* 2. Writes files directly to the user's Downloads folder via the main process |
|||
* 3. Provides exact file path feedback and proper error handling |
|||
* 4. Falls back to web-style downloads if IPC is unavailable |
|||
* |
|||
* @param fileName - The name of the file to save (with date stamp) |
|||
* @param content - The content to write to the file |
|||
* @returns Promise that resolves when the file is successfully saved |
|||
* @throws {Error} If both native IPC and fallback mechanisms fail |
|||
* |
|||
* @example |
|||
* ```typescript
|
|||
* await electronService.writeAndShareFile('TimeSafari-backup-contacts-2025-07-06.json', jsonData); |
|||
* ``` |
|||
* |
|||
* @note This implementation follows Electron's security best practices by: |
|||
* - Using contextBridge to expose safe IPC methods |
|||
* - Handling file operations in the main process with full filesystem access |
|||
* - Providing exact file paths for better user experience |
|||
* - Maintaining secure separation between renderer and main processes |
|||
*/ |
|||
async writeAndShareFile(fileName: string, content: string): Promise<void> { |
|||
logger.info( |
|||
`[ElectronPlatformService] Using native IPC for reliable file export: ${fileName}`, |
|||
); |
|||
|
|||
try { |
|||
// Check if we're running in Electron with the API available
|
|||
if (typeof window !== "undefined" && window.electronAPI) { |
|||
// Use the native Electron IPC API for file exports
|
|||
const result = await window.electronAPI.exportData(fileName, content); |
|||
|
|||
if (result.success) { |
|||
logger.info( |
|||
`[ElectronPlatformService] File exported successfully to: ${result.path}`, |
|||
); |
|||
logger.info( |
|||
`[ElectronPlatformService] File saved to Downloads folder: ${fileName}`, |
|||
); |
|||
} else { |
|||
logger.error( |
|||
`[ElectronPlatformService] Native export failed: ${result.error}`, |
|||
); |
|||
throw new Error(`Native file export failed: ${result.error}`); |
|||
} |
|||
} else { |
|||
// Fallback to web-style download if Electron API is not available
|
|||
logger.warn( |
|||
"[ElectronPlatformService] Electron API not available, falling back to web download", |
|||
); |
|||
|
|||
const blob = new Blob([content], { type: "application/json" }); |
|||
const url = URL.createObjectURL(blob); |
|||
const downloadLink = document.createElement("a"); |
|||
downloadLink.href = url; |
|||
downloadLink.download = fileName; |
|||
downloadLink.style.display = "none"; |
|||
|
|||
document.body.appendChild(downloadLink); |
|||
downloadLink.click(); |
|||
document.body.removeChild(downloadLink); |
|||
|
|||
setTimeout(() => URL.revokeObjectURL(url), 1000); |
|||
|
|||
logger.info( |
|||
`[ElectronPlatformService] Fallback download initiated: ${fileName}`, |
|||
); |
|||
} |
|||
} catch (error) { |
|||
logger.error("[ElectronPlatformService] File export failed:", error); |
|||
throw new Error(`Failed to export file: ${error}`); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Checks if running on Electron platform. |
|||
* |
|||
* @returns true, as this is the Electron implementation |
|||
*/ |
|||
isElectron(): boolean { |
|||
return true; |
|||
} |
|||
|
|||
/** |
|||
* Checks if running on Capacitor platform. |
|||
* |
|||
* @returns false, as this is Electron, not pure Capacitor |
|||
*/ |
|||
isCapacitor(): boolean { |
|||
return false; |
|||
} |
|||
|
|||
/** |
|||
* Checks if running on web platform. |
|||
* |
|||
* @returns false, as this is not web |
|||
*/ |
|||
isWeb(): boolean { |
|||
return false; |
|||
} |
|||
} |
Loading…
Reference in new issue