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 { 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 { 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 { 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 }