* Session data plugin — reads/writes files under ~/.openroom/sessions/ * API: /api/session-data?path={charId}/{modId}/chat/history.json * Supports GET, POST, DELETE.
()
| 75 | * Supports GET, POST, DELETE. |
| 76 | */ |
| 77 | function sessionDataPlugin(): Plugin { |
| 78 | return { |
| 79 | name: 'session-data', |
| 80 | configureServer(server) { |
| 81 | server.middlewares.use('/api/session-data', (req, res) => { |
| 82 | res.setHeader('Content-Type', 'application/json'); |
| 83 | |
| 84 | const url = new URL(req.url || '', 'http://localhost'); |
| 85 | const relPath = url.searchParams.get('path') || ''; |
| 86 | const action = url.searchParams.get('action') || ''; |
| 87 | |
| 88 | if (!relPath) { |
| 89 | res.writeHead(400); |
| 90 | res.end(JSON.stringify({ error: 'Missing path parameter' })); |
| 91 | return; |
| 92 | } |
| 93 | |
| 94 | // Sanitize: only allow alphanumeric, underscore, hyphen, dot, forward slash |
| 95 | const safePath = relPath.replace(/[^a-zA-Z0-9_\-./]/g, '_').replace(/\.\./g, ''); |
| 96 | const filePath = join(SESSIONS_DIR, safePath); |
| 97 | |
| 98 | // Directory listing: ?action=list&path=... |
| 99 | if (action === 'list' && req.method === 'GET') { |
| 100 | try { |
| 101 | if (!fs.existsSync(filePath) || !fs.statSync(filePath).isDirectory()) { |
| 102 | res.writeHead(200); |
| 103 | res.end(JSON.stringify({ files: [], not_exists: !fs.existsSync(filePath) })); |
| 104 | return; |
| 105 | } |
| 106 | const entries = fs.readdirSync(filePath, { withFileTypes: true }); |
| 107 | const files = entries.map((e) => ({ |
| 108 | path: safePath === '' || safePath === '/' ? e.name : `${safePath}/${e.name}`, |
| 109 | type: e.isDirectory() ? 1 : 0, |
| 110 | size: e.isDirectory() ? 0 : fs.statSync(join(filePath, e.name)).size, |
| 111 | })); |
| 112 | res.writeHead(200); |
| 113 | res.end(JSON.stringify({ files, not_exists: false })); |
| 114 | } catch (err) { |
| 115 | res.writeHead(500); |
| 116 | res.end(JSON.stringify({ error: String(err) })); |
| 117 | } |
| 118 | return; |
| 119 | } |
| 120 | |
| 121 | if (req.method === 'GET') { |
| 122 | try { |
| 123 | if (fs.existsSync(filePath)) { |
| 124 | const ext = filePath.split('.').pop()?.toLowerCase() || ''; |
| 125 | const binaryMimes: Record<string, string> = { |
| 126 | png: 'image/png', |
| 127 | jpg: 'image/jpeg', |
| 128 | jpeg: 'image/jpeg', |
| 129 | gif: 'image/gif', |
| 130 | webp: 'image/webp', |
| 131 | svg: 'image/svg+xml', |
| 132 | mp4: 'video/mp4', |
| 133 | webm: 'video/webm', |
| 134 | }; |