- routes/notes.js CRUD + 文件名安全校验 + 实体 .txt 存储 workspace/
- NoteEditor 浮窗(可拖拽)↔固定(280px)切换,📌 工具栏入口
- 编辑器 900ms 防抖自动保存 + Ctrl+S + dirty 指示
- 文件管理器:新建/重命名/删除/空态/错误态
- server 挂载 /api/notes,gitignore workspace/
131 lines
5.0 KiB
JavaScript
131 lines
5.0 KiB
JavaScript
const express = require('express');
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
const { authMiddleware, adminOnly } = require('../middleware/auth');
|
||
|
||
/* ============================================================
|
||
* 工作台「记事本」API(仅管理员)
|
||
*
|
||
* 存储:实体 .txt 文件,目录 frontend/src/tools/workbench/workspace/
|
||
* (.gitignore 已排除,不入库)
|
||
*
|
||
* GET /api/notes → 文件列表(按修改时间倒序)
|
||
* GET /api/notes/:filename → 读取文件内容 { name, content, size, mtime }
|
||
* POST /api/notes → 创建 { filename, content }(同名 409)
|
||
* PUT /api/notes/:filename → 更新 { content }(不存在 404)
|
||
* DELETE /api/notes/:filename → 删除
|
||
*
|
||
* 安全:文件名只允许 .txt、拒绝路径穿越(/、\、..、隐藏文件)
|
||
* ============================================================ */
|
||
|
||
const WORKSPACE_DIR = path.join(__dirname, '..', 'frontend', 'src', 'tools', 'workbench', 'workspace');
|
||
const router = express.Router();
|
||
|
||
function ensureDir() {
|
||
fs.mkdirSync(WORKSPACE_DIR, { recursive: true });
|
||
}
|
||
|
||
/** 文件名安全校验:返回规范化文件名(自动补 .txt),非法返回 null */
|
||
function sanitizeFilename(raw) {
|
||
if (typeof raw !== 'string') return null;
|
||
let name = raw.trim();
|
||
if (!name || name.length > 120) return null;
|
||
if (name.includes('/') || name.includes('\\') || name.includes('\0')) return null;
|
||
if (name === '.' || name === '..' || name.startsWith('.')) return null;
|
||
if (!name.toLowerCase().endsWith('.txt')) name += '.txt';
|
||
// 兜底:解析后必须仍位于 workspace 目录内,杜绝路径穿越
|
||
const full = path.join(WORKSPACE_DIR, name);
|
||
if (!full.startsWith(WORKSPACE_DIR + path.sep)) return null;
|
||
return name;
|
||
}
|
||
|
||
/** 列出所有 .txt 笔记,按修改时间倒序 */
|
||
function listNotes() {
|
||
ensureDir();
|
||
return fs.readdirSync(WORKSPACE_DIR, { withFileTypes: true })
|
||
.filter((d) => d.isFile() && d.name.toLowerCase().endsWith('.txt'))
|
||
.map((d) => {
|
||
const st = fs.statSync(path.join(WORKSPACE_DIR, d.name));
|
||
return { name: d.name, size: st.size, mtime: st.mtimeMs };
|
||
})
|
||
.sort((a, b) => b.mtime - a.mtime);
|
||
}
|
||
|
||
// 列表
|
||
router.get('/', authMiddleware, adminOnly, (req, res) => {
|
||
try {
|
||
res.json(listNotes());
|
||
} catch (e) {
|
||
console.error('Notes list error:', e.message);
|
||
res.status(500).json({ error: '读取笔记列表失败' });
|
||
}
|
||
});
|
||
|
||
// 读取内容
|
||
router.get('/:filename', authMiddleware, adminOnly, (req, res) => {
|
||
const name = sanitizeFilename(req.params.filename);
|
||
if (!name) return res.status(400).json({ error: '文件名不合法' });
|
||
try {
|
||
const full = path.join(WORKSPACE_DIR, name);
|
||
if (!fs.existsSync(full)) return res.status(404).json({ error: '笔记不存在' });
|
||
const content = fs.readFileSync(full, 'utf8');
|
||
const st = fs.statSync(full);
|
||
res.json({ name, content, size: st.size, mtime: st.mtimeMs });
|
||
} catch (e) {
|
||
console.error('Note read error:', e.message);
|
||
res.status(500).json({ error: '读取笔记失败' });
|
||
}
|
||
});
|
||
|
||
// 创建
|
||
router.post('/', authMiddleware, adminOnly, (req, res) => {
|
||
const name = sanitizeFilename(req.body && req.body.filename);
|
||
if (!name) return res.status(400).json({ error: '文件名不合法' });
|
||
const content = typeof req.body.content === 'string' ? req.body.content : '';
|
||
try {
|
||
ensureDir();
|
||
const full = path.join(WORKSPACE_DIR, name);
|
||
if (fs.existsSync(full)) return res.status(409).json({ error: '同名笔记已存在' });
|
||
fs.writeFileSync(full, content, 'utf8');
|
||
const st = fs.statSync(full);
|
||
res.json({ name, size: st.size, mtime: st.mtimeMs });
|
||
} catch (e) {
|
||
console.error('Note create error:', e.message);
|
||
res.status(500).json({ error: '创建笔记失败' });
|
||
}
|
||
});
|
||
|
||
// 更新内容
|
||
router.put('/:filename', authMiddleware, adminOnly, (req, res) => {
|
||
const name = sanitizeFilename(req.params.filename);
|
||
if (!name) return res.status(400).json({ error: '文件名不合法' });
|
||
const content = typeof req.body.content === 'string' ? req.body.content : '';
|
||
try {
|
||
const full = path.join(WORKSPACE_DIR, name);
|
||
if (!fs.existsSync(full)) return res.status(404).json({ error: '笔记不存在' });
|
||
fs.writeFileSync(full, content, 'utf8');
|
||
const st = fs.statSync(full);
|
||
res.json({ name, size: st.size, mtime: st.mtimeMs });
|
||
} catch (e) {
|
||
console.error('Note update error:', e.message);
|
||
res.status(500).json({ error: '保存笔记失败' });
|
||
}
|
||
});
|
||
|
||
// 删除
|
||
router.delete('/:filename', authMiddleware, adminOnly, (req, res) => {
|
||
const name = sanitizeFilename(req.params.filename);
|
||
if (!name) return res.status(400).json({ error: '文件名不合法' });
|
||
try {
|
||
const full = path.join(WORKSPACE_DIR, name);
|
||
if (!fs.existsSync(full)) return res.status(404).json({ error: '笔记不存在' });
|
||
fs.unlinkSync(full);
|
||
res.json({ message: '删除成功' });
|
||
} catch (e) {
|
||
console.error('Note delete error:', e.message);
|
||
res.status(500).json({ error: '删除笔记失败' });
|
||
}
|
||
});
|
||
|
||
module.exports = router;
|