mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 02:29:33 +01:00
feat(html-view): answer Range requests through the shared sendFileBody()
A rendered page's own <video> or <audio> could not seek, because the route streamed a plain 200 without Accept-Ranges. sendFileBody() and localFileSource() are now exported from file-routes.ts and the serving route uses them, so it advertises Accept-Ranges and answers a Range with 206 + Content-Range (or 416), exactly like file-raw, the attachment raw route and /api/download. The body sender hijacks the reply and copies every header set before it (the sandbox CSP, CORS, no-store), which the new test pins on a 206. Media types stay in ASSET_TYPES: with Range support they work, and they are served under the same sandbox CSP as everything else. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
bc940a3953
commit
fa23f8be2a
5 files changed
+37
-8
No files matched your search
@@ -200,8 +200,8 @@ interface FileBodySource {
|
||||
cleanup?: () => void;
|
||||
}
|
||||
|
||||
/** Byte source for a LOCAL file (the historical, only path). */
|
||||
function localFileSource(resolvedPath: string) {
|
||||
/** Byte source for a LOCAL file (the historical, only path). Shared with html-view-routes.ts. */
|
||||
export function localFileSource(resolvedPath: string) {
|
||||
return (range?: { start: number; end: number }): FileBodySource => ({
|
||||
content: createReadStream(resolvedPath, range ? { start: range.start, end: range.end } : undefined),
|
||||
});
|
||||
@@ -221,8 +221,11 @@ function localFileSource(resolvedPath: string) {
|
||||
* `open` supplies the bytes for the (optional) range, which is what lets a remote
|
||||
* case reuse this instead of re-implementing the 206/416 contract: same headers,
|
||||
* same status codes, whether the file sits on this host or behind an ssh pipe.
|
||||
*
|
||||
* Exported for the rendered-HTML route (html-view-routes.ts), so a page's own
|
||||
* `<video>`/`<audio>` can seek as well.
|
||||
*/
|
||||
function sendFileBody(
|
||||
export function sendFileBody(
|
||||
reply: FastifyReply,
|
||||
size: number,
|
||||
rangeHeader: string | string[] | undefined,
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
*/
|
||||
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import { createReadStream, realpathSync } from 'node:fs';
|
||||
import { realpathSync } from 'node:fs';
|
||||
import fs from 'node:fs/promises';
|
||||
import { homedir, tmpdir } from 'node:os';
|
||||
import { basename, dirname, extname, isAbsolute, relative, resolve, sep } from 'node:path';
|
||||
@@ -34,6 +34,7 @@ import { isBlockedAttachmentPath, isUnderTree, loadAttachmentGuardConfig } from
|
||||
import { HTML_VIEW_PREFIX, htmlViewCapabilities } from '../../html-view-capabilities.js';
|
||||
import { CASES_DIR, findSessionOrFail, ownerFor, parseBody, validateSessionFilePath } from '../route-helpers.js';
|
||||
import { HtmlViewOpenSchema } from '../schemas.js';
|
||||
import { localFileSource, sendFileBody } from './file-routes.js';
|
||||
import { getUserSpacesDir } from '../../config/multiuser.js';
|
||||
import { exceedsDownloadLimit } from '../../config/buffer-limits.js';
|
||||
import type { SessionPort } from '../ports/index.js';
|
||||
@@ -254,7 +255,6 @@ export function registerHtmlViewRoutes(app: FastifyInstance, ctx: SessionPort):
|
||||
|
||||
reply.header('Content-Security-Policy', SANDBOX_CSP);
|
||||
reply.header('Content-Type', contentType);
|
||||
reply.header('Content-Length', String(stat.size));
|
||||
reply.header('Cache-Control', 'no-store');
|
||||
reply.header('Referrer-Policy', 'no-referrer');
|
||||
// The sandboxed page is opaque-origin, so even its fetch() of a sibling
|
||||
@@ -262,6 +262,10 @@ export function registerHtmlViewRoutes(app: FastifyInstance, ctx: SessionPort):
|
||||
// enough, and widens nothing: these bytes are already readable by anyone
|
||||
// holding the capability URL.
|
||||
reply.header('Access-Control-Allow-Origin', '*');
|
||||
return reply.send(createReadStream(resolvedPath));
|
||||
// The file routes' body sender: Content-Length, and `Range` answered with 206
|
||||
// (or 416), so a page's own <video>/<audio> can seek. It hijacks the reply, and
|
||||
// carries every header set above across to it.
|
||||
sendFileBody(reply, stat.size, req.headers.range, localFileSource(resolvedPath));
|
||||
return reply;
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user