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
This commit is contained in:
Jesse Vincent
2026-06-09 14:53:48 -07:00
parent f55642e0dd
commit e0442fba00
2 changed files with 41 additions and 6 deletions

View File

@@ -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(() => {