説明なし
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

static.js 3.6KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172
  1. // Static file serving + authenticated recording/transcript downloads.
  2. // handleGet() is the fallback for any GET that didn't match an API route.
  3. const fs = require('fs');
  4. const path = require('path');
  5. const R = require('./repos');
  6. const { json } = require('./lib');
  7. const { currentUser } = require('./session');
  8. const { PUBLIC_DIR, REC_DIR, TRANS_DIR } = require('./config');
  9. const MIME = { '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css', '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.svg': 'image/svg+xml', '.ico': 'image/x-icon' };
  10. function serveStatic(req, res) {
  11. let p = req.url.split('?')[0];
  12. if (p === '/') p = '/index.html';
  13. if (p === '/home') p = '/home.html';
  14. // Console was replaced by Dashboard; keep the old path working.
  15. if (p === '/console' || p === '/dashboard') p = '/dashboard.html';
  16. if (p === '/share') p = '/share.html';
  17. if (p === '/connect') p = '/connect.html';
  18. const fp = path.join(PUBLIC_DIR, path.normalize(p));
  19. if (!fp.startsWith(PUBLIC_DIR)) return json(res, 403, { error: 'forbidden' });
  20. fs.readFile(fp, (err, data) => {
  21. if (err) return json(res, 404, { error: 'not found' });
  22. const ct = MIME[path.extname(fp)] || 'application/octet-stream';
  23. res.writeHead(200, { 'Content-Type': ct, 'Cache-Control': 'no-cache' });
  24. res.end(data);
  25. });
  26. }
  27. // GET fallback: authenticated transcript/recording downloads, else static files.
  28. function handleGet(req, res) {
  29. const pathOnly = req.url.split('?')[0];
  30. if (pathOnly.startsWith('/transcripts/')) {
  31. const u = currentUser(req);
  32. if (!u) return json(res, 401, { error: 'unauthorized' });
  33. const name = path.basename(decodeURIComponent(pathOnly));
  34. const sid = name.replace(/\.txt$/i, '');
  35. const row = R.sessionsLog.byIdInTenant(sid, u.team_id);
  36. if (!row || !row.transcript) return json(res, 404, { error: 'not found' });
  37. const fp = path.join(TRANS_DIR, row.transcript);
  38. if (!fp.startsWith(TRANS_DIR)) return json(res, 403, { error: 'forbidden' });
  39. return fs.stat(fp, (err, st) => {
  40. if (err) return json(res, 404, { error: 'not found' });
  41. res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8', 'Content-Length': st.size, 'Content-Disposition': 'attachment; filename="transcript-' + sid + '.txt"', 'Cache-Control': 'no-store' });
  42. const rs = fs.createReadStream(fp);
  43. rs.on('error', () => { try { res.destroy(); } catch (e) {} });
  44. rs.pipe(res);
  45. });
  46. }
  47. if (pathOnly.startsWith('/recordings/')) {
  48. const u = currentUser(req);
  49. if (!u) return json(res, 401, { error: 'unauthorized' });
  50. const name = path.basename(decodeURIComponent(pathOnly));
  51. const sid = name.replace(/\.(webm|mp4)$/i, '');
  52. const row = R.sessionsLog.byIdInTenant(sid, u.team_id);
  53. if (!row || !row.recording) return json(res, 404, { error: 'not found' });
  54. const fp = path.join(REC_DIR, row.recording);
  55. if (!fp.startsWith(REC_DIR)) return json(res, 403, { error: 'forbidden' });
  56. const ext = path.extname(row.recording).toLowerCase() === '.mp4' ? 'mp4' : 'webm';
  57. const ctype = ext === 'mp4' ? 'video/mp4' : 'video/webm';
  58. return fs.stat(fp, (err, st) => {
  59. if (err) return json(res, 404, { error: 'not found' });
  60. res.writeHead(200, { 'Content-Type': ctype, 'Content-Length': st.size, 'Content-Disposition': 'attachment; filename="session-' + sid + '.' + ext + '"', 'Cache-Control': 'no-store', 'Accept-Ranges': 'bytes' });
  61. const rs = fs.createReadStream(fp);
  62. rs.on('error', () => { try { res.destroy(); } catch (e) {} });
  63. rs.pipe(res);
  64. });
  65. }
  66. return serveStatic(req, res);
  67. }
  68. module.exports = { handleGet, serveStatic };