diff --git a/server-node/src/routes/api/workflow-controller.ts b/server-node/src/routes/api/workflow-controller.ts index a84e42f20..ec91cdd40 100644 --- a/server-node/src/routes/api/workflow-controller.ts +++ b/server-node/src/routes/api/workflow-controller.ts @@ -7,7 +7,17 @@ import { traverseOperations } from "@stirling-pdf/shared-operations/src/workflow import { PdfFile, RepresentationType } from '@stirling-pdf/shared-operations/src/wrappers/PdfFile'; import { respondWithPdfFiles } from '../../utils/endpoint-utils'; -const activeWorkflows: any = {}; +interface Workflow { + 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 = {}; const router = express.Router(); @@ -47,10 +57,17 @@ router.post("/:workflowUuid?", [ console.log("Download"); await respondWithPdfFiles(res, pdfResults, "workflow-results"); }).catch((err) => { - if(err.validationError) + if(err.validationError) { + // Bad Request res.status(400).json({error: err}); - else + } + 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; + } }) } else { @@ -62,10 +79,7 @@ router.post("/:workflowUuid?", [ activeWorkflows[workflowID] = { createdAt: Date.now(), - finished: false, - eventStream: null, - result: null, - // TODO: When auth is implemented: owner + finished: false } const activeWorkflow = activeWorkflows[workflowID]; @@ -77,20 +91,46 @@ router.post("/:workflowUuid?", [ } }); - // TODO: Handle when this throws errors - let pdfResults = await traverseOperations(workflow.operations, inputs, (state) => { + traverseOperations(workflow.operations, inputs, (state) => { console.log("State: ", state); if(activeWorkflow.eventStream) activeWorkflow.eventStream.write(`data: ${state}\n\n`); - }) + }).then(async (pdfResults) => { + if(activeWorkflow.eventStream) { + 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(); - if(activeWorkflow.eventStream) { - activeWorkflow.eventStream.write(`data: processing done\n\n`); - activeWorkflow.eventStream.end(); - } + // 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(); - activeWorkflow.result = pdfResults; - activeWorkflow.finished = true; + // Internal Server Error + if(activeWorkflow.eventStream) { + activeWorkflow.eventStream.write(`data: ${activeWorkflow.error}\n\n`); + activeWorkflow.eventStream.end(); + } + } else { + throw err; + } + }); } } ]); @@ -107,7 +147,7 @@ router.get("/progress/:workflowUuid", (req: Request, res: Response) => { // Return current progress const workflow = activeWorkflows[req.params.workflowUuid]; - res.status(200).json({ createdAt: workflow.createdAt, finished: workflow.finished }); + res.status(200).json({ createdAt: workflow.createdAt, finished: workflow.finished, finishedAt: workflow.finishedAt, error: workflow.error }); }); router.get("/progress-stream/:workflowUuid", (req: Request, res: Response) => { diff --git a/shared-operations/src/functions/impose.ts b/shared-operations/src/functions/impose.ts index fddda60d5..f34545274 100644 --- a/shared-operations/src/functions/impose.ts +++ b/shared-operations/src/functions/impose.ts @@ -21,7 +21,7 @@ export class Impose extends Operator { /** PDF-Imposition, PDF-N-Up: Put multiple pages of the input document into a single page of the output document. - see: {@link https://en.wikipedia.org/wiki/N-up} */ async run(input: PdfFile[], progressCallback: (state: Progress) => void): Promise { - return this.nToN(input, async (input, index, max) => { + return this.oneToOne(input, async (input, index, max) => { // https://pdfcpu.io/generate/nup.html const uint8Array = await pdfcpuWrapper.oneToOne( [ @@ -47,7 +47,7 @@ export class Impose extends Operator { progressCallback({ curFileProgress: 1, operationProgress: index/max }) console.log("ImposeResult: ", result); - return [result]; + return result; }) } diff --git a/shared-operations/src/functions/index.ts b/shared-operations/src/functions/index.ts index 758a251a7..480f64dd5 100644 --- a/shared-operations/src/functions/index.ts +++ b/shared-operations/src/functions/index.ts @@ -4,10 +4,15 @@ export enum IOType { PDF, Image, Text // TODO: Extend with Document File Types } +export interface ValidationResult { + valid: boolean, + reason?: string +} + export interface Progress { - /** 0-1 */ + /** A percentage between 0-1 describing the progress on the currently processed file */ curFileProgress: number, - /** 0-1 */ + /** A percentage between 0-1 describing the progress on all input files / operations */ operationProgress: number, } @@ -25,27 +30,24 @@ export class Operator { this.actionValues = action.values; } - // TODO: Type callback state, it should give updates on the progress of the current operator async run(input: any[], progressCallback: (progress: Progress) => void): Promise { return []; } - validate(): { valid: boolean, reason?: string } { + validate(): ValidationResult { if(!this.actionValues) { return { valid: false, reason: "The Operators action values were empty."} } return { valid: true }; } + /** This function should be used if the Operation may take multiple files as inputs and only outputs one file */ protected async nToOne (inputs: I[], callback: (input: I[]) => Promise): Promise { return [await callback(inputs)]; } + /** This function should be used if the Operation takes one file as input and may output multiple files */ protected async oneToN (inputs: I[], callback: (input: I, index: number, max: number) => Promise): Promise { - return this.nToN(inputs, callback); // nToN is able to handle single inputs now. - } - - protected async nToN (inputs: I[], callback: (input: I, index: number, max: number) => Promise): Promise { let output: O[] = [] for (let i = 0; i < inputs.length; i++) { output = output.concat(await callback(inputs[i], i, inputs.length)); @@ -53,4 +55,11 @@ export class Operator { return output; } + + /** This function should be used if the Operation takes one file as input and outputs only one file */ + protected async oneToOne (inputs: I[], callback: (input: I, index: number, max: number) => Promise): Promise { + return this.oneToN(inputs, async (input, index, max) => { + return [await callback(input, index, max)] + }); + } } \ No newline at end of file diff --git a/shared-operations/src/workflow/getOperatorByName.ts b/shared-operations/src/workflow/getOperatorByName.ts index 289736b44..f7da50479 100644 --- a/shared-operations/src/workflow/getOperatorByName.ts +++ b/shared-operations/src/workflow/getOperatorByName.ts @@ -1,6 +1,6 @@ import { Operator } from "../functions"; -// TODO: Import other Operators +// TODO: Import other Operators (could make this dynamic?) import { Impose } from "../functions/impose"; export const Operators = { Impose: Impose @@ -23,4 +23,9 @@ export function getOperatorByName(name: string): typeof Operator { }); return foundClass; +} + +export function listOperatorNames(): string[] { + // TODO: Implement this + return } \ No newline at end of file diff --git a/shared-operations/src/workflow/traverseOperations.ts b/shared-operations/src/workflow/traverseOperations.ts index fbf1bf139..366791f6c 100644 --- a/shared-operations/src/workflow/traverseOperations.ts +++ b/shared-operations/src/workflow/traverseOperations.ts @@ -42,78 +42,13 @@ export async function traverseOperations(operations: Action[], input: PdfFile[], case "wait": const waitOperation = waitOperations[(action as WaitAction).values.id]; - if(Array.isArray(input)) { - waitOperation.input.concat(input); // TODO: May have unexpected concequences. Needs further testing! - } - else { - waitOperation.input.push(input); - } + waitOperation.input.concat(input); // TODO: May have unexpected concequences. Needs further testing! waitOperation.waitCount--; if(waitOperation.waitCount == 0 && waitOperation.doneOperation.actions) { await nextOperation(waitOperation.doneOperation.actions, waitOperation.input, progressCallback); } break; - /*case "extract": - yield* nToN(input, action, async (input) => { - const newPdf = await Operations.extractPages({file: input, pageIndexes: action.values["pageIndexes"]}); - return newPdf; - }); - break; - case "impose": - let impose = new Impose(action); - input = await impose.run(input, progressCallback); - await nextOperation(action.actions, input, progressCallback); - break; - case "merge": - yield* nToOne(input, action, async (inputs) => { - const newPdf = await Operations.mergePDFs({files: inputs}); - return newPdf; - }); - break; - case "removeBlankPages": - yield* nToN(input, action, async (input) => { - const newPdf = await Operations.removeBlankPages({file: input, whiteThreashold: action.values["whiteThreashold"]}); - return newPdf; - }); - break; - case "rotate": - yield* nToN(input, action, async (input) => { - const newPdf = await Operations.rotatePages({file: input, rotation: action.values["rotation"]}); - return newPdf; - }); - break; - case "sortPagesWithPreset": - yield* nToN(input, action, async (input) => { - const newPdf = await Operations.arrangePages({file: input, arrangementConfig: action.values["arrangementConfig"]}); - return newPdf; - }); - break; - case "split": - // TODO: A split might break the done condition, it may count multiple times. Needs further testing! - yield* oneToN(input, action, async (input) => { - const splitResult = await Operations.splitPdfByIndex({file: input, pageIndexes: action.values["splitAfterPageArray"]}); - for (let j = 0; j < splitResult.length; j++) { - splitResult[j].filename = splitResult[j].filename + "_split" + j; - } - return splitResult; - }); - break; - case "splitOn": - yield* oneToN(input, action, async (input) => { - const splitResult = await Operations.splitPagesByPreset({file: input, type: action.values["type"], whiteThreashold: action.values["whiteThreashold"]}); - for (let j = 0; j < splitResult.length; j++) { - splitResult[j].filename = splitResult[j].filename + "_split" + j; - } - return splitResult; - }); - break; - case "updateMetadata": - yield* nToN(input, action, async (input) => { - const newPdf = await Operations.updateMetadata({file: input, ...action.values["metadata"]}); - return newPdf; - }); - break;*/ default: const operator = getOperatorByName(action.type); if(operator) {