forked from trent_larson/crowd-funder-for-time-pwa
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,10 +1,12 @@
|
|||||||
import type { CapacitorElectronConfig } from '@capacitor-community/electron';
|
import type { CapacitorElectronConfig } from '@capacitor-community/electron';
|
||||||
import { getCapacitorElectronConfig, setupElectronDeepLinking } from '@capacitor-community/electron';
|
import { getCapacitorElectronConfig, setupElectronDeepLinking } from '@capacitor-community/electron';
|
||||||
import type { MenuItemConstructorOptions } from 'electron';
|
import type { MenuItemConstructorOptions } from 'electron';
|
||||||
import { app, MenuItem } from 'electron';
|
import { app, MenuItem, ipcMain } from 'electron';
|
||||||
import electronIsDev from 'electron-is-dev';
|
import electronIsDev from 'electron-is-dev';
|
||||||
import unhandled from 'electron-unhandled';
|
import unhandled from 'electron-unhandled';
|
||||||
import { autoUpdater } from 'electron-updater';
|
import { autoUpdater } from 'electron-updater';
|
||||||
|
import { promises as fs } from 'fs';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
import { ElectronCapacitorApp, setupContentSecurityPolicy, setupReloadWatcher } from './setup';
|
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
|
// 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'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
@@ -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)
|
||||||
|
});
|
||||||
|
|||||||
@@ -23,27 +23,16 @@ messages * - Conditional UI based on platform capabilities * * @component *
|
|||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
:class="{ hidden: isDownloadInProgress }"
|
:disabled="isExporting"
|
||||||
class="block w-full text-center text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
|
class="block w-full text-center text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
@click="exportDatabase()"
|
@click="exportDatabase()"
|
||||||
>
|
>
|
||||||
Download Contacts
|
{{ isExporting ? "Exporting..." : "Download Contacts" }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Hidden download link for web platform - always rendered for ref access -->
|
|
||||||
<a
|
|
||||||
v-if="isWebPlatform"
|
|
||||||
ref="downloadLink"
|
|
||||||
:href="downloadUrl"
|
|
||||||
:download="fileName"
|
|
||||||
:class="{ hidden: !downloadUrl }"
|
|
||||||
class="block w-full text-center text-md bg-gradient-to-b from-green-500 to-green-800 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-6"
|
|
||||||
>
|
|
||||||
If no download happened yet, click again here to download now.
|
|
||||||
</a>
|
|
||||||
<div v-if="capabilities.needsFileHandlingInstructions" class="mt-4">
|
<div v-if="capabilities.needsFileHandlingInstructions" class="mt-4">
|
||||||
<p>
|
<p>
|
||||||
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.
|
location.
|
||||||
</p>
|
</p>
|
||||||
<ul>
|
<ul>
|
||||||
@@ -76,6 +65,11 @@ import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
|||||||
* @vue-component
|
* @vue-component
|
||||||
* Data Export Section Component
|
* Data Export Section Component
|
||||||
* Handles database export and seed backup functionality with platform-specific behavior
|
* Handles database export and seed backup functionality with platform-specific behavior
|
||||||
|
*
|
||||||
|
* Features:
|
||||||
|
* - Automatic date stamping of backup files (YYYY-MM-DD format)
|
||||||
|
* - Platform-specific export handling with proper abstraction
|
||||||
|
* - Robust error handling and user notifications
|
||||||
*/
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
mixins: [PlatformServiceMixin],
|
mixins: [PlatformServiceMixin],
|
||||||
@@ -95,11 +89,10 @@ export default class DataExportSection extends Vue {
|
|||||||
@Prop({ required: true }) readonly activeDid!: string;
|
@Prop({ required: true }) readonly activeDid!: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* URL for the database export download
|
* Flag indicating if export is currently in progress
|
||||||
* Created and revoked dynamically during export process
|
* Used to show loading state and prevent multiple simultaneous exports
|
||||||
* Only used in web platform
|
|
||||||
*/
|
*/
|
||||||
downloadUrl = "";
|
isExporting = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notification helper for consistent notification patterns
|
* Notification helper for consistent notification patterns
|
||||||
@@ -116,116 +109,52 @@ export default class DataExportSection extends Vue {
|
|||||||
*/
|
*/
|
||||||
declare readonly platformService: import("@/services/PlatformService").PlatformService;
|
declare readonly platformService: import("@/services/PlatformService").PlatformService;
|
||||||
|
|
||||||
/**
|
|
||||||
* Computed property to check if we're on web platform
|
|
||||||
*/
|
|
||||||
private get isWebPlatform(): boolean {
|
|
||||||
return this.capabilities.hasFileDownload;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Computed property to check if download is in progress
|
|
||||||
*/
|
|
||||||
private get isDownloadInProgress(): boolean {
|
|
||||||
return Boolean(this.downloadUrl && this.isWebPlatform);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Computed property for the export file name
|
* Computed property for the export file name
|
||||||
|
* Includes today's date for easy identification of backup files
|
||||||
*/
|
*/
|
||||||
private get fileName(): string {
|
private get fileName(): string {
|
||||||
return `${AppString.APP_NAME_NO_SPACES}-backup-contacts.json`;
|
const today = new Date();
|
||||||
}
|
const dateString = today.toISOString().split("T")[0]; // YYYY-MM-DD format
|
||||||
|
return `${AppString.APP_NAME_NO_SPACES}-backup-contacts-${dateString}.json`;
|
||||||
/**
|
|
||||||
* Lifecycle hook to clean up resources
|
|
||||||
* Revokes object URL when component is unmounted (web platform only)
|
|
||||||
*/
|
|
||||||
beforeUnmount() {
|
|
||||||
if (this.downloadUrl && this.isWebPlatform) {
|
|
||||||
URL.revokeObjectURL(this.downloadUrl);
|
|
||||||
this.downloadUrl = "";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Exports the database to a JSON file
|
* Exports the database to a JSON file
|
||||||
* Uses platform-specific methods for saving the exported data
|
* Uses the platform service to handle platform-specific export logic
|
||||||
* Shows success/error notifications to user
|
* Shows success/error notifications to user
|
||||||
*
|
*
|
||||||
* @throws {Error} If export fails
|
* @throws {Error} If export fails
|
||||||
*/
|
*/
|
||||||
public async exportDatabase(): Promise<void> {
|
public async exportDatabase(): Promise<void> {
|
||||||
|
if (this.isExporting) {
|
||||||
|
return; // Prevent multiple simultaneous exports
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
this.isExporting = true;
|
||||||
|
|
||||||
// Fetch contacts from database using mixin's cached method
|
// Fetch contacts from database using mixin's cached method
|
||||||
const allContacts = await this.$contacts();
|
const allContacts = await this.$contacts();
|
||||||
|
|
||||||
// Convert contacts to export format
|
// Convert contacts to export format
|
||||||
const exportData = contactsToExportJson(allContacts);
|
const exportData = contactsToExportJson(allContacts);
|
||||||
const jsonStr = JSON.stringify(exportData, null, 2);
|
const jsonStr = JSON.stringify(exportData, null, 2);
|
||||||
const blob = new Blob([jsonStr], { type: "application/json" });
|
|
||||||
|
|
||||||
// Handle export based on platform capabilities
|
// Use platform service to handle export (no platform-specific logic here!)
|
||||||
if (this.isWebPlatform) {
|
await this.platformService.writeAndShareFile(this.fileName, jsonStr);
|
||||||
await this.handleWebExport(blob);
|
|
||||||
} else if (this.capabilities.hasFileSystem) {
|
|
||||||
await this.handleNativeExport(jsonStr);
|
|
||||||
} else {
|
|
||||||
throw new Error("This platform does not support file downloads.");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.notify.success(
|
this.notify.success(
|
||||||
this.isWebPlatform
|
"Contact export completed successfully. Check your downloads or share dialog.",
|
||||||
? "See your downloads directory for the backup."
|
|
||||||
: "The backup file has been saved.",
|
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Export Error:", error);
|
logger.error("Export Error:", error);
|
||||||
this.notify.error(
|
this.notify.error(
|
||||||
`There was an error exporting the data: ${error instanceof Error ? error.message : "Unknown error"}`,
|
`There was an error exporting the data: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||||
);
|
);
|
||||||
|
} finally {
|
||||||
|
this.isExporting = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles export for web platform using download link
|
|
||||||
* @param blob The blob to download
|
|
||||||
*/
|
|
||||||
private async handleWebExport(blob: Blob): Promise<void> {
|
|
||||||
this.downloadUrl = URL.createObjectURL(blob);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Wait for next tick to ensure DOM is updated
|
|
||||||
await this.$nextTick();
|
|
||||||
|
|
||||||
const downloadAnchor = this.$refs.downloadLink as HTMLAnchorElement;
|
|
||||||
if (!downloadAnchor) {
|
|
||||||
throw new Error("Download link element not found. Please try again.");
|
|
||||||
}
|
|
||||||
|
|
||||||
downloadAnchor.click();
|
|
||||||
|
|
||||||
// Clean up the URL after a delay
|
|
||||||
setTimeout(() => {
|
|
||||||
URL.revokeObjectURL(this.downloadUrl);
|
|
||||||
this.downloadUrl = "";
|
|
||||||
}, 1000);
|
|
||||||
} catch (error) {
|
|
||||||
// Clean up the URL on error
|
|
||||||
if (this.downloadUrl) {
|
|
||||||
URL.revokeObjectURL(this.downloadUrl);
|
|
||||||
this.downloadUrl = "";
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles export for native platforms using file system
|
|
||||||
* @param jsonStr The JSON string to save
|
|
||||||
*/
|
|
||||||
private async handleNativeExport(jsonStr: string): Promise<void> {
|
|
||||||
await this.platformService.writeAndShareFile(this.fileName, jsonStr);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { PlatformService } from "./PlatformService";
|
import { PlatformService } from "./PlatformService";
|
||||||
import { WebPlatformService } from "./platforms/WebPlatformService";
|
import { WebPlatformService } from "./platforms/WebPlatformService";
|
||||||
import { CapacitorPlatformService } from "./platforms/CapacitorPlatformService";
|
import { CapacitorPlatformService } from "./platforms/CapacitorPlatformService";
|
||||||
|
import { ElectronPlatformService } from "./platforms/ElectronPlatformService";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Factory class for creating platform-specific service implementations.
|
* 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.
|
* Downloads a file in the web platform using blob URLs and download links.
|
||||||
* @param _fileName - Unused fileName parameter
|
* Creates a temporary download link and triggers the browser's download mechanism.
|
||||||
* @param _content - Unused content parameter
|
* @param fileName - The name of the file to download
|
||||||
* @throws Error indicating file system access is not available
|
* @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> {
|
async writeAndShareFile(fileName: string, content: string): Promise<void> {
|
||||||
throw new Error("File system access not available in web platform");
|
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"}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
33
src/types/global.d.ts
vendored
33
src/types/global.d.ts
vendored
@@ -33,4 +33,37 @@ declare module '@jlongster/sql.js' {
|
|||||||
}) => Promise<SQL>;
|
}) => Promise<SQL>;
|
||||||
|
|
||||||
export default initSqlJs;
|
export default initSqlJs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Electron API types for the main world context bridge.
|
||||||
|
*
|
||||||
|
* These types define the secure IPC APIs exposed by the preload script
|
||||||
|
* to the renderer process for native Electron functionality.
|
||||||
|
*/
|
||||||
|
interface 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 with success status, file path, or error message
|
||||||
|
*/
|
||||||
|
exportData: (fileName: string, data: string) => Promise<{
|
||||||
|
success: boolean;
|
||||||
|
path?: string;
|
||||||
|
error?: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Global window interface extension for Electron APIs.
|
||||||
|
*
|
||||||
|
* This makes the electronAPI available on the window object
|
||||||
|
* in TypeScript without type errors.
|
||||||
|
*/
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
electronAPI: ElectronAPI;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user