| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287 |
- const express = require('express');
- const path = require('path');
- const fs = require('fs').promises;
- const cors = require('cors');
- const app = express();
- const PORT = 3000;
- // index.md 缓存
- let indexCache = null;
- let indexCacheTime = 0;
- const CACHE_DURATION = 5000; // 5秒缓存
- // 路径安全验证函数 - 防止路径遍历攻击
- function sanitizePath(input) {
- if (!input || typeof input !== 'string') {
- return '';
- }
- // 移除 ../ 和 ..\ 防止路径遍历
- // 移除路径分隔符 / 和 \
- // 保留中文等 Unicode 字符
- return input
- .replace(/\.\./g, '') // 移除 ..
- .replace(/^[\/\\]+/, '') // 移除开头的 / 和 \
- .replace(/[\/\\]/g, ''); // 移除所有 / 和 \
- }
- // 验证路径是否在允许的目录内
- function validatePath(fullPath, baseDir) {
- const resolvedPath = path.resolve(fullPath);
- const resolvedBaseDir = path.resolve(baseDir);
- return resolvedPath.startsWith(resolvedBaseDir);
- }
- // 中间件
- app.use(cors());
- app.use(express.json({ limit: '10mb' })); // 增加请求体大小限制到10MB
- app.use(express.urlencoded({ limit: '10mb', extended: true }));
- app.use(express.static('public'));
- // 获取 index.md 结构(带缓存)
- async function getIndexStructure() {
- const now = Date.now();
- if (indexCache && (now - indexCacheTime < CACHE_DURATION)) {
- return indexCache;
- }
- const indexPath = path.join(__dirname, 'index.md');
- const content = await fs.readFile(indexPath, 'utf-8');
- indexCache = parseIndexMd(content);
- indexCacheTime = now;
- return indexCache;
- }
- // API: 解析 index.md 结构
- app.get('/api/structure', async (req, res) => {
- try {
- const structure = await getIndexStructure();
- res.json(structure);
- } catch (error) {
- res.status(500).json({ error: '无法解析文档结构', details: error.message });
- }
- });
- // API: 获取指定分类的文档列表
- app.get('/api/category/:category', async (req, res) => {
- try {
- const category = sanitizePath(req.params.category);
- if (!category) {
- return res.status(400).json({ error: '无效的分类名称' });
- }
- const structure = await getIndexStructure();
- const categoryData = structure.find(cat => cat.name === category);
- if (!categoryData) {
- return res.status(404).json({ error: '分类不存在' });
- }
- res.json(categoryData);
- } catch (error) {
- res.status(500).json({ error: '无法获取分类信息', details: error.message });
- }
- });
- // API: 获取指定文档的内容
- app.get('/api/doc/:category/:docName', async (req, res) => {
- try {
- const category = sanitizePath(req.params.category);
- const docName = sanitizePath(req.params.docName);
- if (!category || !docName) {
- return res.status(400).json({ error: '无效的分类或文档名称' });
- }
- const docsDir = path.join(__dirname, 'docs');
- const docPath = path.join(docsDir, category, `${docName}.md`);
- // 验证路径是否在 docs 目录内
- if (!validatePath(docPath, docsDir)) {
- return res.status(403).json({ error: '拒绝访问:无效的路径' });
- }
- const content = await fs.readFile(docPath, 'utf-8');
- res.json({ content, category, docName });
- } catch (error) {
- res.status(500).json({ error: '无法读取文档', details: error.message });
- }
- });
- // API: 保存文档内容
- app.put('/api/doc/:category/:docName', async (req, res) => {
- try {
- const category = sanitizePath(req.params.category);
- const docName = sanitizePath(req.params.docName);
- const { content } = req.body;
- // 验证输入
- if (!category || !docName) {
- return res.status(400).json({ error: '无效的分类或文档名称' });
- }
- if (!content || typeof content !== 'string') {
- return res.status(400).json({ error: '无效的文档内容' });
- }
- const docsDir = path.join(__dirname, 'docs');
- const docPath = path.join(docsDir, category, `${docName}.md`);
- // 验证路径是否在 docs 目录内
- if (!validatePath(docPath, docsDir)) {
- return res.status(403).json({ error: '拒绝访问:无效的路径' });
- }
- // 检查文件是否存在
- try {
- await fs.access(docPath);
- } catch (error) {
- return res.status(404).json({ error: '文档不存在' });
- }
- // 保存文件
- await fs.writeFile(docPath, content, 'utf-8');
- res.json({
- success: true,
- message: '文档保存成功',
- category,
- docName
- });
- } catch (error) {
- console.error('Save document error:', error);
- res.status(500).json({ error: '无法保存文档', details: error.message });
- }
- });
- // API: 搜索文档
- app.get('/api/search/:category', async (req, res) => {
- try {
- const category = sanitizePath(req.params.category);
- const currentDoc = sanitizePath(req.query.currentDoc);
- const { q } = req.query;
- if (!category) {
- return res.status(400).json({ error: '无效的分类名称' });
- }
- if (!q || q.trim().length === 0) {
- return res.json({ currentDoc: [], otherDocs: [] });
- }
- const query = q.toLowerCase();
- const docsDir = path.join(__dirname, 'docs');
- const categoryPath = path.join(docsDir, category);
- // 验证路径是否在 docs 目录内
- if (!validatePath(categoryPath, docsDir)) {
- return res.status(403).json({ error: '拒绝访问:无效的路径' });
- }
- // 读取分类下的所有文档
- const files = await fs.readdir(categoryPath);
- const mdFiles = files.filter(file => file.endsWith('.md'));
- const currentDocResults = [];
- const otherDocsResults = [];
- // 搜索每个文档
- for (const file of mdFiles) {
- const docName = file.replace('.md', '');
- const filePath = path.join(categoryPath, file);
- const content = await fs.readFile(filePath, 'utf-8');
- // 搜索匹配的行
- const lines = content.split('\n');
- const matches = [];
- lines.forEach((line, index) => {
- const lowerLine = line.toLowerCase();
- if (lowerLine.includes(query)) {
- // 获取上下文(前后各50个字符)
- const startIndex = Math.max(0, lowerLine.indexOf(query) - 50);
- const endIndex = Math.min(line.length, lowerLine.indexOf(query) + query.length + 50);
- let snippet = line.substring(startIndex, endIndex);
- // 如果不是从头开始,添加省略号
- if (startIndex > 0) snippet = '...' + snippet;
- if (endIndex < line.length) snippet = snippet + '...';
- matches.push({
- line: index + 1,
- snippet: snippet,
- fullLine: line
- });
- }
- });
- if (matches.length > 0) {
- const result = {
- docName,
- matchCount: matches.length,
- matches: matches.slice(0, 5) // 最多返回5个匹配
- };
- // 区分当前文档和其他文档
- if (docName === currentDoc) {
- currentDocResults.push(result);
- } else {
- otherDocsResults.push(result);
- }
- }
- }
- res.json({
- query: q,
- currentDoc: currentDocResults,
- otherDocs: otherDocsResults
- });
- } catch (error) {
- res.status(500).json({ error: '搜索失败', details: error.message });
- }
- });
- // 解析 index.md 的函数
- function parseIndexMd(content) {
- const lines = content.split('\n');
- const structure = [];
- let currentCategory = null;
- for (const line of lines) {
- const trimmedLine = line.trim();
- // 匹配分类 [category]
- const categoryMatch = trimmedLine.match(/^\[(.+?)\]$/);
- if (categoryMatch) {
- currentCategory = {
- name: categoryMatch[1],
- docs: []
- };
- structure.push(currentCategory);
- continue;
- }
- // 匹配文档项 1: testa.md 或 3.1: testc1.md
- const docMatch = trimmedLine.match(/^([\d.]+):\s*(.+?)\.md$/);
- if (docMatch && currentCategory) {
- const [, number, docName] = docMatch;
- const level = (number.match(/\./g) || []).length;
- currentCategory.docs.push({
- number,
- name: docName,
- level,
- fullName: `${docName}.md`
- });
- }
- }
- return structure;
- }
- // 启动服务器
- app.listen(PORT, () => {
- console.log(`服务器运行在 http://localhost:${PORT}`);
- });
|