Forráskód Böngészése

fix(brainstorm-server): ignore macOS resource-fork dotfiles

On macOS (and ExFAT/SMB volumes) the OS writes ._<name>.html sidecar files
holding binary resource-fork metadata. These end with .html, so they passed the
content filter and could be picked as the newest screen — serving binary garbage
to the browser instead of the mockup — or fetched via /files/.

Skip dotfiles (leading '.') at all four sites that list or serve content:
getNewestScreen, the /files/ endpoint, the known-files seed, and the fs.watch
handler. Tests cover serving (/ and /files/) and the watch path (a ._ file must
not trigger a reload).

Refs #950
Jesse Vincent 2 hónapja
szülő
commit
2b108b7dc2

+ 6 - 6
skills/brainstorming/scripts/server.cjs

@@ -124,7 +124,7 @@ function wrapInFrame(content) {
 
 function getNewestScreen() {
   const files = fs.readdirSync(CONTENT_DIR)
-    .filter(f => f.endsWith('.html'))
+    .filter(f => !f.startsWith('.') && f.endsWith('.html'))
     .map(f => {
       const fp = path.join(CONTENT_DIR, f);
       return { path: fp, mtime: fs.statSync(fp).mtime.getTime() };
@@ -152,9 +152,9 @@ function handleRequest(req, res) {
     res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
     res.end(html);
   } else if (req.method === 'GET' && req.url.startsWith('/files/')) {
-    const fileName = req.url.slice(7);
-    const filePath = path.join(CONTENT_DIR, path.basename(fileName));
-    if (!fs.existsSync(filePath)) {
+    const fileName = path.basename(req.url.slice(7));
+    const filePath = path.join(CONTENT_DIR, fileName);
+    if (fileName.startsWith('.') || !fs.existsSync(filePath)) {
       res.writeHead(404);
       res.end('Not found');
       return;
@@ -276,14 +276,14 @@ function startServer() {
   // macOS fs.watch reports 'rename' for both new files and overwrites,
   // so we can't rely on eventType alone.
   const knownFiles = new Set(
-    fs.readdirSync(CONTENT_DIR).filter(f => f.endsWith('.html'))
+    fs.readdirSync(CONTENT_DIR).filter(f => !f.startsWith('.') && f.endsWith('.html'))
   );
 
   const server = http.createServer(handleRequest);
   server.on('upgrade', handleUpgrade);
 
   const watcher = fs.watch(CONTENT_DIR, (eventType, filename) => {
-    if (!filename || !filename.endsWith('.html')) return;
+    if (!filename || filename.startsWith('.') || !filename.endsWith('.html')) return;
 
     if (debounceTimers.has(filename)) clearTimeout(debounceTimers.get(filename));
     debounceTimers.set(filename, setTimeout(() => {

+ 35 - 0
tests/brainstorm-server/server.test.js

@@ -179,6 +179,25 @@ async function runTests() {
       assert(!res.body.includes('"not"'), 'Should not serve JSON');
     });
 
+    await test('ignores macOS resource-fork dotfiles (._*.html) when serving', async () => {
+      // On macOS/ExFAT/SMB, the OS writes ._name.html sidecar files holding
+      // binary metadata. They end with .html but must never be served as a screen.
+      fs.writeFileSync(path.join(CONTENT_DIR, 'real-screen.html'), '<h2>Real Screen Content</h2>');
+      await sleep(100);
+      fs.writeFileSync(path.join(CONTENT_DIR, '._real-screen.html'), 'Mac OS X resource fork garbage');
+      await sleep(300);
+
+      const res = await fetch(`http://localhost:${TEST_PORT}/`);
+      assert(res.body.includes('Real Screen Content'), 'should serve the real screen, not the newer ._ sidecar');
+      assert(!res.body.includes('resource fork garbage'), 'must not serve ._*.html dotfile content');
+    });
+
+    await test('does not serve dotfiles via /files/', async () => {
+      fs.writeFileSync(path.join(CONTENT_DIR, '._secret.html'), 'dotfile body should not be served');
+      const res = await fetch(`http://localhost:${TEST_PORT}/files/._secret.html`);
+      assert.strictEqual(res.status, 404, '/files/ must 404 on dotfiles');
+    });
+
     await test('returns 404 for non-root paths', async () => {
       const res = await fetch(`http://localhost:${TEST_PORT}/other`);
       assert.strictEqual(res.status, 404);
@@ -350,6 +369,22 @@ async function runTests() {
       ws.close();
     });
 
+    await test('does NOT send reload for ._*.html resource-fork dotfiles', async () => {
+      const ws = new WebSocket(`ws://localhost:${TEST_PORT}`);
+      await new Promise(resolve => ws.on('open', resolve));
+
+      let gotReload = false;
+      ws.on('message', (data) => {
+        if (JSON.parse(data.toString()).type === 'reload') gotReload = true;
+      });
+
+      fs.writeFileSync(path.join(CONTENT_DIR, '._sidecar.html'), 'resource fork');
+      await sleep(500);
+
+      assert(!gotReload, 'a ._ dotfile appearing must not trigger a reload');
+      ws.close();
+    });
+
     await test('clears state/events on new screen', async () => {
       // Create an events file
       const eventsFile = path.join(STATE_DIR, 'events');