UNPKG

donobu

Version:

Create browser automations with an LLM agent and replay them as Playwright scripts.

194 lines 8.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.FlowsFilesApi = void 0; const FlowNotFoundException_1 = require("../exceptions/FlowNotFoundException"); const Logger_1 = require("../utils/Logger"); const MiscUtils_1 = require("../utils/MiscUtils"); /** * API controller for serving flow-related binary files including images, video, * and arbitrary file attachments. * * The FlowsFilesApi provides HTTP endpoints for retrieving binary assets associated with * Donobu flows — screenshots, video recordings, and generic files (e.g. treatment plans) * stored via {@link FlowsPersistence.setFlowFile}. This class handles proper content type * detection, HTTP range requests for video streaming, and multi-layer persistence fallback * for file retrieval. */ class FlowsFilesApi { constructor(flowsPersistenceRegistry) { this.flowsPersistenceRegistry = flowsPersistenceRegistry; } /** * Retrieves and serves a flow-related image file (screenshot). * * This endpoint serves screenshot images captured during flow execution. The method: * 1. Iterates through all available persistence layers in priority order * 2. Attempts to locate the requested image in each layer * 3. Automatically detects the image format (PNG/JPEG) and sets appropriate Content-Type * 4. Returns 404 if the image is not found in any persistence layer * * **Supported Image Formats:** * - PNG (`image/png`) * - JPEG (`image/jpeg`) * * @throws {FlowNotFoundException} When the flow doesn't exist in any persistence layer */ async getFlowImage(req, res) { const flowId = String(req.params.flowId); const imageId = String(req.params.imageId); for (const flowsPersistence of await this.flowsPersistenceRegistry.getAll()) { try { const image = await flowsPersistence.getScreenShot(flowId, imageId); if (!image) { continue; } const imageType = MiscUtils_1.MiscUtils.detectImageType(image); const mimeType = `image/${imageType}`; res.contentType(mimeType); res.send(image); return; } catch (error) { if (error instanceof FlowNotFoundException_1.FlowNotFoundException) { continue; } Logger_1.appLogger.error('Error getting flow image:', error); throw error; } } res.sendStatus(404); } /** * Retrieves and serves flow video recordings with HTTP range request support. * * This endpoint serves video recordings of flow executions, typically in WebM format. * The method supports HTTP range requests for efficient video streaming and seeking, * allowing clients to request specific byte ranges of large video files. * * **Key Features:** * - HTTP Range Request support for video streaming * - Automatic Content-Type detection (typically `video/webm`) * - Partial content responses (HTTP 206) for range requests * - Multi-layer persistence fallback * - Proper Accept-Ranges header for client compatibility * * **Range Request Handling:** * - Parses Range header to determine requested byte range * - Supports single-range requests (e.g., `bytes=0-1023`) * - Supports open-ended ranges (e.g., `bytes=1024-`) * - Returns appropriate Content-Range header for partial responses * - Falls back to full video if no Range header is provided * * @throws {FlowNotFoundException} When the flow doesn't exist in any persistence layer */ async getFlowVideo(req, res) { const flowId = String(req.params.flowId); const rangeHeader = req.header('Range'); for (const flowsPersistence of await this.flowsPersistenceRegistry.getAll()) { try { let startOffset = 0; let length; if (rangeHeader) { const ranges = rangeHeader.split('=')[1].split('-'); const start = parseInt(ranges[0], 10); const end = ranges.length > 1 && ranges[1] ? parseInt(ranges[1], 10) : Number.MAX_SAFE_INTEGER; if (end < start) { res.sendStatus(416); // Requested Range Not Satisfiable return; } length = end === Number.MAX_SAFE_INTEGER ? Number.MAX_SAFE_INTEGER : Math.min(end - start + 1, Number.MAX_SAFE_INTEGER); startOffset = start; } else { length = Number.MAX_SAFE_INTEGER; // Request the whole video } const videoSegment = await flowsPersistence.getVideoSegment(flowId, startOffset, length); if (!videoSegment) { continue; } res.header('Content-Type', 'video/webm'); res.header('Accept-Ranges', 'bytes'); res.header('Content-Length', videoSegment.bytes.length.toString()); if (rangeHeader) { const endOffset = startOffset + videoSegment.bytes.length - 1; res.status(206); // Partial Content res.header('Content-Range', `bytes ${startOffset}-${endOffset}/${videoSegment.totalLength}`); } res.send(videoSegment.bytes); return; } catch (error) { if (error instanceof FlowNotFoundException_1.FlowNotFoundException) { continue; } Logger_1.appLogger.error('Error getting flow video:', error); throw error; } } res.sendStatus(404); } async getFlowState(req, res) { const flowId = String(req.params.flowId); for (const flowsPersistence of await this.flowsPersistenceRegistry.getAll()) { try { const browserState = await flowsPersistence.getBrowserState(flowId); if (!browserState) { continue; } res.contentType('application/json'); res.send(browserState); return; } catch (error) { if (error instanceof FlowNotFoundException_1.FlowNotFoundException) { continue; } Logger_1.appLogger.error('Error getting browser state:', error); throw error; } } res.sendStatus(404); } /** * Retrieves an arbitrary file stored against a flow by its file ID. * * This endpoint serves any file previously persisted via * {@link FlowsPersistence.setFlowFile}, such as treatment plans or other * attachments. The Content-Type is inferred from the file ID extension; * when the extension is unrecognised it falls back to * `application/octet-stream`. * * @throws {FlowNotFoundException} When the flow doesn't exist in any persistence layer */ async getFlowFile(req, res) { const flowId = String(req.params.flowId); const fileId = String(req.params.fileId); for (const flowsPersistence of await this.flowsPersistenceRegistry.getAll()) { try { const file = await flowsPersistence.getFlowFile(flowId, fileId); if (!file) { continue; } const contentType = MiscUtils_1.MiscUtils.inferMimeType(fileId); res.contentType(contentType); res.send(file); return; } catch (error) { if (error instanceof FlowNotFoundException_1.FlowNotFoundException) { continue; } Logger_1.appLogger.error('Error getting flow file:', error); throw error; } } res.sendStatus(404); } } exports.FlowsFilesApi = FlowsFilesApi; //# sourceMappingURL=FlowsFilesApi.js.map