/** * @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, type ReadStream } from 'node:fs'; 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 { fileStreamManager } from '../../file-stream-manager.js'; import { AUDIO_ATTACHMENT_EXTENSIONS, AttachmentRegistrationError, attachmentRecordToEvent, attachmentRegistry, buildFileThumbnailRoute, isSupportedAttachmentExtension, registerExternalAttachment, 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, loadAttachmentGuardConfig } from '../../config/attachment-guard.js'; import { isMultiUserMode, userSpacePath } from '../../config/multiuser.js'; import { CASES_DIR, canAccessOwned, findSessionOrFail, getAuthUser, parseBody, validateSessionFilePath, } from '../route-helpers.js'; import type { FastifyRequest } from 'fastify'; import type { SessionAttachmentHistoryItem, SessionState } from '../../types/session.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'; 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