server.js 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. const express = require('express');
  2. const path = require('path');
  3. const fs = require('fs').promises;
  4. const cors = require('cors');
  5. const app = express();
  6. const PORT = 3000;
  7. // index.md 缓存
  8. let indexCache = null;
  9. let indexCacheTime = 0;
  10. const CACHE_DURATION = 5000; // 5秒缓存
  11. // 路径安全验证函数 - 防止路径遍历攻击
  12. function sanitizePath(input) {
  13. if (!input || typeof input !== 'string') {
  14. return '';
  15. }
  16. // 移除 ../ 和 ..\ 防止路径遍历
  17. // 移除路径分隔符 / 和 \
  18. // 保留中文等 Unicode 字符
  19. return input
  20. .replace(/\.\./g, '') // 移除 ..
  21. .replace(/^[\/\\]+/, '') // 移除开头的 / 和 \
  22. .replace(/[\/\\]/g, ''); // 移除所有 / 和 \
  23. }
  24. // 验证路径是否在允许的目录内
  25. function validatePath(fullPath, baseDir) {
  26. const resolvedPath = path.resolve(fullPath);
  27. const resolvedBaseDir = path.resolve(baseDir);
  28. return resolvedPath.startsWith(resolvedBaseDir);
  29. }
  30. // 中间件
  31. app.use(cors());
  32. app.use(express.json({ limit: '10mb' })); // 增加请求体大小限制到10MB
  33. app.use(express.urlencoded({ limit: '10mb', extended: true }));
  34. app.use(express.static('public'));
  35. // 获取 index.md 结构(带缓存)
  36. async function getIndexStructure() {
  37. const now = Date.now();
  38. if (indexCache && (now - indexCacheTime < CACHE_DURATION)) {
  39. return indexCache;
  40. }
  41. const indexPath = path.join(__dirname, 'docs', 'index.md');
  42. const content = await fs.readFile(indexPath, 'utf-8');
  43. indexCache = parseIndexMd(content);
  44. indexCacheTime = now;
  45. return indexCache;
  46. }
  47. // API: 解析 index.md 结构
  48. app.get('/api/structure', async (req, res) => {
  49. try {
  50. const structure = await getIndexStructure();
  51. res.json(structure);
  52. } catch (error) {
  53. res.status(500).json({ error: '无法解析文档结构', details: error.message });
  54. }
  55. });
  56. // API: 获取指定分类的文档列表
  57. app.get('/api/category/:category', async (req, res) => {
  58. try {
  59. const category = sanitizePath(req.params.category);
  60. if (!category) {
  61. return res.status(400).json({ error: '无效的分类名称' });
  62. }
  63. const structure = await getIndexStructure();
  64. const categoryData = structure.find(cat => cat.name === category);
  65. if (!categoryData) {
  66. return res.status(404).json({ error: '分类不存在' });
  67. }
  68. res.json(categoryData);
  69. } catch (error) {
  70. res.status(500).json({ error: '无法获取分类信息', details: error.message });
  71. }
  72. });
  73. // API: 获取指定文档的内容
  74. app.get('/api/doc/:category/:docName', async (req, res) => {
  75. try {
  76. const category = sanitizePath(req.params.category);
  77. const docName = sanitizePath(req.params.docName);
  78. if (!category || !docName) {
  79. return res.status(400).json({ error: '无效的分类或文档名称' });
  80. }
  81. const docsDir = path.join(__dirname, 'docs');
  82. const docPath = path.join(docsDir, category, `${docName}.md`);
  83. // 验证路径是否在 docs 目录内
  84. if (!validatePath(docPath, docsDir)) {
  85. return res.status(403).json({ error: '拒绝访问:无效的路径' });
  86. }
  87. const content = await fs.readFile(docPath, 'utf-8');
  88. res.json({ content, category, docName });
  89. } catch (error) {
  90. res.status(500).json({ error: '无法读取文档', details: error.message });
  91. }
  92. });
  93. // API: 保存文档内容
  94. app.put('/api/doc/:category/:docName', async (req, res) => {
  95. try {
  96. const category = sanitizePath(req.params.category);
  97. const docName = sanitizePath(req.params.docName);
  98. const { content } = req.body;
  99. // 验证输入
  100. if (!category || !docName) {
  101. return res.status(400).json({ error: '无效的分类或文档名称' });
  102. }
  103. if (!content || typeof content !== 'string') {
  104. return res.status(400).json({ error: '无效的文档内容' });
  105. }
  106. const docsDir = path.join(__dirname, 'docs');
  107. const docPath = path.join(docsDir, category, `${docName}.md`);
  108. // 验证路径是否在 docs 目录内
  109. if (!validatePath(docPath, docsDir)) {
  110. return res.status(403).json({ error: '拒绝访问:无效的路径' });
  111. }
  112. // 检查文件是否存在
  113. try {
  114. await fs.access(docPath);
  115. } catch (error) {
  116. return res.status(404).json({ error: '文档不存在' });
  117. }
  118. // 保存文件
  119. await fs.writeFile(docPath, content, 'utf-8');
  120. res.json({
  121. success: true,
  122. message: '文档保存成功',
  123. category,
  124. docName
  125. });
  126. } catch (error) {
  127. console.error('Save document error:', error);
  128. res.status(500).json({ error: '无法保存文档', details: error.message });
  129. }
  130. });
  131. // API: 搜索文档
  132. app.get('/api/search/:category', async (req, res) => {
  133. try {
  134. const category = sanitizePath(req.params.category);
  135. const currentDoc = sanitizePath(req.query.currentDoc);
  136. const { q } = req.query;
  137. if (!category) {
  138. return res.status(400).json({ error: '无效的分类名称' });
  139. }
  140. if (!q || q.trim().length === 0) {
  141. return res.json({ currentDoc: [], otherDocs: [] });
  142. }
  143. const query = q.toLowerCase();
  144. const docsDir = path.join(__dirname, 'docs');
  145. const categoryPath = path.join(docsDir, category);
  146. // 验证路径是否在 docs 目录内
  147. if (!validatePath(categoryPath, docsDir)) {
  148. return res.status(403).json({ error: '拒绝访问:无效的路径' });
  149. }
  150. // 读取分类下的所有文档
  151. const files = await fs.readdir(categoryPath);
  152. const mdFiles = files.filter(file => file.endsWith('.md'));
  153. const currentDocResults = [];
  154. const otherDocsResults = [];
  155. // 搜索每个文档
  156. for (const file of mdFiles) {
  157. const docName = file.replace('.md', '');
  158. const filePath = path.join(categoryPath, file);
  159. const content = await fs.readFile(filePath, 'utf-8');
  160. // 搜索匹配的行
  161. const lines = content.split('\n');
  162. const matches = [];
  163. lines.forEach((line, index) => {
  164. const lowerLine = line.toLowerCase();
  165. if (lowerLine.includes(query)) {
  166. // 获取上下文(前后各50个字符)
  167. const startIndex = Math.max(0, lowerLine.indexOf(query) - 50);
  168. const endIndex = Math.min(line.length, lowerLine.indexOf(query) + query.length + 50);
  169. let snippet = line.substring(startIndex, endIndex);
  170. // 如果不是从头开始,添加省略号
  171. if (startIndex > 0) snippet = '...' + snippet;
  172. if (endIndex < line.length) snippet = snippet + '...';
  173. matches.push({
  174. line: index + 1,
  175. snippet: snippet,
  176. fullLine: line
  177. });
  178. }
  179. });
  180. if (matches.length > 0) {
  181. const result = {
  182. docName,
  183. matchCount: matches.length,
  184. matches: matches.slice(0, 5) // 最多返回5个匹配
  185. };
  186. // 区分当前文档和其他文档
  187. if (docName === currentDoc) {
  188. currentDocResults.push(result);
  189. } else {
  190. otherDocsResults.push(result);
  191. }
  192. }
  193. }
  194. res.json({
  195. query: q,
  196. currentDoc: currentDocResults,
  197. otherDocs: otherDocsResults
  198. });
  199. } catch (error) {
  200. res.status(500).json({ error: '搜索失败', details: error.message });
  201. }
  202. });
  203. // 解析 index.md 的函数
  204. function parseIndexMd(content) {
  205. const lines = content.split('\n');
  206. const structure = [];
  207. let currentCategory = null;
  208. for (const line of lines) {
  209. const trimmedLine = line.trim();
  210. // 匹配分类 [category]
  211. const categoryMatch = trimmedLine.match(/^\[(.+?)\]$/);
  212. if (categoryMatch) {
  213. currentCategory = {
  214. name: categoryMatch[1],
  215. docs: []
  216. };
  217. structure.push(currentCategory);
  218. continue;
  219. }
  220. // 匹配文档项 1: testa.md 或 3.1: testc1.md
  221. const docMatch = trimmedLine.match(/^([\d.]+):\s*(.+?)\.md$/);
  222. if (docMatch && currentCategory) {
  223. const [, number, docName] = docMatch;
  224. const level = (number.match(/\./g) || []).length;
  225. currentCategory.docs.push({
  226. number,
  227. name: docName,
  228. level,
  229. fullName: `${docName}.md`
  230. });
  231. }
  232. }
  233. return structure;
  234. }
  235. // 启动服务器
  236. app.listen(PORT, () => {
  237. console.log(`服务器运行在 http://localhost:${PORT}`);
  238. });