file browser improvements and hardening.

This commit is contained in:
KingKDot
2026-07-14 09:50:09 -05:00
parent 172703c33b
commit 9178bd0e63
17 changed files with 700 additions and 105 deletions
@@ -3,6 +3,8 @@ import type { FileHandle } from "fs/promises";
import { logger } from "../logger";
export const STREAM_REORDER_BUFFER_LIMIT = 16;
export const STREAM_REORDER_BUFFER_MAX_BYTES = 32 * 1024 * 1024;
export const STREAM_MAX_CHUNK_BYTES = 4 * 1024 * 1024;
export type PendingHttpDownload = {
commandId: string;
@@ -27,7 +29,10 @@ export type PendingHttpDownload = {
nextExpectedOffset?: number;
streamErrored?: boolean;
onFirstChunk?: () => void;
onFirstChunkError?: (error: Error) => void;
firstChunkSignaled?: boolean;
userId?: number;
maxBytes?: number;
};
function isStreamMode(pending: PendingHttpDownload): boolean {
@@ -41,6 +46,10 @@ async function failStream(pending: PendingHttpDownload, err: Error): Promise<voi
pending.streamController?.error(err);
} catch {}
pending.reorderBuffer?.clear();
if (!pending.firstChunkSignaled && pending.onFirstChunkError) {
pending.firstChunkSignaled = true;
try { pending.onFirstChunkError(err); } catch {}
}
}
function enqueueOrdered(pending: PendingHttpDownload, offset: number, data: Uint8Array): boolean {
@@ -77,6 +86,12 @@ function enqueueOrdered(pending: PendingHttpDownload, offset: number, data: Uint
void failStream(pending, new Error("download chunks arrived too far out of order"));
return false;
}
let bufferedBytes = 0;
for (const chunk of pending.reorderBuffer.values()) bufferedBytes += chunk.byteLength;
if (bufferedBytes > STREAM_REORDER_BUFFER_MAX_BYTES) {
void failStream(pending, new Error("download reorder buffer byte limit exceeded"));
return false;
}
return true;
}
@@ -134,6 +149,13 @@ export async function consumeHttpDownloadPayload(
pending.total = total;
}
}
if (pending.maxBytes && pending.total > pending.maxBytes) {
await failStream(pending, new Error("download exceeded requested byte limit"));
clearTimeout(pending.timeout);
pendingHttpDownloads.delete(commandId);
pending.resolve(pending);
return;
}
if (pending.total > 0 && !pending.loggedTotal) {
pending.loggedTotal = true;
logger.debug("[filebrowser] http download total", {
@@ -159,6 +181,21 @@ export async function consumeHttpDownloadPayload(
const chunkIndex = toNumber(payload?.chunkIndex);
const chunksTotal = toNumber(payload?.chunksTotal);
if (data.byteLength > STREAM_MAX_CHUNK_BYTES) {
await failStream(pending, new Error("download chunk exceeded size limit"));
clearTimeout(pending.timeout);
pendingHttpDownloads.delete(commandId);
pending.resolve(pending);
return;
}
if (pending.maxBytes && pending.receivedBytes + data.byteLength > pending.maxBytes) {
await failStream(pending, new Error("download exceeded requested byte limit"));
clearTimeout(pending.timeout);
pendingHttpDownloads.delete(commandId);
pending.resolve(pending);
return;
}
if (offset === null) {
logger.debug("[filebrowser] http download missing offset", {
commandId,
@@ -191,6 +228,15 @@ export async function consumeHttpDownloadPayload(
}
const effectiveOffset = offset ?? 0;
if (effectiveOffset < 0
|| !Number.isSafeInteger(effectiveOffset)
|| (pending.total > 0 && effectiveOffset + data.byteLength > pending.total)) {
await failStream(pending, new Error("invalid download chunk offset"));
clearTimeout(pending.timeout);
pendingHttpDownloads.delete(commandId);
pending.resolve(pending);
return;
}
const shouldWrite = chunkIndex !== null
? !pending.receivedChunks.has(chunkIndex)
: !pending.receivedOffsets.has(effectiveOffset);