## 构建、打包与分发系统重构 (build/) - 构建系统收敛至 build/ 目录:build.mjs + electron-builder.js + 6 个脚本 - electron-builder 配置从 package.json 提取为 build/electron-builder.js - 新增 --edition (personal/enterprise) 和 --channel (stable/beta/alpha) 参数 - OSS 路径按渠道隔离 - 新增 pre-build-check.mjs:Git/CHANGELOG/版本/环境变量校验 - 新增 bump-version.mjs:版本号管理 + CHANGELOG 自动生成 - updater.ts 发送 channel/edition/deviceFingerprint - 新增 src/shared/utils/rollout.ts 灰度测试工具 - 新增 10 条 NPM 脚本 ## VCHSM 五层架构迁移 - 7 个业务模块 + ~500 处导入路径同步 ## 修复 - MediaCardContextMenu 改用共享 ContextMenu 组件 - ComboBox 尺寸参数显示统一 (size-format.ts) ## 文档 - UpdateA.md / build/README.md / README.md / .env.example 更新 Co-Authored-By: Claude <noreply@anthropic.com>
170 lines
6.1 KiB
TypeScript
170 lines
6.1 KiB
TypeScript
// ============================================================
|
||
// 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';
|
||
|
||
// ---------- 内部工具 ----------
|
||
|
||
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');
|
||
}
|
||
|
||
/**
|
||
* 将相对路径解析为绝对路径,并校验是否在 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) {
|
||
logger.warn('storage', '获取文件大小失败', err instanceof Error ? err : undefined);
|
||
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 处理器已注册');
|
||
}
|