mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 03:20:46 +02:00
fix: harden multi-file response detection so merge can't fail silently (#6516)
Co-authored-by: Reece Browne <[email protected]>
This commit is contained in:
co-authored by
Reece Browne
parent
cb687fbf99
commit
bd9ef0586b
@@ -0,0 +1,58 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { processMultiFileResponse } from "@app/utils/automationExecutor";
|
||||
|
||||
// Regression coverage for the automation-side mirror of the merge bug:
|
||||
// non-canonical Content-Types previously misrouted a PDF into ZIP extraction
|
||||
// and yielded a bogus `automation_*.zip`.
|
||||
|
||||
const PDF_BYTES = new Uint8Array([
|
||||
0x25,
|
||||
0x50,
|
||||
0x44,
|
||||
0x46,
|
||||
0x2d,
|
||||
0x31,
|
||||
0x2e,
|
||||
0x37, // "%PDF-1.7"
|
||||
0x0a,
|
||||
0x25,
|
||||
0xe2,
|
||||
0xe3,
|
||||
0xcf,
|
||||
0xd3,
|
||||
0x0a,
|
||||
]);
|
||||
|
||||
const inputFiles = [
|
||||
new File(["fake"], "input.pdf", { type: "application/pdf" }),
|
||||
];
|
||||
|
||||
async function run(contentType: string) {
|
||||
return processMultiFileResponse(
|
||||
new Blob([PDF_BYTES]),
|
||||
{ "content-type": contentType },
|
||||
inputFiles,
|
||||
"automated_",
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
describe("processMultiFileResponse (automation execution)", () => {
|
||||
test('PDF body + "application/octet-stream" -> PDF, not .zip', async () => {
|
||||
const result = await run("application/octet-stream");
|
||||
expect(result.length).toBe(1);
|
||||
expect(result[0].name).not.toMatch(/\.zip$/);
|
||||
});
|
||||
|
||||
test('PDF body + "application/pdf;charset=UTF-8" -> PDF, not .zip', async () => {
|
||||
const result = await run("application/pdf;charset=UTF-8");
|
||||
expect(result.length).toBe(1);
|
||||
expect(result[0].name).not.toMatch(/\.zip$/);
|
||||
});
|
||||
|
||||
test('PDF body + "APPLICATION/PDF" -> PDF, not .zip', async () => {
|
||||
const result = await run("APPLICATION/PDF");
|
||||
expect(result.length).toBe(1);
|
||||
expect(result[0].name).not.toMatch(/\.zip$/);
|
||||
});
|
||||
});
|
||||
@@ -4,54 +4,48 @@ import { ToolId } from "@app/types/toolId";
|
||||
import { AUTOMATION_CONSTANTS } from "@app/constants/automation";
|
||||
import { AutomationFileProcessor } from "@app/utils/automationFileProcessor";
|
||||
import { ToolType } from "@app/hooks/tools/shared/useToolOperation";
|
||||
import { zipFileService } from "@app/services/zipFileService";
|
||||
import { processResponse } from "@app/utils/toolResponseProcessor";
|
||||
|
||||
/**
|
||||
* Process multi-file tool response (handles ZIP or single PDF responses)
|
||||
*/
|
||||
const processMultiFileResponse = async (
|
||||
export const processMultiFileResponse = async (
|
||||
responseData: Blob,
|
||||
responseHeaders: any,
|
||||
files: File[],
|
||||
filePrefix: string,
|
||||
preserveBackendFilename?: boolean,
|
||||
): Promise<File[]> => {
|
||||
// Multi-file responses are typically ZIP files, but may be single files (e.g. split with merge=true)
|
||||
if (
|
||||
responseData.type === "application/pdf" ||
|
||||
(responseHeaders && responseHeaders["content-type"] === "application/pdf")
|
||||
) {
|
||||
// Single PDF response - use processResponse to respect preserveBackendFilename
|
||||
const processedFiles = await processResponse(
|
||||
const contentTypeHeader = responseHeaders?.["content-type"];
|
||||
const looksLikeZip = await zipFileService.isZipResponse(
|
||||
responseData,
|
||||
typeof contentTypeHeader === "string" ? contentTypeHeader : undefined,
|
||||
);
|
||||
|
||||
if (!looksLikeZip) {
|
||||
return processResponse(
|
||||
responseData,
|
||||
files,
|
||||
filePrefix,
|
||||
undefined,
|
||||
preserveBackendFilename ? responseHeaders : undefined,
|
||||
);
|
||||
return processedFiles;
|
||||
} else {
|
||||
// ZIP response
|
||||
const result =
|
||||
await AutomationFileProcessor.extractAutomationZipFiles(responseData);
|
||||
|
||||
if (result.errors.length > 0) {
|
||||
console.warn(`⚠️ File processing warnings:`, result.errors);
|
||||
}
|
||||
|
||||
// Apply prefix to files, replacing any existing prefix
|
||||
const processedFiles =
|
||||
filePrefix && !preserveBackendFilename
|
||||
? result.files.map((file) => {
|
||||
const nameWithoutPrefix = file.name.replace(/^[^_]*_/, "");
|
||||
return new File([file], `${filePrefix}${nameWithoutPrefix}`, {
|
||||
type: file.type,
|
||||
});
|
||||
})
|
||||
: result.files;
|
||||
|
||||
return processedFiles;
|
||||
}
|
||||
|
||||
const result =
|
||||
await AutomationFileProcessor.extractAutomationZipFiles(responseData);
|
||||
|
||||
if (result.errors.length > 0) {
|
||||
console.warn(`⚠️ File processing warnings:`, result.errors);
|
||||
}
|
||||
|
||||
if (!filePrefix || preserveBackendFilename) {
|
||||
return result.files;
|
||||
}
|
||||
return result.files.map((file) => {
|
||||
const nameWithoutPrefix = file.name.replace(/^[^_]*_/, "");
|
||||
return new File([file], `${filePrefix}${nameWithoutPrefix}`, {
|
||||
type: file.type,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -19,18 +19,6 @@ export interface AutomationProcessingResult {
|
||||
}
|
||||
|
||||
export class AutomationFileProcessor {
|
||||
/**
|
||||
* Check if a blob is a ZIP file by examining its header
|
||||
*/
|
||||
static isZipFile(blob: Blob): boolean {
|
||||
// This is a simple check - in a real implementation you might want to read the first few bytes
|
||||
// For now, we'll rely on the extraction attempt and fallback
|
||||
return (
|
||||
blob.type === "application/zip" ||
|
||||
blob.type === "application/x-zip-compressed"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract files from a ZIP blob during automation execution, with fallback for non-ZIP files
|
||||
* Extracts all file types (PDFs, images, etc.) except HTML files which stay zipped
|
||||
|
||||
Reference in New Issue
Block a user