Files
ele-HeiXiu/electron/main/storage-ipc.ts
YoungestSongMo e4c9ae8fc6 feat: 三栏比例缩放 + SeedanceContent视频上传 + 本地存储基建 (0.0.22)
三栏布局
- ResizeObserver 等比例分配替代固定默认值恢复
- 修复拖拽+窗口调整竞态卡死(needsRecalcRef 延迟补算)
- containerRef.current 动态读取 DOM 替代闭包 el

Seedance2Content
- 从占位文本重构为内容编排控件(文本+图片/视频/音频上传)
- 根据 spec.image_files/video_files/audio_files 动态渲染上传区域
- 值 JSON 序列化 → 提交时转 ContentItem[](text/image_url/video_url/audio_url)

图片上传兼容视频文件
- buildAccept 根据 file_filter 区分 image/video 前缀
- createMediaBeforeUpload 动态放行 video/* MIME
- 公共模块 media-upload-utils.ts

本地存储基建
- sql.js (WASM SQLite) + safeStorage 加密持久化
- 8个IPC文件操作通道 + resolveSafe 路径穿越防护
- <userData>/heixiu-data/ 沙箱 + JWT用户隔离
- 7表DDL + 游标分页 + LRU媒体淘汰

其他
- 修复 errorMessageRef 渲染期写入警告
- 媒体预览回退直接URL(<img>无CORS限制)+ output_type类型补充
- CSP wasm-unsafe-eval + https: CDN媒体源
2026-06-18 19:22:28 +08:00

162 lines
5.7 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 } from '../../shared/constants/ipc-channels';
import { logger } from './logger';
// ---------- 内部工具 ----------
/** 获取 heixiu-data 根目录 */
function getDataDir(): string {
return path.join(app.getPath('userData'), 'heixiu-data');
}
/**
* 将相对路径解析为绝对路径,并校验是否在 dataDir 内。
* 防止路径穿越攻击。
*/
function resolveSafe(dataDir: string, relativePath: string): string {
const resolved = path.resolve(dataDir, relativePath);
if (!resolved.startsWith(dataDir + path.sep) && resolved !== dataDir) {
throw new Error(`路径越界:${relativePath}`);
}
return resolved;
}
/** 确保目录存在(递归创建) */
function ensureDir(dirPath: string): void {
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
}
// ---------- 注册 IPC 处理器 ----------
export function registerStorageIpcHandlers(): void {
const dataDir = getDataDir();
ensureDir(dataDir);
logger.info('app', `本地存储目录:${dataDir}`);
// ---- 获取数据目录 ----
ipcMain.handle(BIDIRECTIONAL.GET_DATA_DIR, () => {
return { success: true, data: dataDir };
});
// ---- 文件写入 ----
ipcMain.handle(
BIDIRECTIONAL.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);
console.log(`[storage IPC] 文件写入成功:${filePath}${buffer.length} bytes`);
return { success: true, data: filePath };
} catch (err) {
const msg = (err as Error).message;
console.error(`[storage IPC] 文件写入失败:${params.relativePath}${msg}`);
logger.warn('storage', `文件写入失败:${params.relativePath}`, err instanceof Error ? err : undefined);
return { success: false, error: msg };
}
},
);
// ---- 文件读取(返回 Base64 ----
ipcMain.handle(BIDIRECTIONAL.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.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.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.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.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) {
return { success: false, error: (err as Error).message };
}
});
// ---- 文件/目录是否存在 ----
ipcMain.handle(BIDIRECTIONAL.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 };
}
});
logger.info('app', '存储 IPC 处理器已注册');
}