Files
ele-HeiXiu/electron/main/ipc/storage-ipc.ts
YoungestSongMo 136a17c0bf feat: 任务列表右键菜单回调 + 主进程下载 + sharp 缩略图 + IPC 精简重构
- 任务右键菜单:实现打开输出目录(inputs/outputs/.cache 结构)、
    复制资源链接、重新编辑、再次生成、拉取结果、重新下载、收藏、删除记录
  - 主进程下载:通过 Electron net 模块绕过 CORS,支持 CDN 媒体文件下载
  - sharp 缩略图:替换 nativeImage,支持 4K/8K+ 大尺寸素材的流式处理
  - IPC 精简:40+ 冗余通道 → 15 个分组通道,移除未使用的 preload 桥接方法
  - 任务存储:新增收藏字段、schema 迁移、task-store 持久化
  - 清理废弃模块:canvas-ipc / updater / request / pricing

refactor: IPC/Preload/Services 模块化拆分 — 单一职责 + 向后兼容

  主要变更:
  - shared/constants/ipc-channels.ts → ipc/ 模块 (base/storage/canvas/updater)
  - electron/preload.ts → preload/ 模块 (base/safe-storage/file-storage/canvas)
  - electron/main/ipc/canvas-ipc.ts → canvas/ 模块 (types/walk/sidecar/mime/thumbnail)
  - electron/main/ipc/storage-ipc.ts → 提取 utils/ (path/download/thumbnail)
  - electron/main/updater.ts → updater/ 模块 (provider/platform-updaters)
  - src/services/request.ts → request/ 模块 (token/interceptors/methods)
  - src/shared/utils/pricing.ts → pricing/ 模块 (types/helpers/format/calculate/fields)
  - src/modules/home/center/views/ModelInputForm.tsx → 提取 utils + DependentFormItem

  架构优化:
  - 所有拆分均保持向后兼容(旧导入路径仍可用)
  - TypeScript 编译通过 ✓
  - 每个模块单一职责,平均 ~130 行
2026-07-03 11:13:36 +08:00

226 lines
9.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ============================================================
// 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<string, string> },
): 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 处理器已注册');
}