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'); |
require('./rt/electron-rt'); |
||||
//////////////////////////////
|
//////////////////////////////
|
||||
// User Defined Preload scripts below
|
// User Defined Preload scripts below
|
||||
console.log('User Preload!'); |
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