Files
MarkLite/src/main/file-system.ts
T
thzxx 9c92dcfa9d refactor: 删除分屏功能 + 代码审计修复 + 安全加固
## 删除分屏功能
- 删除 ViewMode 'split' 类型,仅保留 editor/preview
- 删除 splitRatio 状态和 Resizer 组件
- 删除滚动同步模块 (scrollSync.ts, rehypeSourceLine.ts)
- 更新快捷键: Ctrl+1 编辑, Ctrl+2 预览

## Bug 修复
- 修复 Toast setTimeout 内存泄漏 (App.tsx)
- 修复 ModifiedBanner reload 更新错误标签 (改用 modifiedFilePath 匹配)
- 修复 FileWatcher error 未重置 isSelfWriting (file-watcher.ts)
- 修复另存为时未 stop watcher (ipc-handlers.ts)

## 死代码清理 (10 项)
- 删除 main/ipc-channels.ts (与 shared/ 重复)
- 删除 main/file-system.ts 未使用的 formatBytes
- 删除 constants.ts 6 个未使用常量
- 删除 fileUtils.ts 未使用的 formatBytes
- 删除 Icons.tsx 5 个未使用图标 (SplitView/ChevronUp/ChevronDown/X/ArrowUp/ArrowDown)
- 删除 useCodeMirror 未使用的 getContent/scrollTo
- 删除 TabBar 未使用的 menuRef
- 删除 Tab.scrollLeft/previewScrollTop 字段
- 删除 tabStore 未使用的 getTabIndex
- 删除 Toast/ModifiedBanner 多余 React import

## 安全加固
- ipc-handlers: 添加路径遍历防护 (validatePath 函数)
- preload: openExternal 仅允许 http/https 协议
- window-manager: 启用 sandbox: true
- preload: removeAllListeners 改为精确取消订阅 (返回 Unsubscribe 函数)

## 状态持久化
- activeTabId 持久化到 IndexedDB (刷新后恢复正确标签)
- sidebar 状态持久化到 IndexedDB (isVisible/sidebarWidth)

## 代码优化
- preload 使用 IPC_CHANNELS 常量替代硬编码字符串
- ipc-handlers _event 类型改为 IpcMainInvokeEvent
- settingsRepository 删除 splitRatio 字段
2026-05-28 13:42:11 +08:00

77 lines
2.7 KiB
TypeScript

import { readFile, stat, writeFile, rename, readdir, unlink } from 'fs/promises'
import { join, extname, dirname, basename } from 'path'
import type { ReadFileResult, SaveFileResult, FileNode } from '../shared/types'
const MAX_FILE_SIZE = 20 * 1024 * 1024 // 20MB
const ALLOWED_EXTENSIONS = new Set(['.md', '.markdown', '.txt'])
const SKIP_DIRS = new Set([
'node_modules', '.git', '.svn', '.hg', 'dist', 'out',
'.next', '.nuxt', '__pycache__', '.DS_Store'
])
export async function readFileContent(filePath: string): Promise<ReadFileResult> {
try {
const fileStat = await stat(filePath)
if (fileStat.size > MAX_FILE_SIZE) {
return { success: false, error: `文件过大(${(fileStat.size / 1024 / 1024).toFixed(1)} MB),暂不支持超过 20MB 的文件` }
}
let content = await readFile(filePath, 'utf-8')
if (content.charCodeAt(0) === 0xFEFF) content = content.slice(1)
return { success: true, content }
} catch (err) {
return { success: false, error: (err as Error).message }
}
}
// C-02: 临时文件写到与目标相同目录(避免跨盘 rename 失败)
export async function saveFileContent(filePath: string, content: string): Promise<SaveFileResult> {
const tmpFile = join(dirname(filePath), `.marklite-tmp-${Date.now()}-${basename(filePath)}`)
try {
await writeFile(tmpFile, content, 'utf-8')
await rename(tmpFile, filePath)
return { success: true, filePath }
} catch (err) {
// 清理残留临时文件
try { await unlink(tmpFile) } catch { /* ignore */ }
return { success: false, error: (err as Error).message }
}
}
// M-05: 递归深度限制 + 错误边界
export async function buildDirTree(dirPath: string, depth = 0, maxDepth = 10): Promise<FileNode[]> {
if (depth > maxDepth) return []
let entries
try {
entries = await readdir(dirPath, { withFileTypes: true })
} catch {
return []
}
entries.sort((a, b) => {
if (a.isDirectory() && !b.isDirectory()) return -1
if (!a.isDirectory() && b.isDirectory()) return 1
return a.name.localeCompare(b.name)
})
const children: FileNode[] = []
for (const entry of entries) {
if (SKIP_DIRS.has(entry.name)) continue
if (entry.name.startsWith('.')) continue
const childPath = join(dirPath, entry.name)
if (entry.isDirectory()) {
const subChildren = await buildDirTree(childPath, depth + 1, maxDepth)
if (subChildren.length > 0) {
children.push({ name: entry.name, path: childPath, type: 'dir', children: subChildren })
}
} else {
const ext = extname(entry.name).toLowerCase()
if (ALLOWED_EXTENSIONS.has(ext)) {
children.push({ name: entry.name, path: childPath, type: 'file' })
}
}
}
return children
}