/** * @fileoverview File browser and streaming routes. * Provides directory listing, file content preview, raw file serving, tail * streaming, and the File Viewer edit-mode write path (edit=1 read + * PUT /api/sessions/:id/file-content; policy in src/config/file-editing.ts, * design in docs/file-viewer-edit-plan.md). */ import { FastifyInstance, type FastifyReply } from 'fastify'; import { basename as pathBasename, dirname, extname, isAbsolute, join, relative, resolve, sep } from 'node:path'; import { createReadStream, realpathSync } from 'node:fs'; import type { Readable } from 'node:stream'; import fs from 'node:fs/promises'; import { createHash, randomBytes } from 'node:crypto'; import { homedir } from 'node:os'; import type { ApiResponse, FilesystemBrowseData, FilesystemBrowseEntry, FilesystemBrowseRoot, FilesystemPreviewKind, FileWriteData, } from '../../types.js'; import { ApiErrorCode, createErrorResponse, getErrorMessage } from '../../types.js'; import { compileFileQuery } from '../../utils/file-query.js'; import { fileStreamManager } from '../../file-stream-manager.js'; import { AUDIO_ATTACHMENT_EXTENSIONS, AttachmentRegistrationError, attachmentRecordToEvent, attachmentRegistry, buildFileThumbnailRoute, isSupportedAttachmentExtension, registerExternalAttachment, TEXT_ATTACHMENT_EXTENSIONS, VIDEO_ATTACHMENT_EXTENSIONS, type AttachmentRecord, } from '../../attachment-registry.js'; import { generateFirstPageThumbnail } from '../../document-thumbnailer.js'; import { getOfficePreviewPdfPath, getPreviewPdfDownloadName } from '../../document-preview-cache.js'; import { sanitizeAttachmentHistoryItem } from '../../session-attachment-history.js'; import { isBlockedAttachmentPath, isUnderTree, loadAttachmentGuardConfig } from '../../config/attachment-guard.js'; import { isMultiUserMode, userSpacePath } from '../../config/multiuser.js'; import { CASES_DIR, canAccessOwned, findSessionOrFail, getAuthUser, parseBody, validateSessionFilePath, validateSessionFilePathLexical, } from '../route-helpers.js'; import type { FastifyRequest } from 'fastify'; import type { SessionAttachmentHistoryItem, SessionRemote, SessionState } from '../../types/session.js'; import { RemoteFileAccessError, remoteCreateReadStream, remoteProbePaths, remoteReadFile, type RemoteProbe, } from '../../remote-files.js'; import { downloadTooLargeMessage, exceedsDownloadLimit } from '../../config/buffer-limits.js'; import { parseByteRange } from '../http-range.js'; import { isSensitivePath } from '../sensitive-path.js'; import { SseEvent } from '../sse-events.js'; import type { ConfigPort, EventPort, SessionPort } from '../ports/index.js'; import { FilesystemBrowseQuerySchema, FilesystemPreviewQuerySchema, FileWriteSchema } from '../schemas.js'; import { MAX_EDITABLE_BYTES, applyEol, detectEol, isDeniedEditRelativePath, isEditableFileName, } from '../../config/file-editing.js'; /** * Upper bound on an XLSX the browser preview will fetch (`?preview=true`). * Parsing happens client-side in spreadsheet-preview-worker.js, so this caps * what a single preview can hand the worker; the renderer refuses the same size * before fetching. An explicit download is unaffected (global download cap). */ export const MAX_XLSX_BROWSER_PREVIEW_BYTES = 10 * 1024 * 1024; /** Whether a raw request is an XLSX browser preview over the preview cap. */ function exceedsXlsxPreviewLimit(extension: string, query: { preview?: string; download?: string }, size: number) { return ( extension === 'xlsx' && query.preview === 'true' && query.download !== 'true' && size > MAX_XLSX_BROWSER_PREVIEW_BYTES ); } function sendXlsxPreviewTooLarge(reply: FastifyReply, size: number): void { reply .code(413) .send( createErrorResponse( ApiErrorCode.INVALID_INPUT, `File too large to preview (${Math.ceil(size / 1024 / 1024)}MB > ${MAX_XLSX_BROWSER_PREVIEW_BYTES / 1024 / 1024}MB limit)` ) ); } const MIME_TYPES: Record = { png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif', webp: 'image/webp', ico: 'image/x-icon', bmp: 'image/bmp', // Media needs a real type, not the octet-stream fallback: a