donobu
Version:
Create browser automations with an LLM agent and replay them as Playwright scripts.
194 lines • 8.4 kB
JavaScript
;
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