fix(cctv): cap buffered snapshot downloads

Adapt the streamed snapshot size cap contributed in public PR #82 to the CCTV media module.
This commit is contained in:
Ethan Stoner authored and Sameh Khamis committed 2026-09-12 16:34:51 -07:00
1 parent c78ea4194c
commit 59a20e2d18
2 files changed
+31 -7

No files matched your search

+3
View File
@@ -38,3 +38,6 @@ export const CCTV_SOURCE_FETCH_TIMEOUT_MS = 15 * 1000;
* client refresh cadence. A bounded miss can fall through to Street View or * client refresh cadence. A bounded miss can fall through to Street View or
* the synthetic frame instead of leaving the browser preview pending. */ * the synthetic frame instead of leaving the browser preview pending. */
export const CCTV_FRAME_FETCH_TIMEOUT_MS = 8 * 1000; export const CCTV_FRAME_FETCH_TIMEOUT_MS = 8 * 1000;
/** Maximum buffered snapshot size. */
export const CCTV_FRAME_MAX_BODY_BYTES = 16 * 1024 * 1024;
+28 -7
View File
@@ -1,6 +1,6 @@
import { Readable } from 'node:stream'; import { Readable } from 'node:stream';
import { hashSeed, escapeXml } from './normalize.js'; import { hashSeed, escapeXml } from './normalize.js';
import { CCTV_FRAME_FETCH_TIMEOUT_MS } from './constants.js'; import { CCTV_FRAME_FETCH_TIMEOUT_MS, CCTV_FRAME_MAX_BODY_BYTES } from './constants.js';
/** /**
* Generate a synthetic SVG billboard image for a CCTV camera placeholder. * Generate a synthetic SVG billboard image for a CCTV camera placeholder.
* *
@@ -166,9 +166,32 @@ export async function proxyMediaResponse(
* @param {number} [options.timeoutMs=CCTV_FRAME_FETCH_TIMEOUT_MS] - Abort timeout. * @param {number} [options.timeoutMs=CCTV_FRAME_FETCH_TIMEOUT_MS] - Abort timeout.
* @returns {Promise<{ok:true,body:Buffer,contentType:string}|null>} * @returns {Promise<{ok:true,body:Buffer,contentType:string}|null>}
*/ */
async function readCappedResponseBytes(upstream, maxBytes) {
const declared = Number(upstream.headers.get('content-length'));
if (Number.isFinite(declared) && declared > maxBytes) {
try { await upstream.body?.cancel(); } catch { /* no-op */ }
return null;
}
if (!upstream.body || typeof upstream.body[Symbol.asyncIterator] !== 'function') {
const buffered = Buffer.from(await upstream.arrayBuffer());
return buffered.length > maxBytes ? null : buffered;
}
const chunks = [];
let total = 0;
for await (const chunk of upstream.body) {
total += chunk.length;
if (total > maxBytes) {
try { await upstream.body.cancel(); } catch { /* no-op */ }
return null;
}
chunks.push(Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength));
}
return Buffer.concat(chunks, total);
}
export async function fetchCctvImageFromUpstream( export async function fetchCctvImageFromUpstream(
url, url,
{ fetchImpl = fetch, timeoutMs = CCTV_FRAME_FETCH_TIMEOUT_MS } = {}, { fetchImpl = fetch, timeoutMs = CCTV_FRAME_FETCH_TIMEOUT_MS, maxBytes = CCTV_FRAME_MAX_BODY_BYTES } = {},
) { ) {
if (!url || !/^https?:\/\//i.test(url)) return null; if (!url || !/^https?:\/\//i.test(url)) return null;
const controller = new AbortController(); const controller = new AbortController();
@@ -184,11 +207,9 @@ export async function fetchCctvImageFromUpstream(
}); });
const contentType = upstream.headers.get('content-type') || ''; const contentType = upstream.headers.get('content-type') || '';
if (!upstream.ok || !contentType.startsWith('image/')) return null; if (!upstream.ok || !contentType.startsWith('image/')) return null;
return { const body = await readCappedResponseBytes(upstream, maxBytes);
ok: true, if (!body) return null;
body: Buffer.from(await upstream.arrayBuffer()), return { ok: true, body, contentType };
contentType,
};
} catch { } catch {
return null; return null;
} finally { } finally {