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媒体源
This commit is contained in:
176
src/infrastructure/storage/db-core.ts
Normal file
176
src/infrastructure/storage/db-core.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
// ============================================================
|
||||
// infrastructure/storage/db-core.ts — sql.js 数据库核心封装
|
||||
//
|
||||
// 职责:
|
||||
// - 初始化 sql.js WASM 运行时 + 创建/加载 Database
|
||||
// - 执行 DDL 建表 / 迁移
|
||||
// - 提供通用 CRUD 操作(execute / query / run / get)
|
||||
// - 导出数据库为 Uint8Array(供加密落盘)
|
||||
//
|
||||
// 安全:
|
||||
// - 数据库完全在内存中运行,不直接操作磁盘
|
||||
// - 数据导出由 db-encrypt.ts 加密后写入磁盘
|
||||
// ============================================================
|
||||
|
||||
import initSqlJs, { type Database, type SqlJsStatic, type SqlValue } from 'sql.js';
|
||||
import { DDL_STATEMENTS, INIT_VERSION_SQL } from './schema';
|
||||
import { logger } from '@/utils/logger';
|
||||
|
||||
// ---------- 内部状态 ----------
|
||||
|
||||
let SQL: SqlJsStatic | null = null;
|
||||
let db: Database | null = null;
|
||||
|
||||
// ---------- 初始化 ----------
|
||||
|
||||
/**
|
||||
* 初始化 sql.js 运行时(WASM 加载),仅需调用一次。
|
||||
*/
|
||||
async function ensureRuntime(): Promise<SqlJsStatic> {
|
||||
if (SQL) return SQL;
|
||||
try {
|
||||
SQL = await initSqlJs({
|
||||
locateFile: (file: string) => {
|
||||
const url = import.meta.env.DEV ? `/${file}` : `./${file}`;
|
||||
console.log(`[storage] 加载 sql.js WASM:${url}`);
|
||||
return url;
|
||||
},
|
||||
});
|
||||
console.log('[storage] sql.js 运行时初始化成功');
|
||||
return SQL;
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
console.error(`[storage] sql.js WASM 加载失败:${msg}`);
|
||||
logger.error('storage', `sql.js WASM 加载失败:${msg}`, err instanceof Error ? err : undefined);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开或创建数据库。
|
||||
*
|
||||
* @param encryptedData - 从磁盘读取的加密数据(由 db-encrypt.ts 解密后提供),
|
||||
* 未提供则创建空库。
|
||||
* @returns true 表示成功
|
||||
*/
|
||||
export async function openDatabase(encryptedData?: Uint8Array): Promise<boolean> {
|
||||
try {
|
||||
const sql = await ensureRuntime();
|
||||
db = new sql.Database(encryptedData ?? undefined);
|
||||
|
||||
// 执行建表 DDL
|
||||
db.run('PRAGMA journal_mode=OFF');
|
||||
db.run('PRAGMA synchronous=0');
|
||||
for (const stmt of DDL_STATEMENTS) {
|
||||
db.run(stmt);
|
||||
}
|
||||
db.run(INIT_VERSION_SQL);
|
||||
|
||||
const size = (db.export()?.byteLength ?? 0);
|
||||
console.log(`[storage] 数据库已打开(${encryptedData ? '从磁盘恢复' : '新建'}),${DDL_STATEMENTS.length} 条 DDL,${size} bytes`);
|
||||
logger.info('storage', `数据库已打开(${encryptedData ? '从磁盘恢复' : '新建'})`);
|
||||
return true;
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
console.error(`[storage] 数据库打开失败:${msg}`);
|
||||
logger.error('storage', `数据库打开失败:${msg}`, err instanceof Error ? err : undefined);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 导出 ----------
|
||||
|
||||
/**
|
||||
* 导出当前数据库为 Uint8Array(供加密后写入磁盘)。
|
||||
*/
|
||||
export function exportDatabase(): Uint8Array | null {
|
||||
if (!db) return null;
|
||||
try {
|
||||
return db.export();
|
||||
} catch (err) {
|
||||
logger.error('storage', '数据库导出失败', err instanceof Error ? err : undefined);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 关闭 ----------
|
||||
|
||||
export function closeDatabase(): void {
|
||||
if (db) {
|
||||
db.close();
|
||||
db = null;
|
||||
logger.info('storage', '数据库已关闭');
|
||||
}
|
||||
}
|
||||
|
||||
export function isOpen(): boolean {
|
||||
return db !== null;
|
||||
}
|
||||
|
||||
// ---------- 查询 API ----------
|
||||
|
||||
/** 确保数据库已打开 */
|
||||
function guard(): Database {
|
||||
if (!db) throw new Error('数据库未初始化 — 请确认已调用 initStorage() 并等待其完成');
|
||||
return db;
|
||||
}
|
||||
|
||||
/** 执行一条写操作 SQL(INSERT / UPDATE / DELETE),返回受影响行数 */
|
||||
export function execute(sql: string, params?: SqlValue[]): number {
|
||||
try {
|
||||
guard().run(sql, params);
|
||||
return guard().getRowsModified();
|
||||
} catch (err) {
|
||||
logger.error('storage', `SQL 执行失败:${sql.substring(0, 80)}`, err instanceof Error ? err : undefined);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询多条记录 */
|
||||
export function queryAll<T = Record<string, unknown>>(sql: string, params?: SqlValue[]): T[] {
|
||||
try {
|
||||
const stmt = guard().prepare(sql);
|
||||
if (params) stmt.bind(params);
|
||||
const rows: T[] = [];
|
||||
while (stmt.step()) {
|
||||
rows.push(stmt.getAsObject() as T);
|
||||
}
|
||||
stmt.free();
|
||||
return rows;
|
||||
} catch (err) {
|
||||
logger.error('storage', `SQL 查询失败:${sql.substring(0, 80)}`, err instanceof Error ? err : undefined);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询单条记录 */
|
||||
export function queryOne<T = Record<string, unknown>>(sql: string, params?: SqlValue[]): T | null {
|
||||
try {
|
||||
const stmt = guard().prepare(sql);
|
||||
if (params) stmt.bind(params);
|
||||
let row: T | null = null;
|
||||
if (stmt.step()) {
|
||||
row = stmt.getAsObject() as T;
|
||||
}
|
||||
stmt.free();
|
||||
return row;
|
||||
} catch (err) {
|
||||
logger.error('storage', `SQL 查询失败:${sql.substring(0, 80)}`, err instanceof Error ? err : undefined);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** 执行多条 SQL(事务包裹) */
|
||||
export function executeBatch(statements: string[]): void {
|
||||
const d = guard();
|
||||
try {
|
||||
d.run('BEGIN');
|
||||
for (const stmt of statements) {
|
||||
d.run(stmt);
|
||||
}
|
||||
d.run('COMMIT');
|
||||
} catch (err) {
|
||||
try { d.run('ROLLBACK'); } catch { /* ignore */ }
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user