Files
rainblogweb/routes/notes.js
T
miaomiao 0ae8815f33 feat: 工作台记事本(浮窗↔固定侧边栏双形态/自动保存/文件管理器/实体文件存储)
- routes/notes.js CRUD + 文件名安全校验 + 实体 .txt 存储 workspace/
- NoteEditor 浮窗(可拖拽)↔固定(280px)切换,📌 工具栏入口
- 编辑器 900ms 防抖自动保存 + Ctrl+S + dirty 指示
- 文件管理器:新建/重命名/删除/空态/错误态
- server 挂载 /api/notes,gitignore workspace/
2026-08-07 22:38:10 +08:00

131 lines
5.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;