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.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { PlatformService } from "./PlatformService";
|
||||
import { WebPlatformService } from "./platforms/WebPlatformService";
|
||||
import { CapacitorPlatformService } from "./platforms/CapacitorPlatformService";
|
||||
import { ElectronPlatformService } from "./platforms/ElectronPlatformService";
|
||||
|
||||
/**
|
||||
* Factory class for creating platform-specific service implementations.
|
||||
@@ -76,56 +77,3 @@ export class PlatformServiceFactory {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Electron-specific platform service implementation.
|
||||
* Extends CapacitorPlatformService with electron-specific overrides.
|
||||
*
|
||||
* This service handles the unique requirements of the Electron platform:
|
||||
* - Desktop-specific capabilities
|
||||
* - Electron-specific file system access
|
||||
* - Desktop UI patterns
|
||||
* - Native desktop integration
|
||||
*/
|
||||
class ElectronPlatformService extends CapacitorPlatformService {
|
||||
/**
|
||||
* Gets the capabilities of the Electron platform
|
||||
* Overrides the mobile-focused capabilities from CapacitorPlatformService
|
||||
* @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
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
||||
166
src/services/platforms/ElectronPlatformService.ts
Normal file
166
src/services/platforms/ElectronPlatformService.ts
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -559,13 +559,39 @@ export class WebPlatformService implements PlatformService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Not supported in web platform.
|
||||
* @param _fileName - Unused fileName parameter
|
||||
* @param _content - Unused content parameter
|
||||
* @throws Error indicating file system access is not available
|
||||
* Downloads a file in the web platform using blob URLs and download links.
|
||||
* Creates a temporary download link and triggers the browser's download mechanism.
|
||||
* @param fileName - The name of the file to download
|
||||
* @param content - The content to write to the file
|
||||
* @returns Promise that resolves when the download is initiated
|
||||
*/
|
||||
async writeAndShareFile(_fileName: string, _content: string): Promise<void> {
|
||||
throw new Error("File system access not available in web platform");
|
||||
async writeAndShareFile(fileName: string, content: string): Promise<void> {
|
||||
try {
|
||||
// Create a blob with the content
|
||||
const blob = new Blob([content], { type: "application/json" });
|
||||
|
||||
// Create a temporary download link
|
||||
const url = URL.createObjectURL(blob);
|
||||
const downloadLink = document.createElement("a");
|
||||
downloadLink.href = url;
|
||||
downloadLink.download = fileName;
|
||||
downloadLink.style.display = "none";
|
||||
|
||||
// Add to DOM, click, and remove
|
||||
document.body.appendChild(downloadLink);
|
||||
downloadLink.click();
|
||||
document.body.removeChild(downloadLink);
|
||||
|
||||
// Clean up the object URL after a short delay
|
||||
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
||||
|
||||
logger.log("[WebPlatformService] File download initiated:", fileName);
|
||||
} catch (error) {
|
||||
logger.error("[WebPlatformService] Error downloading file:", error);
|
||||
throw new Error(
|
||||
`Failed to download file: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user