-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathserver.js
More file actions
73 lines (64 loc) · 2.58 KB
/
server.js
File metadata and controls
73 lines (64 loc) · 2.58 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
import express from 'express';
import cors from 'cors';
import { readdir, readFile, stat } from 'fs/promises';
import { join, resolve, relative, extname } from 'path';
import { fileURLToPath } from 'url';
const __dirname = fileURLToPath(new URL('.', import.meta.url));
const PORT = process.env.DRAFTS_PORT || 3333;
const app = express();
app.use(cors());
const SOURCES = [
{ id: 'drafts', label: 'Drafts', path: resolve(process.env.HOME, 'drafts') },
{ id: 'workspace', label: 'Workspace', path: resolve(process.env.HOME, '.openclaw/workspace') }
];
function safePath(base, rel) {
const full = resolve(base, rel);
if (!full.startsWith(base)) throw new Error('Path traversal blocked');
return full;
}
async function scanDir(dir, base, prefix = '') {
const entries = [];
try {
const items = await readdir(dir, { withFileTypes: true });
for (const item of items) {
if (item.name.startsWith('.') || item.name === 'node_modules') continue;
const rel = prefix ? `${prefix}/${item.name}` : item.name;
if (item.isDirectory()) {
const children = await scanDir(join(dir, item.name), base, rel);
if (children.length > 0) {
entries.push({ name: item.name, path: rel, type: 'dir', children });
}
} else if (extname(item.name) === '.md') {
entries.push({ name: item.name, path: rel, type: 'file' });
}
}
} catch (e) { /* skip unreadable */ }
return entries;
}
app.get('/api/sources', (_req, res) => {
res.json(SOURCES.map(s => ({ id: s.id, label: s.label })));
});
app.get('/api/tree/:sourceId', async (req, res) => {
const source = SOURCES.find(s => s.id === req.params.sourceId);
if (!source) return res.status(404).json({ error: 'Source not found' });
const tree = await scanDir(source.path, source.path);
res.json(tree);
});
app.get('/api/file/:sourceId/*', async (req, res) => {
const source = SOURCES.find(s => s.id === req.params.sourceId);
if (!source) return res.status(404).json({ error: 'Source not found' });
const relPath = req.params[0];
try {
const full = safePath(source.path, relPath);
const content = await readFile(full, 'utf-8');
res.json({ content, path: relPath });
} catch (e) {
res.status(e.message === 'Path traversal blocked' ? 403 : 404).json({ error: e.message });
}
});
// Serve built frontend in production
if (process.env.NODE_ENV === 'production') {
app.use(express.static(join(__dirname, 'dist')));
app.get('*', (_req, res) => res.sendFile(join(__dirname, 'dist/index.html')));
}
app.listen(PORT, () => console.log(`Drafts viewer running on http://localhost:${PORT}`));