Stirling-PDF/server-node/src/routes/api/workflow-controller.ts

232 lines
8.7 KiB
TypeScript
Raw Normal View History

2024-01-04 20:17:54 -05:00
import express, { Request, Response } from "express";
import crypto from "crypto";
import multer from "multer";
2023-10-26 21:53:02 +03:00
const upload = multer();
2023-10-19 19:46:23 +02:00
2023-11-13 00:09:12 +01:00
import { traverseOperations } from "@stirling-pdf/shared-operations/src/workflow/traverseOperations";
2024-01-04 20:17:54 -05:00
import { PdfFile, RepresentationType } from "@stirling-pdf/shared-operations/src/wrappers/PdfFile";
import { respondWithPdfFiles } from "../../utils/endpoint-utils";
import { JoiPDFFileSchema } from "@stirling-pdf/shared-operations/src/wrappers/PdfFileJoi";
2023-10-19 19:46:23 +02:00
interface Workflow {
2024-01-04 20:17:54 -05:00
eventStream?: express.Response,
result?: PdfFile[],
finished: boolean,
createdAt: EpochTimeStamp,
finishedAt?: EpochTimeStamp,
error?: { type: number, error: string, stack?: string }
// TODO: When auth is implemented: owner
}
const activeWorkflows: Record<string, Workflow> = {};
2023-10-19 19:46:23 +02:00
const router = express.Router();
2023-11-13 00:09:12 +01:00
2023-10-19 21:23:58 +02:00
router.post("/:workflowUuid?", [
2023-11-13 00:09:12 +01:00
upload.array("files"),
async (req: Request, res: Response) => {
2023-11-13 00:09:12 +01:00
// TODO: Maybe replace with another validator
if(req.files?.length == 0) {
2023-10-20 00:10:03 +02:00
res.status(400).json({"error": "No files were uploaded."});
return;
}
2023-11-13 00:09:12 +01:00
try {
var workflow = JSON.parse(req.body.workflow);
} catch (err) {
if (err instanceof Error) {
console.error("malformed workflow-json was provided", err.message);
res.status(400).json({error: "Malformed workflow-JSON was provided. See Server-Logs for more info", details: err.message});
return;
} else {
throw err;
}
}
2023-10-19 19:46:23 +02:00
2023-12-21 22:52:05 +01:00
const validationResults = JoiPDFFileSchema.validate(req.files);
if(validationResults.error) {
res.status(400).json({error: "PDF validation failed", details: validationResults.error.message});
return;
}
const inputs: PdfFile[] = validationResults.value;
2023-10-19 19:46:23 +02:00
// Allow option to do it synchronously and just make a long request
if(req.body.async === "false") {
console.log("Don't do async");
// TODO: Check if file type == inputType for operator
2023-12-21 15:57:51 +01:00
traverseOperations(workflow.actions, inputs, (state) => {
console.log("State: ", state);
}).then(async (pdfResults) => {
console.log("Download");
await respondWithPdfFiles(res, pdfResults, "workflow-results");
}).catch((err) => {
if(err.validationError) {
// Bad Request
res.status(400).json({error: err});
}
else if (err instanceof Error) {
console.error("Internal Server Error", err);
// Internal Server Error
res.status(500).json({error: err.message, stack: err.stack});
} else {
throw err;
}
2024-01-04 20:17:54 -05:00
});
}
else {
console.log("Start Aync Workflow");
// TODO: UUID collision checks
2024-01-04 20:17:54 -05:00
let workflowID = req.params.workflowUuid;
if(!workflowID)
workflowID = generateWorkflowID();
activeWorkflows[workflowID] = {
createdAt: Date.now(),
finished: false
2024-01-04 20:17:54 -05:00
};
const activeWorkflow = activeWorkflows[workflowID];
res.status(200).json({
"workflowID": workflowID,
"data-recieved": {
"fileCount": inputs.length,
"workflow": workflow
}
});
// TODO: Check if file type == inputType for operator
2023-12-21 15:57:51 +01:00
traverseOperations(workflow.actions, inputs, (state) => {
console.log("State: ", state);
if(activeWorkflow.eventStream)
activeWorkflow.eventStream.write(`data: ${state}\n\n`);
}).then(async (pdfResults) => {
if(activeWorkflow.eventStream) {
2024-01-04 20:17:54 -05:00
activeWorkflow.eventStream.write("data: processing done\n\n");
activeWorkflow.eventStream.end();
}
activeWorkflow.result = pdfResults;
activeWorkflow.finished = true;
activeWorkflow.finishedAt = Date.now();
}).catch((err) => {
if(err.validationError) {
activeWorkflow.error = {type: 500, error: err};
activeWorkflow.finished = true;
activeWorkflow.finishedAt = Date.now();
// Bad Request
if(activeWorkflow.eventStream) {
activeWorkflow.eventStream.write(`data: ${activeWorkflow.error}\n\n`);
activeWorkflow.eventStream.end();
}
}
else if (err instanceof Error) {
console.error("Internal Server Error", err);
activeWorkflow.error = {type: 400, error: err.message, stack: err.stack};
activeWorkflow.finished = true;
activeWorkflow.finishedAt = Date.now();
// Internal Server Error
if(activeWorkflow.eventStream) {
activeWorkflow.eventStream.write(`data: ${activeWorkflow.error}\n\n`);
activeWorkflow.eventStream.end();
}
} else {
throw err;
}
});
}
2023-10-19 19:46:23 +02:00
}
]);
router.get("/progress/:workflowUuid", (req: Request, res: Response) => {
2023-10-20 00:10:03 +02:00
if(!req.params.workflowUuid) {
res.status(400).json({"error": "No workflowUuid weres provided."});
return;
}
if(!activeWorkflows.hasOwnProperty(req.params.workflowUuid)) {
res.status(400).json({"error": `No workflow with workflowUuid "${req.params.workflowUuid}" was found.`});
return;
}
2023-10-19 19:46:23 +02:00
// Return current progress
const workflow = activeWorkflows[req.params.workflowUuid];
res.status(200).json({ createdAt: workflow.createdAt, finished: workflow.finished, finishedAt: workflow.finishedAt, error: workflow.error });
2023-10-19 19:46:23 +02:00
});
router.get("/progress-stream/:workflowUuid", (req: Request, res: Response) => {
2023-10-20 00:10:03 +02:00
if(!req.params.workflowUuid) {
res.status(400).json({"error": "No workflowUuid weres provided."});
return;
}
if(!activeWorkflows.hasOwnProperty(req.params.workflowUuid)) {
res.status(400).json({"error": `No workflow with workflowUuid "${req.params.workflowUuid}" was found.`});
return;
}
// TODO: Check if already done
2023-10-19 21:23:58 +02:00
// Send realtime updates
2024-01-04 20:17:54 -05:00
res.setHeader("Cache-Control", "no-cache");
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Connection", "keep-alive");
2023-10-19 21:23:58 +02:00
res.flushHeaders(); // flush the headers to establish SSE with client
const workflow = activeWorkflows[req.params.workflowUuid];
workflow.eventStream = res;
2024-01-04 20:17:54 -05:00
res.on("close", () => {
2023-10-19 21:23:58 +02:00
res.end();
// TODO: Abort if not already done?
});
2023-10-19 19:46:23 +02:00
});
router.get("/result/:workflowUuid", async (req: Request, res: Response) => {
2023-10-20 00:10:03 +02:00
if(!req.params.workflowUuid) {
res.status(400).json({"error": "No workflowUuid weres provided."});
return;
}
if(!activeWorkflows.hasOwnProperty(req.params.workflowUuid)) {
res.status(400).json({"error": `No workflow with workflowUuid "${req.params.workflowUuid}" was found.`});
return;
}
2023-10-19 19:46:23 +02:00
/*
* If workflow isn't done return error
2023-11-21 00:20:19 +01:00
* Send file, if there are multiple outputs return as zip
2023-10-19 19:46:23 +02:00
* If download is done, delete results / allow deletion within the next 5-60 mins
2023-11-13 00:09:12 +01:00
*/
2023-10-19 19:46:23 +02:00
const workflow = activeWorkflows[req.params.workflowUuid];
if(!workflow.finished) {
res.status(202).json({ message: "Workflow hasn't finished yet. Check progress or connect to progress-steam to get notified when its done." });
2024-01-04 20:17:54 -05:00
return;
2023-10-19 19:46:23 +02:00
}
await respondWithPdfFiles(res, workflow.result, "workflow-results");
2023-10-19 19:46:23 +02:00
// Delete workflow / results when done.
delete activeWorkflows[req.params.workflowUuid];
});
router.post("/abort/:workflowUuid", (req: Request, res: Response) => {
2023-10-20 00:10:03 +02:00
if(!req.params.workflowUuid) {
res.status(400).json({"error": "No workflowUuid weres provided."});
return;
}
if(!activeWorkflows.hasOwnProperty(req.params.workflowUuid)) {
res.status(400).json({"error": `No workflow with workflowUuid "${req.params.workflowUuid}" was found.`});
return;
}
2023-10-19 19:46:23 +02:00
// TODO: Abort workflow
res.status(501).json({"warning": "Abortion has not been implemented yet."});
});
function generateWorkflowID() {
return crypto.randomUUID();
}
export default router;