// ============================================================ // storage-ipc.ts — 本地存储 IPC 处理器(主进程) // // 职责: // - 文件写入/读取/删除(Buffer 基础操作) // - 递归创建目录 // - shell.showItemInFolder(右键"在文件夹中查看") // - 获取应用数据目录路径 // - 所有操作在 heixiu-data/ 沙箱内进行 // // 安全: // - 路径校验:拒绝相对路径中的 ../ 穿越 // - 所有写入操作 base 于 getDataDir() // ============================================================ import { ipcMain, shell, app } from 'electron'; import fs from 'node:fs'; import path from 'node:path'; import { BIDIRECTIONAL_STORAGE } from '../../../shared/constants/ipc/storage'; import { logger } from '../logger'; import { resolveSafe, ensureDir, downloadToFile, generateThumbnail } from './utils'; // ---------- 内部工具 ---------- let dataDirResolver: (() => string) | null = null; /** 注入自定义数据目录解析器(在 registerStorageIpcHandlers() 之前调用) */ export function setDataDirResolver(resolver: () => string): void { dataDirResolver = resolver; } /** 获取 heixiu-data 根目录 */ function getDataDir(): string { return dataDirResolver ? dataDirResolver() : path.join(app.getPath('userData'), 'heixiu-data'); } // ---------- 注册 IPC 处理器 ---------- export function registerStorageIpcHandlers(): void { const dataDir = getDataDir(); ensureDir(dataDir); logger.info('app', `本地存储目录:${dataDir}`); // ---- 获取数据目录 ---- ipcMain.handle(BIDIRECTIONAL_STORAGE.GET_DATA_DIR, () => { return { success: true, data: dataDir }; }); // ---- 文件写入 ---- ipcMain.handle( BIDIRECTIONAL_STORAGE.FILE_WRITE, async (_event, params: { relativePath: string; data: string; encoding?: 'base64' | 'utf8' }) => { try { const filePath = resolveSafe(dataDir, params.relativePath); ensureDir(path.dirname(filePath)); const buffer = Buffer.from(params.data, params.encoding || 'base64'); fs.writeFileSync(filePath, buffer); logger.info('storage', `文件写入成功:${filePath}(${buffer.length} bytes)`); return { success: true, data: filePath }; } catch (err) { const msg = (err as Error).message; logger.warn('storage', `文件写入失败:${params.relativePath}`, err instanceof Error ? err : undefined); return { success: false, error: msg }; } }, ); // ---- 文件读取(返回 Base64) ---- ipcMain.handle(BIDIRECTIONAL_STORAGE.FILE_READ, async (_event, params: { relativePath: string }) => { try { const filePath = resolveSafe(dataDir, params.relativePath); if (!fs.existsSync(filePath)) { return { success: false, error: '文件不存在' }; } const buffer = fs.readFileSync(filePath); return { success: true, data: buffer.toString('base64') }; } catch (err) { logger.warn('storage', `文件读取失败:${params.relativePath}`, err instanceof Error ? err : undefined); return { success: false, error: (err as Error).message }; } }); // ---- 文件删除 ---- ipcMain.handle(BIDIRECTIONAL_STORAGE.FILE_DELETE, async (_event, params: { relativePath: string }) => { try { const filePath = resolveSafe(dataDir, params.relativePath); if (fs.existsSync(filePath)) { fs.unlinkSync(filePath); } return { success: true }; } catch (err) { logger.warn('storage', `文件删除失败:${params.relativePath}`, err instanceof Error ? err : undefined); return { success: false, error: (err as Error).message }; } }); // ---- 创建目录 ---- ipcMain.handle(BIDIRECTIONAL_STORAGE.MAKE_DIR, async (_event, params: { relativePath: string }) => { try { const dirPath = resolveSafe(dataDir, params.relativePath); ensureDir(dirPath); return { success: true, data: dirPath }; } catch (err) { logger.warn('storage', `目录创建失败:${params.relativePath}`, err instanceof Error ? err : undefined); return { success: false, error: (err as Error).message }; } }); // ---- 在文件管理器中显示 ---- ipcMain.handle(BIDIRECTIONAL_STORAGE.SHOW_ITEM_IN_FOLDER, async (_event, params: { filePath: string }) => { try { shell.showItemInFolder(params.filePath); return { success: true }; } catch (err) { logger.warn('storage', `showItemInFolder 失败:${params.filePath}`, err instanceof Error ? err : undefined); return { success: false, error: (err as Error).message }; } }); // ---- 文件大小 ---- ipcMain.handle(BIDIRECTIONAL_STORAGE.FILE_SIZE, async (_event, params: { relativePath: string }) => { try { const filePath = resolveSafe(dataDir, params.relativePath); if (!fs.existsSync(filePath)) { return { success: false, error: '文件不存在' }; } const stat = fs.statSync(filePath); return { success: true, data: stat.size }; } catch (err) { logger.warn('storage', '获取文件大小失败', err instanceof Error ? err : undefined); return { success: false, error: (err as Error).message }; } }); // ---- 文件/目录是否存在 ---- ipcMain.handle(BIDIRECTIONAL_STORAGE.FILE_EXISTS, async (_event, params: { relativePath: string }) => { try { const filePath = resolveSafe(dataDir, params.relativePath); return { success: true, data: fs.existsSync(filePath) }; } catch { return { success: true, data: false }; } }); // ---- 绝对路径文件写入(绕过沙箱,用于用户自定义输出目录) ---- ipcMain.handle( BIDIRECTIONAL_STORAGE.FILE_WRITE_ABSOLUTE, async (_event, params: { absolutePath: string; data: string; encoding?: 'base64' | 'utf8' }) => { try { ensureDir(path.dirname(params.absolutePath)); const buffer = Buffer.from(params.data, params.encoding || 'base64'); fs.writeFileSync(params.absolutePath, buffer); logger.info('storage', `绝对路径写入成功:${params.absolutePath}(${buffer.length} bytes)`); return { success: true, data: params.absolutePath }; } catch (err) { const msg = (err as Error).message; logger.warn('storage', `绝对路径写入失败:${params.absolutePath}`, err instanceof Error ? err : undefined); return { success: false, error: msg }; } }, ); // ---- 绝对路径目录创建(绕过沙箱) ---- ipcMain.handle(BIDIRECTIONAL_STORAGE.MAKE_DIR_ABSOLUTE, async (_event, params: { absolutePath: string }) => { try { ensureDir(params.absolutePath); return { success: true, data: params.absolutePath }; } catch (err) { logger.warn('storage', `绝对路径目录创建失败:${params.absolutePath}`, err instanceof Error ? err : undefined); return { success: false, error: (err as Error).message }; } }); // ---- 获取系统下载目录 ---- ipcMain.handle(BIDIRECTIONAL_STORAGE.GET_DOWNLOADS_PATH, () => { try { return { success: true, data: app.getPath('downloads') }; } catch { return { success: false, error: '无法获取下载目录' }; } }); // ---- 主进程下载文件到绝对路径(绕过浏览器 CORS) ---- ipcMain.handle( BIDIRECTIONAL_STORAGE.DOWNLOAD_TO_PATH, async ( _event, params: { url: string; destPath: string; headers?: Record }, ): Promise<{ success: boolean; data?: string; error?: string }> => { const { url, destPath, headers } = params; try { await downloadToFile(url, destPath, headers); logger.info('storage', `主进程下载成功:${destPath}`); return { success: true, data: destPath }; } catch (err) { const msg = (err as Error).message || '下载失败'; logger.warn('storage', `主进程下载失败:${url}`, err instanceof Error ? err : undefined); return { success: false, error: msg }; } }, ); // ---- 生成图片缩略图(sharp 流式管线) ---- ipcMain.handle( BIDIRECTIONAL_STORAGE.GENERATE_THUMBNAIL, async ( _event, params: { srcPath: string; destPath: string; maxWidth?: number; quality?: number }, ): Promise<{ success: boolean; data?: string; error?: string }> => { const { srcPath, destPath, maxWidth = 256, quality = 60 } = params; try { await generateThumbnail(srcPath, destPath, maxWidth, quality); logger.info('storage', `缩略图生成成功:${destPath}`); return { success: true, data: destPath }; } catch (err) { const msg = (err as Error).message || '缩略图生成失败'; logger.warn('storage', `缩略图生成失败:${srcPath}`, err instanceof Error ? err : undefined); return { success: false, error: msg }; } }, ); logger.info('app', '存储 IPC 处理器已注册'); }