## 删除分屏功能 - 删除 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 字段
77 lines
2.7 KiB
TypeScript
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
|
|
}
|