diff --git a/app/common/src/main/java/stirling/software/common/service/FileStorage.java b/app/common/src/main/java/stirling/software/common/service/FileStorage.java index 46b4a57082..8b0f3a53a2 100644 --- a/app/common/src/main/java/stirling/software/common/service/FileStorage.java +++ b/app/common/src/main/java/stirling/software/common/service/FileStorage.java @@ -1,8 +1,10 @@ package stirling.software.common.service; import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; import java.util.UUID; @@ -10,6 +12,7 @@ import java.util.UUID; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -143,6 +146,24 @@ public class FileStorage { return new StoredFile(fileId, size); } + public String storeFromStreamingBody(StreamingResponseBody body, String originalName) + throws IOException { + String fileId = generateFileId(); + Path filePath = getFilePath(fileId); + Files.createDirectories(filePath.getParent()); + boolean success = false; + try (OutputStream os = new BufferedOutputStream(Files.newOutputStream(filePath))) { + body.writeTo(os); + success = true; + } finally { + if (!success) { + Files.deleteIfExists(filePath); + } + } + log.debug("Stored StreamingResponseBody with ID: {}", fileId); + return fileId; + } + /** * Delete a file by its ID * diff --git a/app/common/src/main/java/stirling/software/common/service/JobExecutorService.java b/app/common/src/main/java/stirling/software/common/service/JobExecutorService.java index 459e77c0fa..dd53eef3c1 100644 --- a/app/common/src/main/java/stirling/software/common/service/JobExecutorService.java +++ b/app/common/src/main/java/stirling/software/common/service/JobExecutorService.java @@ -16,6 +16,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import jakarta.servlet.http.HttpServletRequest; @@ -305,33 +306,21 @@ public class JobExecutorService { Object body = response.getBody(); if (body instanceof byte[]) { - // Extract filename from content-disposition header if available - String filename = "result.pdf"; - String contentType = MediaType.APPLICATION_PDF_VALUE; + String filename = extractResponseFilename(response); + String contentType = extractResponseContentType(response); - if (response.getHeaders().getContentDisposition() != null) { - String disposition = - response.getHeaders().getContentDisposition().toString(); - if (disposition.contains("filename=")) { - filename = - disposition.substring( - disposition.indexOf("filename=") + 9, - disposition.lastIndexOf('"')); - } - } - - MediaType mediaType = response.getHeaders().getContentType(); - - if (mediaType != null) { - contentType = mediaType.toString(); - } - - // Store byte array directly to disk String fileId = fileStorage.storeBytes((byte[]) body, filename); taskManager.setFileResult(jobId, fileId, filename, contentType); log.debug("Stored ResponseEntity result with fileId: {}", fileId); + } else if (body instanceof StreamingResponseBody streamingBody) { + String filename = extractResponseFilename(response); + String contentType = extractResponseContentType(response); - // Let the GC handle the memory naturally + String fileId = fileStorage.storeFromStreamingBody(streamingBody, filename); + taskManager.setFileResult(jobId, fileId, filename, contentType); + log.debug( + "Stored ResponseEntity result with fileId: {}", + fileId); } else { // Check if the response body contains a fileId if (body != null && body.toString().contains("fileId")) { @@ -481,6 +470,21 @@ public class JobExecutorService { } } + private static String extractResponseFilename(ResponseEntity response) { + if (response.getHeaders().getContentDisposition() != null) { + String filename = response.getHeaders().getContentDisposition().getFilename(); + if (filename != null && !filename.isEmpty()) { + return filename; + } + } + return "result.pdf"; + } + + private static String extractResponseContentType(ResponseEntity response) { + MediaType mediaType = response.getHeaders().getContentType(); + return mediaType != null ? mediaType.toString() : MediaType.APPLICATION_PDF_VALUE; + } + /** * Parse session timeout string (e.g., "30m", "1h") to milliseconds * diff --git a/app/common/src/main/java/stirling/software/common/service/JobQueue.java b/app/common/src/main/java/stirling/software/common/service/JobQueue.java index 595cc6e395..28d94baced 100644 --- a/app/common/src/main/java/stirling/software/common/service/JobQueue.java +++ b/app/common/src/main/java/stirling/software/common/service/JobQueue.java @@ -401,7 +401,7 @@ public class JobQueue implements SmartLifecycle { * @throws Exception If there is an execution error */ private T executeWithTimeout(Supplier supplier, long timeoutMs) throws Exception { - CompletableFuture future = CompletableFuture.supplyAsync(supplier); + CompletableFuture future = CompletableFuture.supplyAsync(supplier, jobExecutor); try { if (timeoutMs <= 0) { diff --git a/app/common/src/main/java/stirling/software/common/service/PostHogService.java b/app/common/src/main/java/stirling/software/common/service/PostHogService.java index 1ec3e87946..92093762fc 100644 --- a/app/common/src/main/java/stirling/software/common/service/PostHogService.java +++ b/app/common/src/main/java/stirling/software/common/service/PostHogService.java @@ -7,11 +7,8 @@ import java.lang.management.MemoryMXBean; import java.lang.management.OperatingSystemMXBean; import java.lang.management.RuntimeMXBean; import java.lang.management.ThreadMXBean; -import java.net.InetAddress; -import java.net.NetworkInterface; import java.nio.file.Files; import java.nio.file.Paths; -import java.util.Enumeration; import java.util.HashMap; import java.util.Locale; import java.util.Map; @@ -94,21 +91,12 @@ public class PostHogService { metrics.put("os_name", System.getProperty("os.name")); metrics.put("os_version", System.getProperty("os.version")); metrics.put("java_version", System.getProperty("java.version")); - metrics.put("user_name", System.getProperty("user.name")); - metrics.put("user_home", System.getProperty("user.home")); - metrics.put("user_dir", System.getProperty("user.dir")); // CPU and Memory metrics.put("cpu_cores", Runtime.getRuntime().availableProcessors()); metrics.put("total_memory", Runtime.getRuntime().totalMemory()); metrics.put("free_memory", Runtime.getRuntime().freeMemory()); - // Network and Server Identity - InetAddress localHost = InetAddress.getLocalHost(); - metrics.put("ip_address", localHost.getHostAddress()); - metrics.put("hostname", localHost.getHostName()); - metrics.put("mac_address", getMacAddress()); - // JVM info metrics.put("jvm_vendor", System.getProperty("java.vendor")); metrics.put("jvm_version", System.getProperty("java.vm.version")); @@ -153,9 +141,6 @@ public class PostHogService { metrics.put("gc_" + gcBean.getName() + "_time", gcBean.getCollectionTime()); } - // Network interfaces - metrics.put("network_interfaces", getNetworkInterfacesInfo()); - // Docker detection and stats boolean isDocker = isRunningInDocker(); if (isDocker) { @@ -353,30 +338,6 @@ public class PostHogService { .getProFeatures() .getCustomMetadata() .isAutoUpdateMetadata()); - addIfNotEmpty( - properties, - "enterpriseEdition_customMetadata_author", - applicationProperties - .getPremium() - .getProFeatures() - .getCustomMetadata() - .getAuthor()); - addIfNotEmpty( - properties, - "enterpriseEdition_customMetadata_creator", - applicationProperties - .getPremium() - .getProFeatures() - .getCustomMetadata() - .getCreator()); - addIfNotEmpty( - properties, - "enterpriseEdition_customMetadata_producer", - applicationProperties - .getPremium() - .getProFeatures() - .getCustomMetadata() - .getProducer()); } // Capture AutoPipeline properties addIfNotEmpty( @@ -386,39 +347,4 @@ public class PostHogService { return properties; } - - private String getMacAddress() { - try { - Enumeration networkInterfaces = - NetworkInterface.getNetworkInterfaces(); - while (networkInterfaces.hasMoreElements()) { - NetworkInterface ni = networkInterfaces.nextElement(); - byte[] hardwareAddress = ni.getHardwareAddress(); - if (hardwareAddress != null) { - String[] hexadecimal = new String[hardwareAddress.length]; - for (int i = 0; i < hardwareAddress.length; i++) { - hexadecimal[i] = String.format("%02X", hardwareAddress[i]); - } - return String.join("-", hexadecimal); - } - } - } catch (Exception e) { - // Handle exception - } - return "Unknown"; - } - - private Map getNetworkInterfacesInfo() { - Map interfacesInfo = new HashMap<>(); - try { - Enumeration nets = NetworkInterface.getNetworkInterfaces(); - while (nets.hasMoreElements()) { - NetworkInterface netint = nets.nextElement(); - interfacesInfo.put(netint.getName(), netint.getDisplayName()); - } - } catch (Exception e) { - interfacesInfo.put("error", e.getMessage()); - } - return interfacesInfo; - } } diff --git a/app/common/src/main/java/stirling/software/common/util/FileToPdf.java b/app/common/src/main/java/stirling/software/common/util/FileToPdf.java index 3f8c3aa5c3..83bc0e35bc 100644 --- a/app/common/src/main/java/stirling/software/common/util/FileToPdf.java +++ b/app/common/src/main/java/stirling/software/common/util/FileToPdf.java @@ -1,6 +1,5 @@ package stirling.software.common.util; -import java.io.ByteArrayInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.UncheckedIOException; @@ -66,16 +65,7 @@ public class FileToPdf { ProcessExecutor.getInstance(ProcessExecutor.Processes.WEASYPRINT) .runCommandWithOutputHandling(command); - byte[] pdfBytes = Files.readAllBytes(tempOutputFile.getPath()); - try { - return pdfBytes; - } catch (Exception e) { - pdfBytes = Files.readAllBytes(tempOutputFile.getPath()); - if (pdfBytes.length < 1) { - throw e; - } - return pdfBytes; - } + return Files.readAllBytes(tempOutputFile.getPath()); } // tempInputFile auto-closed } // tempOutputFile auto-closed } @@ -92,8 +82,7 @@ public class FileToPdf { throws IOException { try (TempDirectory tempUnzippedDir = new TempDirectory(tempFileManager)) { try (ZipInputStream zipIn = - ZipSecurity.createHardenedInputStream( - new ByteArrayInputStream(Files.readAllBytes(zipFilePath)))) { + ZipSecurity.createHardenedInputStream(Files.newInputStream(zipFilePath))) { ZipEntry entry = zipIn.getNextEntry(); while (entry != null) { Path filePath = diff --git a/app/common/src/main/java/stirling/software/common/util/PDFToFile.java b/app/common/src/main/java/stirling/software/common/util/PDFToFile.java index 680eb50d62..1300e165ff 100644 --- a/app/common/src/main/java/stirling/software/common/util/PDFToFile.java +++ b/app/common/src/main/java/stirling/software/common/util/PDFToFile.java @@ -1,9 +1,9 @@ package stirling.software.common.util; -import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; +import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; @@ -20,6 +20,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import com.vladsch.flexmark.html2md.converter.FlexmarkHtmlConverter; import com.vladsch.flexmark.util.data.MutableDataSet; @@ -48,7 +49,7 @@ public class PDFToFile { this.runtimePathConfig = runtimePathConfig; } - public ResponseEntity processPdfToMarkdown(MultipartFile inputFile) + public ResponseEntity processPdfToMarkdown(MultipartFile inputFile) throws IOException, InterruptedException { if (!MediaType.APPLICATION_PDF_VALUE.equals(inputFile.getContentType())) { return new ResponseEntity<>(HttpStatus.BAD_REQUEST); @@ -85,78 +86,77 @@ public class PDFToFile { pdfBaseName = originalPdfFileName.substring(0, originalPdfFileName.lastIndexOf('.')); } - byte[] fileBytes; - String fileName; + String fileName = pdfBaseName + "ToMarkdown.zip"; + TempFile finalOut = tempFileManager.createManagedTempFile(".zip"); + try { + try (TempFile tempInputFile = new TempFile(tempFileManager, ".pdf"); + TempDirectory tempOutputDir = new TempDirectory(tempFileManager)) { + inputFile.transferTo(tempInputFile.getFile()); - try (TempFile tempInputFile = new TempFile(tempFileManager, ".pdf"); - TempDirectory tempOutputDir = new TempDirectory(tempFileManager)) { - inputFile.transferTo(tempInputFile.getFile()); + List command = + new ArrayList<>( + Arrays.asList( + "pdftohtml", + "-s", + "-noframes", + "-c", + tempInputFile.getAbsolutePath(), + pdfBaseName)); - List command = - new ArrayList<>( - Arrays.asList( - "pdftohtml", - "-s", - "-noframes", - "-c", - tempInputFile.getAbsolutePath(), - pdfBaseName)); + ProcessExecutorResult returnCode = + ProcessExecutor.getInstance(ProcessExecutor.Processes.PDFTOHTML) + .runCommandWithOutputHandling( + command, tempOutputDir.getPath().toFile()); + // Process HTML files to Markdown + File[] outputFiles = + Objects.requireNonNull(tempOutputDir.getPath().toFile().listFiles()); + List markdownFiles = new ArrayList<>(); + List imageFiles = new ArrayList<>(); - ProcessExecutorResult returnCode = - ProcessExecutor.getInstance(ProcessExecutor.Processes.PDFTOHTML) - .runCommandWithOutputHandling( - command, tempOutputDir.getPath().toFile()); - // Process HTML files to Markdown - File[] outputFiles = - Objects.requireNonNull(tempOutputDir.getPath().toFile().listFiles()); - List markdownFiles = new ArrayList<>(); - List imageFiles = new ArrayList<>(); + // Convert HTML files to Markdown and collect image files + for (File outputFile : outputFiles) { + if (outputFile.getName().endsWith(".html")) { + String html = Files.readString(outputFile.toPath()); + String markdown = htmlToMarkdownConverter.convert(html); - // Convert HTML files to Markdown and collect image files - for (File outputFile : outputFiles) { - if (outputFile.getName().endsWith(".html")) { - String html = Files.readString(outputFile.toPath()); - String markdown = htmlToMarkdownConverter.convert(html); + // Update image references to point to images/ folder + markdown = updateImageReferences(markdown); - // Update image references to point to images/ folder - markdown = updateImageReferences(markdown); + String mdFileName = outputFile.getName().replace(".html", ".md"); + File mdFile = new File(tempOutputDir.getPath().toFile(), mdFileName); + Files.writeString(mdFile.toPath(), markdown); + markdownFiles.add(mdFile); + } else if (!outputFile.getName().endsWith(".md")) { + // Collect non-HTML, non-MD files as images/assets + imageFiles.add(outputFile); + } + } - String mdFileName = outputFile.getName().replace(".html", ".md"); - File mdFile = new File(tempOutputDir.getPath().toFile(), mdFileName); - Files.writeString(mdFile.toPath(), markdown); - markdownFiles.add(mdFile); - } else if (!outputFile.getName().endsWith(".md")) { - // Collect non-HTML, non-MD files as images/assets - imageFiles.add(outputFile); + try (OutputStream fos = Files.newOutputStream(finalOut.getPath()); + ZipOutputStream zipOutputStream = new ZipOutputStream(fos)) { + // Add markdown files to root of ZIP + for (File mdFile : markdownFiles) { + ZipEntry mdEntry = new ZipEntry(mdFile.getName()); + zipOutputStream.putNextEntry(mdEntry); + Files.copy(mdFile.toPath(), zipOutputStream); + zipOutputStream.closeEntry(); + } + + // Add images and other assets to images/ folder + for (File imageFile : imageFiles) { + ZipEntry assetEntry = new ZipEntry("images/" + imageFile.getName()); + zipOutputStream.putNextEntry(assetEntry); + Files.copy(imageFile.toPath(), zipOutputStream); + zipOutputStream.closeEntry(); + } } } - - // Always create a ZIP file - fileName = pdfBaseName + "ToMarkdown.zip"; - ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); - - try (ZipOutputStream zipOutputStream = new ZipOutputStream(byteArrayOutputStream)) { - // Add markdown files to root of ZIP - for (File mdFile : markdownFiles) { - ZipEntry mdEntry = new ZipEntry(mdFile.getName()); - zipOutputStream.putNextEntry(mdEntry); - Files.copy(mdFile.toPath(), zipOutputStream); - zipOutputStream.closeEntry(); - } - - // Add images and other assets to images/ folder - for (File imageFile : imageFiles) { - ZipEntry assetEntry = new ZipEntry("images/" + imageFile.getName()); - zipOutputStream.putNextEntry(assetEntry); - Files.copy(imageFile.toPath(), zipOutputStream); - zipOutputStream.closeEntry(); - } - } - - fileBytes = byteArrayOutputStream.toByteArray(); + } catch (Exception e) { + finalOut.close(); + throw e; } - return WebResponseUtils.bytesToWebResponse( - fileBytes, fileName, MediaType.APPLICATION_OCTET_STREAM); + return WebResponseUtils.fileToWebResponse( + finalOut, fileName, MediaType.APPLICATION_OCTET_STREAM); } /** @@ -169,7 +169,7 @@ public class PDFToFile { return PATTERN.matcher(markdown).replaceAll("$1(images/$2)"); } - public ResponseEntity processPdfToHtml(MultipartFile inputFile) + public ResponseEntity processPdfToHtml(MultipartFile inputFile) throws IOException, InterruptedException { if (!MediaType.APPLICATION_PDF_VALUE.equals(inputFile.getContentType())) { return new ResponseEntity<>(HttpStatus.BAD_REQUEST); @@ -182,56 +182,57 @@ public class PDFToFile { pdfBaseName = originalPdfFileName.substring(0, originalPdfFileName.lastIndexOf('.')); } - byte[] fileBytes; - String fileName; + String fileName = pdfBaseName + "ToHtml.zip"; + TempFile finalOut = tempFileManager.createManagedTempFile(".zip"); + try { + try (TempFile inputFileTemp = new TempFile(tempFileManager, ".pdf"); + TempDirectory outputDirTemp = new TempDirectory(tempFileManager)) { - try (TempFile inputFileTemp = new TempFile(tempFileManager, ".pdf"); - TempDirectory outputDirTemp = new TempDirectory(tempFileManager)) { + Path tempInputFile = inputFileTemp.getPath(); + Path tempOutputDir = outputDirTemp.getPath(); - Path tempInputFile = inputFileTemp.getPath(); - Path tempOutputDir = outputDirTemp.getPath(); + // Save the uploaded file to a temporary location + inputFile.transferTo(tempInputFile); - // Save the uploaded file to a temporary location - inputFile.transferTo(tempInputFile); + // Run the pdftohtml command with complex output + List command = + new ArrayList<>( + Arrays.asList( + "pdftohtml", "-c", tempInputFile.toString(), pdfBaseName)); - // Run the pdftohtml command with complex output - List command = - new ArrayList<>( - Arrays.asList( - "pdftohtml", "-c", tempInputFile.toString(), pdfBaseName)); + ProcessExecutorResult returnCode = + ProcessExecutor.getInstance(ProcessExecutor.Processes.PDFTOHTML) + .runCommandWithOutputHandling(command, tempOutputDir.toFile()); - ProcessExecutorResult returnCode = - ProcessExecutor.getInstance(ProcessExecutor.Processes.PDFTOHTML) - .runCommandWithOutputHandling(command, tempOutputDir.toFile()); + // Get output files + File[] outputFiles = Objects.requireNonNull(tempOutputDir.toFile().listFiles()); - // Get output files - File[] outputFiles = Objects.requireNonNull(tempOutputDir.toFile().listFiles()); - - // Return output files in a ZIP archive - fileName = pdfBaseName + "ToHtml.zip"; - ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); - try (ZipOutputStream zipOutputStream = new ZipOutputStream(byteArrayOutputStream)) { - for (File outputFile : outputFiles) { - ZipEntry entry = new ZipEntry(outputFile.getName()); - zipOutputStream.putNextEntry(entry); - try (FileInputStream fis = new FileInputStream(outputFile)) { - IOUtils.copy(fis, zipOutputStream); - } catch (IOException e) { - log.error("Exception writing zip entry", e); + try (OutputStream fos = Files.newOutputStream(finalOut.getPath()); + ZipOutputStream zipOutputStream = new ZipOutputStream(fos)) { + for (File outputFile : outputFiles) { + ZipEntry entry = new ZipEntry(outputFile.getName()); + zipOutputStream.putNextEntry(entry); + try (FileInputStream fis = new FileInputStream(outputFile)) { + IOUtils.copy(fis, zipOutputStream); + } catch (IOException e) { + log.error("Exception writing zip entry", e); + } + zipOutputStream.closeEntry(); } - zipOutputStream.closeEntry(); + } catch (IOException e) { + log.error("Exception writing zip", e); } - } catch (IOException e) { - log.error("Exception writing zip", e); } - fileBytes = byteArrayOutputStream.toByteArray(); + } catch (Exception e) { + finalOut.close(); + throw e; } - return WebResponseUtils.bytesToWebResponse( - fileBytes, fileName, MediaType.APPLICATION_OCTET_STREAM); + return WebResponseUtils.fileToWebResponse( + finalOut, fileName, MediaType.APPLICATION_OCTET_STREAM); } - public ResponseEntity processPdfToOfficeFormat( + public ResponseEntity processPdfToOfficeFormat( MultipartFile inputFile, String outputFormat, String libreOfficeFilter) throws IOException, InterruptedException { @@ -257,109 +258,115 @@ public class PDFToFile { return new ResponseEntity<>(HttpStatus.BAD_REQUEST); } - byte[] fileBytes; String fileName; - + TempFile finalOut = + tempFileManager.createManagedTempFile("." + resolvePrimaryExtension(outputFormat)); Path libreOfficeProfile = null; - try (TempFile inputFileTemp = new TempFile(tempFileManager, ".pdf"); - TempDirectory outputDirTemp = new TempDirectory(tempFileManager)) { + try { + try (TempFile inputFileTemp = new TempFile(tempFileManager, ".pdf"); + TempDirectory outputDirTemp = new TempDirectory(tempFileManager)) { - Path tempInputFile = inputFileTemp.getPath(); - Path tempOutputDir = outputDirTemp.getPath(); - Path unoOutputFile = - tempOutputDir.resolve( - pdfBaseName + "." + resolvePrimaryExtension(outputFormat)); + Path tempInputFile = inputFileTemp.getPath(); + Path tempOutputDir = outputDirTemp.getPath(); + Path unoOutputFile = + tempOutputDir.resolve( + pdfBaseName + "." + resolvePrimaryExtension(outputFormat)); - // Save the uploaded file to a temporary location - inputFile.transferTo(tempInputFile); + // Save the uploaded file to a temporary location + inputFile.transferTo(tempInputFile); - // Run the LibreOffice command - ProcessExecutorResult returnCode = null; - IOException unoconvertException = null; + // Run the LibreOffice command + ProcessExecutorResult returnCode = null; + IOException unoconvertException = null; - if (isUnoConvertEnabled()) { - try { - List unoCommand = - buildUnoConvertCommand( - tempInputFile, unoOutputFile, outputFormat, libreOfficeFilter); - returnCode = - ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE) - .runCommandWithOutputHandling(unoCommand); - } catch (IOException e) { - unoconvertException = e; - log.warn( - "Unoconvert command failed ({}). Falling back to soffice command.", - e.getMessage()); - } - } - - if (returnCode == null) { - // Run the LibreOffice command as a fallback - libreOfficeProfile = Files.createTempDirectory("libreoffice_profile_"); - List command = new ArrayList<>(); - command.add(runtimePathConfig.getSOfficePath()); - command.add("-env:UserInstallation=" + libreOfficeProfile.toUri().toString()); - command.add("--headless"); - command.add("--nologo"); - command.add("--infilter=" + libreOfficeFilter); - command.add("--convert-to"); - command.add(outputFormat); - command.add("--outdir"); - command.add(tempOutputDir.toString()); - command.add(tempInputFile.toString()); - - try { - returnCode = - ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE) - .runCommandWithOutputHandling(command); - } catch (IOException e) { - if (unoconvertException != null) { - e.addSuppressed(unoconvertException); + if (isUnoConvertEnabled()) { + try { + List unoCommand = + buildUnoConvertCommand( + tempInputFile, + unoOutputFile, + outputFormat, + libreOfficeFilter); + returnCode = + ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE) + .runCommandWithOutputHandling(unoCommand); + } catch (IOException e) { + unoconvertException = e; + log.warn( + "Unoconvert command failed ({}). Falling back to soffice command.", + e.getMessage()); } - throw e; } - } - // Get output files - List outputFiles = Arrays.asList(tempOutputDir.toFile().listFiles()); + if (returnCode == null) { + // Run the LibreOffice command as a fallback + libreOfficeProfile = Files.createTempDirectory("libreoffice_profile_"); + List command = new ArrayList<>(); + command.add(runtimePathConfig.getSOfficePath()); + command.add("-env:UserInstallation=" + libreOfficeProfile.toUri().toString()); + command.add("--headless"); + command.add("--nologo"); + command.add("--infilter=" + libreOfficeFilter); + command.add("--convert-to"); + command.add(outputFormat); + command.add("--outdir"); + command.add(tempOutputDir.toString()); + command.add(tempInputFile.toString()); - if (outputFiles.size() == 1) { - // Return single output file - File outputFile = outputFiles.get(0); - if ("txt:Text".equals(outputFormat)) { - outputFormat = "txt"; - } - fileName = pdfBaseName + "." + outputFormat; - fileBytes = FileUtils.readFileToByteArray(outputFile); - } else { - // Return output files in a ZIP archive - fileName = pdfBaseName + "To" + outputFormat + ".zip"; - ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); - try (ZipOutputStream zipOutputStream = new ZipOutputStream(byteArrayOutputStream)) { - for (File outputFile : outputFiles) { - ZipEntry entry = new ZipEntry(outputFile.getName()); - zipOutputStream.putNextEntry(entry); - try (FileInputStream fis = new FileInputStream(outputFile)) { - IOUtils.copy(fis, zipOutputStream); - } catch (IOException e) { - log.error("Exception writing zip entry", e); + try { + returnCode = + ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE) + .runCommandWithOutputHandling(command); + } catch (IOException e) { + if (unoconvertException != null) { + e.addSuppressed(unoconvertException); } - - zipOutputStream.closeEntry(); + throw e; } - } catch (IOException e) { - log.error("Exception writing zip", e); } - fileBytes = byteArrayOutputStream.toByteArray(); + // Get output files + List outputFiles = Arrays.asList(tempOutputDir.toFile().listFiles()); + + if (outputFiles.size() == 1) { + // Return single output file + File outputFile = outputFiles.get(0); + if ("txt:Text".equals(outputFormat)) { + outputFormat = "txt"; + } + fileName = pdfBaseName + "." + outputFormat; + FileUtils.copyFile(outputFile, finalOut.getFile()); + } else { + // Return output files in a ZIP archive + fileName = pdfBaseName + "To" + outputFormat + ".zip"; + try (OutputStream fos = Files.newOutputStream(finalOut.getPath()); + ZipOutputStream zipOutputStream = new ZipOutputStream(fos)) { + for (File outputFile : outputFiles) { + ZipEntry entry = new ZipEntry(outputFile.getName()); + zipOutputStream.putNextEntry(entry); + try (FileInputStream fis = new FileInputStream(outputFile)) { + IOUtils.copy(fis, zipOutputStream); + } catch (IOException e) { + log.error("Exception writing zip entry", e); + } + + zipOutputStream.closeEntry(); + } + } catch (IOException e) { + log.error("Exception writing zip", e); + } + } } + } catch (Exception e) { + finalOut.close(); + throw e; } finally { if (libreOfficeProfile != null) { FileUtils.deleteQuietly(libreOfficeProfile.toFile()); } } - return WebResponseUtils.bytesToWebResponse( - fileBytes, fileName, MediaType.APPLICATION_OCTET_STREAM); + return WebResponseUtils.fileToWebResponse( + finalOut, fileName, MediaType.APPLICATION_OCTET_STREAM); } private boolean isUnoConvertEnabled() { diff --git a/app/common/src/main/java/stirling/software/common/util/WebResponseUtils.java b/app/common/src/main/java/stirling/software/common/util/WebResponseUtils.java index a5132e2a19..ab8311dd4e 100644 --- a/app/common/src/main/java/stirling/software/common/util/WebResponseUtils.java +++ b/app/common/src/main/java/stirling/software/common/util/WebResponseUtils.java @@ -73,6 +73,19 @@ public class WebResponseUtils { return baosToWebResponse(baos, docName); } + public static ResponseEntity pdfDocToWebResponse( + PDDocument document, String docName, TempFileManager tempFileManager) + throws IOException { + TempFile tempFile = tempFileManager.createManagedTempFile(".pdf"); + try { + document.save(tempFile.getFile()); + } catch (IOException e) { + tempFile.close(); + throw e; + } + return pdfFileToWebResponse(tempFile, docName); + } + /** * Convert a File to a web response (PDF default). * @@ -108,23 +121,37 @@ public class WebResponseUtils { public static ResponseEntity fileToWebResponse( TempFile outputTempFile, String docName, MediaType mediaType) throws IOException { - Path path = outputTempFile.getFile().toPath().normalize(); - long len = Files.size(path); - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(mediaType); - headers.setContentLength(len); - headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + docName + "\""); + try { + Path path = outputTempFile.getFile().toPath().normalize(); + long len = Files.size(path); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(mediaType); + headers.setContentLength(len); + String encodedDocName = + RegexPatternUtils.getInstance() + .getPlusSignPattern() + .matcher(URLEncoder.encode(docName, StandardCharsets.UTF_8)) + .replaceAll("%20"); + headers.setContentDispositionFormData("attachment", encodedDocName); - StreamingResponseBody body = - os -> { - try (os) { - Files.copy(path, os); - os.flush(); - } finally { - outputTempFile.close(); - } - }; + StreamingResponseBody body = + os -> { + try (os) { + Files.copy(path, os); + os.flush(); + } finally { + outputTempFile.close(); + } + }; - return new ResponseEntity<>(body, headers, HttpStatus.OK); + return new ResponseEntity<>(body, headers, HttpStatus.OK); + } catch (IOException | RuntimeException e) { + try { + outputTempFile.close(); + } catch (Exception closeEx) { + e.addSuppressed(closeEx); + } + throw e; + } } } diff --git a/app/common/src/test/java/stirling/software/common/util/PDFToFileTest.java b/app/common/src/test/java/stirling/software/common/util/PDFToFileTest.java index 459b64a45c..69db94c34b 100644 --- a/app/common/src/test/java/stirling/software/common/util/PDFToFileTest.java +++ b/app/common/src/test/java/stirling/software/common/util/PDFToFileTest.java @@ -9,6 +9,7 @@ import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.when; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.nio.file.Files; @@ -29,6 +30,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.github.pixee.security.ZipSecurity; @@ -59,6 +61,19 @@ class PDFToFileTest { .thenAnswer( invocation -> Files.createTempFile("test", invocation.getArgument(0)).toFile()); + lenient() + .when(mockTempFileManager.createManagedTempFile(anyString())) + .thenAnswer( + invocation -> { + File f = + Files.createTempFile("test", invocation.getArgument(0)) + .toFile(); + TempFile tf = org.mockito.Mockito.mock(TempFile.class); + lenient().when(tf.getFile()).thenReturn(f); + lenient().when(tf.getPath()).thenReturn(f.toPath()); + lenient().when(tf.getAbsolutePath()).thenReturn(f.getAbsolutePath()); + return tf; + }); lenient() .when(mockTempFileManager.createTempDirectory()) .thenAnswer(invocation -> Files.createTempDirectory("test")); @@ -68,6 +83,12 @@ class PDFToFileTest { pdfToFile = new PDFToFile(mockTempFileManager, mockRuntimePathConfig); } + private static byte[] drain(ResponseEntity response) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + response.getBody().writeTo(baos); + return baos.toByteArray(); + } + @Test void testProcessPdfToMarkdown_InvalidContentType() throws IOException, InterruptedException { // Prepare @@ -79,7 +100,7 @@ class PDFToFileTest { "This is not a PDF".getBytes()); // Execute - ResponseEntity response = pdfToFile.processPdfToMarkdown(nonPdfFile); + ResponseEntity response = pdfToFile.processPdfToMarkdown(nonPdfFile); // Verify assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); @@ -96,7 +117,7 @@ class PDFToFileTest { "This is not a PDF".getBytes()); // Execute - ResponseEntity response = pdfToFile.processPdfToHtml(nonPdfFile); + ResponseEntity response = pdfToFile.processPdfToHtml(nonPdfFile); // Verify assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); @@ -114,7 +135,7 @@ class PDFToFileTest { "This is not a PDF".getBytes()); // Execute - ResponseEntity response = + ResponseEntity response = pdfToFile.processPdfToOfficeFormat(nonPdfFile, "docx", "draw_pdf_import"); // Verify @@ -133,7 +154,7 @@ class PDFToFileTest { "Fake PDF content".getBytes()); // Execute with invalid format - ResponseEntity response = + ResponseEntity response = pdfToFile.processPdfToOfficeFormat(pdfFile, "invalid_format", "draw_pdf_import"); // Verify @@ -184,12 +205,14 @@ class PDFToFileTest { }); // Execute the method - ResponseEntity response = pdfToFile.processPdfToMarkdown(pdfFile); + ResponseEntity response = + pdfToFile.processPdfToMarkdown(pdfFile); // Verify - should now return a ZIP file instead of plain markdown assertEquals(HttpStatus.OK, response.getStatusCode()); - assertNotNull(response.getBody()); - assertTrue(response.getBody().length > 0); + byte[] bodyBytes = drain(response); + assertNotNull(bodyBytes); + assertTrue(bodyBytes.length > 0); // Verify content disposition indicates a ZIP file assertTrue( @@ -201,7 +224,7 @@ class PDFToFileTest { // Verify the content by unzipping it try (ZipInputStream zipStream = ZipSecurity.createHardenedInputStream( - new java.io.ByteArrayInputStream(response.getBody()))) { + new java.io.ByteArrayInputStream(bodyBytes))) { ZipEntry entry; boolean foundMdFile = false; boolean foundImageInFolder = false; @@ -275,12 +298,14 @@ class PDFToFileTest { }); // Execute the method - ResponseEntity response = pdfToFile.processPdfToMarkdown(pdfFile); + ResponseEntity response = + pdfToFile.processPdfToMarkdown(pdfFile); // Verify assertEquals(HttpStatus.OK, response.getStatusCode()); - assertNotNull(response.getBody()); - assertTrue(response.getBody().length > 0); + byte[] bodyBytes = drain(response); + assertNotNull(bodyBytes); + assertTrue(bodyBytes.length > 0); // Verify content disposition indicates a zip file assertTrue( @@ -292,7 +317,7 @@ class PDFToFileTest { // Verify the content by unzipping it try (ZipInputStream zipStream = ZipSecurity.createHardenedInputStream( - new java.io.ByteArrayInputStream(response.getBody()))) { + new java.io.ByteArrayInputStream(bodyBytes))) { ZipEntry entry; boolean foundMdFiles = false; boolean foundImage = false; @@ -352,12 +377,13 @@ class PDFToFileTest { }); // Execute the method - ResponseEntity response = pdfToFile.processPdfToHtml(pdfFile); + ResponseEntity response = pdfToFile.processPdfToHtml(pdfFile); // Verify assertEquals(HttpStatus.OK, response.getStatusCode()); - assertNotNull(response.getBody()); - assertTrue(response.getBody().length > 0); + byte[] bodyBytes = drain(response); + assertNotNull(bodyBytes); + assertTrue(bodyBytes.length > 0); // Verify content disposition indicates a zip file assertTrue( @@ -369,7 +395,7 @@ class PDFToFileTest { // Verify the content by unzipping it try (ZipInputStream zipStream = ZipSecurity.createHardenedInputStream( - new java.io.ByteArrayInputStream(response.getBody()))) { + new java.io.ByteArrayInputStream(bodyBytes))) { ZipEntry entry; boolean foundMainHtml = false; boolean foundIndexHtml = false; @@ -437,13 +463,14 @@ class PDFToFileTest { }); // Execute the method with docx format - ResponseEntity response = + ResponseEntity response = pdfToFile.processPdfToOfficeFormat(pdfFile, "docx", "draw_pdf_import"); // Verify assertEquals(HttpStatus.OK, response.getStatusCode()); - assertNotNull(response.getBody()); - assertTrue(response.getBody().length > 0); + byte[] bodyBytes = drain(response); + assertNotNull(bodyBytes); + assertTrue(bodyBytes.length > 0); // Verify content disposition has correct filename assertTrue( @@ -508,13 +535,14 @@ class PDFToFileTest { }); // Execute the method with ODP format - ResponseEntity response = + ResponseEntity response = pdfToFile.processPdfToOfficeFormat(pdfFile, "odp", "draw_pdf_import"); // Verify assertEquals(HttpStatus.OK, response.getStatusCode()); - assertNotNull(response.getBody()); - assertTrue(response.getBody().length > 0); + byte[] bodyBytes = drain(response); + assertNotNull(bodyBytes); + assertTrue(bodyBytes.length > 0); // Verify content disposition for zip file assertTrue( @@ -526,7 +554,7 @@ class PDFToFileTest { // Verify the content by unzipping it try (ZipInputStream zipStream = ZipSecurity.createHardenedInputStream( - new java.io.ByteArrayInputStream(response.getBody()))) { + new java.io.ByteArrayInputStream(bodyBytes))) { ZipEntry entry; boolean foundMainFile = false; boolean foundMediaFiles = false; @@ -592,13 +620,14 @@ class PDFToFileTest { }); // Execute the method with text format - ResponseEntity response = + ResponseEntity response = pdfToFile.processPdfToOfficeFormat(pdfFile, "txt:Text", "draw_pdf_import"); // Verify assertEquals(HttpStatus.OK, response.getStatusCode()); - assertNotNull(response.getBody()); - assertTrue(response.getBody().length > 0); + byte[] bodyBytes = drain(response); + assertNotNull(bodyBytes); + assertTrue(bodyBytes.length > 0); // Verify content disposition has txt extension assertTrue( @@ -650,13 +679,14 @@ class PDFToFileTest { }); // Execute the method - ResponseEntity response = + ResponseEntity response = pdfToFile.processPdfToOfficeFormat(pdfFile, "docx", "draw_pdf_import"); // Verify assertEquals(HttpStatus.OK, response.getStatusCode()); - assertNotNull(response.getBody()); - assertTrue(response.getBody().length > 0); + byte[] bodyBytes = drain(response); + assertNotNull(bodyBytes); + assertTrue(bodyBytes.length > 0); // Verify content disposition contains output.docx assertTrue( @@ -696,12 +726,13 @@ class PDFToFileTest { return mockExecutorResult; }); - ResponseEntity response = + ResponseEntity response = pdfToFileWithUno.processPdfToOfficeFormat(pdfFile, "docx", "writer_pdf_import"); assertEquals(HttpStatus.OK, response.getStatusCode()); - assertNotNull(response.getBody()); - assertTrue(response.getBody().length > 0); + byte[] bodyBytes = drain(response); + assertNotNull(bodyBytes); + assertTrue(bodyBytes.length > 0); assertTrue( response.getHeaders() .getContentDisposition() @@ -759,12 +790,13 @@ class PDFToFileTest { return mockExecutorResult; }); - ResponseEntity response = + ResponseEntity response = pdfToFileWithUno.processPdfToOfficeFormat(pdfFile, "docx", "writer_pdf_import"); assertEquals(HttpStatus.OK, response.getStatusCode()); - assertNotNull(response.getBody()); - assertTrue(response.getBody().length > 0); + byte[] bodyBytes = drain(response); + assertNotNull(bodyBytes); + assertTrue(bodyBytes.length > 0); assertTrue( response.getHeaders() .getContentDisposition() diff --git a/app/core/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java b/app/core/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java index a3712bf975..205a0d5734 100644 --- a/app/core/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java +++ b/app/core/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java @@ -47,7 +47,7 @@ public class OpenApiConfig { .version(version) .license( new License() - .name("MIT") + .name("Open-Core - MIT Licensed") .url( "https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/refs/heads/main/LICENSE")) .termsOfService("https://www.stirlingpdf.com/terms") diff --git a/app/core/src/main/java/stirling/software/SPDF/config/SpringDocConfig.java b/app/core/src/main/java/stirling/software/SPDF/config/SpringDocConfig.java index 6733eb22dc..42c9e97b35 100644 --- a/app/core/src/main/java/stirling/software/SPDF/config/SpringDocConfig.java +++ b/app/core/src/main/java/stirling/software/SPDF/config/SpringDocConfig.java @@ -28,7 +28,17 @@ public class SpringDocConfig { "/api/v1/proprietary/ui-data/**", "/api/v1/info/**", "/api/v1/general/job/**", - "/api/v1/general/files/**") + "/api/v1/general/files/**", + "/api/v1/general/signatures/**", + "/api/v1/database/**", + "/api/v1/storage/**", + "/api/v1/proprietary/signatures/**", + "/api/v1/workflow/participant/**", + "/api/v1/security/cert-sign/sessions", + "/api/v1/security/cert-sign/sessions/**", + "/api/v1/security/cert-sign/sign-requests", + "/api/v1/security/cert-sign/sign-requests/**", + "/api/v1/security/cert-sign/validate-certificate") .addOpenApiCustomizer(pdfFileOneOfCustomizer) .addOpenApiCustomizer( openApi -> { @@ -53,7 +63,16 @@ public class SpringDocConfig { "/api/v1/team/**", "/api/v1/auth/**", "/api/v1/invite/**", - "/api/v1/audit/**") + "/api/v1/audit/**", + "/api/v1/database/**", + "/api/v1/storage/**", + "/api/v1/proprietary/signatures/**", + "/api/v1/workflow/participant/**", + "/api/v1/security/cert-sign/sessions", + "/api/v1/security/cert-sign/sessions/**", + "/api/v1/security/cert-sign/sign-requests", + "/api/v1/security/cert-sign/sign-requests/**", + "/api/v1/security/cert-sign/validate-certificate") .addOpenApiCustomizer( openApi -> { openApi.info( @@ -75,7 +94,8 @@ public class SpringDocConfig { "/api/v1/proprietary/ui-data/**", "/api/v1/info/**", "/api/v1/general/job/**", - "/api/v1/general/files/**") + "/api/v1/general/files/**", + "/api/v1/general/signatures/**") .addOpenApiCustomizer( openApi -> { openApi.info( diff --git a/app/core/src/main/java/stirling/software/SPDF/config/TauriProcessMonitor.java b/app/core/src/main/java/stirling/software/SPDF/config/TauriProcessMonitor.java index 9ec1bb37e6..2d5ca8b17c 100644 --- a/app/core/src/main/java/stirling/software/SPDF/config/TauriProcessMonitor.java +++ b/app/core/src/main/java/stirling/software/SPDF/config/TauriProcessMonitor.java @@ -1,7 +1,6 @@ package stirling.software.SPDF.config; import java.lang.management.ManagementFactory; -import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; @@ -106,25 +105,27 @@ public class TauriProcessMonitor { logger.info("Orphaned Java backend detected. Shutting down gracefully..."); // Shutdown asynchronously to avoid blocking the monitor thread - CompletableFuture.runAsync( - () -> { - try { - // Give a small delay to ensure logging completes - Thread.sleep(1000); + Thread.ofVirtual() + .name("tauri-graceful-shutdown") + .start( + () -> { + try { + // Give a small delay to ensure logging completes + Thread.sleep(1000); - if (applicationContext instanceof ConfigurableApplicationContext) { - ((ConfigurableApplicationContext) applicationContext).close(); - } else { - // Fallback to system exit - logger.warn( - "Unable to shutdown Spring context gracefully, using System.exit"); - System.exit(0); - } - } catch (Exception e) { - logger.error("Error during graceful shutdown", e); - System.exit(1); - } - }); + if (applicationContext instanceof ConfigurableApplicationContext) { + ((ConfigurableApplicationContext) applicationContext).close(); + } else { + // Fallback to system exit + logger.warn( + "Unable to shutdown Spring context gracefully, using System.exit"); + System.exit(0); + } + } catch (Exception e) { + logger.error("Error during graceful shutdown", e); + System.exit(1); + } + }); } @PreDestroy diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/BookletImpositionController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/BookletImpositionController.java index fdf3c716f2..8a3313e881 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/BookletImpositionController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/BookletImpositionController.java @@ -1,7 +1,6 @@ package stirling.software.SPDF.controller.api; import java.awt.*; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -19,6 +18,7 @@ import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.github.pixee.security.Filenames; import io.swagger.v3.oas.annotations.Operation; @@ -30,6 +30,7 @@ import stirling.software.SPDF.model.api.general.BookletImpositionRequest; import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.GeneralUtils; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @RestController @@ -39,6 +40,7 @@ import stirling.software.common.util.WebResponseUtils; public class BookletImpositionController { private final CustomPDFDocumentFactory pdfDocumentFactory; + private final TempFileManager tempFileManager; @AutoJobPostMapping( value = "/booklet-imposition", @@ -49,7 +51,7 @@ public class BookletImpositionController { "This operation combines page reordering for booklet printing with multi-page layout. " + "It rearranges pages in the correct order for booklet printing and places multiple pages " + "on each sheet for proper folding and binding. Input:PDF Output:PDF Type:SISO") - public ResponseEntity createBookletImposition( + public ResponseEntity createBookletImposition( @ModelAttribute BookletImpositionRequest request) throws IOException { MultipartFile file = request.getFileInput(); @@ -85,15 +87,12 @@ public class BookletImpositionController { duplexPass, flipOnShortEdge)) { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - newDocument.save(baos); - - byte[] result = baos.toByteArray(); - return WebResponseUtils.bytesToWebResponse( - result, + return WebResponseUtils.pdfDocToWebResponse( + newDocument, GeneralUtils.generateFilename( Filenames.toSimpleFileName(file.getOriginalFilename()), - "_booklet.pdf")); + "_booklet.pdf"), + tempFileManager); } } } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/CropController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/CropController.java index 5e9eca5510..845d3519cd 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/CropController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/CropController.java @@ -1,10 +1,7 @@ package stirling.software.SPDF.controller.api; import java.awt.image.BufferedImage; -import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; import java.util.List; import org.apache.pdfbox.multipdf.LayerUtility; @@ -18,6 +15,7 @@ import org.apache.pdfbox.rendering.PDFRenderer; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.swagger.v3.oas.annotations.Operation; @@ -32,6 +30,8 @@ import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.ProcessExecutor; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @GeneralApi @@ -46,6 +46,7 @@ public class CropController { private static final String PDF_EXTENSION = ".pdf"; private final CustomPDFDocumentFactory pdfDocumentFactory; + private final TempFileManager tempFileManager; private static int[] detectContentBounds(BufferedImage image) { int width = image.getWidth(); @@ -131,7 +132,8 @@ public class CropController { description = "This operation takes an input PDF file and crops it according to the given" + " coordinates. Input:PDF Output:PDF Type:SISO") - public ResponseEntity cropPdf(@ModelAttribute CropPdfForm request) throws IOException { + public ResponseEntity cropPdf(@ModelAttribute CropPdfForm request) + throws IOException { if (request.isAutoCrop()) { return cropWithAutomaticDetection(request); } @@ -151,8 +153,8 @@ public class CropController { } } - private ResponseEntity cropWithAutomaticDetection(@ModelAttribute CropPdfForm request) - throws IOException { + private ResponseEntity cropWithAutomaticDetection( + @ModelAttribute CropPdfForm request) throws IOException { try (PDDocument sourceDocument = pdfDocumentFactory.load(request)) { try (PDDocument newDocument = @@ -196,20 +198,17 @@ public class CropController { cropBounds.height)); } - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - newDocument.save(baos); - byte[] pdfContent = baos.toByteArray(); - - return WebResponseUtils.bytesToWebResponse( - pdfContent, + return WebResponseUtils.pdfDocToWebResponse( + newDocument, GeneralUtils.generateFilename( - request.getFileInput().getOriginalFilename(), "_cropped.pdf")); + request.getFileInput().getOriginalFilename(), "_cropped.pdf"), + tempFileManager); } } } - private ResponseEntity cropWithPDFBox(@ModelAttribute CropPdfForm request) - throws IOException { + private ResponseEntity cropWithPDFBox( + @ModelAttribute CropPdfForm request) throws IOException { try (PDDocument sourceDocument = pdfDocumentFactory.load(request)) { try (PDDocument newDocument = @@ -255,22 +254,19 @@ public class CropController { request.getHeight())); } - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - newDocument.save(baos); - - byte[] pdfContent = baos.toByteArray(); - return WebResponseUtils.bytesToWebResponse( - pdfContent, + return WebResponseUtils.pdfDocToWebResponse( + newDocument, GeneralUtils.generateFilename( - request.getFileInput().getOriginalFilename(), "_cropped.pdf")); + request.getFileInput().getOriginalFilename(), "_cropped.pdf"), + tempFileManager); } } } - private ResponseEntity cropWithGhostscript(@ModelAttribute CropPdfForm request) - throws IOException { - Path tempInputFile = null; - Path tempOutputFile = null; + private ResponseEntity cropWithGhostscript( + @ModelAttribute CropPdfForm request) throws IOException { + TempFile tempInputFile = null; + TempFile tempOutputFile = null; try (PDDocument sourceDocument = pdfDocumentFactory.load(request)) { for (int i = 0; i < sourceDocument.getNumberOfPages(); i++) { @@ -284,11 +280,11 @@ public class CropController { page.setCropBox(cropBox); } - tempInputFile = Files.createTempFile(TEMP_INPUT_PREFIX, PDF_EXTENSION); - tempOutputFile = Files.createTempFile(TEMP_OUTPUT_PREFIX, PDF_EXTENSION); + tempInputFile = tempFileManager.createManagedTempFile(PDF_EXTENSION); + tempOutputFile = tempFileManager.createManagedTempFile(PDF_EXTENSION); // Save the source document with crop boxes - sourceDocument.save(tempInputFile.toFile()); + sourceDocument.save(tempInputFile.getFile()); // Execute Ghostscript to process the crop boxes ProcessExecutor processExecutor = @@ -299,15 +295,15 @@ public class CropController { "-sDEVICE=pdfwrite", "-dUseCropBox", "-o", - tempOutputFile.toString(), - tempInputFile.toString()); + tempOutputFile.getAbsolutePath(), + tempInputFile.getAbsolutePath()); processExecutor.runCommandWithOutputHandling(command); - byte[] pdfContent = Files.readAllBytes(tempOutputFile); - - return WebResponseUtils.bytesToWebResponse( - pdfContent, + TempFile out = tempOutputFile; + tempOutputFile = null; // ownership transferred to StreamingResponseBody + return WebResponseUtils.pdfFileToWebResponse( + out, GeneralUtils.generateFilename( request.getFileInput().getOriginalFilename(), "_cropped.pdf")); @@ -316,10 +312,10 @@ public class CropController { throw ExceptionUtils.createProcessingInterruptedException("Ghostscript", e); } finally { if (tempInputFile != null) { - Files.deleteIfExists(tempInputFile); + tempInputFile.close(); } if (tempOutputFile != null) { - Files.deleteIfExists(tempOutputFile); + tempOutputFile.close(); } } } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/EditTableOfContentsController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/EditTableOfContentsController.java index c97ca11256..bd51dfbd16 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/EditTableOfContentsController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/EditTableOfContentsController.java @@ -1,6 +1,5 @@ package stirling.software.SPDF.controller.api; -import java.io.ByteArrayOutputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -14,6 +13,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.swagger.v3.oas.annotations.Operation; @@ -27,6 +27,7 @@ import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.GeneralApi; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.GeneralUtils; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; import tools.jackson.core.type.TypeReference; @@ -39,6 +40,7 @@ public class EditTableOfContentsController { private final CustomPDFDocumentFactory pdfDocumentFactory; private final ObjectMapper objectMapper; + private final TempFileManager tempFileManager; @AutoJobPostMapping( value = "/extract-bookmarks", @@ -149,12 +151,11 @@ public class EditTableOfContentsController { @Operation( summary = "Edit Table of Contents", description = "Add or edit bookmarks/table of contents in a PDF document.") - public ResponseEntity editTableOfContents( + public ResponseEntity editTableOfContents( @ModelAttribute EditTableOfContentsRequest request) throws Exception { MultipartFile file = request.getFileInput(); - try (PDDocument document = pdfDocumentFactory.load(file); - ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + try (PDDocument document = pdfDocumentFactory.load(file)) { // Parse the bookmark data from JSON List bookmarks = @@ -168,13 +169,10 @@ public class EditTableOfContentsController { // Add bookmarks to the outline addBookmarksToOutline(document, outline, bookmarks); - // Save the document to a byte array - document.save(baos); - - return WebResponseUtils.bytesToWebResponse( - baos.toByteArray(), + return WebResponseUtils.pdfDocToWebResponse( + document, GeneralUtils.generateFilename(file.getOriginalFilename(), "_with_toc.pdf"), - MediaType.APPLICATION_PDF); + tempFileManager); } } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/MergeController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/MergeController.java index 7a4f12d697..3a7718fe8e 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/MergeController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/MergeController.java @@ -3,7 +3,6 @@ package stirling.software.SPDF.controller.api; import java.io.File; import java.io.IOException; import java.io.InputStream; -import java.nio.file.Files; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; @@ -29,6 +28,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.swagger.v3.oas.annotations.Operation; @@ -279,7 +279,7 @@ public class MergeController { "This endpoint merges multiple PDF files into a single PDF file. The merged" + " file will contain all pages from the input files in the order they were" + " provided. Input:PDF Output:PDF Type:MISO") - public ResponseEntity mergePdfs( + public ResponseEntity mergePdfs( @ModelAttribute MergePdfsRequest request, @RequestParam(value = "fileOrder", required = false) String fileOrder) throws IOException { @@ -399,12 +399,6 @@ public class MergeController { String mergedFileName = GeneralUtils.generateFilename(firstFilename, "_merged_unsigned.pdf"); - byte[] pdfBytes; - try { - pdfBytes = Files.readAllBytes(outputTempFile.getPath()); - } finally { - outputTempFile.close(); - } - return WebResponseUtils.bytesToWebResponse(pdfBytes, mergedFileName); + return WebResponseUtils.pdfFileToWebResponse(outputTempFile, mergedFileName); } } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/MultiPageLayoutController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/MultiPageLayoutController.java index 7066a47c9a..4fad2c5b1b 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/MultiPageLayoutController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/MultiPageLayoutController.java @@ -1,7 +1,6 @@ package stirling.software.SPDF.controller.api; import java.awt.Color; -import java.io.ByteArrayOutputStream; import java.io.IOException; import org.apache.pdfbox.multipdf.LayerUtility; @@ -15,6 +14,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.swagger.v3.oas.annotations.Operation; @@ -28,6 +28,7 @@ import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.GeneralFormCopyUtils; import stirling.software.common.util.GeneralUtils; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @GeneralApi @@ -36,6 +37,7 @@ import stirling.software.common.util.WebResponseUtils; public class MultiPageLayoutController { private final CustomPDFDocumentFactory pdfDocumentFactory; + private final TempFileManager tempFileManager; @AutoJobPostMapping( value = "/multi-page-layout", @@ -45,7 +47,7 @@ public class MultiPageLayoutController { description = "This operation takes an input PDF file and the number of pages to merge into a" + " single sheet in the output PDF file. Input:PDF Output:PDF Type:SISO") - public ResponseEntity mergeMultiplePagesIntoOne( + public ResponseEntity mergeMultiplePagesIntoOne( @ModelAttribute MergeMultiplePagesRequest request) throws IOException { int MAX_PAGES = 100000; @@ -338,13 +340,11 @@ public class MultiPageLayoutController { } } - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - newDocument.save(baos); - byte[] result = baos.toByteArray(); - return WebResponseUtils.bytesToWebResponse( - result, + return WebResponseUtils.pdfDocToWebResponse( + newDocument, GeneralUtils.generateFilename( - file.getOriginalFilename(), "_multi_page_layout.pdf")); + file.getOriginalFilename(), "_multi_page_layout.pdf"), + tempFileManager); } // newDocument is closed here } // sourceDocument is closed here } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/PdfOverlayController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/PdfOverlayController.java index df146ea9a0..826ffcb1fb 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/PdfOverlayController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/PdfOverlayController.java @@ -1,6 +1,5 @@ package stirling.software.SPDF.controller.api; -import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.nio.file.Files; @@ -16,6 +15,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.swagger.v3.oas.annotations.Operation; @@ -28,6 +28,8 @@ import stirling.software.common.annotations.api.GeneralApi; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.GeneralUtils; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @GeneralApi @@ -35,6 +37,7 @@ import stirling.software.common.util.WebResponseUtils; public class PdfOverlayController { private final CustomPDFDocumentFactory pdfDocumentFactory; + private final TempFileManager tempFileManager; @AutoJobPostMapping(value = "/overlay-pdfs", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @StandardPdfResponse @@ -43,8 +46,8 @@ public class PdfOverlayController { description = "Overlay PDF files onto a base PDF with different modes: Sequential," + " Interleaved, or Fixed Repeat. Input:PDF Output:PDF Type:MIMO") - public ResponseEntity overlayPdfs(@ModelAttribute OverlayPdfsRequest request) - throws IOException { + public ResponseEntity overlayPdfs( + @ModelAttribute OverlayPdfsRequest request) throws IOException { MultipartFile baseFile = request.getFileInput(); int overlayPos = request.getOverlayPosition(); @@ -52,6 +55,7 @@ public class PdfOverlayController { File[] overlayPdfFiles = new File[overlayFiles.length]; List tempFiles = new ArrayList<>(); // List to keep track of temporary files + TempFile tempOut = null; try { for (int i = 0; i < overlayFiles.length; i++) { overlayPdfFiles[i] = GeneralUtils.multipartToFile(overlayFiles[i]); @@ -62,8 +66,7 @@ public class PdfOverlayController { int[] counts = request.getCounts(); // Used for FixedRepeatOverlay mode try (PDDocument basePdf = pdfDocumentFactory.load(baseFile); - Overlay overlay = new Overlay(); - ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + Overlay overlay = new Overlay()) { Map overlayGuide = prepareOverlayGuide( basePdf.getNumberOfPages(), @@ -79,15 +82,21 @@ public class PdfOverlayController { overlay.setOverlayPosition(Overlay.Position.BACKGROUND); } - overlay.overlay(overlayGuide).save(outputStream); - byte[] data = outputStream.toByteArray(); + tempOut = tempFileManager.createManagedTempFile(".pdf"); + overlay.overlay(overlayGuide).save(tempOut.getFile()); String outputFilename = GeneralUtils.generateFilename( baseFile.getOriginalFilename(), "_overlayed.pdf"); - return WebResponseUtils.bytesToWebResponse( - data, outputFilename, MediaType.APPLICATION_PDF); + TempFile out = tempOut; + tempOut = null; // ownership transferred to StreamingResponseBody + return WebResponseUtils.pdfFileToWebResponse(out, outputFilename); } + } catch (Exception e) { + if (tempOut != null) { + tempOut.close(); + } + throw e; } finally { for (File overlayPdfFile : overlayPdfFiles) { if (overlayPdfFile != null) { diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/RearrangePagesPDFController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/RearrangePagesPDFController.java index 9ad66a2dd6..de58a882c8 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/RearrangePagesPDFController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/RearrangePagesPDFController.java @@ -12,6 +12,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.swagger.v3.oas.annotations.Operation; @@ -27,6 +28,7 @@ import stirling.software.common.annotations.api.GeneralApi; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.GeneralUtils; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @GeneralApi @@ -35,6 +37,7 @@ import stirling.software.common.util.WebResponseUtils; public class RearrangePagesPDFController { private final CustomPDFDocumentFactory pdfDocumentFactory; + private final TempFileManager tempFileManager; @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/remove-pages") @StandardPdfResponse @@ -44,8 +47,8 @@ public class RearrangePagesPDFController { "This endpoint removes specified pages from a given PDF file. Users can provide" + " a comma-separated list of page numbers or ranges to delete. Input:PDF" + " Output:PDF Type:SISO") - public ResponseEntity deletePages(@ModelAttribute PDFWithPageNums request) - throws IOException { + public ResponseEntity deletePages( + @ModelAttribute PDFWithPageNums request) throws IOException { MultipartFile pdfFile = request.getFileInput(); String pagesToDelete = request.getPageNumbers(); @@ -67,7 +70,8 @@ public class RearrangePagesPDFController { return WebResponseUtils.pdfDocToWebResponse( document, GeneralUtils.generateFilename( - pdfFile.getOriginalFilename(), "_removed_pages.pdf")); + pdfFile.getOriginalFilename(), "_removed_pages.pdf"), + tempFileManager); } } @@ -224,8 +228,8 @@ public class RearrangePagesPDFController { + " order or custom mode. Users can provide a page order as a" + " comma-separated list of page numbers or page ranges, or a custom mode." + " Input:PDF Output:PDF") - public ResponseEntity rearrangePages(@ModelAttribute RearrangePagesRequest request) - throws IOException { + public ResponseEntity rearrangePages( + @ModelAttribute RearrangePagesRequest request) throws IOException { MultipartFile pdfFile = request.getFileInput(); String pageOrder = request.getPageNumbers(); String sortType = request.getCustomMode(); @@ -264,7 +268,8 @@ public class RearrangePagesPDFController { return WebResponseUtils.pdfDocToWebResponse( rearrangedDocument, GeneralUtils.generateFilename( - pdfFile.getOriginalFilename(), "_rearranged.pdf")); + pdfFile.getOriginalFilename(), "_rearranged.pdf"), + tempFileManager); } } } catch (IOException e) { diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/RotationController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/RotationController.java index 9b305fdc5c..9049595cb2 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/RotationController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/RotationController.java @@ -9,6 +9,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.swagger.v3.oas.annotations.Operation; @@ -21,6 +22,7 @@ import stirling.software.common.annotations.api.GeneralApi; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.GeneralUtils; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @GeneralApi @@ -28,6 +30,7 @@ import stirling.software.common.util.WebResponseUtils; public class RotationController { private final CustomPDFDocumentFactory pdfDocumentFactory; + private final TempFileManager tempFileManager; @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/rotate-pdf") @StandardPdfResponse @@ -36,7 +39,7 @@ public class RotationController { description = "This endpoint rotates a given PDF file by a specified angle. The angle must be" + " a multiple of 90. Input:PDF Output:PDF Type:SISO") - public ResponseEntity rotatePDF(@ModelAttribute RotatePDFRequest request) + public ResponseEntity rotatePDF(@ModelAttribute RotatePDFRequest request) throws IOException { MultipartFile pdfFile = request.getFileInput(); Integer angle = request.getAngle(); @@ -60,7 +63,8 @@ public class RotationController { // Return the rotated PDF as a response return WebResponseUtils.pdfDocToWebResponse( document, - GeneralUtils.generateFilename(pdfFile.getOriginalFilename(), "_rotated.pdf")); + GeneralUtils.generateFilename(pdfFile.getOriginalFilename(), "_rotated.pdf"), + tempFileManager); } } } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/ScalePagesController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/ScalePagesController.java index 06053274cc..1cc77452d9 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/ScalePagesController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/ScalePagesController.java @@ -1,6 +1,5 @@ package stirling.software.SPDF.controller.api; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.HashMap; import java.util.Map; @@ -16,6 +15,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.swagger.v3.oas.annotations.Operation; @@ -28,6 +28,7 @@ import stirling.software.common.annotations.api.GeneralApi; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.GeneralUtils; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @GeneralApi @@ -36,6 +37,7 @@ import stirling.software.common.util.WebResponseUtils; public class ScalePagesController { private final CustomPDFDocumentFactory pdfDocumentFactory; + private final TempFileManager tempFileManager; private static PDRectangle getTargetSize(String targetPDRectangle, PDDocument sourceDocument) { if ("KEEP".equals(targetPDRectangle)) { @@ -118,16 +120,15 @@ public class ScalePagesController { description = "This operation takes an input PDF file and the size to scale the pages to in" + " the output PDF file. Input:PDF Output:PDF Type:SISO") - public ResponseEntity scalePages(@ModelAttribute ScalePagesRequest request) - throws IOException { + public ResponseEntity scalePages( + @ModelAttribute ScalePagesRequest request) throws IOException { MultipartFile file = request.getFileInput(); String targetPDRectangle = request.getPageSize(); float scaleFactor = request.getScaleFactor(); try (PDDocument sourceDocument = pdfDocumentFactory.load(file); PDDocument outputDocument = - pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDocument); - ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDocument)) { PDRectangle targetSize = getTargetSize(targetPDRectangle, sourceDocument); @@ -168,11 +169,10 @@ public class ScalePagesController { } } - outputDocument.save(baos); - - return WebResponseUtils.bytesToWebResponse( - baos.toByteArray(), - GeneralUtils.generateFilename(file.getOriginalFilename(), "_scaled.pdf")); + return WebResponseUtils.pdfDocToWebResponse( + outputDocument, + GeneralUtils.generateFilename(file.getOriginalFilename(), "_scaled.pdf"), + tempFileManager); } } } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/SplitPdfBySectionsController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/SplitPdfBySectionsController.java index 216c618c40..58c150aa37 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/SplitPdfBySectionsController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/SplitPdfBySectionsController.java @@ -1,6 +1,5 @@ package stirling.software.SPDF.controller.api; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.file.Files; import java.util.*; @@ -20,6 +19,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.swagger.v3.oas.annotations.Operation; @@ -59,8 +59,8 @@ public class SplitPdfBySectionsController { + " which page to split, and how to split" + " ( halves, thirds, quarters, etc.), both vertically and horizontally." + " Input:PDF Output:ZIP-PDF Type:SISO") - public ResponseEntity splitPdf(@Valid @ModelAttribute SplitPdfBySectionsRequest request) - throws Exception { + public ResponseEntity splitPdf( + @Valid @ModelAttribute SplitPdfBySectionsRequest request) throws Exception { MultipartFile file = request.getFileInput(); String pageNumbers = request.getPageNumbers(); SplitTypes splitMode = @@ -80,9 +80,7 @@ public class SplitPdfBySectionsController { if (merge) { try (PDDocument mergedDoc = - pdfDocumentFactory.createNewDocumentBasedOnOldDocument( - sourceDocument); - ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDocument)) { LayerUtility layerUtility = new LayerUtility(mergedDoc); for (int pageIndex = 0; pageIndex < sourceDocument.getNumberOfPages(); @@ -99,11 +97,12 @@ public class SplitPdfBySectionsController { addPageToTarget(sourceDocument, pageIndex, mergedDoc, layerUtility); } } - mergedDoc.save(baos); - return WebResponseUtils.baosToWebResponse(baos, filename + ".pdf"); + return WebResponseUtils.pdfDocToWebResponse( + mergedDoc, filename + ".pdf", tempFileManager); } } else { - try (TempFile zipTempFile = new TempFile(tempFileManager, ".zip")) { + TempFile zipTempFile = tempFileManager.createManagedTempFile(".zip"); + try { try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(zipTempFile.getPath()))) { for (int pageIndex = 0; @@ -161,9 +160,10 @@ public class SplitPdfBySectionsController { log.error("Error creating ZIP file with split PDF sections", e); throw e; } - byte[] zipBytes = Files.readAllBytes(zipTempFile.getPath()); - return WebResponseUtils.bytesToWebResponse( - zipBytes, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM); + return WebResponseUtils.zipFileToWebResponse(zipTempFile, filename + ".zip"); + } catch (Exception ex) { + zipTempFile.close(); + throw ex; } } } catch (Exception e) { diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/ToSinglePageController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/ToSinglePageController.java index 2cf2517747..da6322a087 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/ToSinglePageController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/ToSinglePageController.java @@ -1,7 +1,6 @@ package stirling.software.SPDF.controller.api; import java.awt.geom.AffineTransform; -import java.io.ByteArrayOutputStream; import java.io.IOException; import org.apache.pdfbox.multipdf.LayerUtility; @@ -12,6 +11,7 @@ import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.swagger.v3.oas.annotations.Operation; @@ -23,6 +23,7 @@ import stirling.software.common.annotations.api.GeneralApi; import stirling.software.common.model.api.PDFFile; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.GeneralUtils; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @GeneralApi @@ -30,6 +31,7 @@ import stirling.software.common.util.WebResponseUtils; public class ToSinglePageController { private final CustomPDFDocumentFactory pdfDocumentFactory; + private final TempFileManager tempFileManager; @AutoJobPostMapping( consumes = MediaType.MULTIPART_FORM_DATA_VALUE, @@ -42,7 +44,7 @@ public class ToSinglePageController { + " document. The width of the single page will be same as the input's" + " width, but the height will be the sum of all the pages' heights." + " Input:PDF Output:PDF Type:SISO") - public ResponseEntity pdfToSinglePage(@ModelAttribute PDFFile request) + public ResponseEntity pdfToSinglePage(@ModelAttribute PDFFile request) throws IOException { // Load the source document @@ -85,14 +87,11 @@ public class ToSinglePageController { pageIndex++; } - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - newDocument.save(baos); - - byte[] result = baos.toByteArray(); - return WebResponseUtils.bytesToWebResponse( - result, + return WebResponseUtils.pdfDocToWebResponse( + newDocument, GeneralUtils.generateFilename( - request.getFileInput().getOriginalFilename(), "_singlePage.pdf")); + request.getFileInput().getOriginalFilename(), "_singlePage.pdf"), + tempFileManager); } } } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/UIDataController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/UIDataController.java index b9af74ddbf..c1afe8c406 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/UIDataController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/UIDataController.java @@ -95,9 +95,8 @@ public class UIDataController { Resource resource = new ClassPathResource("static/3rdPartyLicenses.json"); try (InputStream is = resource.getInputStream()) { - String json = new String(is.readAllBytes(), StandardCharsets.UTF_8); Map> licenseData = - objectMapper.readValue(json, new TypeReference<>() {}); + objectMapper.readValue(is, new TypeReference<>() {}); data.setDependencies(licenseData.get("dependencies")); } catch (IOException e) { log.error("Failed to load licenses data", e); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertEbookToPDFController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertEbookToPDFController.java index 50157aa970..136eba30ef 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertEbookToPDFController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertEbookToPDFController.java @@ -16,6 +16,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.github.pixee.security.Filenames; import io.swagger.v3.oas.annotations.Operation; @@ -31,6 +32,7 @@ import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.ProcessExecutor; import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult; +import stirling.software.common.util.TempFile; import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @@ -60,7 +62,7 @@ public class ConvertEbookToPDFController { description = "This endpoint converts common eBook formats (EPUB, MOBI, AZW3, FB2, TXT, DOCX)" + " to PDF using Calibre. Input:BOOK Output:PDF Type:SISO") - public ResponseEntity convertEbookToPdf( + public ResponseEntity convertEbookToPdf( @ModelAttribute ConvertEbookToPdfRequest request) throws Exception { if (!isCalibreEnabled()) { throw new IllegalStateException("Calibre support is disabled"); @@ -140,24 +142,35 @@ public class ConvertEbookToPDFController { String outputFilename = GeneralUtils.generateFilename(originalFilename, "_convertedToPDF.pdf"); + TempFile tempOut = null; try { + tempOut = tempFileManager.createManagedTempFile(".pdf"); if (optimizeForEbook) { byte[] pdfBytes = Files.readAllBytes(outputPath); try { byte[] optimizedPdf = GeneralUtils.optimizePdfWithGhostscript(pdfBytes); - return WebResponseUtils.bytesToWebResponse(optimizedPdf, outputFilename); + Files.write(tempOut.getPath(), optimizedPdf); } catch (IOException e) { log.warn( "Ghostscript optimization failed for ebook conversion, returning" + " original PDF", e); - return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename); + Files.write(tempOut.getPath(), pdfBytes); + } + } else { + try (PDDocument document = pdfDocumentFactory.load(outputPath.toFile())) { + document.save(tempOut.getFile()); } } - - try (PDDocument document = pdfDocumentFactory.load(outputPath.toFile())) { - return WebResponseUtils.pdfDocToWebResponse(document, outputFilename); + ResponseEntity response = + WebResponseUtils.pdfFileToWebResponse(tempOut, outputFilename); + tempOut = null; + return response; + } catch (Exception e) { + if (tempOut != null) { + tempOut.close(); } + throw e; } finally { cleanupTempFiles(workingDirectory, inputPath, outputPath); } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertEmlToPDF.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertEmlToPDF.java index 5cf98556f3..42bbe6b020 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertEmlToPDF.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertEmlToPDF.java @@ -2,6 +2,7 @@ package stirling.software.SPDF.controller.api.converters; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.util.Locale; import org.jetbrains.annotations.NotNull; @@ -10,6 +11,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import org.springframework.web.util.HtmlUtils; import io.github.pixee.security.Filenames; @@ -26,6 +28,7 @@ import stirling.software.common.model.api.converters.EmlToPdfRequest; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.CustomHtmlSanitizer; import stirling.software.common.util.EmlToPdf; +import stirling.software.common.util.TempFile; import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @@ -48,7 +51,8 @@ public class ConvertEmlToPDF { + " with extensive customization options. Features include font settings," + " image constraints, display modes, attachment handling, and HTML debug" + " output. Input: EML or MSG file, Output: PDF or HTML file. Type: SISO") - public ResponseEntity convertEmlToPdf(@ModelAttribute EmlToPdfRequest request) { + public ResponseEntity convertEmlToPdf( + @ModelAttribute EmlToPdfRequest request) { MultipartFile inputFile = request.getFileInput(); String originalFilename = inputFile.getOriginalFilename(); @@ -56,22 +60,19 @@ public class ConvertEmlToPDF { // Validate input if (inputFile.isEmpty()) { log.error("No file provided for EML/MSG to PDF conversion."); - return ResponseEntity.badRequest() - .body("No file provided".getBytes(StandardCharsets.UTF_8)); + return errorResponse(HttpStatus.BAD_REQUEST, "No file provided"); } if (originalFilename == null || originalFilename.trim().isEmpty()) { log.error("Filename is null or empty."); - return ResponseEntity.badRequest() - .body("Please provide a valid filename".getBytes(StandardCharsets.UTF_8)); + return errorResponse(HttpStatus.BAD_REQUEST, "Please provide a valid filename"); } // Validate file type - support EML and MSG (Outlook) files String lowerFilename = originalFilename.toLowerCase(Locale.ROOT); if (!lowerFilename.endsWith(".eml") && !lowerFilename.endsWith(".msg")) { log.error("Invalid file type for EML/MSG to PDF: {}", originalFilename); - return ResponseEntity.badRequest() - .body("Please upload a valid EML or MSG file".getBytes(StandardCharsets.UTF_8)); + return errorResponse(HttpStatus.BAD_REQUEST, "Please upload a valid EML or MSG file"); } String baseFilename = Filenames.toSimpleFileName(originalFilename); // Use Filenames utility @@ -84,16 +85,20 @@ public class ConvertEmlToPDF { String htmlContent = EmlToPdf.convertEmlToHtml(fileBytes, request, customHtmlSanitizer); log.info("Successfully converted email to HTML: {}", originalFilename); - return WebResponseUtils.bytesToWebResponse( - htmlContent.getBytes(StandardCharsets.UTF_8), - baseFilename + ".html", - MediaType.TEXT_HTML); + TempFile tempOut = tempFileManager.createManagedTempFile(".html"); + try { + Files.writeString(tempOut.getPath(), htmlContent, StandardCharsets.UTF_8); + } catch (Exception ex) { + tempOut.close(); + throw ex; + } + return WebResponseUtils.fileToWebResponse( + tempOut, baseFilename + ".html", MediaType.TEXT_HTML); } catch (IOException | IllegalArgumentException e) { log.error("HTML conversion failed for {}", originalFilename, e); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body( - ("HTML conversion failed: " + e.getMessage()) - .getBytes(StandardCharsets.UTF_8)); + return errorResponse( + HttpStatus.INTERNAL_SERVER_ERROR, + "HTML conversion failed: " + e.getMessage()); } } @@ -111,20 +116,25 @@ public class ConvertEmlToPDF { if (pdfBytes == null || pdfBytes.length == 0) { log.error("PDF conversion failed - empty output for {}", originalFilename); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body( - "PDF conversion failed - empty output" - .getBytes(StandardCharsets.UTF_8)); + return errorResponse( + HttpStatus.INTERNAL_SERVER_ERROR, + "PDF conversion failed - empty output"); } log.info("Successfully converted email to PDF: {}", originalFilename); - return WebResponseUtils.bytesToWebResponse( - pdfBytes, baseFilename + ".pdf", MediaType.APPLICATION_PDF); + TempFile tempOut = tempFileManager.createManagedTempFile(".pdf"); + try { + Files.write(tempOut.getPath(), pdfBytes); + } catch (Exception ex) { + tempOut.close(); + throw ex; + } + return WebResponseUtils.pdfFileToWebResponse(tempOut, baseFilename + ".pdf"); } catch (InterruptedException e) { Thread.currentThread().interrupt(); log.error("Email to PDF conversion was interrupted for {}", originalFilename, e); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body("Conversion was interrupted".getBytes(StandardCharsets.UTF_8)); + return errorResponse( + HttpStatus.INTERNAL_SERVER_ERROR, "Conversion was interrupted"); } catch (IllegalArgumentException e) { String errorMessage = buildErrorMessage(e, originalFilename); log.error( @@ -132,8 +142,7 @@ public class ConvertEmlToPDF { originalFilename, errorMessage, e); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(errorMessage.getBytes(StandardCharsets.UTF_8)); + return errorResponse(HttpStatus.INTERNAL_SERVER_ERROR, errorMessage); } catch (RuntimeException e) { String errorMessage = buildErrorMessage(e, originalFilename); log.error( @@ -141,17 +150,25 @@ public class ConvertEmlToPDF { originalFilename, errorMessage, e); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(errorMessage.getBytes(StandardCharsets.UTF_8)); + return errorResponse(HttpStatus.INTERNAL_SERVER_ERROR, errorMessage); } } catch (IOException e) { log.error("File processing error for email to PDF: {}", originalFilename, e); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body("File processing error".getBytes(StandardCharsets.UTF_8)); + return errorResponse(HttpStatus.INTERNAL_SERVER_ERROR, "File processing error"); } } + private ResponseEntity errorResponse(HttpStatus status, String message) { + byte[] body = message.getBytes(StandardCharsets.UTF_8); + StreamingResponseBody streaming = + os -> { + os.write(body); + os.flush(); + }; + return ResponseEntity.status(status).body(streaming); + } + private static @NotNull String buildErrorMessage(Exception e, String originalFilename) { String safeFilename = HtmlUtils.htmlEscape(originalFilename); String exceptionMessage = e.getMessage(); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertHtmlToPDF.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertHtmlToPDF.java index 85ef34313d..7d09eeb3b5 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertHtmlToPDF.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertHtmlToPDF.java @@ -1,9 +1,12 @@ package stirling.software.SPDF.controller.api.converters; +import java.nio.file.Files; + import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.github.pixee.security.Filenames; import io.swagger.v3.oas.annotations.Operation; @@ -37,7 +40,7 @@ public class ConvertHtmlToPDF { description = "This endpoint takes an HTML or ZIP file input and converts it to a PDF format." + " Input:HTML Output:PDF Type:SISO") - public ResponseEntity HtmlToPdf(@ModelAttribute HTMLToPdfRequest request) + public ResponseEntity HtmlToPdf(@ModelAttribute HTMLToPdfRequest request) throws Exception { MultipartFile fileInput = request.getFileInput(); @@ -65,6 +68,13 @@ public class ConvertHtmlToPDF { String outputFilename = GeneralUtils.generateFilename(originalFilename, ".pdf"); - return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename); + TempFile tempOut = tempFileManager.createManagedTempFile(".pdf"); + try { + Files.write(tempOut.getPath(), pdfBytes); + } catch (Exception e) { + tempOut.close(); + throw e; + } + return WebResponseUtils.pdfFileToWebResponse(tempOut, outputFilename); } } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertMarkdownToPdf.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertMarkdownToPdf.java index f96e17f95a..83384a21ed 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertMarkdownToPdf.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertMarkdownToPdf.java @@ -1,5 +1,6 @@ package stirling.software.SPDF.controller.api.converters; +import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Map; @@ -14,6 +15,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.github.pixee.security.Filenames; import io.swagger.v3.oas.annotations.Operation; @@ -46,8 +48,8 @@ public class ConvertMarkdownToPdf { description = "This endpoint takes a Markdown file or ZIP (containing Markdown + images) input, converts it to HTML, and then to" + " PDF format. Input:MARKDOWN Output:PDF Type:SISO") - public ResponseEntity markdownToPdf(@ModelAttribute GeneralFile generalFile) - throws Exception { + public ResponseEntity markdownToPdf( + @ModelAttribute GeneralFile generalFile) throws Exception { MultipartFile fileInput = generalFile.getFileInput(); if (fileInput == null) { @@ -79,7 +81,7 @@ public class ConvertMarkdownToPdf { java.nio.file.Path tempDirPath = tempDir.getPath(); try (java.util.zip.ZipInputStream zipIn = io.github.pixee.security.ZipSecurity.createHardenedInputStream( - new java.io.ByteArrayInputStream(fileInput.getBytes()))) { + fileInput.getInputStream())) { java.util.zip.ZipEntry entry; while ((entry = zipIn.getNextEntry()) != null) { if (!entry.isDirectory()) { @@ -141,7 +143,7 @@ public class ConvertMarkdownToPdf { List extensions = List.of(TablesExtension.create()); Parser parser = Parser.builder().extensions(extensions).build(); - Node document = parser.parse(new String(fileInput.getBytes())); + Node document = parser.parse(new String(fileInput.getBytes(), StandardCharsets.UTF_8)); HtmlRenderer renderer = HtmlRenderer.builder() .attributeProviderFactory(context -> new TableAttributeProvider()) @@ -154,7 +156,7 @@ public class ConvertMarkdownToPdf { FileToPdf.convertHtmlToPdf( runtimePathConfig.getWeasyPrintPath(), null, - htmlContent.getBytes(), + htmlContent.getBytes(StandardCharsets.UTF_8), "converted.html", tempFileManager, customHtmlSanitizer); @@ -163,7 +165,15 @@ public class ConvertMarkdownToPdf { } pdfBytes = pdfDocumentFactory.createNewBytesBasedOnOldDocument(pdfBytes); - return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename); + + TempFile tempOut = tempFileManager.createManagedTempFile(".pdf"); + try { + java.nio.file.Files.write(tempOut.getPath(), pdfBytes); + } catch (Exception e) { + tempOut.close(); + throw e; + } + return WebResponseUtils.pdfFileToWebResponse(tempOut, outputFilename); } /** diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertOfficeController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertOfficeController.java index 492febc496..ba4be7b8ea 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertOfficeController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertOfficeController.java @@ -17,6 +17,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.github.pixee.security.Filenames; import io.swagger.v3.oas.annotations.Operation; @@ -36,6 +37,8 @@ import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.ProcessExecutor; import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult; import stirling.software.common.util.RegexPatternUtils; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @ConvertApi @@ -47,6 +50,7 @@ public class ConvertOfficeController { private final RuntimePathConfig runtimePathConfig; private final CustomHtmlSanitizer customHtmlSanitizer; private final EndpointConfiguration endpointConfiguration; + private final TempFileManager tempFileManager; private boolean isUnoconvertAvailable() { return endpointConfiguration.isGroupEnabled("Unoconvert") @@ -202,21 +206,32 @@ public class ConvertOfficeController { description = "This endpoint converts a given file to a PDF using LibreOffice API Input:ANY" + " Output:PDF Type:SISO") - public ResponseEntity processFileToPDF(@ModelAttribute GeneralFile generalFile) - throws Exception { + public ResponseEntity processFileToPDF( + @ModelAttribute GeneralFile generalFile) throws Exception { MultipartFile inputFile = generalFile.getFileInput(); // unused but can start server instance if startup time is to long // LibreOfficeListener.getInstance().start(); File file = null; + TempFile tempOut = null; try { file = convertToPdf(inputFile); + tempOut = tempFileManager.createManagedTempFile(".pdf"); try (PDDocument doc = pdfDocumentFactory.load(file)) { - return WebResponseUtils.pdfDocToWebResponse( - doc, - GeneralUtils.generateFilename( - inputFile.getOriginalFilename(), "_convertedToPDF.pdf")); + doc.save(tempOut.getFile()); } + String filename = + GeneralUtils.generateFilename( + inputFile.getOriginalFilename(), "_convertedToPDF.pdf"); + ResponseEntity response = + WebResponseUtils.pdfFileToWebResponse(tempOut, filename); + tempOut = null; + return response; + } catch (Exception e) { + if (tempOut != null) { + tempOut.close(); + } + throw e; } finally { if (file != null && file.getParent() != null) { FileUtils.deleteDirectory(file.getParentFile()); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToEpubController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToEpubController.java index 61faf63677..3fd8c46dba 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToEpubController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToEpubController.java @@ -13,6 +13,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.github.pixee.security.Filenames; import io.swagger.v3.oas.annotations.Operation; @@ -29,6 +30,7 @@ import stirling.software.common.annotations.api.ConvertApi; import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.ProcessExecutor; import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult; +import stirling.software.common.util.TempFile; import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @@ -85,8 +87,8 @@ public class ConvertPDFToEpubController { description = "Convert a PDF file to a high-quality EPUB or AZW3 ebook using Calibre. Input:PDF" + " Output:EPUB/AZW3 Type:SISO") - public ResponseEntity convertPdfToEpub(@ModelAttribute ConvertPdfToEpubRequest request) - throws Exception { + public ResponseEntity convertPdfToEpub( + @ModelAttribute ConvertPdfToEpubRequest request) throws Exception { if (!endpointConfiguration.isGroupEnabled(CALIBRE_GROUP)) { throw new IllegalStateException( @@ -170,9 +172,16 @@ public class ConvertPDFToEpubController { + "." + outputFormat.getExtension()); - byte[] outputBytes = Files.readAllBytes(outputPath); MediaType mediaType = MediaType.valueOf(outputFormat.getMediaType()); - return WebResponseUtils.bytesToWebResponse(outputBytes, outputFilename, mediaType); + TempFile tempOut = + tempFileManager.createManagedTempFile("." + outputFormat.getExtension()); + try { + Files.copy(outputPath, tempOut.getPath(), StandardCopyOption.REPLACE_EXISTING); + } catch (Exception e) { + tempOut.close(); + throw e; + } + return WebResponseUtils.fileToWebResponse(tempOut, outputFilename, mediaType); } finally { cleanupTempFiles(workingDirectory, inputPath, outputPath); } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToExcelController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToExcelController.java index bafaf86310..974e5451f7 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToExcelController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToExcelController.java @@ -1,6 +1,7 @@ package stirling.software.SPDF.controller.api.converters; -import java.io.ByteArrayOutputStream; +import java.io.OutputStream; +import java.nio.file.Files; import java.util.List; import java.util.Locale; @@ -11,11 +12,10 @@ import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.usermodel.Workbook; import org.apache.poi.ss.util.WorkbookUtil; import org.apache.poi.xssf.usermodel.XSSFWorkbook; -import org.springframework.http.ContentDisposition; -import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.swagger.v3.oas.annotations.Operation; @@ -27,6 +27,9 @@ import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.ConvertApi; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.GeneralUtils; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; +import stirling.software.common.util.WebResponseUtils; import technology.tabula.ObjectExtractor; import technology.tabula.Page; @@ -40,6 +43,7 @@ import technology.tabula.extractors.SpreadsheetExtractionAlgorithm; public class ConvertPDFToExcelController { private final CustomPDFDocumentFactory pdfDocumentFactory; + private final TempFileManager tempFileManager; @AutoJobPostMapping(value = "/pdf/xlsx", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @Operation( @@ -47,11 +51,12 @@ public class ConvertPDFToExcelController { description = "Extracts tabular data from each page of a PDF and writes it into an Excel" + " workbook, one sheet per table. Input:PDF Output:XLSX Type:SISO") - public ResponseEntity pdfToExcel(@ModelAttribute PDFWithPageNums request) + public ResponseEntity pdfToExcel(@ModelAttribute PDFWithPageNums request) throws Exception { String baseName = GeneralUtils.removeExtension(request.getFileInput().getOriginalFilename()); + TempFile tempOut = tempFileManager.createManagedTempFile(".xlsx"); try (PDDocument document = pdfDocumentFactory.load(request); XSSFWorkbook workbook = new XSSFWorkbook(); ObjectExtractor extractor = new ObjectExtractor(document)) { @@ -89,21 +94,22 @@ public class ConvertPDFToExcelController { } if (sheetCount == 0) { + tempOut.close(); return ResponseEntity.noContent().build(); } - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - workbook.write(baos); - - HttpHeaders headers = new HttpHeaders(); - headers.setContentDisposition( - ContentDisposition.builder("attachment").filename(baseName + ".xlsx").build()); - headers.setContentType( - MediaType.parseMediaType( - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")); - - return ResponseEntity.ok().headers(headers).body(baos.toByteArray()); + try (OutputStream os = Files.newOutputStream(tempOut.getPath())) { + workbook.write(os); + } + } catch (Exception e) { + tempOut.close(); + throw e; } + + MediaType mediaType = + MediaType.parseMediaType( + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + return WebResponseUtils.fileToWebResponse(tempOut, baseName + ".xlsx", mediaType); } private String getUniqueSheetName(Workbook workbook, String baseName) { diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToHtml.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToHtml.java index 849e0af84b..13558d61aa 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToHtml.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToHtml.java @@ -4,6 +4,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.swagger.v3.oas.annotations.Operation; @@ -28,7 +29,8 @@ public class ConvertPDFToHtml { summary = "Convert PDF to HTML", description = "This endpoint converts a PDF file to HTML format. Input:PDF Output:HTML Type:SISO") - public ResponseEntity processPdfToHTML(@ModelAttribute PDFFile file) throws Exception { + public ResponseEntity processPdfToHTML(@ModelAttribute PDFFile file) + throws Exception { MultipartFile inputFile = file.getFileInput(); PDFToFile pdfToFile = new PDFToFile(tempFileManager, runtimePathConfig); return pdfToFile.processPdfToHtml(inputFile); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToOffice.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToOffice.java index 950a9600d6..391ce52398 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToOffice.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToOffice.java @@ -1,6 +1,8 @@ package stirling.software.SPDF.controller.api.converters; import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.text.PDFTextStripper; @@ -8,6 +10,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.swagger.v3.oas.annotations.Operation; @@ -23,6 +26,7 @@ import stirling.software.common.model.api.PDFFile; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.PDFToFile; +import stirling.software.common.util.TempFile; import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @@ -40,7 +44,7 @@ public class ConvertPDFToOffice { description = "This endpoint converts a given PDF file to a Presentation format. Input:PDF" + " Output:PPT Type:SISO") - public ResponseEntity processPdfToPresentation( + public ResponseEntity processPdfToPresentation( @ModelAttribute PdfToPresentationRequest request) throws IOException, InterruptedException { MultipartFile inputFile = request.getFileInput(); @@ -55,20 +59,24 @@ public class ConvertPDFToOffice { description = "This endpoint converts a given PDF file to Text or RTF format. Input:PDF" + " Output:TXT Type:SISO") - public ResponseEntity processPdfToRTForTXT( + public ResponseEntity processPdfToRTForTXT( @ModelAttribute PdfToTextOrRTFRequest request) throws IOException, InterruptedException { MultipartFile inputFile = request.getFileInput(); String outputFormat = request.getOutputFormat(); if ("txt".equals(request.getOutputFormat())) { + String fileName = + GeneralUtils.generateFilename(inputFile.getOriginalFilename(), ".txt"); + TempFile finalOut = tempFileManager.createManagedTempFile(".txt"); try (PDDocument document = pdfDocumentFactory.load(inputFile)) { PDFTextStripper stripper = new PDFTextStripper(); String text = stripper.getText(document); - return WebResponseUtils.bytesToWebResponse( - text.getBytes(), - GeneralUtils.generateFilename(inputFile.getOriginalFilename(), ".txt"), - MediaType.TEXT_PLAIN); + Files.writeString(finalOut.getPath(), text, StandardCharsets.UTF_8); + } catch (Exception e) { + finalOut.close(); + throw e; } + return WebResponseUtils.fileToWebResponse(finalOut, fileName, MediaType.TEXT_PLAIN); } else { PDFToFile pdfToFile = new PDFToFile(tempFileManager, runtimePathConfig); return pdfToFile.processPdfToOfficeFormat(inputFile, outputFormat, "writer_pdf_import"); @@ -81,8 +89,8 @@ public class ConvertPDFToOffice { description = "This endpoint converts a given PDF file to a Word document format. Input:PDF" + " Output:WORD Type:SISO") - public ResponseEntity processPdfToWord(@ModelAttribute PdfToWordRequest request) - throws IOException, InterruptedException { + public ResponseEntity processPdfToWord( + @ModelAttribute PdfToWordRequest request) throws IOException, InterruptedException { MultipartFile inputFile = request.getFileInput(); String outputFormat = request.getOutputFormat(); PDFToFile pdfToFile = new PDFToFile(tempFileManager, runtimePathConfig); @@ -95,7 +103,8 @@ public class ConvertPDFToOffice { description = "This endpoint converts a PDF file to an XML file. Input:PDF Output:XML" + " Type:SISO") - public ResponseEntity processPdfToXML(@ModelAttribute PDFFile file) throws Exception { + public ResponseEntity processPdfToXML(@ModelAttribute PDFFile file) + throws Exception { MultipartFile inputFile = file.getFileInput(); PDFToFile pdfToFile = new PDFToFile(tempFileManager, runtimePathConfig); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToPDFA.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToPDFA.java index d544d6ddab..98706bc98f 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToPDFA.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToPDFA.java @@ -77,6 +77,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.server.ResponseStatusException; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.github.pixee.security.Filenames; import io.swagger.v3.oas.annotations.Operation; @@ -92,6 +93,8 @@ import stirling.software.common.configuration.RuntimePathConfig; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.ProcessExecutor; import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @ConvertApi @@ -102,6 +105,7 @@ public class ConvertPDFToPDFA { private static final Pattern NON_PRINTABLE_ASCII = Pattern.compile("[^\\x20-\\x7E]"); private final RuntimePathConfig runtimePathConfig; private final stirling.software.SPDF.service.VeraPDFService veraPDFService; + private final TempFileManager tempFileManager; private static final String ICC_RESOURCE_PATH = "/icc/sRGB2014.icc"; private static final int PDFA_COMPATIBILITY_POLICY = 1; @@ -573,7 +577,7 @@ public class ConvertPDFToPDFA { summary = "Convert a PDF to a PDF/A or PDF/X", description = "This endpoint converts a PDF file to a PDF/A or PDF/X file using Ghostscript (preferred) or PDFBox/LibreOffice (fallback). PDF/A is a format designed for long-term archiving, while PDF/X is optimized for print production. Input:PDF Output:PDF Type:SISO") - public ResponseEntity pdfToPdfA(@ModelAttribute PdfToPdfARequest request) + public ResponseEntity pdfToPdfA(@ModelAttribute PdfToPdfARequest request) throws Exception { MultipartFile inputFile = request.getFileInput(); String outputFormat = request.getOutputFormat(); @@ -609,7 +613,7 @@ public class ConvertPDFToPDFA { return missing; } - private ResponseEntity handlePdfXConversion( + private ResponseEntity handlePdfXConversion( MultipartFile inputFile, String outputFormat) throws Exception { PdfXProfile profile = PdfXProfile.fromRequest(outputFormat); @@ -640,8 +644,14 @@ public class ConvertPDFToPDFA { log.info("PDF/X conversion completed successfully to {}", profile.getDisplayName()); - return WebResponseUtils.bytesToWebResponse( - converted, outputFilename, MediaType.APPLICATION_PDF); + TempFile tempOut = tempFileManager.createManagedTempFile(".pdf"); + try { + Files.write(tempOut.getPath(), converted); + } catch (Exception ex) { + tempOut.close(); + throw ex; + } + return WebResponseUtils.pdfFileToWebResponse(tempOut, outputFilename); } catch (IOException | InterruptedException e) { log.error("PDF/X conversion failed", e); @@ -1796,7 +1806,7 @@ public class ConvertPDFToPDFA { return Files.readAllBytes(outputPdf); } - private ResponseEntity handlePdfAConversion( + private ResponseEntity handlePdfAConversion( MultipartFile inputFile, String outputFormat, boolean strict) throws Exception { PdfaProfile profile = PdfaProfile.fromRequest(outputFormat); @@ -1830,8 +1840,14 @@ public class ConvertPDFToPDFA { verifyStrictCompliance(converted); } - return WebResponseUtils.bytesToWebResponse( - converted, outputFilename, MediaType.APPLICATION_PDF); + TempFile tempOut = tempFileManager.createManagedTempFile(".pdf"); + try { + Files.write(tempOut.getPath(), converted); + } catch (Exception ex) { + tempOut.close(); + throw ex; + } + return WebResponseUtils.pdfFileToWebResponse(tempOut, outputFilename); } catch (IOException | InterruptedException e) { log.warn( "Ghostscript conversion failed, falling back to PDFBox/LibreOffice method", @@ -1851,8 +1867,14 @@ public class ConvertPDFToPDFA { verifyStrictCompliance(converted); } - return WebResponseUtils.bytesToWebResponse( - converted, outputFilename, MediaType.APPLICATION_PDF); + TempFile tempOut = tempFileManager.createManagedTempFile(".pdf"); + try { + Files.write(tempOut.getPath(), converted); + } catch (Exception ex) { + tempOut.close(); + throw ex; + } + return WebResponseUtils.pdfFileToWebResponse(tempOut, outputFilename); } finally { deleteQuietly(workingDir); } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPdfJsonController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPdfJsonController.java index 2ab4a32798..7b7924ba3c 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPdfJsonController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPdfJsonController.java @@ -1,6 +1,7 @@ package stirling.software.SPDF.controller.api.converters; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.util.Optional; import java.util.UUID; import java.util.regex.Pattern; @@ -14,6 +15,7 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.github.pixee.security.Filenames; import io.swagger.v3.oas.annotations.Operation; @@ -31,6 +33,8 @@ import stirling.software.common.model.api.GeneralFile; import stirling.software.common.model.api.PDFFile; import stirling.software.common.service.JobOwnershipService; import stirling.software.common.util.ExceptionUtils; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @Slf4j @@ -42,6 +46,7 @@ public class ConvertPdfJsonController { private static final Pattern WHITESPACE_PATTERN = Pattern.compile("[\\r\\n\\t]+"); private static final Pattern NON_PRINTABLE_PATTERN = Pattern.compile("[^\\x20-\\x7E]"); private final PdfJsonConversionService pdfJsonConversionService; + private final TempFileManager tempFileManager; @Autowired(required = false) private JobOwnershipService jobOwnershipService; @@ -51,7 +56,7 @@ public class ConvertPdfJsonController { summary = "Convert PDF to Text Editor Format", description = "Extracts PDF text, fonts, and metadata into an editable JSON structure for the text editor tool. Input:PDF Output:JSON Type:SISO") - public ResponseEntity convertPdfToJson( + public ResponseEntity convertPdfToJson( @ModelAttribute PDFFile request, @RequestParam(value = "lightweight", defaultValue = "false") boolean lightweight) throws Exception { @@ -60,6 +65,8 @@ public class ConvertPdfJsonController { throw ExceptionUtils.createNullArgumentException("fileInput"); } + // TODO: Refactor PdfJsonConversionService to write directly to an OutputStream + // instead of returning byte[], avoiding the intermediate heap allocation + temp file write byte[] jsonBytes = pdfJsonConversionService.convertPdfToJson(inputFile, lightweight); logJsonResponse("pdf/text-editor", jsonBytes); String originalName = inputFile.getOriginalFilename(); @@ -70,7 +77,14 @@ public class ConvertPdfJsonController { .replaceFirst("") : "document"; String docName = baseName + ".json"; - return WebResponseUtils.bytesToWebResponse(jsonBytes, docName, MediaType.APPLICATION_JSON); + TempFile tempOut = tempFileManager.createManagedTempFile(".json"); + try { + Files.write(tempOut.getPath(), jsonBytes); + } catch (Exception e) { + tempOut.close(); + throw e; + } + return WebResponseUtils.fileToWebResponse(tempOut, docName, MediaType.APPLICATION_JSON); } @AutoJobPostMapping(consumes = "multipart/form-data", value = "/text-editor/pdf") @@ -79,8 +93,8 @@ public class ConvertPdfJsonController { summary = "Convert Text Editor Format to PDF", description = "Rebuilds a PDF from the editable JSON structure generated by the text editor tool. Input:JSON Output:PDF Type:SISO") - public ResponseEntity convertJsonToPdf(@ModelAttribute GeneralFile request) - throws Exception { + public ResponseEntity convertJsonToPdf( + @ModelAttribute GeneralFile request) throws Exception { MultipartFile jsonFile = request.getFileInput(); if (jsonFile == null) { throw ExceptionUtils.createNullArgumentException("fileInput"); @@ -95,7 +109,14 @@ public class ConvertPdfJsonController { .replaceFirst("") : "document"; String docName = baseName.endsWith(".pdf") ? baseName : baseName + ".pdf"; - return WebResponseUtils.bytesToWebResponse(pdfBytes, docName); + TempFile tempOut = tempFileManager.createManagedTempFile(".pdf"); + try { + Files.write(tempOut.getPath(), pdfBytes); + } catch (Exception e) { + tempOut.close(); + throw e; + } + return WebResponseUtils.pdfFileToWebResponse(tempOut, docName); } @AutoJobPostMapping(consumes = "multipart/form-data", value = "/pdf/text-editor/metadata") @@ -105,17 +126,15 @@ public class ConvertPdfJsonController { "Extracts document metadata, fonts, and page dimensions for the text editor tool. Caches the document for" + " subsequent page requests. Returns a server-generated jobId scoped to the" + " authenticated user. Input:PDF Output:JSON Type:SISO") - public ResponseEntity extractPdfMetadata(@ModelAttribute PDFFile request) + public ResponseEntity extractPdfMetadata(@ModelAttribute PDFFile request) throws Exception { MultipartFile inputFile = request.getFileInput(); if (inputFile == null) { throw ExceptionUtils.createNullArgumentException("fileInput"); } - // Generate server-side UUID for job String baseJobId = UUID.randomUUID().toString(); - // Scope job to authenticated user if security is enabled String scopedJobKey = getScopedJobKey(baseJobId); log.debug("Extracting metadata for PDF, assigned jobId: {}", scopedJobKey); @@ -123,20 +142,27 @@ public class ConvertPdfJsonController { byte[] jsonBytes = pdfJsonConversionService.extractDocumentMetadata(inputFile, scopedJobKey); logJsonResponse("pdf/text-editor/metadata", jsonBytes); - String originalName = inputFile.getOriginalFilename(); - String baseName = - (originalName != null && !originalName.isBlank()) - ? FILE_EXTENSION_PATTERN - .matcher(Filenames.toSimpleFileName(originalName)) - .replaceFirst("") - : "document"; - String docName = baseName + "_metadata.json"; - // Return jobId in response header for client + TempFile tempOut = tempFileManager.createManagedTempFile(".json"); + try { + Files.write(tempOut.getPath(), jsonBytes); + } catch (Exception e) { + tempOut.close(); + throw e; + } return ResponseEntity.ok() .header("X-Job-Id", scopedJobKey) .contentType(MediaType.APPLICATION_JSON) - .body(jsonBytes); + .contentLength(java.nio.file.Files.size(tempOut.getPath())) + .body( + os -> { + try (os) { + Files.copy(tempOut.getPath(), os); + os.flush(); + } finally { + tempOut.close(); + } + }); } @AutoJobPostMapping( @@ -149,7 +175,7 @@ public class ConvertPdfJsonController { "Applies edits for the specified pages of a cached PDF and returns an updated PDF." + " Requires the PDF to have been previously cached via the text editor metadata endpoint." + " The jobId must be obtained from the metadata extraction endpoint.") - public ResponseEntity exportPartialPdf( + public ResponseEntity exportPartialPdf( @PathVariable String jobId, @RequestBody PdfJsonDocument document, @RequestParam(value = "filename", required = false) String filename) @@ -158,7 +184,6 @@ public class ConvertPdfJsonController { throw ExceptionUtils.createNullArgumentException("document"); } - // Validate job ownership validateJobAccess(jobId); byte[] pdfBytes = pdfJsonConversionService.exportUpdatedPages(jobId, document); @@ -173,7 +198,14 @@ public class ConvertPdfJsonController { .filter(title -> title != null && !title.isBlank()) .orElse("document"); String docName = baseName.endsWith(".pdf") ? baseName : baseName + ".pdf"; - return WebResponseUtils.bytesToWebResponse(pdfBytes, docName); + TempFile tempOut = tempFileManager.createManagedTempFile(".pdf"); + try { + Files.write(tempOut.getPath(), pdfBytes); + } catch (Exception e) { + tempOut.close(); + throw e; + } + return WebResponseUtils.pdfFileToWebResponse(tempOut, docName); } @GetMapping(value = "/pdf/text-editor/page/{jobId}/{pageNumber}") @@ -183,16 +215,22 @@ public class ConvertPdfJsonController { "Retrieves a single page's content from a previously cached PDF document for the text editor tool." + " Requires prior call to /pdf/text-editor/metadata. The jobId must belong to the" + " authenticated user. Output:JSON") - public ResponseEntity extractSinglePage( + public ResponseEntity extractSinglePage( @PathVariable String jobId, @PathVariable int pageNumber) throws Exception { - // Validate job ownership validateJobAccess(jobId); byte[] jsonBytes = pdfJsonConversionService.extractSinglePage(jobId, pageNumber); logJsonResponse("pdf/text-editor/page", jsonBytes); String docName = "page_" + pageNumber + ".json"; - return WebResponseUtils.bytesToWebResponse(jsonBytes, docName, MediaType.APPLICATION_JSON); + TempFile tempOut = tempFileManager.createManagedTempFile(".json"); + try { + Files.write(tempOut.getPath(), jsonBytes); + } catch (Exception e) { + tempOut.close(); + throw e; + } + return WebResponseUtils.fileToWebResponse(tempOut, docName, MediaType.APPLICATION_JSON); } @GetMapping(value = "/pdf/text-editor/fonts/{jobId}/{pageNumber}") @@ -202,16 +240,22 @@ public class ConvertPdfJsonController { "Retrieves the font payloads used by a single page from a previously cached PDF document." + " Requires prior call to /pdf/text-editor/metadata. The jobId must belong to the" + " authenticated user. Output:JSON") - public ResponseEntity extractPageFonts( + public ResponseEntity extractPageFonts( @PathVariable String jobId, @PathVariable int pageNumber) throws Exception { - // Validate job ownership validateJobAccess(jobId); byte[] jsonBytes = pdfJsonConversionService.extractPageFonts(jobId, pageNumber); logJsonResponse("pdf/text-editor/fonts/page", jsonBytes); String docName = "page_fonts_" + pageNumber + ".json"; - return WebResponseUtils.bytesToWebResponse(jsonBytes, docName, MediaType.APPLICATION_JSON); + TempFile tempOut = tempFileManager.createManagedTempFile(".json"); + try { + Files.write(tempOut.getPath(), jsonBytes); + } catch (Exception e) { + tempOut.close(); + throw e; + } + return WebResponseUtils.fileToWebResponse(tempOut, docName, MediaType.APPLICATION_JSON); } @AutoJobPostMapping( @@ -225,24 +269,16 @@ public class ConvertPdfJsonController { + " authenticated user.") public ResponseEntity clearCache(@PathVariable String jobId) { - // Validate job ownership validateJobAccess(jobId); pdfJsonConversionService.clearCachedDocument(jobId); return ResponseEntity.ok().build(); } - /** - * Get a scoped job key that includes user ownership when security is enabled. - * - * @param baseJobId the base job identifier - * @return scoped job key, or just baseJobId if no ownership service available - */ private String getScopedJobKey(String baseJobId) { if (jobOwnershipService != null) { return jobOwnershipService.createScopedJobKey(baseJobId); } - // Security disabled, return unsecured job key return baseJobId; } @@ -252,7 +288,6 @@ public class ConvertPdfJsonController { return; } - // Only perform expensive tail extraction if debug logging is enabled if (log.isDebugEnabled()) { int length = jsonBytes.length; boolean endsWithJson = @@ -431,16 +466,9 @@ public class ConvertPdfJsonController { return WHITESPACE_PATTERN.matcher(value.substring(0, max)).replaceAll(" ") + "..."; } - /** - * Validate that the current user has access to the given job. - * - * @param jobId the job identifier to validate - * @throws SecurityException if current user does not own the job - */ private void validateJobAccess(String jobId) { if (jobOwnershipService != null) { jobOwnershipService.validateJobAccess(jobId); } - // If jobOwnershipService is null (security disabled), allow all access } } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertSvgToPDF.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertSvgToPDF.java index 0e90f01f53..dc79805236 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertSvgToPDF.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertSvgToPDF.java @@ -14,6 +14,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.github.pixee.security.Filenames; import io.swagger.v3.oas.annotations.Operation; @@ -53,7 +54,8 @@ public class ConvertSvgToPDF { + "SVG dimensions (width/height) determine the PDF page size; defaults to A4 if not specified. " + "SVG content is sanitized to prevent XSS attacks. " + "Input: SVG file(s), Output: PDF file(s) or ZIP. Type: MIMO") - public ResponseEntity convertSvgToPdf(@ModelAttribute SvgToPdfRequest request) { + public ResponseEntity convertSvgToPdf( + @ModelAttribute SvgToPdfRequest request) { MultipartFile[] inputFiles = request.getFileInput(); boolean combineIntoSinglePdf = Boolean.TRUE.equals(request.getCombineIntoSinglePdf()); @@ -61,8 +63,7 @@ public class ConvertSvgToPDF { // Validate input if (inputFiles == null || inputFiles.length == 0) { log.error("No files provided for SVG to PDF conversion."); - return ResponseEntity.badRequest() - .body("No files provided".getBytes(StandardCharsets.UTF_8)); + return errorResponse(HttpStatus.BAD_REQUEST, "No files provided"); } try { @@ -103,8 +104,7 @@ public class ConvertSvgToPDF { if (sanitizedSvgs.isEmpty()) { log.error("No valid SVG files were found"); - return ResponseEntity.status(HttpStatus.BAD_REQUEST) - .body("No valid SVG files were found".getBytes(StandardCharsets.UTF_8)); + return errorResponse(HttpStatus.BAD_REQUEST, "No valid SVG files were found"); } if (combineIntoSinglePdf) { @@ -115,14 +115,23 @@ public class ConvertSvgToPDF { } catch (Exception e) { log.error("Unexpected error during SVG to PDF conversion", e); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body( - "An unexpected error occurred during conversion" - .getBytes(StandardCharsets.UTF_8)); + return errorResponse( + HttpStatus.INTERNAL_SERVER_ERROR, + "An unexpected error occurred during conversion"); } } - private ResponseEntity handleCombinedConversion( + private ResponseEntity errorResponse(HttpStatus status, String message) { + byte[] body = message.getBytes(StandardCharsets.UTF_8); + StreamingResponseBody streaming = + os -> { + os.write(body); + os.flush(); + }; + return ResponseEntity.status(status).body(streaming); + } + + private ResponseEntity handleCombinedConversion( List sanitizedSvgs, List filenames) { try { log.info("Combining {} SVG files into single PDF", sanitizedSvgs.size()); @@ -131,10 +140,8 @@ public class ConvertSvgToPDF { if (pdfBytes == null || pdfBytes.length == 0) { log.error("PDF conversion failed - empty output"); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body( - "PDF conversion failed - empty output" - .getBytes(StandardCharsets.UTF_8)); + return errorResponse( + HttpStatus.INTERNAL_SERVER_ERROR, "PDF conversion failed - empty output"); } pdfBytes = pdfDocumentFactory.createNewBytesBasedOnOldDocument(pdfBytes); @@ -146,19 +153,23 @@ public class ConvertSvgToPDF { log.info("Successfully combined {} SVGs into single PDF", sanitizedSvgs.size()); - return WebResponseUtils.bytesToWebResponse( - pdfBytes, outputFilename, MediaType.APPLICATION_PDF); + TempFile tempOut = tempFileManager.createManagedTempFile(".pdf"); + try { + Files.write(tempOut.getPath(), pdfBytes); + } catch (Exception e) { + tempOut.close(); + throw e; + } + return WebResponseUtils.pdfFileToWebResponse(tempOut, outputFilename); } catch (IOException e) { log.error("Error combining SVGs into PDF", e); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body( - ("Conversion failed: " + e.getMessage()) - .getBytes(StandardCharsets.UTF_8)); + return errorResponse( + HttpStatus.INTERNAL_SERVER_ERROR, "Conversion failed: " + e.getMessage()); } } - private ResponseEntity handleSeparateConversion( + private ResponseEntity handleSeparateConversion( List sanitizedSvgs, List filenames) { List convertedPdfs = new ArrayList<>(); @@ -188,15 +199,21 @@ public class ConvertSvgToPDF { if (convertedPdfs.isEmpty()) { log.error("No files were successfully converted"); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body("No files were successfully converted".getBytes(StandardCharsets.UTF_8)); + return errorResponse( + HttpStatus.INTERNAL_SERVER_ERROR, "No files were successfully converted"); } try { if (convertedPdfs.size() == 1) { ConvertedPdf pdf = convertedPdfs.get(0); - return WebResponseUtils.bytesToWebResponse( - pdf.content, pdf.filename, MediaType.APPLICATION_PDF); + TempFile tempOut = tempFileManager.createManagedTempFile(".pdf"); + try { + Files.write(tempOut.getPath(), pdf.content); + } catch (Exception e) { + tempOut.close(); + throw e; + } + return WebResponseUtils.pdfFileToWebResponse(tempOut, pdf.filename); } String zipFilename = @@ -204,22 +221,18 @@ public class ConvertSvgToPDF { ? "converted_svgs.zip" : GeneralUtils.generateFilename( filenames.get(0), "_converted_svgs.zip"); - byte[] zipBytes = createZipFromPdfs(convertedPdfs); - - return WebResponseUtils.bytesToWebResponse( - zipBytes, zipFilename, MediaType.APPLICATION_OCTET_STREAM); + TempFile zipFile = createZipFromPdfs(convertedPdfs); + return WebResponseUtils.zipFileToWebResponse(zipFile, zipFilename); } catch (IOException e) { log.error("Failed to create response", e); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body("Failed to create response".getBytes(StandardCharsets.UTF_8)); + return errorResponse(HttpStatus.INTERNAL_SERVER_ERROR, "Failed to create response"); } } - private byte[] createZipFromPdfs(List pdfs) throws IOException { - try (TempFile tempZipFile = new TempFile(tempFileManager, ".zip"); - ZipOutputStream zipOut = - new ZipOutputStream(Files.newOutputStream(tempZipFile.getPath()))) { - + private TempFile createZipFromPdfs(List pdfs) throws IOException { + TempFile tempZipFile = tempFileManager.createManagedTempFile(".zip"); + try (ZipOutputStream zipOut = + new ZipOutputStream(Files.newOutputStream(tempZipFile.getPath()))) { for (ConvertedPdf pdf : pdfs) { ZipEntry pdfEntry = new ZipEntry(pdf.filename); zipOut.putNextEntry(pdfEntry); @@ -227,9 +240,11 @@ public class ConvertSvgToPDF { zipOut.closeEntry(); log.debug("Added {} to ZIP", pdf.filename); } - - return Files.readAllBytes(tempZipFile.getPath()); + } catch (IOException e) { + tempZipFile.close(); + throw e; } + return tempZipFile; } private static class ConvertedPdf { diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertWebsiteToPDF.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertWebsiteToPDF.java index 00f5762858..1fa927c113 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertWebsiteToPDF.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertWebsiteToPDF.java @@ -1,6 +1,5 @@ package stirling.software.SPDF.controller.api.converters; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.net.URI; import java.net.http.HttpClient; @@ -39,6 +38,8 @@ import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.ProcessExecutor; import stirling.software.common.util.RegexPatternUtils; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @ConvertApi @@ -49,6 +50,7 @@ public class ConvertWebsiteToPDF { private final CustomPDFDocumentFactory pdfDocumentFactory; private final RuntimePathConfig runtimePathConfig; private final ApplicationProperties applicationProperties; + private final TempFileManager tempFileManager; private static final Pattern FILE_SCHEME_PATTERN = Pattern.compile("(? createZipResponse(List entries, String baseName) - throws IOException { + throws Exception { ByteArrayOutputStream baos = new ByteArrayOutputStream(); try (ZipOutputStream zipOut = new ZipOutputStream(baos)) { for (CsvEntry entry : entries) { @@ -101,14 +101,10 @@ public class ExtractCSVController { } } - HttpHeaders headers = new HttpHeaders(); - headers.setContentDisposition( - ContentDisposition.builder("attachment") - .filename(baseName + "_extracted.zip") - .build()); - headers.setContentType(MediaType.parseMediaType("application/zip")); - - return ResponseEntity.ok().headers(headers).body(baos.toByteArray()); + return WebResponseUtils.bytesToWebResponse( + baos.toByteArray(), + baseName + "_extracted.zip", + MediaType.APPLICATION_OCTET_STREAM); } private ResponseEntity createCsvResponse(CsvEntry entry, String baseName) { diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/PdfVectorExportController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/PdfVectorExportController.java index ce5549c9f4..fcee92f025 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/PdfVectorExportController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/PdfVectorExportController.java @@ -13,6 +13,7 @@ import org.apache.commons.io.FilenameUtils; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.swagger.v3.oas.annotations.Operation; @@ -38,7 +39,6 @@ import stirling.software.common.util.WebResponseUtils; @RequiredArgsConstructor public class PdfVectorExportController { - private static final MediaType PDF_MEDIA_TYPE = MediaType.APPLICATION_PDF; private static final Set GHOSTSCRIPT_INPUTS = Set.of("ps", "eps", "epsf"); // PCL/PXL/XPS require GhostPDL (gpcl6/gxps) @@ -51,7 +51,7 @@ public class PdfVectorExportController { description = "Converts PostScript vector inputs (PS, EPS, EPSF) to PDF using Ghostscript." + " Input:PS/EPS Output:PDF Type:SISO") - public ResponseEntity convertGhostscriptInputsToPdf( + public ResponseEntity convertGhostscriptInputsToPdf( @Valid @ModelAttribute PdfVectorExportRequest request) throws Exception { String originalName = @@ -63,9 +63,9 @@ public class PdfVectorExportController { ? FilenameUtils.getExtension(originalName).toLowerCase(Locale.ROOT) : ""; + TempFile outputTemp = tempFileManager.createManagedTempFile(".pdf"); try (TempFile inputTemp = - new TempFile(tempFileManager, extension.isEmpty() ? "" : "." + extension); - TempFile outputTemp = new TempFile(tempFileManager, ".pdf")) { + new TempFile(tempFileManager, extension.isEmpty() ? "" : "." + extension)) { request.getFileInput().transferTo(inputTemp.getFile()); @@ -83,11 +83,13 @@ public class PdfVectorExportController { "Unsupported Ghostscript input format {0}", extension); } - - byte[] pdfBytes = Files.readAllBytes(outputTemp.getPath()); - String outputName = GeneralUtils.generateFilename(originalName, "_converted.pdf"); - return WebResponseUtils.bytesToWebResponse(pdfBytes, outputName, PDF_MEDIA_TYPE); + } catch (Exception e) { + outputTemp.close(); + throw e; } + + String outputName = GeneralUtils.generateFilename(originalName, "_converted.pdf"); + return WebResponseUtils.pdfFileToWebResponse(outputTemp, outputName); } @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/vector") @@ -96,7 +98,7 @@ public class PdfVectorExportController { description = "Converts PDF to Ghostscript vector formats (EPS, PS, PCL, or XPS)." + " Input:PDF Output:VECTOR Type:SISO") - public ResponseEntity convertPdfToVector( + public ResponseEntity convertPdfToVector( @Valid @ModelAttribute PdfVectorExportRequest request) throws Exception { String originalName = @@ -110,35 +112,37 @@ public class PdfVectorExportController { } outputFormat = outputFormat.toLowerCase(Locale.ROOT); - try (TempFile inputTemp = new TempFile(tempFileManager, ".pdf"); - TempFile outputTemp = new TempFile(tempFileManager, "." + outputFormat)) { + TempFile outputTemp = tempFileManager.createManagedTempFile("." + outputFormat); + try (TempFile inputTemp = new TempFile(tempFileManager, ".pdf")) { request.getFileInput().transferTo(inputTemp.getFile()); runGhostscriptPdfToVector(inputTemp.getPath(), outputTemp.getPath(), outputFormat); - - byte[] vectorBytes = Files.readAllBytes(outputTemp.getPath()); - String outputName = - GeneralUtils.generateFilename(originalName, "_converted." + outputFormat); - - MediaType mediaType; - switch (outputFormat.toLowerCase(Locale.ROOT)) { - case "eps": - case "ps": - mediaType = MediaType.parseMediaType("application/postscript"); - break; - case "pcl": - mediaType = MediaType.parseMediaType("application/vnd.hp-PCL"); - break; - case "xps": - mediaType = MediaType.parseMediaType("application/vnd.ms-xpsdocument"); - break; - default: - mediaType = MediaType.APPLICATION_OCTET_STREAM; - } - - return WebResponseUtils.bytesToWebResponse(vectorBytes, outputName, mediaType); + } catch (Exception e) { + outputTemp.close(); + throw e; } + + String outputName = + GeneralUtils.generateFilename(originalName, "_converted." + outputFormat); + + MediaType mediaType; + switch (outputFormat.toLowerCase(Locale.ROOT)) { + case "eps": + case "ps": + mediaType = MediaType.parseMediaType("application/postscript"); + break; + case "pcl": + mediaType = MediaType.parseMediaType("application/vnd.hp-PCL"); + break; + case "xps": + mediaType = MediaType.parseMediaType("application/vnd.ms-xpsdocument"); + break; + default: + mediaType = MediaType.APPLICATION_OCTET_STREAM; + } + + return WebResponseUtils.fileToWebResponse(outputTemp, outputName, mediaType); } private void runGhostscriptPdfToVector(Path inputPath, Path outputPath, String outputFormat) diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/filters/FilterController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/filters/FilterController.java index 795151970a..514d028c6b 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/filters/FilterController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/filters/FilterController.java @@ -9,6 +9,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.github.pixee.security.Filenames; import io.swagger.v3.oas.annotations.Operation; @@ -29,6 +30,7 @@ import stirling.software.common.annotations.api.FilterApi; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.PdfUtils; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @FilterApi @@ -36,6 +38,7 @@ import stirling.software.common.util.WebResponseUtils; public class FilterController { private final CustomPDFDocumentFactory pdfDocumentFactory; + private final TempFileManager tempFileManager; @AutoJobPostMapping( consumes = MediaType.MULTIPART_FORM_DATA_VALUE, @@ -53,8 +56,8 @@ public class FilterController { description = "PDF did not pass filter", content = @Content()) }) - public ResponseEntity containsText(@ModelAttribute ContainsTextRequest request) - throws IOException, InterruptedException { + public ResponseEntity containsText( + @ModelAttribute ContainsTextRequest request) throws IOException, InterruptedException { MultipartFile inputFile = request.getFileInput(); String text = request.getText(); String pageNumber = request.getPageNumbers(); @@ -62,7 +65,9 @@ public class FilterController { try (PDDocument pdfDocument = pdfDocumentFactory.load(inputFile)) { if (PdfUtils.hasText(pdfDocument, pageNumber, text)) { return WebResponseUtils.pdfDocToWebResponse( - pdfDocument, Filenames.toSimpleFileName(inputFile.getOriginalFilename())); + pdfDocument, + Filenames.toSimpleFileName(inputFile.getOriginalFilename()), + tempFileManager); } } return ResponseEntity.noContent().build(); @@ -84,15 +89,17 @@ public class FilterController { description = "PDF did not pass filter", content = @Content()) }) - public ResponseEntity containsImage(@ModelAttribute PDFWithPageNums request) - throws IOException, InterruptedException { + public ResponseEntity containsImage( + @ModelAttribute PDFWithPageNums request) throws IOException, InterruptedException { MultipartFile inputFile = request.getFileInput(); String pageNumber = request.getPageNumbers(); try (PDDocument pdfDocument = pdfDocumentFactory.load(inputFile)) { if (PdfUtils.hasImages(pdfDocument, pageNumber)) { return WebResponseUtils.pdfDocToWebResponse( - pdfDocument, Filenames.toSimpleFileName(inputFile.getOriginalFilename())); + pdfDocument, + Filenames.toSimpleFileName(inputFile.getOriginalFilename()), + tempFileManager); } } return ResponseEntity.noContent().build(); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/form/FormFillController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/form/FormFillController.java index 5c96c7030a..8692b04bcc 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/form/FormFillController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/form/FormFillController.java @@ -18,6 +18,7 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import com.opencsv.CSVWriter; @@ -34,6 +35,7 @@ import stirling.software.common.model.FormFieldWithCoordinates; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.FormUtils; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; import tools.jackson.core.type.TypeReference; @@ -59,12 +61,11 @@ public class FormFillController { private final CustomPDFDocumentFactory pdfDocumentFactory; private final ObjectMapper objectMapper; + private final TempFileManager tempFileManager; - private static ResponseEntity saveDocument(PDDocument document, String baseName) + private ResponseEntity saveDocument(PDDocument document, String baseName) throws IOException { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - document.save(baos); - return WebResponseUtils.bytesToWebResponse(baos.toByteArray(), baseName + ".pdf"); + return WebResponseUtils.pdfDocToWebResponse(document, baseName + ".pdf", tempFileManager); } private static String buildBaseName(MultipartFile file, String suffix) { @@ -261,7 +262,7 @@ public class FormFillController { summary = "Modify existing form fields", description = "Updates existing fields in the provided PDF and returns the updated file") - public ResponseEntity modifyFields( + public ResponseEntity modifyFields( @Parameter( description = "The input PDF file", required = true, @@ -292,7 +293,7 @@ public class FormFillController { @Operation( summary = "Delete form fields", description = "Removes the specified fields from the PDF and returns the updated file") - public ResponseEntity deleteFields( + public ResponseEntity deleteFields( @Parameter( description = "The input PDF file", required = true, @@ -328,7 +329,7 @@ public class FormFillController { description = "Populates the supplied PDF form using values from the provided JSON payload" + " and returns the filled PDF") - public ResponseEntity fillForm( + public ResponseEntity fillForm( @Parameter( description = "The input PDF file", required = true, @@ -355,7 +356,7 @@ public class FormFillController { document -> FormUtils.applyFieldValues(document, values, flatten, true)); } - private ResponseEntity processSingleFile( + private ResponseEntity processSingleFile( MultipartFile file, String suffix, DocumentProcessor processor) throws IOException { requirePdf(file); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/AttachmentController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/AttachmentController.java index 197a9f52db..9ee659c35c 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/AttachmentController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/AttachmentController.java @@ -1,7 +1,7 @@ package stirling.software.SPDF.controller.api.misc; -import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.nio.file.Files; import java.util.List; import java.util.Optional; @@ -10,6 +10,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.github.pixee.security.Filenames; import io.swagger.v3.oas.annotations.Operation; @@ -30,6 +31,8 @@ import stirling.software.common.annotations.api.MiscApi; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.GeneralUtils; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @MiscApi @@ -43,14 +46,16 @@ public class AttachmentController { private final ConvertPDFToPDFA convertPDFToPDFA; + private final TempFileManager tempFileManager; + @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/add-attachments") @StandardPdfResponse @Operation( summary = "Add attachments to PDF", description = "This endpoint adds attachments to a PDF. Input:PDF, Output:PDF Type:MISO") - public ResponseEntity addAttachments(@ModelAttribute AddAttachmentRequest request) - throws Exception { + public ResponseEntity addAttachments( + @ModelAttribute AddAttachmentRequest request) throws Exception { MultipartFile fileInput = request.getFileInput(); List attachments = request.getAttachments(); boolean convertToPdfA3b = request.isConvertToPdfA3b(); @@ -79,13 +84,9 @@ public class AttachmentController { ConvertPDFToPDFA.fixType1FontCharSet(pdfaDocument); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - pdfaDocument.save(baos); - byte[] resultBytes = baos.toByteArray(); - String outputFilename = baseFileName + "_with_attachments_PDFA-3b.pdf"; - return WebResponseUtils.bytesToWebResponse( - resultBytes, outputFilename, MediaType.APPLICATION_PDF); + return WebResponseUtils.pdfDocToWebResponse( + pdfaDocument, outputFilename, tempFileManager); } } else { try (PDDocument document = pdfDocumentFactory.load(request, false)) { @@ -94,7 +95,8 @@ public class AttachmentController { document, GeneralUtils.generateFilename( Filenames.toSimpleFileName(fileInput.getOriginalFilename()), - "_with_attachments.pdf")); + "_with_attachments.pdf"), + tempFileManager); } } } @@ -141,7 +143,7 @@ public class AttachmentController { description = "This endpoint extracts all embedded attachments from a PDF into a ZIP archive." + " Input:PDF Output:ZIP Type:SISO") - public ResponseEntity extractAttachments( + public ResponseEntity extractAttachments( @ModelAttribute ExtractAttachmentsRequest request) throws IOException { try (PDDocument document = pdfDocumentFactory.load(request, true)) { Optional extracted = pdfAttachmentService.extractAttachments(document); @@ -159,8 +161,14 @@ public class AttachmentController { Filenames.toSimpleFileName( GeneralUtils.generateFilename(sourceName, "_attachments.zip")); - return WebResponseUtils.bytesToWebResponse( - extracted.get(), outputName, MediaType.APPLICATION_OCTET_STREAM); + TempFile tempOut = tempFileManager.createManagedTempFile(".zip"); + try { + Files.write(tempOut.getFile().toPath(), extracted.get()); + } catch (IOException e) { + tempOut.close(); + throw e; + } + return WebResponseUtils.zipFileToWebResponse(tempOut, outputName); } } @@ -187,8 +195,8 @@ public class AttachmentController { summary = "Rename attachment in PDF", description = "This endpoint renames an embedded attachment in a PDF. Input:PDF Output:PDF Type:MISO") - public ResponseEntity renameAttachment(@ModelAttribute RenameAttachmentRequest request) - throws Exception { + public ResponseEntity renameAttachment( + @ModelAttribute RenameAttachmentRequest request) throws Exception { MultipartFile fileInput = request.getFileInput(); String attachmentName = request.getAttachmentName(); String newName = request.getNewName(); @@ -209,7 +217,8 @@ public class AttachmentController { document, GeneralUtils.generateFilename( Filenames.toSimpleFileName(fileInput.getOriginalFilename()), - "_attachment_renamed.pdf")); + "_attachment_renamed.pdf"), + tempFileManager); } } @@ -221,8 +230,8 @@ public class AttachmentController { summary = "Delete attachment from PDF", description = "This endpoint deletes an embedded attachment from a PDF. Input:PDF Output:PDF Type:MISO") - public ResponseEntity deleteAttachment(@ModelAttribute DeleteAttachmentRequest request) - throws Exception { + public ResponseEntity deleteAttachment( + @ModelAttribute DeleteAttachmentRequest request) throws Exception { MultipartFile fileInput = request.getFileInput(); String attachmentName = request.getAttachmentName(); @@ -238,7 +247,8 @@ public class AttachmentController { document, GeneralUtils.generateFilename( Filenames.toSimpleFileName(fileInput.getOriginalFilename()), - "_attachment_deleted.pdf")); + "_attachment_deleted.pdf"), + tempFileManager); } } } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/AutoRenameController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/AutoRenameController.java index 32eef1a1e9..58fd995f6d 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/AutoRenameController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/AutoRenameController.java @@ -12,6 +12,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.github.pixee.security.Filenames; import io.swagger.v3.oas.annotations.Operation; @@ -24,6 +25,7 @@ import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.MiscApi; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.RegexPatternUtils; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @MiscApi @@ -35,6 +37,7 @@ public class AutoRenameController { private static final int LINE_LIMIT = 200; private final CustomPDFDocumentFactory pdfDocumentFactory; + private final TempFileManager tempFileManager; @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/auto-rename") @Operation( @@ -42,8 +45,8 @@ public class AutoRenameController { description = "This endpoint accepts a PDF file and attempts to extract its title or header" + " based on heuristics. Input:PDF Output:PDF Type:SISO") - public ResponseEntity extractHeader(@ModelAttribute ExtractHeaderRequest request) - throws Exception { + public ResponseEntity extractHeader( + @ModelAttribute ExtractHeaderRequest request) throws Exception { MultipartFile file = request.getFileInput(); boolean useFirstTextAsFallback = Boolean.TRUE.equals(request.getUseFirstTextAsFallback()); @@ -140,11 +143,14 @@ public class AutoRenameController { .matcher(header) .replaceAll("") .trim(); - return WebResponseUtils.pdfDocToWebResponse(document, header + ".pdf"); + return WebResponseUtils.pdfDocToWebResponse( + document, header + ".pdf", tempFileManager); } else { log.info("File has no good title to be found"); return WebResponseUtils.pdfDocToWebResponse( - document, Filenames.toSimpleFileName(file.getOriginalFilename())); + document, + Filenames.toSimpleFileName(file.getOriginalFilename()), + tempFileManager); } } } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/AutoSplitPdfController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/AutoSplitPdfController.java index 2de7de4ca9..d771df580a 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/AutoSplitPdfController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/AutoSplitPdfController.java @@ -22,6 +22,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import com.google.zxing.*; import com.google.zxing.common.GlobalHistogramBinarizer; @@ -275,8 +276,8 @@ public class AutoSplitPdfController { + " splits the document at the QR code boundaries. The output is a zip" + " file containing each separate PDF document. Input:PDF Output:ZIP-PDF" + " Type:SISO") - public ResponseEntity autoSplitPdf(@ModelAttribute AutoSplitPdfRequest request) - throws IOException { + public ResponseEntity autoSplitPdf( + @ModelAttribute AutoSplitPdfRequest request) throws IOException { MultipartFile file = request.getFileInput(); boolean duplexMode = Boolean.TRUE.equals(request.getDuplexMode()); @@ -287,8 +288,8 @@ public class AutoSplitPdfController { duplexMode); List splitDocuments = new ArrayList<>(); - try (TempFile outputTempFile = new TempFile(tempFileManager, ".zip"); - PDDocument document = pdfDocumentFactory.load(file.getInputStream())) { + TempFile outputTempFile = new TempFile(tempFileManager, ".zip"); + try (PDDocument document = pdfDocumentFactory.load(file.getInputStream())) { int totalPages = document.getNumberOfPages(); log.info("PDF loaded, totalPages={}", totalPages); @@ -357,11 +358,10 @@ public class AutoSplitPdfController { } } - byte[] data = Files.readAllBytes(outputTempFile.getPath()); - return WebResponseUtils.bytesToWebResponse( - data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM); + return WebResponseUtils.zipFileToWebResponse(outputTempFile, filename + ".zip"); } catch (Exception e) { + outputTempFile.close(); log.error("Error in auto split", e); throw e; } finally { diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/BlankPageController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/BlankPageController.java index 486e9d1176..98364d9c2f 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/BlankPageController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/BlankPageController.java @@ -1,8 +1,9 @@ package stirling.software.SPDF.controller.api.misc; import java.awt.image.BufferedImage; -import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Files; import java.util.ArrayList; import java.util.List; import java.util.Locale; @@ -19,6 +20,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.github.pixee.security.Filenames; import io.swagger.v3.oas.annotations.Operation; @@ -35,6 +37,8 @@ import stirling.software.common.util.ApplicationContextProvider; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.PdfUtils; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @MiscApi @@ -43,6 +47,7 @@ import stirling.software.common.util.WebResponseUtils; public class BlankPageController { private final CustomPDFDocumentFactory pdfDocumentFactory; + private final TempFileManager tempFileManager; public static boolean isBlankImage( BufferedImage image, int threshold, double whitePercent, int blurSize) { @@ -83,7 +88,8 @@ public class BlankPageController { "This endpoint removes blank pages from a given PDF file. Users can specify the" + " threshold and white percentage to tune the detection of blank pages." + " Input:PDF Output:PDF Type:SISO") - public ResponseEntity removeBlankPages(@ModelAttribute RemoveBlankPagesRequest request) + public ResponseEntity removeBlankPages( + @ModelAttribute RemoveBlankPagesRequest request) throws IOException, InterruptedException { MultipartFile inputFile = request.getFileInput(); int threshold = request.getThreshold(); @@ -149,28 +155,29 @@ public class BlankPageController { pageIndex++; } - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - ZipOutputStream zos = new ZipOutputStream(baos); - String filename = GeneralUtils.removeExtension( Filenames.toSimpleFileName(inputFile.getOriginalFilename())); - if (!nonBlankPages.isEmpty()) { - createZipEntry(zos, nonBlankPages, filename + "_nonBlankPages.pdf"); - } else { - createZipEntry(zos, blankPages, filename + "_allBlankPages.pdf"); - } + TempFile tempOut = tempFileManager.createManagedTempFile(".zip"); + try (OutputStream fos = Files.newOutputStream(tempOut.getFile().toPath()); + ZipOutputStream zos = new ZipOutputStream(fos)) { + if (!nonBlankPages.isEmpty()) { + createZipEntry(zos, nonBlankPages, filename + "_nonBlankPages.pdf"); + } else { + createZipEntry(zos, blankPages, filename + "_allBlankPages.pdf"); + } - if (!nonBlankPages.isEmpty() && !blankPages.isEmpty()) { - createZipEntry(zos, blankPages, filename + "_blankPages.pdf"); + if (!nonBlankPages.isEmpty() && !blankPages.isEmpty()) { + createZipEntry(zos, blankPages, filename + "_blankPages.pdf"); + } + } catch (IOException e) { + tempOut.close(); + throw e; } - zos.close(); - log.info("Returning ZIP file: {}", filename + "_processed.zip"); - return WebResponseUtils.baosToWebResponse( - baos, filename + "_processed.zip", MediaType.APPLICATION_OCTET_STREAM); + return WebResponseUtils.zipFileToWebResponse(tempOut, filename + "_processed.zip"); } catch (ExceptionUtils.OutOfMemoryDpiException e) { throw e; diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/CompressController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/CompressController.java index 0a321ff3b8..d4ad134ece 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/CompressController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/CompressController.java @@ -38,6 +38,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.server.ResponseStatusException; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.swagger.v3.oas.annotations.Operation; @@ -328,7 +329,8 @@ public class CompressController { + "_" + image.getBitsPerComponent(); - return bytesToHexString(generateMD5(enhancedData.getBytes())); + return bytesToHexString( + generateMD5(enhancedData.getBytes(StandardCharsets.UTF_8))); } return "empty-stream"; } @@ -727,7 +729,8 @@ public class CompressController { params.append("_").append(image.getDecode().toString()); } - return bytesToHexString(generateMD5(params.toString().getBytes())); + return bytesToHexString( + generateMD5(params.toString().getBytes(StandardCharsets.UTF_8))); } catch (Exception e) { return "fallback-decode-" + System.identityHashCode(image); } @@ -798,7 +801,8 @@ public class CompressController { metadata.append("_softmask"); } - return bytesToHexString(generateMD5(metadata.toString().getBytes())); + return bytesToHexString( + generateMD5(metadata.toString().getBytes(StandardCharsets.UTF_8))); } catch (Exception e) { return "fallback-meta-" + System.identityHashCode(image); } @@ -924,8 +928,8 @@ public class CompressController { description = "This endpoint accepts a PDF file and optimizes it based on the provided" + " parameters. Input:PDF Output:PDF Type:SISO") - public ResponseEntity optimizePdf(@ModelAttribute OptimizePdfRequest request) - throws Exception { + public ResponseEntity optimizePdf( + @ModelAttribute OptimizePdfRequest request) throws Exception { MultipartFile inputFile = request.getFileInput(); // Validate input file @@ -1097,7 +1101,8 @@ public class CompressController { try { try (PDDocument document = pdfDocumentFactory.load(currentFile.toFile())) { - return WebResponseUtils.pdfDocToWebResponse(document, outputFilename); + return WebResponseUtils.pdfDocToWebResponse( + document, outputFilename, tempFileManager); } } catch (IOException e) { throw ExceptionUtils.handlePdfException(e, "PDF optimization"); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/DecompressPdfController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/DecompressPdfController.java index 2abfaa8af6..9afe8ba235 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/DecompressPdfController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/DecompressPdfController.java @@ -1,6 +1,5 @@ package stirling.software.SPDF.controller.api.misc; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.HashSet; @@ -14,6 +13,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.swagger.v3.oas.annotations.Operation; @@ -26,6 +26,8 @@ import stirling.software.common.model.api.PDFFile; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.GeneralUtils; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @MiscApi @@ -34,12 +36,13 @@ import stirling.software.common.util.WebResponseUtils; public class DecompressPdfController { private final CustomPDFDocumentFactory pdfDocumentFactory; + private final TempFileManager tempFileManager; @AutoJobPostMapping(value = "/decompress-pdf", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @Operation( summary = "Decompress PDF streams", description = "Fully decompresses all PDF streams including text content") - public ResponseEntity decompressPdf(@ModelAttribute PDFFile request) + public ResponseEntity decompressPdf(@ModelAttribute PDFFile request) throws IOException { MultipartFile file = request.getFileInput(); @@ -48,13 +51,18 @@ public class DecompressPdfController { // Process all objects in document processAllObjects(document); - // Save with explicit no compression - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - document.save(baos, CompressParameters.NO_COMPRESSION); + // Save with explicit no compression to a temp file + TempFile tempOut = tempFileManager.createManagedTempFile(".pdf"); + try { + document.save(tempOut.getFile(), CompressParameters.NO_COMPRESSION); + } catch (IOException e) { + tempOut.close(); + throw e; + } - // Return the PDF as a response - return WebResponseUtils.bytesToWebResponse( - baos.toByteArray(), + // Return the PDF as a streaming response + return WebResponseUtils.pdfFileToWebResponse( + tempOut, GeneralUtils.generateFilename(file.getOriginalFilename(), "_decompressed.pdf")); } } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ExtractImageScansController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ExtractImageScansController.java index bcf57d17a9..c5cfef2c79 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ExtractImageScansController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ExtractImageScansController.java @@ -1,8 +1,8 @@ package stirling.software.SPDF.controller.api.misc; import java.awt.image.BufferedImage; -import java.io.FileOutputStream; import java.io.IOException; +import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; @@ -21,6 +21,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.swagger.v3.oas.annotations.Operation; @@ -39,6 +40,8 @@ import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.ProcessExecutor; import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @MiscApi @@ -49,6 +52,7 @@ public class ExtractImageScansController { private static final String REPLACEFIRST = "[.][^.]+$"; private final CustomPDFDocumentFactory pdfDocumentFactory; + private final TempFileManager tempFileManager; @AutoJobPostMapping( consumes = MediaType.MULTIPART_FORM_DATA_VALUE, @@ -61,7 +65,7 @@ public class ExtractImageScansController { + " parameters. Users can specify angle threshold, tolerance, minimum area," + " minimum contour area, and border size. Input:PDF Output:IMAGE/ZIP" + " Type:SIMO") - public ResponseEntity extractImageScans( + public ResponseEntity extractImageScans( @ModelAttribute ExtractImageScansRequest request) throws IOException, InterruptedException { MultipartFile inputFile = request.getFileInput(); @@ -71,9 +75,8 @@ public class ExtractImageScansController { List images = new ArrayList<>(); - List tempImageFiles = new ArrayList<>(); - Path tempInputFile; - Path tempZipFile = null; + List tempImageFiles = new ArrayList<>(); + TempFile tempInputFile = null; List tempDirs = new ArrayList<>(); if (!CheckProgramInstall.isPythonAvailable()) { @@ -83,6 +86,8 @@ public class ExtractImageScansController { String pythonVersion = CheckProgramInstall.getAvailablePythonCommand(); Path splitPhotosScript = GeneralUtils.extractScript("split_photos.py"); + TempFile finalOutput = null; + boolean finalOutputOwnershipTransferred = false; try { // Check if input file is a PDF if ("pdf".equalsIgnoreCase(extension)) { @@ -96,7 +101,8 @@ public class ExtractImageScansController { // Create images of all pages for (int i = 0; i < pageCount; i++) { // Create temp file to save the image - Path tempFile = Files.createTempFile("image_", ".png"); + TempFile tempImage = tempFileManager.createManagedTempFile(".png"); + tempImageFiles.add(tempImage); // Render image and save as temp file BufferedImage image; @@ -116,18 +122,17 @@ public class ExtractImageScansController { pageIndex + 1, dpi, () -> pdfRenderer.renderImageWithDPI(pageIndex, dpi)); - ImageIO.write(image, "png", tempFile.toFile()); + ImageIO.write(image, "png", tempImage.getFile()); // Add temp file path to images list - images.add(tempFile.toString()); - tempImageFiles.add(tempFile); + images.add(tempImage.getAbsolutePath()); } } } else { - tempInputFile = Files.createTempFile("input_", "." + extension); - inputFile.transferTo(tempInputFile); + tempInputFile = tempFileManager.createManagedTempFile("." + extension); + inputFile.transferTo(tempInputFile.getFile()); // Add input file path to images list - images.add(tempInputFile.toString()); + images.add(tempInputFile.getAbsolutePath()); } List processedImageBytes = new ArrayList<>(); @@ -177,10 +182,10 @@ public class ExtractImageScansController { if (processedImageBytes.size() > 1) { String outputZipFilename = GeneralUtils.generateFilename(fileName, "_processed.zip"); - tempZipFile = Files.createTempFile("output_", ".zip"); + finalOutput = tempFileManager.createManagedTempFile(".zip"); try (ZipOutputStream zipOut = - new ZipOutputStream(new FileOutputStream(tempZipFile.toFile()))) { + new ZipOutputStream(Files.newOutputStream(finalOutput.getPath()))) { // Add processed images to the zip for (int i = 0; i < processedImageBytes.size(); i++) { ZipEntry entry = @@ -193,13 +198,10 @@ public class ExtractImageScansController { } } - byte[] zipBytes = Files.readAllBytes(tempZipFile); - - // Clean up the temporary zip file - Files.deleteIfExists(tempZipFile); - - return WebResponseUtils.bytesToWebResponse( - zipBytes, outputZipFilename, MediaType.APPLICATION_OCTET_STREAM); + ResponseEntity response = + WebResponseUtils.zipFileToWebResponse(finalOutput, outputZipFilename); + finalOutputOwnershipTransferred = true; + return response; } if (processedImageBytes.isEmpty()) { throw ExceptionUtils.createIllegalArgumentException( @@ -208,28 +210,28 @@ public class ExtractImageScansController { // Return the processed image as a response byte[] imageBytes = processedImageBytes.get(0); - return WebResponseUtils.bytesToWebResponse( - imageBytes, - GeneralUtils.generateFilename(fileName, ".png"), - MediaType.IMAGE_PNG); + finalOutput = tempFileManager.createManagedTempFile(".png"); + try (OutputStream out = Files.newOutputStream(finalOutput.getPath())) { + out.write(imageBytes); + } + + ResponseEntity response = + WebResponseUtils.fileToWebResponse( + finalOutput, + GeneralUtils.generateFilename(fileName, ".png"), + MediaType.IMAGE_PNG); + finalOutputOwnershipTransferred = true; + return response; } } finally { + if (finalOutput != null && !finalOutputOwnershipTransferred) { + finalOutput.close(); + } // Cleanup logic for all temporary files and directories - tempImageFiles.forEach( - path -> { - try { - Files.deleteIfExists(path); - } catch (IOException e) { - log.error("Failed to delete temporary image file: {}", path, e); - } - }); + tempImageFiles.forEach(TempFile::close); - if (tempZipFile != null && Files.exists(tempZipFile)) { - try { - Files.deleteIfExists(tempZipFile); - } catch (IOException e) { - log.error("Failed to delete temporary zip file: {}", tempZipFile, e); - } + if (tempInputFile != null) { + tempInputFile.close(); } tempDirs.forEach( diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/FlattenController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/FlattenController.java index eecf1269f9..7eac5c2b83 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/FlattenController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/FlattenController.java @@ -15,6 +15,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.github.pixee.security.Filenames; import io.swagger.v3.oas.annotations.Operation; @@ -30,6 +31,7 @@ import stirling.software.common.model.ApplicationProperties; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ApplicationContextProvider; import stirling.software.common.util.ExceptionUtils; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @MiscApi @@ -38,6 +40,7 @@ import stirling.software.common.util.WebResponseUtils; public class FlattenController { private final CustomPDFDocumentFactory pdfDocumentFactory; + private final TempFileManager tempFileManager; @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/flatten") @StandardPdfResponse @@ -46,7 +49,8 @@ public class FlattenController { description = "Flattening just PDF form fields or converting each page to images to make text" + " unselectable. Input:PDF, Output:PDF. Type:SISO") - public ResponseEntity flatten(@ModelAttribute FlattenRequest request) throws Exception { + public ResponseEntity flatten(@ModelAttribute FlattenRequest request) + throws Exception { MultipartFile file = request.getFileInput(); try (PDDocument document = pdfDocumentFactory.load(file)) { @@ -58,7 +62,9 @@ public class FlattenController { acroForm.flatten(); } return WebResponseUtils.pdfDocToWebResponse( - document, Filenames.toSimpleFileName(file.getOriginalFilename())); + document, + Filenames.toSimpleFileName(file.getOriginalFilename()), + tempFileManager); } else { // flatten whole page aka convert each page to image and re-add it (making text // unselectable) @@ -143,7 +149,9 @@ public class FlattenController { } } return WebResponseUtils.pdfDocToWebResponse( - newDocument, Filenames.toSimpleFileName(file.getOriginalFilename())); + newDocument, + Filenames.toSimpleFileName(file.getOriginalFilename()), + tempFileManager); } } } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/MetadataController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/MetadataController.java index 1eb8f92681..9a7f7dcb24 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/MetadataController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/MetadataController.java @@ -13,6 +13,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.github.pixee.security.Filenames; import io.swagger.v3.oas.annotations.Operation; @@ -28,6 +29,7 @@ import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.service.PdfMetadataService; import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.RegexPatternUtils; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; import stirling.software.common.util.propertyeditor.StringToMapPropertyEditor; @@ -37,6 +39,7 @@ import stirling.software.common.util.propertyeditor.StringToMapPropertyEditor; public class MetadataController { private final CustomPDFDocumentFactory pdfDocumentFactory; + private final TempFileManager tempFileManager; private String checkUndefined(String entry) { // Check if the string is "undefined" @@ -61,7 +64,7 @@ public class MetadataController { "This endpoint allows you to update the metadata of a given PDF file. You can" + " add, modify, or delete standard and custom metadata fields. Input:PDF" + " Output:PDF Type:SISO") - public ResponseEntity metadata(@ModelAttribute MetadataRequest request) + public ResponseEntity metadata(@ModelAttribute MetadataRequest request) throws IOException { // Extract PDF file from the request object @@ -179,7 +182,8 @@ public class MetadataController { document, GeneralUtils.removeExtension( Filenames.toSimpleFileName(pdfFile.getOriginalFilename())) - + "_metadata.pdf"); + + "_metadata.pdf", + tempFileManager); } } } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/OCRController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/OCRController.java index 22cf60dcbf..990f6dd982 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/OCRController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/OCRController.java @@ -25,6 +25,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.github.pixee.security.Filenames; import io.swagger.v3.oas.annotations.Operation; @@ -89,7 +90,7 @@ public class OCRController { + " specify languages, sidecar, deskew, clean, cleanFinal, ocrType, ocrRenderType," + " and removeImagesAfter options. Uses OCRmyPDF if available, falls back to" + " Tesseract. Input:PDF Output:PDF Type:SI-Conditional") - public ResponseEntity processPdfWithOCR( + public ResponseEntity processPdfWithOCR( @ModelAttribute ProcessPdfWithOcrRequest request) throws IOException, InterruptedException { MultipartFile inputFile = request.getFileInput(); @@ -121,9 +122,11 @@ public class OCRController { throw ExceptionUtils.createOcrInvalidLanguagesException(); } - // Use try-with-resources for proper temp file management + TempFile tempOutputFile = new TempFile(tempFileManager, ".pdf"); + TempFile tempZipFile = null; + boolean pdfOwnershipTransferred = false; + boolean zipOwnershipTransferred = false; try (TempFile tempInputFile = new TempFile(tempFileManager, ".pdf"); - TempFile tempOutputFile = new TempFile(tempFileManager, ".pdf"); TempFile sidecarTextFile = sidecar ? new TempFile(tempFileManager, ".txt") : null) { inputFile.transferTo(tempInputFile.getFile()); @@ -156,9 +159,6 @@ public class OCRController { throw ExceptionUtils.createOcrToolsUnavailableException(); } - // Read the processed PDF file - byte[] pdfBytes = Files.readAllBytes(tempOutputFile.getPath()); - // Return the OCR processed PDF as a response String outputFilename = GeneralUtils.removeExtension( @@ -172,14 +172,14 @@ public class OCRController { Filenames.toSimpleFileName(inputFile.getOriginalFilename())) + "_OCR.zip"; - try (TempFile tempZipFile = new TempFile(tempFileManager, ".zip"); - ZipOutputStream zipOut = - new ZipOutputStream(Files.newOutputStream(tempZipFile.getPath()))) { + tempZipFile = new TempFile(tempFileManager, ".zip"); + try (ZipOutputStream zipOut = + new ZipOutputStream(Files.newOutputStream(tempZipFile.getPath()))) { // Add PDF file to the zip ZipEntry pdfEntry = new ZipEntry(outputFilename); zipOut.putNextEntry(pdfEntry); - zipOut.write(pdfBytes); + Files.copy(tempOutputFile.getPath(), zipOut); zipOut.closeEntry(); // Add text file to the zip @@ -189,16 +189,28 @@ public class OCRController { zipOut.closeEntry(); zipOut.finish(); - - byte[] zipBytes = Files.readAllBytes(tempZipFile.getPath()); - - // Return the zip file containing both the PDF and the text file - return WebResponseUtils.bytesToWebResponse( - zipBytes, outputZipFilename, MediaType.APPLICATION_OCTET_STREAM); } + + // The intermediate PDF temp file is no longer needed; only the zip is streamed. + tempOutputFile.close(); + pdfOwnershipTransferred = true; + ResponseEntity response = + WebResponseUtils.fileToWebResponse( + tempZipFile, outputZipFilename, MediaType.APPLICATION_OCTET_STREAM); + zipOwnershipTransferred = true; + return response; } else { - // Return the OCR processed PDF as a response - return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename); + ResponseEntity response = + WebResponseUtils.pdfFileToWebResponse(tempOutputFile, outputFilename); + pdfOwnershipTransferred = true; + return response; + } + } finally { + if (!pdfOwnershipTransferred) { + tempOutputFile.close(); + } + if (tempZipFile != null && !zipOwnershipTransferred) { + tempZipFile.close(); } } } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/OverlayImageController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/OverlayImageController.java index 3b893c17ab..f7e36340c7 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/OverlayImageController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/OverlayImageController.java @@ -1,6 +1,5 @@ package stirling.software.SPDF.controller.api.misc; -import java.io.ByteArrayOutputStream; import java.io.IOException; import org.apache.pdfbox.pdmodel.PDDocument; @@ -12,6 +11,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.swagger.v3.oas.annotations.Operation; @@ -24,6 +24,8 @@ import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.MiscApi; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.GeneralUtils; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @MiscApi @@ -32,6 +34,7 @@ import stirling.software.common.util.WebResponseUtils; public class OverlayImageController { private final CustomPDFDocumentFactory pdfDocumentFactory; + private final TempFileManager tempFileManager; @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/add-image") @Operation( @@ -42,7 +45,8 @@ public class OverlayImageController { + "SVG files are rendered as vector graphics for crisp output at any resolution. " + "The image can be overlaid on every page of the PDF if specified. " + "Input:PDF/IMAGE/SVG Output:PDF Type:SISO") - public ResponseEntity overlayImage(@ModelAttribute OverlayImageRequest request) { + public ResponseEntity overlayImage( + @ModelAttribute OverlayImageRequest request) { MultipartFile pdfFile = request.getFileInput(); MultipartFile imageFile = request.getImageFile(); float x = request.getX(); @@ -82,14 +86,17 @@ public class OverlayImageController { } } - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - document.save(baos); - - byte[] result = baos.toByteArray(); + TempFile tempOut = tempFileManager.createManagedTempFile(".pdf"); + try { + document.save(tempOut.getFile()); + } catch (IOException e) { + tempOut.close(); + throw e; + } log.info("PDF with overlaid image successfully created"); - return WebResponseUtils.bytesToWebResponse( - result, + return WebResponseUtils.pdfFileToWebResponse( + tempOut, GeneralUtils.generateFilename( pdfFile.getOriginalFilename(), "_overlayed.pdf")); } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/PageNumbersController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/PageNumbersController.java index 6d0d05a28d..4e12a694a3 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/PageNumbersController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/PageNumbersController.java @@ -1,7 +1,6 @@ package stirling.software.SPDF.controller.api.misc; import java.awt.Color; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.List; import java.util.Locale; @@ -16,6 +15,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.github.pixee.security.Filenames; import io.swagger.v3.oas.annotations.Operation; @@ -28,6 +28,8 @@ import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.MiscApi; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.GeneralUtils; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @MiscApi @@ -35,6 +37,7 @@ import stirling.software.common.util.WebResponseUtils; public class PageNumbersController { private final CustomPDFDocumentFactory pdfDocumentFactory; + private final TempFileManager tempFileManager; @AutoJobPostMapping(value = "/add-page-numbers", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @StandardPdfResponse @@ -43,8 +46,8 @@ public class PageNumbersController { description = "This operation takes an input PDF file and adds page numbers to it. Input:PDF" + " Output:PDF Type:SISO") - public ResponseEntity addPageNumbers(@ModelAttribute AddPageNumbersRequest request) - throws IOException { + public ResponseEntity addPageNumbers( + @ModelAttribute AddPageNumbersRequest request) throws IOException { MultipartFile file = request.getFileInput(); String customMargin = request.getCustomMargin(); @@ -175,11 +178,16 @@ public class PageNumbersController { pageNumber++; } - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - document.save(baos); + TempFile tempOut = tempFileManager.createManagedTempFile(".pdf"); + try { + document.save(tempOut.getFile()); + } catch (IOException e) { + tempOut.close(); + throw e; + } - return WebResponseUtils.bytesToWebResponse( - baos.toByteArray(), + return WebResponseUtils.pdfFileToWebResponse( + tempOut, GeneralUtils.generateFilename( file.getOriginalFilename(), "_page_numbers_added.pdf")); } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/RemoveImagesController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/RemoveImagesController.java index 53871f973b..33fcea588b 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/RemoveImagesController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/RemoveImagesController.java @@ -1,6 +1,5 @@ package stirling.software.SPDF.controller.api.misc; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -17,6 +16,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.swagger.v3.oas.annotations.Operation; @@ -29,6 +29,8 @@ import stirling.software.common.model.api.PDFFile; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.GeneralUtils; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @GeneralApi @@ -37,6 +39,7 @@ import stirling.software.common.util.WebResponseUtils; public class RemoveImagesController { private final CustomPDFDocumentFactory pdfDocumentFactory; + private final TempFileManager tempFileManager; @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/remove-image-pdf") @Operation( @@ -44,7 +47,8 @@ public class RemoveImagesController { description = "This endpoint removes all embedded images from a PDF file and returns the" + " modified document. Input:PDF Output:PDF Type:SISO") - public ResponseEntity removeImages(@ModelAttribute PDFFile request) throws IOException { + public ResponseEntity removeImages(@ModelAttribute PDFFile request) + throws IOException { MultipartFile inputFile = request.getFileInput(); @@ -60,12 +64,16 @@ public class RemoveImagesController { log.info("Removed {} images from PDF with {} pages", imagesRemoved, totalPages); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - pdfDoc.save(baos); - byte[] pdfContent = baos.toByteArray(); + TempFile tempOut = tempFileManager.createManagedTempFile(".pdf"); + try { + pdfDoc.save(tempOut.getFile()); + } catch (IOException e) { + tempOut.close(); + throw e; + } - return WebResponseUtils.bytesToWebResponse( - pdfContent, + return WebResponseUtils.pdfFileToWebResponse( + tempOut, GeneralUtils.generateFilename( inputFile.getOriginalFilename(), "_images_removed.pdf")); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/RepairController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/RepairController.java index 8eaf3a892b..b0dc0a03a5 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/RepairController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/RepairController.java @@ -8,6 +8,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.swagger.v3.oas.annotations.Operation; @@ -53,13 +54,12 @@ public class RepairController { "This endpoint repairs a given PDF file by running Ghostscript (primary), qpdf (fallback), or PDFBox (if no external tools available). The PDF is" + " first saved to a temporary location, repaired, read back, and then" + " returned as a response. Input:PDF Output:PDF Type:SISO") - public ResponseEntity repairPdf(@ModelAttribute PDFFile file) + public ResponseEntity repairPdf(@ModelAttribute PDFFile file) throws IOException, InterruptedException { MultipartFile inputFile = file.getFileInput(); - // Use TempFile with try-with-resources for automatic cleanup - try (TempFile tempInputFile = new TempFile(tempFileManager, ".pdf"); - TempFile tempOutputFile = new TempFile(tempFileManager, ".pdf")) { + TempFile tempOutputFile = new TempFile(tempFileManager, ".pdf"); + try (TempFile tempInputFile = new TempFile(tempFileManager, ".pdf")) { // Save the uploaded file to the temporary location inputFile.transferTo(tempInputFile.getFile()); @@ -121,14 +121,17 @@ public class RepairController { } } - // Read the repaired PDF file - byte[] pdfBytes = pdfDocumentFactory.loadToBytes(tempOutputFile.getFile()); - - // Return the repaired PDF as a response - return WebResponseUtils.bytesToWebResponse( - pdfBytes, + // Return the repaired PDF as a streaming response + return WebResponseUtils.pdfFileToWebResponse( + tempOutputFile, GeneralUtils.generateFilename( inputFile.getOriginalFilename(), "_repaired.pdf")); + } catch (IOException | InterruptedException e) { + tempOutputFile.close(); + throw e; + } catch (RuntimeException e) { + tempOutputFile.close(); + throw e; } } } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ReplaceAndInvertColorController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ReplaceAndInvertColorController.java index 72820acb97..defee45756 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ReplaceAndInvertColorController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ReplaceAndInvertColorController.java @@ -1,11 +1,15 @@ package stirling.software.SPDF.controller.api.misc; import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; import org.springframework.core.io.InputStreamResource; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.swagger.v3.oas.annotations.Operation; @@ -16,6 +20,8 @@ import stirling.software.SPDF.service.misc.ReplaceAndInvertColorService; import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.MiscApi; import stirling.software.common.util.GeneralUtils; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @MiscApi @@ -23,6 +29,7 @@ import stirling.software.common.util.WebResponseUtils; public class ReplaceAndInvertColorController { private final ReplaceAndInvertColorService replaceAndInvertColorService; + private final TempFileManager tempFileManager; @AutoJobPostMapping( consumes = MediaType.MULTIPART_FORM_DATA_VALUE, @@ -32,7 +39,7 @@ public class ReplaceAndInvertColorController { description = "This endpoint accepts a PDF file and provides options to invert all colors, replace" + " text and background colors, or convert to CMYK color space for printing. Input:PDF Output:PDF Type:SISO") - public ResponseEntity replaceAndInvertColor( + public ResponseEntity replaceAndInvertColor( @ModelAttribute ReplaceAndInvertColorRequest request) throws IOException { InputStreamResource resource = @@ -47,7 +54,15 @@ public class ReplaceAndInvertColorController { String filename = GeneralUtils.generateFilename( request.getFileInput().getOriginalFilename(), "_inverted.pdf"); - return WebResponseUtils.bytesToWebResponse( - resource.getContentAsByteArray(), filename, MediaType.APPLICATION_PDF); + + TempFile tempOut = tempFileManager.createManagedTempFile(".pdf"); + try (InputStream in = resource.getInputStream()) { + Files.copy(in, tempOut.getFile().toPath(), StandardCopyOption.REPLACE_EXISTING); + } catch (IOException e) { + tempOut.close(); + throw e; + } + + return WebResponseUtils.pdfFileToWebResponse(tempOut, filename); } } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ScannerEffectController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ScannerEffectController.java index e581eb8952..8318960ff9 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ScannerEffectController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ScannerEffectController.java @@ -6,7 +6,6 @@ import java.awt.RenderingHints; import java.awt.geom.AffineTransform; import java.awt.image.BufferedImage; import java.awt.image.DataBufferInt; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.UncheckedIOException; import java.nio.file.Files; @@ -35,6 +34,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.swagger.v3.oas.annotations.Operation; @@ -51,6 +51,7 @@ import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ApplicationContextProvider; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.GeneralUtils; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @MiscApi @@ -59,6 +60,7 @@ import stirling.software.common.util.WebResponseUtils; public class ScannerEffectController { private final CustomPDFDocumentFactory pdfDocumentFactory; + private final TempFileManager tempFileManager; private static final int MAX_IMAGE_WIDTH = 8192; private static final int MAX_IMAGE_HEIGHT = 8192; private static final long MAX_IMAGE_PIXELS = 16_777_216; // 4096x4096 @@ -562,8 +564,8 @@ public class ScannerEffectController { summary = "Apply scanner effect to PDF", description = "Applies various effects to simulate a scanned document, including rotation, noise, and edge softening. Input:PDF Output:PDF Type:SISO") - public ResponseEntity scannerEffect(@Valid @ModelAttribute ScannerEffectRequest request) - throws IOException { + public ResponseEntity scannerEffect( + @Valid @ModelAttribute ScannerEffectRequest request) throws IOException { MultipartFile file = request.getFileInput(); List tempFiles = new ArrayList<>(); @@ -624,8 +626,7 @@ public class ScannerEffectController { sharedPdfBytes != null ? pdfDocumentFactory.load(sharedPdfBytes) : pdfDocumentFactory.load(processingInput); - PDDocument outputDocument = new PDDocument(); - ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + PDDocument outputDocument = new PDDocument()) { int totalPages = document.getNumberOfPages(); if (totalPages == 0) { @@ -708,12 +709,11 @@ public class ScannerEffectController { writeProcessedPagesToDocument(processedPages, outputDocument); - outputDocument.save(outputStream); - - return WebResponseUtils.bytesToWebResponse( - outputStream.toByteArray(), + return WebResponseUtils.pdfDocToWebResponse( + outputDocument, GeneralUtils.generateFilename( - file.getOriginalFilename(), "_scanner_effect.pdf")); + file.getOriginalFilename(), "_scanner_effect.pdf"), + tempFileManager); } } } finally { diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ShowJavascript.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ShowJavascript.java index 29dc983507..0d7dc1d1d2 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ShowJavascript.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ShowJavascript.java @@ -1,6 +1,7 @@ package stirling.software.SPDF.controller.api.misc; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.util.Map; import org.apache.pdfbox.pdmodel.PDDocument; @@ -10,6 +11,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.github.pixee.security.Filenames; import io.swagger.v3.oas.annotations.Operation; @@ -21,6 +23,8 @@ import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.MiscApi; import stirling.software.common.model.api.PDFFile; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @MiscApi @@ -28,13 +32,15 @@ import stirling.software.common.util.WebResponseUtils; public class ShowJavascript { private final CustomPDFDocumentFactory pdfDocumentFactory; + private final TempFileManager tempFileManager; @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/show-javascript") @JavaScriptResponse @Operation( summary = "Grabs all JS from a PDF and returns a single JS file with all code", description = "desc. Input:PDF Output:JS Type:SISO") - public ResponseEntity extractHeader(@ModelAttribute PDFFile file) throws Exception { + public ResponseEntity extractHeader(@ModelAttribute PDFFile file) + throws Exception { MultipartFile inputFile = file.getFileInput(); StringBuilder script = new StringBuilder(); boolean foundScript = false; @@ -77,8 +83,17 @@ public class ShowJavascript { .append("' does not contain Javascript"); } - return WebResponseUtils.bytesToWebResponse( - script.toString().getBytes(StandardCharsets.UTF_8), + TempFile tempOut = tempFileManager.createManagedTempFile(".js"); + try { + Files.write( + tempOut.getFile().toPath(), + script.toString().getBytes(StandardCharsets.UTF_8)); + } catch (Exception e) { + tempOut.close(); + throw e; + } + return WebResponseUtils.fileToWebResponse( + tempOut, Filenames.toSimpleFileName(inputFile.getOriginalFilename()) + ".js", MediaType.TEXT_PLAIN); } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/StampController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/StampController.java index bd596bd12b..0e6fc1d004 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/StampController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/StampController.java @@ -37,6 +37,7 @@ import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.InitBinder; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.swagger.v3.oas.annotations.Operation; @@ -92,7 +93,7 @@ public class StampController { "This endpoint adds a stamp to a given PDF file. Users can specify the stamp" + " type (text or image), rotation, opacity, width spacer, and height" + " spacer. Input:PDF Output:PDF Type:SISO") - public ResponseEntity addStamp(@ModelAttribute AddStampRequest request) + public ResponseEntity addStamp(@ModelAttribute AddStampRequest request) throws IOException, Exception { MultipartFile pdfFile = request.getFileInput(); String pdfFileName = pdfFile.getOriginalFilename(); @@ -199,7 +200,8 @@ public class StampController { // Return the stamped PDF as a response return WebResponseUtils.pdfDocToWebResponse( document, - GeneralUtils.generateFilename(pdfFile.getOriginalFilename(), "_stamped.pdf")); + GeneralUtils.generateFilename(pdfFile.getOriginalFilename(), "_stamped.pdf"), + tempFileManager); } } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/UnlockPDFFormsController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/UnlockPDFFormsController.java index faa98a7ea8..22117b5f35 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/UnlockPDFFormsController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/UnlockPDFFormsController.java @@ -13,6 +13,7 @@ import org.apache.pdfbox.pdmodel.interactive.form.PDField; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.github.pixee.security.Filenames; import io.swagger.v3.oas.annotations.Operation; @@ -26,15 +27,19 @@ import stirling.software.common.model.api.PDFFile; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.RegexPatternUtils; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @MiscApi @Slf4j public class UnlockPDFFormsController { private final CustomPDFDocumentFactory pdfDocumentFactory; + private final TempFileManager tempFileManager; - public UnlockPDFFormsController(CustomPDFDocumentFactory pdfDocumentFactory) { + public UnlockPDFFormsController( + CustomPDFDocumentFactory pdfDocumentFactory, TempFileManager tempFileManager) { this.pdfDocumentFactory = pdfDocumentFactory; + this.tempFileManager = tempFileManager; } @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/unlock-pdf-forms") @@ -44,7 +49,7 @@ public class UnlockPDFFormsController { description = "Removing read-only property from form fields making them fillable" + "Input:PDF, Output:PDF. Type:SISO") - public ResponseEntity unlockPDFForms(@ModelAttribute PDFFile file) { + public ResponseEntity unlockPDFForms(@ModelAttribute PDFFile file) { try (PDDocument document = pdfDocumentFactory.load(file)) { PDAcroForm acroForm = document.getDocumentCatalog().getAcroForm(); @@ -116,7 +121,7 @@ public class UnlockPDFFormsController { GeneralUtils.generateFilename( file.getFileInput().getOriginalFilename(), "_unlocked_forms.pdf"); return WebResponseUtils.pdfDocToWebResponse( - document, Filenames.toSimpleFileName(mergedFileName)); + document, Filenames.toSimpleFileName(mergedFileName), tempFileManager); } catch (Exception e) { log.error(e.getMessage(), e); } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController.java index 5c45f5ab0f..f1b085dea2 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController.java @@ -64,6 +64,7 @@ import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.micrometer.common.util.StringUtils; import io.swagger.v3.oas.annotations.Operation; @@ -78,6 +79,8 @@ import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.service.ServerCertificateServiceInterface; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.GeneralUtils; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @RestController @@ -104,13 +107,15 @@ public class CertSignController { private final CustomPDFDocumentFactory pdfDocumentFactory; private final ServerCertificateServiceInterface serverCertificateService; + private final TempFileManager tempFileManager; public CertSignController( CustomPDFDocumentFactory pdfDocumentFactory, - @Autowired(required = false) - ServerCertificateServiceInterface serverCertificateService) { + @Autowired(required = false) ServerCertificateServiceInterface serverCertificateService, + TempFileManager tempFileManager) { this.pdfDocumentFactory = pdfDocumentFactory; this.serverCertificateService = serverCertificateService; + this.tempFileManager = tempFileManager; } public static void sign( @@ -163,8 +168,8 @@ public class CertSignController { "This endpoint accepts a PDF file, a digital certificate and related" + " information to sign the PDF. It then returns the digitally signed PDF" + " file. Input:PDF Output:PDF Type:SISO") - public ResponseEntity signPDFWithCert(@ModelAttribute SignPDFWithCertRequest request) - throws Exception { + public ResponseEntity signPDFWithCert( + @ModelAttribute SignPDFWithCertRequest request) throws Exception { MultipartFile pdf = request.getFileInput(); String certType = request.getCertType(); MultipartFile privateKeyFile = request.getPrivateKeyFile(); @@ -246,22 +251,26 @@ public class CertSignController { } CreateSignature createSignature = new CreateSignature(ks, keystorePassword.toCharArray()); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - sign( - pdfDocumentFactory, - pdf, - baos, - createSignature, - showSignature, - pageNumber, - name, - location, - reason, - showLogo); + TempFile signedOut = tempFileManager.createManagedTempFile(".pdf"); + try (OutputStream os = new FileOutputStream(signedOut.getFile())) { + sign( + pdfDocumentFactory, + pdf, + os, + createSignature, + showSignature, + pageNumber, + name, + location, + reason, + showLogo); + } catch (IOException e) { + signedOut.close(); + throw e; + } // Return the signed PDF - return WebResponseUtils.bytesToWebResponse( - baos.toByteArray(), - GeneralUtils.generateFilename(pdf.getOriginalFilename(), "_signed.pdf")); + return WebResponseUtils.pdfFileToWebResponse( + signedOut, GeneralUtils.generateFilename(pdf.getOriginalFilename(), "_signed.pdf")); } private MultipartFile validateFilePresent( diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/PasswordController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/PasswordController.java index 567f1dd2ab..e9fa41ff05 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/PasswordController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/PasswordController.java @@ -9,6 +9,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.swagger.v3.oas.annotations.Operation; @@ -22,6 +23,7 @@ import stirling.software.common.annotations.api.SecurityApi; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.GeneralUtils; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @SecurityApi @@ -29,6 +31,7 @@ import stirling.software.common.util.WebResponseUtils; public class PasswordController { private final CustomPDFDocumentFactory pdfDocumentFactory; + private final TempFileManager tempFileManager; @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/remove-password") @StandardPdfResponse @@ -37,8 +40,8 @@ public class PasswordController { description = "This endpoint removes the password from a protected PDF file. Users need to" + " provide the existing password. Input:PDF Output:PDF Type:SISO") - public ResponseEntity removePassword(@ModelAttribute PDFPasswordRequest request) - throws IOException { + public ResponseEntity removePassword( + @ModelAttribute PDFPasswordRequest request) throws IOException { MultipartFile fileInput = request.getFileInput(); String password = request.getPassword(); @@ -47,7 +50,8 @@ public class PasswordController { return WebResponseUtils.pdfDocToWebResponse( document, GeneralUtils.generateFilename( - fileInput.getOriginalFilename(), "_password_removed.pdf")); + fileInput.getOriginalFilename(), "_password_removed.pdf"), + tempFileManager); } catch (IOException e) { // Handle password errors specifically if (ExceptionUtils.isPasswordError(e)) { @@ -66,8 +70,8 @@ public class PasswordController { "This endpoint adds password protection to a PDF file. Users can specify a set" + " of permissions that should be applied to the file. Input:PDF" + " Output:PDF") - public ResponseEntity addPassword(@ModelAttribute AddPasswordRequest request) - throws IOException { + public ResponseEntity addPassword( + @ModelAttribute AddPasswordRequest request) throws IOException { MultipartFile fileInput = request.getFileInput(); String ownerPassword = request.getOwnerPassword(); String password = request.getPassword(); @@ -108,11 +112,13 @@ public class PasswordController { return WebResponseUtils.pdfDocToWebResponse( document, GeneralUtils.generateFilename( - fileInput.getOriginalFilename(), "_permissions.pdf")); + fileInput.getOriginalFilename(), "_permissions.pdf"), + tempFileManager); return WebResponseUtils.pdfDocToWebResponse( document, GeneralUtils.generateFilename( - fileInput.getOriginalFilename(), "_passworded.pdf")); + fileInput.getOriginalFilename(), "_passworded.pdf"), + tempFileManager); } } } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/RedactController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/RedactController.java index 0011fdb34d..377529b5fb 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/RedactController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/RedactController.java @@ -1,7 +1,6 @@ package stirling.software.SPDF.controller.api.security; import java.awt.Color; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; @@ -40,6 +39,7 @@ import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.InitBinder; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.github.pixee.security.Filenames; import io.swagger.v3.oas.annotations.Operation; @@ -64,6 +64,8 @@ import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.PdfUtils; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; import stirling.software.common.util.propertyeditor.StringToArrayListPropertyEditor; @@ -85,6 +87,7 @@ public class RedactController { private static final COSString EMPTY_COS_STRING = new COSString(""); private final CustomPDFDocumentFactory pdfDocumentFactory; + private final TempFileManager tempFileManager; private String removeFileExtension(String filename) { return GeneralUtils.removeExtension(filename); @@ -105,8 +108,8 @@ public class RedactController { "This endpoint redacts content from a PDF file based on manually specified areas. " + "Users can specify areas to redact and optionally convert the PDF to an image. " + "Input:PDF Output:PDF Type:SISO") - public ResponseEntity redactPDF(@ModelAttribute ManualRedactPdfRequest request) - throws IOException { + public ResponseEntity redactPDF( + @ModelAttribute ManualRedactPdfRequest request) throws IOException { MultipartFile file = request.getFileInput(); List redactionAreas = request.getRedactions(); @@ -120,30 +123,24 @@ public class RedactController { if (Boolean.TRUE.equals(request.getConvertPDFToImage())) { try (PDDocument convertedPdf = PdfUtils.convertPdfToPdfImage(document)) { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - convertedPdf.save(baos); - byte[] pdfContent = baos.toByteArray(); - - return WebResponseUtils.bytesToWebResponse( - pdfContent, + return WebResponseUtils.pdfDocToWebResponse( + convertedPdf, removeFileExtension( Objects.requireNonNull( Filenames.toSimpleFileName( file.getOriginalFilename()))) - + "_redacted.pdf"); + + "_redacted.pdf", + tempFileManager); } } - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - document.save(baos); - byte[] pdfContent = baos.toByteArray(); - - return WebResponseUtils.bytesToWebResponse( - pdfContent, + return WebResponseUtils.pdfDocToWebResponse( + document, removeFileExtension( Objects.requireNonNull( Filenames.toSimpleFileName(file.getOriginalFilename()))) - + "_redacted.pdf"); + + "_redacted.pdf", + tempFileManager); } } @@ -504,7 +501,8 @@ public class RedactController { "This endpoint automatically redacts text from a PDF file based on specified patterns. " + "Users can provide text patterns to redact, with options for regex and whole word matching. " + "Input:PDF Output:PDF Type:SISO") - public ResponseEntity redactPdf(@ModelAttribute RedactPdfRequest request) { + public ResponseEntity redactPdf( + @ModelAttribute RedactPdfRequest request) { String[] listOfText = request.getListOfText().split("\n"); boolean useRegex = Boolean.TRUE.equals(request.getUseRegex()); boolean wholeWordSearchBool = Boolean.TRUE.equals(request.getWholeWordSearch()); @@ -545,20 +543,15 @@ public class RedactController { if (allFoundTextsByPage.isEmpty()) { log.info("No text found matching redaction patterns"); - byte[] originalContent; - try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { - document.save(baos); - originalContent = baos.toByteArray(); - } - - return WebResponseUtils.bytesToWebResponse( - originalContent, + return WebResponseUtils.pdfDocToWebResponse( + document, removeFileExtension( Objects.requireNonNull( Filenames.toSimpleFileName( request.getFileInput() .getOriginalFilename()))) - + "_redacted.pdf"); + + "_redacted.pdf", + tempFileManager); } boolean fallbackToBoxOnlyMode; @@ -587,7 +580,7 @@ public class RedactController { findTextToRedact( fallbackDocument, listOfText, useRegex, wholeWordSearchBool); - byte[] pdfContent = + TempFile finalized = finalizeRedaction( fallbackDocument, allFoundTextsByPage, @@ -596,8 +589,8 @@ public class RedactController { request.getConvertPDFToImage(), false); // Box-only mode, use original box sizes - return WebResponseUtils.bytesToWebResponse( - pdfContent, + return WebResponseUtils.pdfFileToWebResponse( + finalized, removeFileExtension( Objects.requireNonNull( Filenames.toSimpleFileName( @@ -606,7 +599,7 @@ public class RedactController { + "_redacted.pdf"); } - byte[] pdfContent = + TempFile finalized = finalizeRedaction( document, allFoundTextsByPage, @@ -615,8 +608,8 @@ public class RedactController { request.getConvertPDFToImage(), true); // Text removal mode, use reduced box sizes - return WebResponseUtils.bytesToWebResponse( - pdfContent, + return WebResponseUtils.pdfFileToWebResponse( + finalized, removeFileExtension( Objects.requireNonNull( Filenames.toSimpleFileName( @@ -733,7 +726,7 @@ public class RedactController { } } - private byte[] finalizeRedaction( + private TempFile finalizeRedaction( PDDocument document, Map> allFoundTextsByPage, String colorString, @@ -759,29 +752,37 @@ public class RedactController { try (PDDocument convertedPdf = PdfUtils.convertPdfToPdfImage(document)) { cleanDocumentMetadata(convertedPdf); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - convertedPdf.save(baos); - byte[] out = baos.toByteArray(); + TempFile tempOut = tempFileManager.createManagedTempFile(".pdf"); + try { + convertedPdf.save(tempOut.getFile()); + } catch (IOException e) { + tempOut.close(); + throw e; + } log.info( "Redaction finalized (image mode): {} pages ➜ {} KB", convertedPdf.getNumberOfPages(), - out.length / 1024); + tempOut.getFile().length() / 1024); - return out; + return tempOut; } } - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - document.save(baos); - byte[] out = baos.toByteArray(); + TempFile tempOut = tempFileManager.createManagedTempFile(".pdf"); + try { + document.save(tempOut.getFile()); + } catch (IOException e) { + tempOut.close(); + throw e; + } log.info( "Redaction finalized: {} pages ➜ {} KB", document.getNumberOfPages(), - out.length / 1024); + tempOut.getFile().length() / 1024); - return out; + return tempOut; } private void cleanDocumentMetadata(PDDocument document) { diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/RemoveCertSignController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/RemoveCertSignController.java index 91644747e6..f23d63765e 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/RemoveCertSignController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/RemoveCertSignController.java @@ -11,6 +11,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.swagger.v3.oas.annotations.Operation; @@ -22,6 +23,7 @@ import stirling.software.common.annotations.api.SecurityApi; import stirling.software.common.model.api.PDFFile; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.GeneralUtils; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @SecurityApi @@ -29,6 +31,7 @@ import stirling.software.common.util.WebResponseUtils; public class RemoveCertSignController { private final CustomPDFDocumentFactory pdfDocumentFactory; + private final TempFileManager tempFileManager; @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/remove-cert-sign") @StandardPdfResponse @@ -37,7 +40,7 @@ public class RemoveCertSignController { description = "This endpoint accepts a PDF file and returns the PDF file without the digital" + " signature. Input:PDF, Output:PDF Type:SISO") - public ResponseEntity removeCertSignPDF(@ModelAttribute PDFFile request) + public ResponseEntity removeCertSignPDF(@ModelAttribute PDFFile request) throws Exception { MultipartFile pdf = request.getFileInput(); @@ -63,7 +66,8 @@ public class RemoveCertSignController { // Return the modified PDF as a response return WebResponseUtils.pdfDocToWebResponse( document, - GeneralUtils.generateFilename(pdf.getOriginalFilename(), "_unsigned.pdf")); + GeneralUtils.generateFilename(pdf.getOriginalFilename(), "_unsigned.pdf"), + tempFileManager); } } } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/SanitizeController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/SanitizeController.java index 9a6f44692f..7db9f2a8a0 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/SanitizeController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/SanitizeController.java @@ -1,6 +1,5 @@ package stirling.software.SPDF.controller.api.security; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.List; @@ -29,6 +28,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.swagger.v3.oas.annotations.Operation; @@ -41,6 +41,7 @@ import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.annotations.api.SecurityApi; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.GeneralUtils; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @Slf4j @@ -49,6 +50,7 @@ import stirling.software.common.util.WebResponseUtils; public class SanitizeController { private final CustomPDFDocumentFactory pdfDocumentFactory; + private final TempFileManager tempFileManager; @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/sanitize-pdf") @StandardPdfResponse @@ -57,8 +59,8 @@ public class SanitizeController { description = "This endpoint processes a PDF file and removes specific elements based on the" + " provided options. Input:PDF Output:PDF Type:SISO") - public ResponseEntity sanitizePDF(@ModelAttribute SanitizePdfRequest request) - throws IOException { + public ResponseEntity sanitizePDF( + @ModelAttribute SanitizePdfRequest request) throws IOException { MultipartFile inputFile = request.getFileInput(); boolean removeJavaScript = Boolean.TRUE.equals(request.getRemoveJavaScript()); boolean removeEmbeddedFiles = Boolean.TRUE.equals(request.getRemoveEmbeddedFiles()); @@ -92,14 +94,11 @@ public class SanitizeController { sanitizeFonts(document); } - // Save the sanitized document to output stream - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - document.save(outputStream); - - return WebResponseUtils.bytesToWebResponse( - outputStream.toByteArray(), + return WebResponseUtils.pdfDocToWebResponse( + document, GeneralUtils.generateFilename( - inputFile.getOriginalFilename(), "_sanitized.pdf")); + inputFile.getOriginalFilename(), "_sanitized.pdf"), + tempFileManager); } } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/TimestampController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/TimestampController.java index f4259e682c..ef744330f7 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/TimestampController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/TimestampController.java @@ -1,6 +1,5 @@ package stirling.software.SPDF.controller.api.security; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -33,6 +32,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.swagger.v3.oas.annotations.Operation; @@ -46,6 +46,8 @@ import stirling.software.common.annotations.api.SecurityApi; import stirling.software.common.model.ApplicationProperties; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.GeneralUtils; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @Slf4j @@ -74,6 +76,7 @@ public class TimestampController { private final CustomPDFDocumentFactory pdfDocumentFactory; private final ApplicationProperties applicationProperties; + private final TempFileManager tempFileManager; @AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/timestamp-pdf") @StandardPdfResponse @@ -84,8 +87,8 @@ public class TimestampController { + " document timestamp into the PDF. Only a SHA-256 hash of the" + " document is sent to the TSA — the PDF itself never leaves the" + " server. Input:PDF Output:PDF Type:SISO") - public ResponseEntity timestampPdf(@ModelAttribute TimestampPdfRequest request) - throws Exception { + public ResponseEntity timestampPdf( + @ModelAttribute TimestampPdfRequest request) throws Exception { MultipartFile inputFile = request.getFileInput(); ApplicationProperties.Security.Timestamp tsConfig = applicationProperties.getSecurity().getTimestamp(); @@ -124,9 +127,10 @@ public class TimestampController { + " via settings.yml (security.timestamp.customTsaUrls)."); } - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - - try (PDDocument document = pdfDocumentFactory.load(inputFile)) { + TempFile tempOutputFile = tempFileManager.createManagedTempFile(".pdf"); + try (PDDocument document = pdfDocumentFactory.load(inputFile); + OutputStream outputStream = + java.nio.file.Files.newOutputStream(tempOutputFile.getPath())) { PDSignature signature = new PDSignature(); signature.setType(COSName.DOC_TIME_STAMP); signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE); @@ -136,10 +140,13 @@ public class TimestampController { document.addSignature(signature, content -> requestTimestampToken(content, tsaUrl)); document.saveIncremental(outputStream); + } catch (Exception e) { + tempOutputFile.close(); + throw e; } - return WebResponseUtils.bytesToWebResponse( - outputStream.toByteArray(), + return WebResponseUtils.pdfFileToWebResponse( + tempOutputFile, GeneralUtils.generateFilename(inputFile.getOriginalFilename(), "_timestamped.pdf")); } @@ -163,7 +170,8 @@ public class TimestampController { TimeStampRequest tsaRequest = generator.generate(digestAlgorithm, hash, nonce); byte[] requestBytes = tsaRequest.getEncoded(); - // Contact the TSA server (redirects disabled to prevent SSRF via redirect) + // Contact the TSA server — tsaUrl is validated against an allowlist above, + // and redirects are disabled below to prevent SSRF via redirect. connection = (HttpURLConnection) URI.create(tsaUrl).toURL().openConnection(); connection.setInstanceFollowRedirects(false); connection.setDoOutput(true); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/WatermarkController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/WatermarkController.java index 773c0e21f2..5e51fd55c6 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/WatermarkController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/WatermarkController.java @@ -30,6 +30,7 @@ import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.InitBinder; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.swagger.v3.oas.annotations.Operation; @@ -45,6 +46,7 @@ import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.PdfUtils; import stirling.software.common.util.RegexPatternUtils; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @SecurityApi @@ -52,6 +54,7 @@ import stirling.software.common.util.WebResponseUtils; public class WatermarkController { private final CustomPDFDocumentFactory pdfDocumentFactory; + private final TempFileManager tempFileManager; @InitBinder public void initBinder(WebDataBinder binder) { @@ -73,8 +76,8 @@ public class WatermarkController { "This endpoint adds a watermark to a given PDF file. Users can specify the" + " watermark type (text or image), rotation, opacity, width spacer, and" + " height spacer. Input:PDF Output:PDF Type:SISO") - public ResponseEntity addWatermark(@Valid @ModelAttribute AddWatermarkRequest request) - throws IOException, Exception { + public ResponseEntity addWatermark( + @Valid @ModelAttribute AddWatermarkRequest request) throws IOException, Exception { MultipartFile pdfFile = request.getFileInput(); String pdfFileName = pdfFile.getOriginalFilename(); if (pdfFileName != null && (pdfFileName.contains("..") || pdfFileName.startsWith("/"))) { @@ -151,14 +154,16 @@ public class WatermarkController { return WebResponseUtils.pdfDocToWebResponse( convertedPdf, GeneralUtils.generateFilename( - pdfFile.getOriginalFilename(), "_watermarked.pdf")); + pdfFile.getOriginalFilename(), "_watermarked.pdf"), + tempFileManager); } } else { // Return the watermarked PDF as a response return WebResponseUtils.pdfDocToWebResponse( document, GeneralUtils.generateFilename( - pdfFile.getOriginalFilename(), "_watermarked.pdf")); + pdfFile.getOriginalFilename(), "_watermarked.pdf"), + tempFileManager); } } } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/web/SignatureImageController.java b/app/core/src/main/java/stirling/software/SPDF/controller/web/SignatureImageController.java index 5d69d60c8c..92f9dc8cab 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/web/SignatureImageController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/web/SignatureImageController.java @@ -11,21 +11,18 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import io.swagger.v3.oas.annotations.tags.Tag; + import lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.service.SharedSignatureService; import stirling.software.common.service.PersonalSignatureServiceInterface; import stirling.software.common.service.UserServiceInterface; -/** - * Unified signature image controller that works for both authenticated and unauthenticated users. - * Uses composition pattern: - Core SharedSignatureService (always available): reads shared - * signatures - PersonalSignatureService (proprietary, optional): reads personal signatures For - * authenticated signature management (save/delete), see proprietary SignatureController. - */ @Slf4j @RestController @RequestMapping("/api/v1/general") +@Tag(name = "Signature Assets", description = "Retrieve saved signature images") public class SignatureImageController { private final SharedSignatureService sharedSignatureService; diff --git a/app/core/src/main/java/stirling/software/SPDF/model/api/converters/ConvertPDFToMarkdown.java b/app/core/src/main/java/stirling/software/SPDF/model/api/converters/ConvertPDFToMarkdown.java index 5b820e6073..013aaa01ef 100644 --- a/app/core/src/main/java/stirling/software/SPDF/model/api/converters/ConvertPDFToMarkdown.java +++ b/app/core/src/main/java/stirling/software/SPDF/model/api/converters/ConvertPDFToMarkdown.java @@ -4,6 +4,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import io.swagger.v3.oas.annotations.Operation; @@ -28,7 +29,7 @@ public class ConvertPDFToMarkdown { summary = "Convert PDF to Markdown", description = "This endpoint converts a PDF file to Markdown format. Input:PDF Output:Markdown Type:SISO") - public ResponseEntity processPdfToMarkdown(@ModelAttribute PDFFile file) + public ResponseEntity processPdfToMarkdown(@ModelAttribute PDFFile file) throws Exception { MultipartFile inputFile = file.getFileInput(); PDFToFile pdfToFile = new PDFToFile(tempFileManager); diff --git a/app/core/src/main/resources/application.properties b/app/core/src/main/resources/application.properties index 751eabd77e..701adb2c2c 100644 --- a/app/core/src/main/resources/application.properties +++ b/app/core/src/main/resources/application.properties @@ -25,6 +25,11 @@ server.http2.enabled=true # Enable virtual threads (Java 21+, pinning fix in Java 25) spring.threads.virtual.enabled=true +# Only run security filters on REQUEST and ERROR dispatches (not ASYNC). +# StreamingResponseBody triggers an ASYNC dispatch on completion; without this, +# Spring Security re-evaluates authorization after the response is already committed. +spring.security.filter.dispatcher-types=REQUEST,ERROR + # Response compression server.compression.enabled=true server.compression.min-response-size=1024 diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/BookletImpositionControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/BookletImpositionControllerTest.java index 93e819c8c3..0e260e3822 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/BookletImpositionControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/BookletImpositionControllerTest.java @@ -1,8 +1,10 @@ package stirling.software.SPDF.controller.api; import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.*; +import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -11,6 +13,7 @@ import org.apache.pdfbox.Loader; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.io.TempDir; @@ -21,17 +24,47 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.SPDF.model.api.general.BookletImpositionRequest; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; @ExtendWith(MockitoExtension.class) class BookletImpositionControllerTest { + private static ResponseEntity streamingOk(byte[] bytes) { + return ResponseEntity.ok(out -> out.write(bytes)); + } + + private static byte[] drainBody(ResponseEntity response) + throws java.io.IOException { + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + response.getBody().writeTo(baos); + return baos.toByteArray(); + } @TempDir Path tempDir; @Mock private CustomPDFDocumentFactory pdfDocumentFactory; + @Mock private TempFileManager tempFileManager; @InjectMocks private BookletImpositionController controller; + @BeforeEach + void setUp() throws Exception { + lenient() + .when(tempFileManager.createManagedTempFile(anyString())) + .thenAnswer( + inv -> { + File f = + Files.createTempFile("test", inv.getArgument(0)) + .toFile(); + TempFile tf = mock(TempFile.class); + lenient().when(tf.getFile()).thenReturn(f); + lenient().when(tf.getPath()).thenReturn(f.toPath()); + return tf; + }); + } + private MockMultipartFile createRealPdf(int numPages) throws IOException { Path path = tempDir.resolve("test.pdf"); try (PDDocument doc = new PDDocument()) { @@ -61,11 +94,12 @@ class BookletImpositionControllerTest { when(pdfDocumentFactory.load(file)).thenReturn(sourceDoc); when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDoc)).thenReturn(newDoc); - ResponseEntity response = controller.createBookletImposition(request); + ResponseEntity response = + controller.createBookletImposition(request); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(response.getBody()).isNotEmpty(); - try (PDDocument result = Loader.loadPDF(response.getBody())) { + assertThat(drainBody(response)).isNotEmpty(); + try (PDDocument result = Loader.loadPDF(drainBody(response))) { assertThat(result.getNumberOfPages()).isGreaterThan(0); } } @@ -92,10 +126,11 @@ class BookletImpositionControllerTest { when(pdfDocumentFactory.load(file)).thenReturn(sourceDoc); when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDoc)).thenReturn(newDoc); - ResponseEntity response = controller.createBookletImposition(request); + ResponseEntity response = + controller.createBookletImposition(request); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(response.getBody()).isNotEmpty(); + assertThat(drainBody(response)).isNotEmpty(); } @Test @@ -109,7 +144,8 @@ class BookletImpositionControllerTest { when(pdfDocumentFactory.load(file)).thenReturn(sourceDoc); when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDoc)).thenReturn(newDoc); - ResponseEntity response = controller.createBookletImposition(request); + ResponseEntity response = + controller.createBookletImposition(request); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); } @@ -126,7 +162,8 @@ class BookletImpositionControllerTest { when(pdfDocumentFactory.load(file)).thenReturn(sourceDoc); when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDoc)).thenReturn(newDoc); - ResponseEntity response = controller.createBookletImposition(request); + ResponseEntity response = + controller.createBookletImposition(request); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); } @@ -143,7 +180,8 @@ class BookletImpositionControllerTest { when(pdfDocumentFactory.load(file)).thenReturn(sourceDoc); when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDoc)).thenReturn(newDoc); - ResponseEntity response = controller.createBookletImposition(request); + ResponseEntity response = + controller.createBookletImposition(request); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); } @@ -160,7 +198,8 @@ class BookletImpositionControllerTest { when(pdfDocumentFactory.load(file)).thenReturn(sourceDoc); when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDoc)).thenReturn(newDoc); - ResponseEntity response = controller.createBookletImposition(request); + ResponseEntity response = + controller.createBookletImposition(request); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); } @@ -177,7 +216,8 @@ class BookletImpositionControllerTest { when(pdfDocumentFactory.load(file)).thenReturn(sourceDoc); when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDoc)).thenReturn(newDoc); - ResponseEntity response = controller.createBookletImposition(request); + ResponseEntity response = + controller.createBookletImposition(request); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); } @@ -192,7 +232,8 @@ class BookletImpositionControllerTest { when(pdfDocumentFactory.load(file)).thenReturn(sourceDoc); when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDoc)).thenReturn(newDoc); - ResponseEntity response = controller.createBookletImposition(request); + ResponseEntity response = + controller.createBookletImposition(request); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); } @@ -220,7 +261,8 @@ class BookletImpositionControllerTest { when(pdfDocumentFactory.load(file)).thenReturn(sourceDoc); when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDoc)).thenReturn(newDoc); - ResponseEntity response = controller.createBookletImposition(request); + ResponseEntity response = + controller.createBookletImposition(request); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); } diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/CropControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/CropControllerTest.java index 3c526a84c5..05389a840a 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/CropControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/CropControllerTest.java @@ -1,9 +1,11 @@ package stirling.software.SPDF.controller.api; import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.*; import java.awt.image.BufferedImage; +import java.io.File; import java.io.IOException; import java.lang.reflect.Method; import java.nio.file.Files; @@ -29,21 +31,47 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.SPDF.model.api.general.CropPdfForm; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; @ExtendWith(MockitoExtension.class) @DisplayName("CropController Tests") class CropControllerTest { + private static ResponseEntity streamingOk(byte[] bytes) { + return ResponseEntity.ok(out -> out.write(bytes)); + } + + private static byte[] drainBody(ResponseEntity response) + throws java.io.IOException { + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + response.getBody().writeTo(baos); + return baos.toByteArray(); + } @TempDir Path tempDir; @Mock private CustomPDFDocumentFactory pdfDocumentFactory; + @Mock private TempFileManager tempFileManager; @InjectMocks private CropController cropController; private TestPdfFactory pdfFactory; @BeforeEach - void setUp() { + void setUp() throws Exception { + lenient() + .when(tempFileManager.createManagedTempFile(anyString())) + .thenAnswer( + inv -> { + File f = + Files.createTempFile("test", inv.getArgument(0)) + .toFile(); + TempFile tf = mock(TempFile.class); + lenient().when(tf.getFile()).thenReturn(f); + lenient().when(tf.getPath()).thenReturn(f.toPath()); + return tf; + }); pdfFactory = new TestPdfFactory(); } @@ -177,7 +205,7 @@ class CropControllerTest { when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(mockDocument)) .thenReturn(newDocument); - ResponseEntity response = cropController.cropPdf(request); + ResponseEntity response = cropController.cropPdf(request); assertThat(response) .isNotNull() @@ -214,7 +242,7 @@ class CropControllerTest { when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(mockDocument)) .thenReturn(newDocument); - ResponseEntity response = cropController.cropPdf(request); + ResponseEntity response = cropController.cropPdf(request); assertThat(response).isNotNull(); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); @@ -234,7 +262,7 @@ class CropControllerTest { private TestPdfFactory autoCropPdfFactory; @BeforeEach - void setUp() { + void setUp() throws Exception { autoCropPdfFactory = new TestPdfFactory(); } @@ -254,13 +282,13 @@ class CropControllerTest { when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDoc)) .thenReturn(newDoc); - ResponseEntity response = cropController.cropPdf(request); + ResponseEntity response = cropController.cropPdf(request); assertThat(response).isNotNull(); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(response.getBody()).isNotEmpty(); + assertThat(drainBody(response)).isNotEmpty(); - try (PDDocument result = Loader.loadPDF(response.getBody())) { + try (PDDocument result = Loader.loadPDF(drainBody(response))) { assertThat(result.getNumberOfPages()).isEqualTo(1); PDPage page = result.getPage(0); @@ -285,13 +313,13 @@ class CropControllerTest { when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDoc)) .thenReturn(newDoc); - ResponseEntity response = cropController.cropPdf(request); + ResponseEntity response = cropController.cropPdf(request); assertThat(response).isNotNull(); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); Assertions.assertNotNull(response.getBody()); - try (PDDocument result = Loader.loadPDF(response.getBody())) { + try (PDDocument result = Loader.loadPDF(drainBody(response))) { assertThat(result.getNumberOfPages()).isEqualTo(1); } } @@ -648,7 +676,7 @@ class CropControllerTest { when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(mockDocument)) .thenReturn(newDocument); - ResponseEntity response = cropController.cropPdf(request); + ResponseEntity response = cropController.cropPdf(request); assertThat(response).isNotNull(); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); @@ -676,7 +704,7 @@ class CropControllerTest { when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(mockDocument)) .thenReturn(newDocument); - ResponseEntity response = cropController.cropPdf(request); + ResponseEntity response = cropController.cropPdf(request); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); verify(mockDocument, times(1)).close(); diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/EditTableOfContentsControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/EditTableOfContentsControllerTest.java index 9ff4687d86..5f8f459a9d 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/EditTableOfContentsControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/EditTableOfContentsControllerTest.java @@ -1,10 +1,12 @@ package stirling.software.SPDF.controller.api; import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.*; -import java.io.ByteArrayOutputStream; +import java.io.File; import java.lang.reflect.Method; +import java.nio.file.Files; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -26,10 +28,13 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.SPDF.controller.api.EditTableOfContentsController.BookmarkItem; import stirling.software.SPDF.model.api.EditTableOfContentsRequest; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; import tools.jackson.core.type.TypeReference; import tools.jackson.databind.ObjectMapper; @@ -40,6 +45,7 @@ class EditTableOfContentsControllerTest { @Mock private CustomPDFDocumentFactory pdfDocumentFactory; @Mock private ObjectMapper objectMapper; + @Mock private TempFileManager tempFileManager; @InjectMocks private EditTableOfContentsController editTableOfContentsController; @@ -53,7 +59,19 @@ class EditTableOfContentsControllerTest { private PDOutlineItem mockOutlineItem; @BeforeEach - void setUp() { + void setUp() throws Exception { + lenient() + .when(tempFileManager.createManagedTempFile(anyString())) + .thenAnswer( + inv -> { + File f = + Files.createTempFile("test", inv.getArgument(0)) + .toFile(); + TempFile tf = mock(TempFile.class); + lenient().when(tf.getFile()).thenReturn(f); + lenient().when(tf.getPath()).thenReturn(f.toPath()); + return tf; + }); mockFile = new MockMultipartFile( "file", @@ -226,18 +244,19 @@ class EditTableOfContentsControllerTest { when(mockDocument.getNumberOfPages()).thenReturn(5); when(mockDocument.getPage(0)).thenReturn(mockPage1); - // Mock saving behavior - doAnswer( - invocation -> { - ByteArrayOutputStream baos = invocation.getArgument(0); - baos.write("mocked pdf content".getBytes()); + lenient() + .doAnswer( + inv -> { + File f = inv.getArgument(0); + java.nio.file.Files.write(f.toPath(), "mock pdf".getBytes()); return null; }) .when(mockDocument) - .save(any(ByteArrayOutputStream.class)); + .save(any(File.class)); // When - ResponseEntity result = editTableOfContentsController.editTableOfContents(request); + ResponseEntity result = + editTableOfContentsController.editTableOfContents(request); // Then assertNotNull(result); @@ -289,17 +308,19 @@ class EditTableOfContentsControllerTest { when(mockDocument.getPage(0)).thenReturn(mockPage1); when(mockDocument.getPage(1)).thenReturn(mockPage2); - doAnswer( - invocation -> { - ByteArrayOutputStream baos = invocation.getArgument(0); - baos.write("mocked pdf content".getBytes()); + lenient() + .doAnswer( + inv -> { + File f = inv.getArgument(0); + java.nio.file.Files.write(f.toPath(), "mock pdf".getBytes()); return null; }) .when(mockDocument) - .save(any(ByteArrayOutputStream.class)); + .save(any(File.class)); // When - ResponseEntity result = editTableOfContentsController.editTableOfContents(request); + ResponseEntity result = + editTableOfContentsController.editTableOfContents(request); // Then assertNotNull(result); @@ -341,17 +362,19 @@ class EditTableOfContentsControllerTest { when(mockDocument.getPage(0)).thenReturn(mockPage1); // For negative page number when(mockDocument.getPage(4)).thenReturn(mockPage2); // For page number exceeding bounds - doAnswer( - invocation -> { - ByteArrayOutputStream baos = invocation.getArgument(0); - baos.write("mocked pdf content".getBytes()); + lenient() + .doAnswer( + inv -> { + File f = inv.getArgument(0); + java.nio.file.Files.write(f.toPath(), "mock pdf".getBytes()); return null; }) .when(mockDocument) - .save(any(ByteArrayOutputStream.class)); + .save(any(File.class)); // When - ResponseEntity result = editTableOfContentsController.editTableOfContents(request); + ResponseEntity result = + editTableOfContentsController.editTableOfContents(request); // Then assertNotNull(result); diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/MultiPageLayoutControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/MultiPageLayoutControllerTest.java index 0a1f3bc3b1..28276a3f39 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/MultiPageLayoutControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/MultiPageLayoutControllerTest.java @@ -1,5 +1,12 @@ package stirling.software.SPDF.controller.api; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; + +import java.io.File; +import java.nio.file.Files; + import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; import org.junit.jupiter.api.Assertions; @@ -15,14 +22,28 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.SPDF.model.api.general.MergeMultiplePagesRequest; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; @ExtendWith(MockitoExtension.class) class MultiPageLayoutControllerTest { + private static ResponseEntity streamingOk(byte[] bytes) { + return ResponseEntity.ok(out -> out.write(bytes)); + } + + private static byte[] drainBody(ResponseEntity response) + throws java.io.IOException { + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + response.getBody().writeTo(baos); + return baos.toByteArray(); + } @Mock private CustomPDFDocumentFactory pdfDocumentFactory; + @Mock private TempFileManager tempFileManager; @InjectMocks private MultiPageLayoutController controller; @@ -30,7 +51,19 @@ class MultiPageLayoutControllerTest { private MockMultipartFile fileNoExt; @BeforeEach - void setup() { + void setup() throws Exception { + lenient() + .when(tempFileManager.createManagedTempFile(anyString())) + .thenAnswer( + inv -> { + File f = + Files.createTempFile("test", inv.getArgument(0)) + .toFile(); + TempFile tf = mock(TempFile.class); + lenient().when(tf.getFile()).thenReturn(f); + lenient().when(tf.getPath()).thenReturn(f.toPath()); + return tf; + }); fileWithExt = new MockMultipartFile( "fileInput", "test.pdf", "application/pdf", new byte[] {1, 2, 3}); @@ -64,11 +97,11 @@ class MultiPageLayoutControllerTest { req.setAddBorder(Boolean.FALSE); req.setFileInput(fileWithExt); - ResponseEntity resp = controller.mergeMultiplePagesIntoOne(req); + ResponseEntity resp = controller.mergeMultiplePagesIntoOne(req); Assertions.assertEquals(HttpStatus.OK, resp.getStatusCode()); Assertions.assertEquals(MediaType.APPLICATION_PDF, resp.getHeaders().getContentType()); Assertions.assertNotNull(resp.getBody()); - Assertions.assertTrue(resp.getBody().length > 0); + Assertions.assertTrue(drainBody(resp).length > 0); Assertions.assertEquals( "test_multi_page_layout.pdf", resp.getHeaders().getContentDisposition().getFilename()); @@ -89,11 +122,11 @@ class MultiPageLayoutControllerTest { req.setAddBorder(Boolean.TRUE); req.setFileInput(fileWithExt); - ResponseEntity resp = controller.mergeMultiplePagesIntoOne(req); + ResponseEntity resp = controller.mergeMultiplePagesIntoOne(req); Assertions.assertEquals(HttpStatus.OK, resp.getStatusCode()); Assertions.assertEquals(MediaType.APPLICATION_PDF, resp.getHeaders().getContentType()); Assertions.assertNotNull(resp.getBody()); - Assertions.assertTrue(resp.getBody().length > 0); + Assertions.assertTrue(drainBody(resp).length > 0); } @Test @@ -112,7 +145,7 @@ class MultiPageLayoutControllerTest { req.setAddBorder(Boolean.TRUE); req.setFileInput(fileNoExt); - ResponseEntity resp = controller.mergeMultiplePagesIntoOne(req); + ResponseEntity resp = controller.mergeMultiplePagesIntoOne(req); Assertions.assertEquals( "name_multi_page_layout.pdf", resp.getHeaders().getContentDisposition().getFilename()); diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/PdfOverlayControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/PdfOverlayControllerTest.java index 0983a0bb7c..66b3eebff5 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/PdfOverlayControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/PdfOverlayControllerTest.java @@ -2,16 +2,22 @@ package stirling.software.SPDF.controller.api; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import java.io.ByteArrayOutputStream; +import java.io.File; import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; import org.apache.pdfbox.Loader; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -24,17 +30,47 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.SPDF.model.api.general.OverlayPdfsRequest; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; @ExtendWith(MockitoExtension.class) class PdfOverlayControllerTest { + private static ResponseEntity streamingOk(byte[] bytes) { + return ResponseEntity.ok(out -> out.write(bytes)); + } + + private static byte[] drainBody(ResponseEntity response) + throws java.io.IOException { + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + response.getBody().writeTo(baos); + return baos.toByteArray(); + } @TempDir Path tempDir; @Mock private CustomPDFDocumentFactory pdfDocumentFactory; + @Mock private TempFileManager tempFileManager; @InjectMocks private PdfOverlayController controller; + @BeforeEach + void setUp() throws Exception { + lenient() + .when(tempFileManager.createManagedTempFile(anyString())) + .thenAnswer( + inv -> { + File f = + Files.createTempFile("test", inv.getArgument(0)) + .toFile(); + TempFile tf = mock(TempFile.class); + lenient().when(tf.getFile()).thenReturn(f); + lenient().when(tf.getPath()).thenReturn(f.toPath()); + return tf; + }); + } + private byte[] createPdf(int numPages) throws IOException { try (PDDocument doc = new PDDocument()) { for (int i = 0; i < numPages; i++) { @@ -71,12 +107,12 @@ class PdfOverlayControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class))) .thenAnswer(inv -> Loader.loadPDF(((MultipartFile) inv.getArgument(0)).getBytes())); - ResponseEntity response = controller.overlayPdfs(request); + ResponseEntity response = controller.overlayPdfs(request); assertNotNull(response); assertEquals(HttpStatus.OK, response.getStatusCode()); assertNotNull(response.getBody()); - assertTrue(response.getBody().length > 0); + assertTrue(drainBody(response).length > 0); } @Test @@ -105,7 +141,7 @@ class PdfOverlayControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class))) .thenAnswer(inv -> Loader.loadPDF(((MultipartFile) inv.getArgument(0)).getBytes())); - ResponseEntity response = controller.overlayPdfs(request); + ResponseEntity response = controller.overlayPdfs(request); assertNotNull(response); assertEquals(HttpStatus.OK, response.getStatusCode()); @@ -137,7 +173,7 @@ class PdfOverlayControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class))) .thenAnswer(inv -> Loader.loadPDF(((MultipartFile) inv.getArgument(0)).getBytes())); - ResponseEntity response = controller.overlayPdfs(request); + ResponseEntity response = controller.overlayPdfs(request); assertNotNull(response); assertEquals(HttpStatus.OK, response.getStatusCode()); @@ -168,7 +204,7 @@ class PdfOverlayControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class))) .thenAnswer(inv -> Loader.loadPDF(((MultipartFile) inv.getArgument(0)).getBytes())); - ResponseEntity response = controller.overlayPdfs(request); + ResponseEntity response = controller.overlayPdfs(request); assertNotNull(response); assertEquals(HttpStatus.OK, response.getStatusCode()); @@ -258,7 +294,7 @@ class PdfOverlayControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class))) .thenAnswer(inv -> Loader.loadPDF(((MultipartFile) inv.getArgument(0)).getBytes())); - ResponseEntity response = controller.overlayPdfs(request); + ResponseEntity response = controller.overlayPdfs(request); assertNotNull(response); assertEquals(HttpStatus.OK, response.getStatusCode()); @@ -291,7 +327,7 @@ class PdfOverlayControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class))) .thenAnswer(inv -> Loader.loadPDF(((MultipartFile) inv.getArgument(0)).getBytes())); - ResponseEntity response = controller.overlayPdfs(request); + ResponseEntity response = controller.overlayPdfs(request); assertNotNull(response); assertEquals(HttpStatus.OK, response.getStatusCode()); diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/RearrangePagesPDFControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/RearrangePagesPDFControllerTest.java index eaec8e9d19..81f85d60a2 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/RearrangePagesPDFControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/RearrangePagesPDFControllerTest.java @@ -1,12 +1,16 @@ package stirling.software.SPDF.controller.api; import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.*; +import java.io.File; import java.io.IOException; +import java.nio.file.Files; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -15,18 +19,38 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.SPDF.model.api.PDFWithPageNums; import stirling.software.SPDF.model.api.general.RearrangePagesRequest; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; @ExtendWith(MockitoExtension.class) class RearrangePagesPDFControllerTest { @Mock private CustomPDFDocumentFactory pdfDocumentFactory; + @Mock private TempFileManager tempFileManager; @InjectMocks private RearrangePagesPDFController controller; + @BeforeEach + void setUp() throws Exception { + lenient() + .when(tempFileManager.createManagedTempFile(anyString())) + .thenAnswer( + inv -> { + File f = + Files.createTempFile("test", inv.getArgument(0)) + .toFile(); + TempFile tf = mock(TempFile.class); + lenient().when(tf.getFile()).thenReturn(f); + lenient().when(tf.getPath()).thenReturn(f.toPath()); + return tf; + }); + } + private MockMultipartFile createMockPdf() { return new MockMultipartFile( "fileInput", "test.pdf", MediaType.APPLICATION_PDF_VALUE, new byte[] {1, 2, 3}); @@ -43,7 +67,7 @@ class RearrangePagesPDFControllerTest { when(pdfDocumentFactory.load(file)).thenReturn(mockDoc); when(mockDoc.getNumberOfPages()).thenReturn(5); - ResponseEntity response = controller.deletePages(request); + ResponseEntity response = controller.deletePages(request); assertNotNull(response); assertEquals(200, response.getStatusCode().value()); @@ -73,7 +97,7 @@ class RearrangePagesPDFControllerTest { when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(mockDoc)) .thenReturn(mockNewDoc); - ResponseEntity response = controller.rearrangePages(request); + ResponseEntity response = controller.rearrangePages(request); assertNotNull(response); assertEquals(200, response.getStatusCode().value()); @@ -103,7 +127,7 @@ class RearrangePagesPDFControllerTest { when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(mockDoc)) .thenReturn(mockNewDoc); - ResponseEntity response = controller.rearrangePages(request); + ResponseEntity response = controller.rearrangePages(request); assertNotNull(response); verify(mockNewDoc).addPage(page1); @@ -131,7 +155,7 @@ class RearrangePagesPDFControllerTest { when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(mockDoc)) .thenReturn(mockNewDoc); - ResponseEntity response = controller.rearrangePages(request); + ResponseEntity response = controller.rearrangePages(request); assertNotNull(response); verify(mockNewDoc).addPage(page0); @@ -157,7 +181,7 @@ class RearrangePagesPDFControllerTest { when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(mockDoc)) .thenReturn(mockNewDoc); - ResponseEntity response = controller.rearrangePages(request); + ResponseEntity response = controller.rearrangePages(request); assertNotNull(response); assertEquals(200, response.getStatusCode().value()); @@ -184,7 +208,7 @@ class RearrangePagesPDFControllerTest { when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(mockDoc)) .thenReturn(mockNewDoc); - ResponseEntity response = controller.rearrangePages(request); + ResponseEntity response = controller.rearrangePages(request); assertNotNull(response); assertEquals(200, response.getStatusCode().value()); @@ -208,7 +232,7 @@ class RearrangePagesPDFControllerTest { when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(mockDoc)) .thenReturn(mockNewDoc); - ResponseEntity response = controller.rearrangePages(request); + ResponseEntity response = controller.rearrangePages(request); assertNotNull(response); assertEquals(200, response.getStatusCode().value()); @@ -232,7 +256,7 @@ class RearrangePagesPDFControllerTest { when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(mockDoc)) .thenReturn(mockNewDoc); - ResponseEntity response = controller.rearrangePages(request); + ResponseEntity response = controller.rearrangePages(request); assertNotNull(response); assertEquals(200, response.getStatusCode().value()); @@ -260,7 +284,7 @@ class RearrangePagesPDFControllerTest { when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(mockDoc)) .thenReturn(mockNewDoc); - ResponseEntity response = controller.rearrangePages(request); + ResponseEntity response = controller.rearrangePages(request); assertNotNull(response); assertEquals(200, response.getStatusCode().value()); @@ -284,7 +308,7 @@ class RearrangePagesPDFControllerTest { when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(mockDoc)) .thenReturn(mockNewDoc); - ResponseEntity response = controller.rearrangePages(request); + ResponseEntity response = controller.rearrangePages(request); assertNotNull(response); // 2 pages * 3 duplicates = 6 addPage calls @@ -309,7 +333,7 @@ class RearrangePagesPDFControllerTest { when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(mockDoc)) .thenReturn(mockNewDoc); - ResponseEntity response = controller.rearrangePages(request); + ResponseEntity response = controller.rearrangePages(request); assertNotNull(response); assertEquals(200, response.getStatusCode().value()); diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/RotationControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/RotationControllerTest.java index ecdafc09a9..bc5f60977f 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/RotationControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/RotationControllerTest.java @@ -1,15 +1,20 @@ package stirling.software.SPDF.controller.api; import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.io.File; import java.io.IOException; +import java.nio.file.Files; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPageTree; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -18,17 +23,37 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.SPDF.model.api.general.RotatePDFRequest; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; @ExtendWith(MockitoExtension.class) public class RotationControllerTest { @Mock private CustomPDFDocumentFactory pdfDocumentFactory; + @Mock private TempFileManager tempFileManager; @InjectMocks private RotationController rotationController; + @BeforeEach + void setUp() throws Exception { + lenient() + .when(tempFileManager.createManagedTempFile(anyString())) + .thenAnswer( + inv -> { + File f = + Files.createTempFile("test", inv.getArgument(0)) + .toFile(); + TempFile tf = mock(TempFile.class); + lenient().when(tf.getFile()).thenReturn(f); + lenient().when(tf.getPath()).thenReturn(f.toPath()); + return tf; + }); + } + @Test public void testRotatePDF() throws IOException { // Create a mock file @@ -50,7 +75,7 @@ public class RotationControllerTest { when(mockPage.getRotation()).thenReturn(0); // Act - ResponseEntity response = rotationController.rotatePDF(request); + ResponseEntity response = rotationController.rotatePDF(request); // Assert verify(mockPage).setRotation(90); diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/ScalePagesControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/ScalePagesControllerTest.java index 41574bf3aa..4fee57b5d6 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/ScalePagesControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/ScalePagesControllerTest.java @@ -2,8 +2,10 @@ package stirling.software.SPDF.controller.api; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.*; +import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -11,6 +13,7 @@ import java.nio.file.Path; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.io.TempDir; @@ -21,17 +24,47 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.SPDF.model.api.general.ScalePagesRequest; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; @ExtendWith(MockitoExtension.class) class ScalePagesControllerTest { + private static ResponseEntity streamingOk(byte[] bytes) { + return ResponseEntity.ok(out -> out.write(bytes)); + } + + private static byte[] drainBody(ResponseEntity response) + throws java.io.IOException { + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + response.getBody().writeTo(baos); + return baos.toByteArray(); + } @TempDir Path tempDir; @Mock private CustomPDFDocumentFactory pdfDocumentFactory; + @Mock private TempFileManager tempFileManager; @InjectMocks private ScalePagesController controller; + @BeforeEach + void setUp() throws Exception { + lenient() + .when(tempFileManager.createManagedTempFile(anyString())) + .thenAnswer( + inv -> { + File f = + Files.createTempFile("test", inv.getArgument(0)) + .toFile(); + TempFile tf = mock(TempFile.class); + lenient().when(tf.getFile()).thenReturn(f); + lenient().when(tf.getPath()).thenReturn(f.toPath()); + return tf; + }); + } + private byte[] createRealPdf(PDRectangle pageSize, int numPages) throws IOException { try (PDDocument doc = new PDDocument()) { for (int i = 0; i < numPages; i++) { @@ -68,12 +101,12 @@ class ScalePagesControllerTest { setupFactory(); - ResponseEntity response = controller.scalePages(request); + ResponseEntity response = controller.scalePages(request); assertNotNull(response); assertEquals(200, response.getStatusCode().value()); assertNotNull(response.getBody()); - assertTrue(response.getBody().length > 0); + assertTrue(drainBody(response).length > 0); } @Test @@ -90,7 +123,7 @@ class ScalePagesControllerTest { setupFactory(); - ResponseEntity response = controller.scalePages(request); + ResponseEntity response = controller.scalePages(request); assertNotNull(response); assertEquals(200, response.getStatusCode().value()); @@ -110,7 +143,7 @@ class ScalePagesControllerTest { setupFactory(); - ResponseEntity response = controller.scalePages(request); + ResponseEntity response = controller.scalePages(request); assertNotNull(response); assertEquals(200, response.getStatusCode().value()); @@ -130,7 +163,7 @@ class ScalePagesControllerTest { setupFactory(); - ResponseEntity response = controller.scalePages(request); + ResponseEntity response = controller.scalePages(request); assertNotNull(response); assertEquals(200, response.getStatusCode().value()); @@ -150,7 +183,7 @@ class ScalePagesControllerTest { setupFactory(); - ResponseEntity response = controller.scalePages(request); + ResponseEntity response = controller.scalePages(request); assertNotNull(response); assertEquals(200, response.getStatusCode().value()); @@ -187,7 +220,7 @@ class ScalePagesControllerTest { setupFactory(); - ResponseEntity response = controller.scalePages(request); + ResponseEntity response = controller.scalePages(request); assertNotNull(response); assertEquals(200, response.getStatusCode().value()); @@ -207,7 +240,7 @@ class ScalePagesControllerTest { setupFactory(); - ResponseEntity response = controller.scalePages(request); + ResponseEntity response = controller.scalePages(request); assertNotNull(response); assertEquals(200, response.getStatusCode().value()); @@ -248,7 +281,7 @@ class ScalePagesControllerTest { setupFactory(); - ResponseEntity response = controller.scalePages(request); + ResponseEntity response = controller.scalePages(request); assertNotNull(response); assertEquals(200, response.getStatusCode().value()); diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/SplitPdfBySectionsControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/SplitPdfBySectionsControllerTest.java index 49330eacad..2cada36a9a 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/SplitPdfBySectionsControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/SplitPdfBySectionsControllerTest.java @@ -4,8 +4,11 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -31,6 +34,7 @@ import org.springframework.web.multipart.MultipartFile; import stirling.software.SPDF.model.api.SplitPdfBySectionsRequest; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.TempFile; import stirling.software.common.util.TempFileManager; @ExtendWith(MockitoExtension.class) @@ -44,6 +48,18 @@ class SplitPdfBySectionsControllerTest { @BeforeEach void setUp() throws IOException { + lenient() + .when(tempFileManager.createManagedTempFile(anyString())) + .thenAnswer( + inv -> { + File f = + Files.createTempFile("test", inv.getArgument(0)) + .toFile(); + TempFile tf = mock(TempFile.class); + lenient().when(tf.getFile()).thenReturn(f); + lenient().when(tf.getPath()).thenReturn(f.toPath()); + return tf; + }); when(tempFileManager.createTempFile(anyString())) .thenAnswer( inv -> { diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertEbookToPDFControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertEbookToPDFControllerTest.java index 95f0de648d..be2295eb08 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertEbookToPDFControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertEbookToPDFControllerTest.java @@ -5,7 +5,10 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import java.io.File; @@ -19,6 +22,7 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Stream; import org.apache.pdfbox.pdmodel.PDDocument; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; @@ -29,6 +33,7 @@ import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.SPDF.config.EndpointConfiguration; import stirling.software.SPDF.model.api.converters.ConvertEbookToPdfRequest; @@ -37,11 +42,22 @@ import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.ProcessExecutor; import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult; import stirling.software.common.util.ProcessExecutor.Processes; +import stirling.software.common.util.TempFile; import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @ExtendWith(MockitoExtension.class) class ConvertEbookToPDFControllerTest { + private static ResponseEntity streamingOk(byte[] bytes) { + return ResponseEntity.ok(out -> out.write(bytes)); + } + + private static byte[] drainBody(ResponseEntity response) + throws java.io.IOException { + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + response.getBody().writeTo(baos); + return baos.toByteArray(); + } @Mock private CustomPDFDocumentFactory pdfDocumentFactory; @Mock private TempFileManager tempFileManager; @@ -49,6 +65,22 @@ class ConvertEbookToPDFControllerTest { @InjectMocks private ConvertEbookToPDFController controller; + @BeforeEach + void setUp() throws Exception { + lenient() + .when(tempFileManager.createManagedTempFile(anyString())) + .thenAnswer( + inv -> { + File f = + Files.createTempFile("test", inv.getArgument(0)) + .toFile(); + TempFile tf = mock(TempFile.class); + lenient().when(tf.getFile()).thenReturn(f); + lenient().when(tf.getPath()).thenReturn(f.toPath()); + return tf; + }); + } + @Test void convertEbookToPdf_buildsCalibreCommandAndCleansUp() throws Exception { when(endpointConfiguration.isGroupEnabled("Calibre")).thenReturn(true); @@ -113,16 +145,14 @@ class ConvertEbookToPDFControllerTest { return execResult; }); - ResponseEntity expectedResponse = ResponseEntity.ok("result".getBytes()); - wr.when( - () -> - WebResponseUtils.pdfDocToWebResponse( - mockDocument, "ebook_convertedToPDF.pdf")) + ResponseEntity expectedResponse = + streamingOk("result".getBytes()); + wr.when(() -> WebResponseUtils.pdfFileToWebResponse(any(TempFile.class), anyString())) .thenReturn(expectedResponse); gu.when(() -> GeneralUtils.generateFilename("ebook.epub", "_convertedToPDF.pdf")) .thenReturn("ebook_convertedToPDF.pdf"); - ResponseEntity response = controller.convertEbookToPdf(request); + ResponseEntity response = controller.convertEbookToPdf(request); assertSame(expectedResponse, response); @@ -232,14 +262,11 @@ class ConvertEbookToPDFControllerTest { gu.when(() -> GeneralUtils.optimizePdfWithGhostscript(Mockito.any(byte[].class))) .thenReturn(optimizedBytes); - ResponseEntity expectedResponse = ResponseEntity.ok(optimizedBytes); - wr.when( - () -> - WebResponseUtils.bytesToWebResponse( - optimizedBytes, "ebook_convertedToPDF.pdf")) + ResponseEntity expectedResponse = streamingOk(optimizedBytes); + wr.when(() -> WebResponseUtils.pdfFileToWebResponse(any(TempFile.class), anyString())) .thenReturn(expectedResponse); - ResponseEntity response = controller.convertEbookToPdf(request); + ResponseEntity response = controller.convertEbookToPdf(request); assertSame(expectedResponse, response); gu.verify(() -> GeneralUtils.optimizePdfWithGhostscript(Mockito.any(byte[].class))); diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertEmlToPDFTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertEmlToPDFTest.java index 2a7d138577..7f4857d830 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertEmlToPDFTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertEmlToPDFTest.java @@ -4,12 +4,18 @@ import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -21,17 +27,29 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.common.configuration.RuntimePathConfig; import stirling.software.common.model.api.converters.EmlToPdfRequest; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.CustomHtmlSanitizer; import stirling.software.common.util.EmlToPdf; +import stirling.software.common.util.TempFile; import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @ExtendWith(MockitoExtension.class) class ConvertEmlToPDFTest { + private static ResponseEntity streamingOk(byte[] bytes) { + return ResponseEntity.ok(out -> out.write(bytes)); + } + + private static byte[] drainBody(ResponseEntity response) + throws java.io.IOException { + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + response.getBody().writeTo(baos); + return baos.toByteArray(); + } @Mock private CustomPDFDocumentFactory pdfDocumentFactory; @Mock private RuntimePathConfig runtimePathConfig; @@ -40,35 +58,51 @@ class ConvertEmlToPDFTest { @InjectMocks private ConvertEmlToPDF controller; + @BeforeEach + void setUp() throws Exception { + lenient() + .when(tempFileManager.createManagedTempFile(anyString())) + .thenAnswer( + inv -> { + File f = + Files.createTempFile("test", inv.getArgument(0)) + .toFile(); + TempFile tf = mock(TempFile.class); + lenient().when(tf.getFile()).thenReturn(f); + lenient().when(tf.getPath()).thenReturn(f.toPath()); + return tf; + }); + } + @Test - void convertEmlToPdf_emptyFileReturnsBadRequest() { + void convertEmlToPdf_emptyFileReturnsBadRequest() throws java.io.IOException { MockMultipartFile emptyFile = new MockMultipartFile("fileInput", "test.eml", "message/rfc822", new byte[0]); EmlToPdfRequest request = new EmlToPdfRequest(); request.setFileInput(emptyFile); - ResponseEntity response = controller.convertEmlToPdf(request); + ResponseEntity response = controller.convertEmlToPdf(request); assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); assertTrue( - new String(response.getBody(), StandardCharsets.UTF_8) + new String(drainBody(response), StandardCharsets.UTF_8) .contains("No file provided")); } @Test - void convertEmlToPdf_nullFilenameReturnsBadRequest() { + void convertEmlToPdf_nullFilenameReturnsBadRequest() throws java.io.IOException { MockMultipartFile file = new MockMultipartFile("fileInput", null, "message/rfc822", "content".getBytes()); EmlToPdfRequest request = new EmlToPdfRequest(); request.setFileInput(file); - ResponseEntity response = controller.convertEmlToPdf(request); + ResponseEntity response = controller.convertEmlToPdf(request); assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); assertTrue( - new String(response.getBody(), StandardCharsets.UTF_8).contains("valid filename")); + new String(drainBody(response), StandardCharsets.UTF_8).contains("valid filename")); } @Test @@ -79,24 +113,24 @@ class ConvertEmlToPDFTest { EmlToPdfRequest request = new EmlToPdfRequest(); request.setFileInput(file); - ResponseEntity response = controller.convertEmlToPdf(request); + ResponseEntity response = controller.convertEmlToPdf(request); assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); } @Test - void convertEmlToPdf_invalidFileTypeReturnsBadRequest() { + void convertEmlToPdf_invalidFileTypeReturnsBadRequest() throws java.io.IOException { MockMultipartFile file = new MockMultipartFile("fileInput", "test.txt", "text/plain", "content".getBytes()); EmlToPdfRequest request = new EmlToPdfRequest(); request.setFileInput(file); - ResponseEntity response = controller.convertEmlToPdf(request); + ResponseEntity response = controller.convertEmlToPdf(request); assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); assertTrue( - new String(response.getBody(), StandardCharsets.UTF_8) + new String(drainBody(response), StandardCharsets.UTF_8) .contains("valid EML or MSG")); } @@ -112,7 +146,7 @@ class ConvertEmlToPDFTest { when(runtimePathConfig.getWeasyPrintPath()).thenReturn("/usr/bin/weasyprint"); - ResponseEntity expectedResponse = ResponseEntity.ok(pdfBytes); + ResponseEntity expectedResponse = streamingOk(pdfBytes); try (MockedStatic emlMock = Mockito.mockStatic(EmlToPdf.class); MockedStatic wrMock = @@ -132,14 +166,14 @@ class ConvertEmlToPDFTest { wrMock.when( () -> - WebResponseUtils.bytesToWebResponse( - pdfBytes, "test.eml.pdf", MediaType.APPLICATION_PDF)) + WebResponseUtils.pdfFileToWebResponse( + any(TempFile.class), anyString())) .thenReturn(expectedResponse); - ResponseEntity response = controller.convertEmlToPdf(request); + ResponseEntity response = controller.convertEmlToPdf(request); assertEquals(HttpStatus.OK, response.getStatusCode()); - assertArrayEquals(pdfBytes, response.getBody()); + assertArrayEquals(pdfBytes, drainBody(response)); } } @@ -154,8 +188,8 @@ class ConvertEmlToPDFTest { request.setFileInput(file); request.setDownloadHtml(true); - ResponseEntity expectedResponse = - ResponseEntity.ok(htmlContent.getBytes(StandardCharsets.UTF_8)); + ResponseEntity expectedResponse = + streamingOk(htmlContent.getBytes(StandardCharsets.UTF_8)); try (MockedStatic emlMock = Mockito.mockStatic(EmlToPdf.class); MockedStatic wrMock = @@ -171,13 +205,11 @@ class ConvertEmlToPDFTest { wrMock.when( () -> - WebResponseUtils.bytesToWebResponse( - htmlContent.getBytes(StandardCharsets.UTF_8), - "test.eml.html", - MediaType.TEXT_HTML)) + WebResponseUtils.fileToWebResponse( + any(TempFile.class), anyString(), any(MediaType.class))) .thenReturn(expectedResponse); - ResponseEntity response = controller.convertEmlToPdf(request); + ResponseEntity response = controller.convertEmlToPdf(request); assertEquals(HttpStatus.OK, response.getStatusCode()); } @@ -203,11 +235,11 @@ class ConvertEmlToPDFTest { eq(customHtmlSanitizer))) .thenThrow(new IOException("Parse error")); - ResponseEntity response = controller.convertEmlToPdf(request); + ResponseEntity response = controller.convertEmlToPdf(request); assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.getStatusCode()); assertTrue( - new String(response.getBody(), StandardCharsets.UTF_8) + new String(drainBody(response), StandardCharsets.UTF_8) .contains("HTML conversion failed")); } } @@ -231,11 +263,11 @@ class ConvertEmlToPDFTest { any(), any(), any(), any(), any(), any(), any())) .thenReturn(null); - ResponseEntity response = controller.convertEmlToPdf(request); + ResponseEntity response = controller.convertEmlToPdf(request); assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.getStatusCode()); assertTrue( - new String(response.getBody(), StandardCharsets.UTF_8) + new String(drainBody(response), StandardCharsets.UTF_8) .contains("empty output")); } } @@ -255,7 +287,7 @@ class ConvertEmlToPDFTest { when(runtimePathConfig.getWeasyPrintPath()).thenReturn("/usr/bin/weasyprint"); - ResponseEntity expectedResponse = ResponseEntity.ok(pdfBytes); + ResponseEntity expectedResponse = streamingOk(pdfBytes); try (MockedStatic emlMock = Mockito.mockStatic(EmlToPdf.class); MockedStatic wrMock = @@ -269,13 +301,11 @@ class ConvertEmlToPDFTest { wrMock.when( () -> - WebResponseUtils.bytesToWebResponse( - any(byte[].class), - any(String.class), - any(MediaType.class))) + WebResponseUtils.pdfFileToWebResponse( + any(TempFile.class), anyString())) .thenReturn(expectedResponse); - ResponseEntity response = controller.convertEmlToPdf(request); + ResponseEntity response = controller.convertEmlToPdf(request); assertEquals(HttpStatus.OK, response.getStatusCode()); } @@ -300,11 +330,12 @@ class ConvertEmlToPDFTest { any(), any(), any(), any(), any(), any(), any())) .thenThrow(new InterruptedException("interrupted")); - ResponseEntity response = controller.convertEmlToPdf(request); + ResponseEntity response = controller.convertEmlToPdf(request); assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.getStatusCode()); assertTrue( - new String(response.getBody(), StandardCharsets.UTF_8).contains("interrupted")); + new String(drainBody(response), StandardCharsets.UTF_8) + .contains("interrupted")); } } } diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertHtmlToPDFTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertHtmlToPDFTest.java index aa9f84b93f..466ab50650 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertHtmlToPDFTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertHtmlToPDFTest.java @@ -3,9 +3,16 @@ package stirling.software.SPDF.controller.api.converters; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import java.io.File; +import java.nio.file.Files; + +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -16,6 +23,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.common.configuration.RuntimePathConfig; import stirling.software.common.model.api.converters.HTMLToPdfRequest; @@ -23,11 +31,22 @@ import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.CustomHtmlSanitizer; import stirling.software.common.util.FileToPdf; import stirling.software.common.util.GeneralUtils; +import stirling.software.common.util.TempFile; import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @ExtendWith(MockitoExtension.class) class ConvertHtmlToPDFTest { + private static ResponseEntity streamingOk(byte[] bytes) { + return ResponseEntity.ok(out -> out.write(bytes)); + } + + private static byte[] drainBody(ResponseEntity response) + throws java.io.IOException { + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + response.getBody().writeTo(baos); + return baos.toByteArray(); + } @Mock private CustomPDFDocumentFactory pdfDocumentFactory; @Mock private RuntimePathConfig runtimePathConfig; @@ -36,6 +55,22 @@ class ConvertHtmlToPDFTest { @InjectMocks private ConvertHtmlToPDF controller; + @BeforeEach + void setUp() throws Exception { + lenient() + .when(tempFileManager.createManagedTempFile(anyString())) + .thenAnswer( + inv -> { + File f = + Files.createTempFile("test", inv.getArgument(0)) + .toFile(); + TempFile tf = mock(TempFile.class); + lenient().when(tf.getFile()).thenReturn(f); + lenient().when(tf.getPath()).thenReturn(f.toPath()); + return tf; + }); + } + @Test void htmlToPdf_nullFileInputThrows() { HTMLToPdfRequest request = new HTMLToPdfRequest(); @@ -69,7 +104,7 @@ class ConvertHtmlToPDFTest { when(pdfDocumentFactory.createNewBytesBasedOnOldDocument(pdfBytes)) .thenReturn(processedPdf); - ResponseEntity expectedResponse = ResponseEntity.ok(processedPdf); + ResponseEntity expectedResponse = streamingOk(processedPdf); try (MockedStatic ftpMock = Mockito.mockStatic(FileToPdf.class); MockedStatic guMock = Mockito.mockStatic(GeneralUtils.class); @@ -90,10 +125,13 @@ class ConvertHtmlToPDFTest { guMock.when(() -> GeneralUtils.generateFilename("test.html", ".pdf")) .thenReturn("test.pdf"); - wrMock.when(() -> WebResponseUtils.bytesToWebResponse(processedPdf, "test.pdf")) + wrMock.when( + () -> + WebResponseUtils.pdfFileToWebResponse( + any(TempFile.class), anyString())) .thenReturn(expectedResponse); - ResponseEntity response = controller.HtmlToPdf(request); + ResponseEntity response = controller.HtmlToPdf(request); assertEquals(HttpStatus.OK, response.getStatusCode()); } @@ -114,7 +152,7 @@ class ConvertHtmlToPDFTest { when(pdfDocumentFactory.createNewBytesBasedOnOldDocument(pdfBytes)) .thenReturn(processedPdf); - ResponseEntity expectedResponse = ResponseEntity.ok(processedPdf); + ResponseEntity expectedResponse = streamingOk(processedPdf); try (MockedStatic ftpMock = Mockito.mockStatic(FileToPdf.class); MockedStatic guMock = Mockito.mockStatic(GeneralUtils.class); @@ -135,10 +173,13 @@ class ConvertHtmlToPDFTest { guMock.when(() -> GeneralUtils.generateFilename("archive.zip", ".pdf")) .thenReturn("archive.pdf"); - wrMock.when(() -> WebResponseUtils.bytesToWebResponse(processedPdf, "archive.pdf")) + wrMock.when( + () -> + WebResponseUtils.pdfFileToWebResponse( + any(TempFile.class), anyString())) .thenReturn(expectedResponse); - ResponseEntity response = controller.HtmlToPdf(request); + ResponseEntity response = controller.HtmlToPdf(request); assertEquals(HttpStatus.OK, response.getStatusCode()); } diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertImgPDFControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertImgPDFControllerTest.java index 011a3fcb61..78ce0233b1 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertImgPDFControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertImgPDFControllerTest.java @@ -14,6 +14,7 @@ import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.SPDF.config.EndpointConfiguration; import stirling.software.SPDF.model.api.converters.ConvertToPdfRequest; @@ -25,6 +26,16 @@ import stirling.software.common.util.WebResponseUtils; @ExtendWith(MockitoExtension.class) class ConvertImgPDFControllerTest { + private static ResponseEntity streamingOk(byte[] bytes) { + return ResponseEntity.ok(out -> out.write(bytes)); + } + + private static byte[] drainBody(ResponseEntity response) + throws java.io.IOException { + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + response.getBody().writeTo(baos); + return baos.toByteArray(); + } @Mock private CustomPDFDocumentFactory pdfDocumentFactory; @Mock private TempFileManager tempFileManager; diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertMarkdownToPdfTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertMarkdownToPdfTest.java index 838c91ea86..00116172cd 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertMarkdownToPdfTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertMarkdownToPdfTest.java @@ -4,10 +4,17 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import java.io.File; +import java.nio.file.Files; + +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -18,6 +25,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.common.configuration.RuntimePathConfig; import stirling.software.common.model.api.GeneralFile; @@ -25,11 +33,22 @@ import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.CustomHtmlSanitizer; import stirling.software.common.util.FileToPdf; import stirling.software.common.util.GeneralUtils; +import stirling.software.common.util.TempFile; import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @ExtendWith(MockitoExtension.class) class ConvertMarkdownToPdfTest { + private static ResponseEntity streamingOk(byte[] bytes) { + return ResponseEntity.ok(out -> out.write(bytes)); + } + + private static byte[] drainBody(ResponseEntity response) + throws java.io.IOException { + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + response.getBody().writeTo(baos); + return baos.toByteArray(); + } @Mock private CustomPDFDocumentFactory pdfDocumentFactory; @Mock private RuntimePathConfig runtimePathConfig; @@ -38,6 +57,22 @@ class ConvertMarkdownToPdfTest { @InjectMocks private ConvertMarkdownToPdf controller; + @BeforeEach + void setUp() throws Exception { + lenient() + .when(tempFileManager.createManagedTempFile(anyString())) + .thenAnswer( + inv -> { + File f = + Files.createTempFile("test", inv.getArgument(0)) + .toFile(); + TempFile tf = mock(TempFile.class); + lenient().when(tf.getFile()).thenReturn(f); + lenient().when(tf.getPath()).thenReturn(f.toPath()); + return tf; + }); + } + @Test void markdownToPdf_nullFileInputThrows() { GeneralFile generalFile = new GeneralFile(); @@ -71,7 +106,7 @@ class ConvertMarkdownToPdfTest { when(pdfDocumentFactory.createNewBytesBasedOnOldDocument(any(byte[].class))) .thenReturn(processedPdf); - ResponseEntity expectedResponse = ResponseEntity.ok(processedPdf); + ResponseEntity expectedResponse = streamingOk(processedPdf); try (MockedStatic ftpMock = Mockito.mockStatic(FileToPdf.class); MockedStatic guMock = Mockito.mockStatic(GeneralUtils.class); @@ -92,10 +127,13 @@ class ConvertMarkdownToPdfTest { guMock.when(() -> GeneralUtils.generateFilename("readme.md", ".pdf")) .thenReturn("readme.pdf"); - wrMock.when(() -> WebResponseUtils.bytesToWebResponse(processedPdf, "readme.pdf")) + wrMock.when( + () -> + WebResponseUtils.pdfFileToWebResponse( + any(TempFile.class), anyString())) .thenReturn(expectedResponse); - ResponseEntity response = controller.markdownToPdf(generalFile); + ResponseEntity response = controller.markdownToPdf(generalFile); assertEquals(HttpStatus.OK, response.getStatusCode()); } diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToEpubControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToEpubControllerTest.java index 65cae373a4..a9d669c052 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToEpubControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToEpubControllerTest.java @@ -3,12 +3,15 @@ package stirling.software.SPDF.controller.api.converters; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -18,6 +21,7 @@ import java.util.List; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; @@ -29,6 +33,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.SPDF.config.EndpointConfiguration; import stirling.software.SPDF.model.api.converters.ConvertPdfToEpubRequest; @@ -38,10 +43,21 @@ import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.ProcessExecutor; import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult; import stirling.software.common.util.ProcessExecutor.Processes; +import stirling.software.common.util.TempFile; import stirling.software.common.util.TempFileManager; @ExtendWith(MockitoExtension.class) class ConvertPDFToEpubControllerTest { + private static ResponseEntity streamingOk(byte[] bytes) { + return ResponseEntity.ok(out -> out.write(bytes)); + } + + private static byte[] drainBody(ResponseEntity response) + throws java.io.IOException { + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + response.getBody().writeTo(baos); + return baos.toByteArray(); + } private static final MediaType EPUB_MEDIA_TYPE = MediaType.valueOf("application/epub+zip"); @@ -50,6 +66,22 @@ class ConvertPDFToEpubControllerTest { @InjectMocks private ConvertPDFToEpubController controller; + @BeforeEach + void setUp() throws Exception { + lenient() + .when(tempFileManager.createManagedTempFile(anyString())) + .thenAnswer( + inv -> { + File f = + Files.createTempFile("test", inv.getArgument(0)) + .toFile(); + TempFile tf = mock(TempFile.class); + lenient().when(tf.getFile()).thenReturn(f); + lenient().when(tf.getPath()).thenReturn(f.toPath()); + return tf; + }); + } + @Test void convertPdfToEpub_buildsGoldenCommandAndCleansUp() throws Exception { when(endpointConfiguration.isGroupEnabled("Calibre")).thenReturn(true); @@ -110,7 +142,7 @@ class ConvertPDFToEpubControllerTest { gu.when(() -> GeneralUtils.generateFilename("novel.pdf", "_convertedToEPUB.epub")) .thenReturn("novel_convertedToEPUB.epub"); - ResponseEntity response = controller.convertPdfToEpub(request); + ResponseEntity response = controller.convertPdfToEpub(request); List command = commandCaptor.getValue(); assertEquals(13, command.size()); @@ -134,7 +166,7 @@ class ConvertPDFToEpubControllerTest { assertEquals( "novel_convertedToEPUB.epub", response.getHeaders().getContentDisposition().getFilename()); - assertEquals("epub", new String(response.getBody(), StandardCharsets.UTF_8)); + assertEquals("epub", new String(drainBody(response), StandardCharsets.UTF_8)); verify(tempFileManager).deleteTempDirectory(workingDir); assertEquals(workingDir, deletedDir.get()); @@ -202,7 +234,7 @@ class ConvertPDFToEpubControllerTest { gu.when(() -> GeneralUtils.generateFilename("story.pdf", "_convertedToEPUB.epub")) .thenReturn("story_convertedToEPUB.epub"); - ResponseEntity response = controller.convertPdfToEpub(request); + ResponseEntity response = controller.convertPdfToEpub(request); List command = commandCaptor.getValue(); assertTrue(command.stream().noneMatch(arg -> "--chapter".equals(arg))); @@ -220,7 +252,7 @@ class ConvertPDFToEpubControllerTest { assertEquals( "story_convertedToEPUB.epub", response.getHeaders().getContentDisposition().getFilename()); - assertEquals("epub", new String(response.getBody(), StandardCharsets.UTF_8)); + assertEquals("epub", new String(drainBody(response), StandardCharsets.UTF_8)); } finally { deleteIfExists(workingDir); } @@ -287,7 +319,7 @@ class ConvertPDFToEpubControllerTest { gu.when(() -> GeneralUtils.generateFilename("book.pdf", "_convertedToAZW3.azw3")) .thenReturn("book_convertedToAZW3.azw3"); - ResponseEntity response = controller.convertPdfToEpub(request); + ResponseEntity response = controller.convertPdfToEpub(request); List command = commandCaptor.getValue(); assertEquals("ebook-convert", command.get(0)); @@ -308,7 +340,7 @@ class ConvertPDFToEpubControllerTest { assertEquals( "book_convertedToAZW3.azw3", response.getHeaders().getContentDisposition().getFilename()); - assertEquals("azw3", new String(response.getBody(), StandardCharsets.UTF_8)); + assertEquals("azw3", new String(drainBody(response), StandardCharsets.UTF_8)); verify(tempFileManager).deleteTempDirectory(workingDir); } finally { diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToExcelControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToExcelControllerTest.java index 6b5bb1e7f5..ca7a7112fa 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToExcelControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToExcelControllerTest.java @@ -2,11 +2,17 @@ package stirling.software.SPDF.controller.api.converters; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import java.io.File; +import java.nio.file.Files; import java.util.List; import org.apache.pdfbox.pdmodel.PDDocument; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -17,18 +23,38 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.SPDF.model.api.PDFWithPageNums; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.GeneralUtils; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; @ExtendWith(MockitoExtension.class) class ConvertPDFToExcelControllerTest { @Mock private CustomPDFDocumentFactory pdfDocumentFactory; + @Mock private TempFileManager tempFileManager; @InjectMocks private ConvertPDFToExcelController controller; + @BeforeEach + void setUp() throws Exception { + lenient() + .when(tempFileManager.createManagedTempFile(anyString())) + .thenAnswer( + inv -> { + File f = + Files.createTempFile("test", inv.getArgument(0)) + .toFile(); + TempFile tf = mock(TempFile.class); + lenient().when(tf.getFile()).thenReturn(f); + lenient().when(tf.getPath()).thenReturn(f.toPath()); + return tf; + }); + } + @Test void pdfToExcel_noTablesReturnsNoContent() throws Exception { MockMultipartFile pdfFile = @@ -55,7 +81,7 @@ class ConvertPDFToExcelControllerTest { Mockito.eq(true))) .thenReturn(List.of(1)); - ResponseEntity response = controller.pdfToExcel(request); + ResponseEntity response = controller.pdfToExcel(request); // tabula may or may not find tables in an empty page assertNotNull(response); diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToOfficeTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToOfficeTest.java index 50991742b9..d768ce1b29 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToOfficeTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToOfficeTest.java @@ -4,10 +4,16 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertSame; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import java.io.File; +import java.nio.file.Files; + import org.apache.pdfbox.pdmodel.PDDocument; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -18,6 +24,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.SPDF.model.api.converters.PdfToPresentationRequest; import stirling.software.SPDF.model.api.converters.PdfToTextOrRTFRequest; @@ -27,11 +34,22 @@ import stirling.software.common.model.api.PDFFile; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.PDFToFile; +import stirling.software.common.util.TempFile; import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @ExtendWith(MockitoExtension.class) class ConvertPDFToOfficeTest { + private static ResponseEntity streamingOk(byte[] bytes) { + return ResponseEntity.ok(out -> out.write(bytes)); + } + + private static byte[] drainBody(ResponseEntity response) + throws java.io.IOException { + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + response.getBody().writeTo(baos); + return baos.toByteArray(); + } @Mock private CustomPDFDocumentFactory pdfDocumentFactory; @Mock private TempFileManager tempFileManager; @@ -39,6 +57,22 @@ class ConvertPDFToOfficeTest { @InjectMocks private ConvertPDFToOffice controller; + @BeforeEach + void setUp() throws Exception { + lenient() + .when(tempFileManager.createManagedTempFile(anyString())) + .thenAnswer( + inv -> { + File f = + Files.createTempFile("test", inv.getArgument(0)) + .toFile(); + TempFile tf = mock(TempFile.class); + lenient().when(tf.getFile()).thenReturn(f); + lenient().when(tf.getPath()).thenReturn(f.toPath()); + return tf; + }); + } + private MockMultipartFile createPdfFile() { return new MockMultipartFile( "fileInput", "document.pdf", "application/pdf", "pdf-content".getBytes()); @@ -51,7 +85,8 @@ class ConvertPDFToOfficeTest { request.setFileInput(pdfFile); request.setOutputFormat("pptx"); - ResponseEntity expectedResponse = ResponseEntity.ok("pptx-content".getBytes()); + ResponseEntity expectedResponse = + streamingOk("pptx-content".getBytes()); try (MockedStatic mock = Mockito.mockStatic(PDFToFile.class, Mockito.CALLS_REAL_METHODS)) { @@ -80,7 +115,8 @@ class ConvertPDFToOfficeTest { realDoc.addPage(new org.apache.pdfbox.pdmodel.PDPage()); when(pdfDocumentFactory.load(pdfFile)).thenReturn(realDoc); - ResponseEntity expectedResponse = ResponseEntity.ok("text content".getBytes()); + ResponseEntity expectedResponse = + streamingOk("text content".getBytes()); try (MockedStatic guMock = Mockito.mockStatic(GeneralUtils.class); MockedStatic wrMock = @@ -91,13 +127,12 @@ class ConvertPDFToOfficeTest { wrMock.when( () -> - WebResponseUtils.bytesToWebResponse( - any(byte[].class), - eq("document.txt"), - eq(MediaType.TEXT_PLAIN))) + WebResponseUtils.fileToWebResponse( + any(TempFile.class), anyString(), any(MediaType.class))) .thenReturn(expectedResponse); - ResponseEntity response = controller.processPdfToRTForTXT(request); + ResponseEntity response = + controller.processPdfToRTForTXT(request); assertSame(expectedResponse, response); } diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertPdfJsonControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertPdfJsonControllerTest.java index d9d499b17d..882c24926a 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertPdfJsonControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertPdfJsonControllerTest.java @@ -4,34 +4,67 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; + +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.MockedStatic; -import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.SPDF.service.PdfJsonConversionService; import stirling.software.common.model.api.GeneralFile; import stirling.software.common.model.api.PDFFile; -import stirling.software.common.util.WebResponseUtils; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; @ExtendWith(MockitoExtension.class) class ConvertPdfJsonControllerTest { @Mock private PdfJsonConversionService pdfJsonConversionService; + @Mock private TempFileManager tempFileManager; @InjectMocks private ConvertPdfJsonController controller; + @BeforeEach + void setUp() throws Exception { + lenient() + .when(tempFileManager.createManagedTempFile(anyString())) + .thenAnswer( + inv -> { + File f = + Files.createTempFile("test", inv.getArgument(0)) + .toFile(); + TempFile tf = mock(TempFile.class); + lenient().when(tf.getFile()).thenReturn(f); + lenient().when(tf.getPath()).thenReturn(f.toPath()); + return tf; + }); + } + + private static byte[] drainBody(ResponseEntity response) + throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + response.getBody().writeTo(baos); + return baos.toByteArray(); + } + @Test void convertPdfToJson_nullFileInputThrows() { PDFFile request = new PDFFile(); @@ -51,19 +84,11 @@ class ConvertPdfJsonControllerTest { when(pdfJsonConversionService.convertPdfToJson(pdfFile, false)).thenReturn(jsonBytes); - ResponseEntity expectedResponse = ResponseEntity.ok(jsonBytes); + ResponseEntity response = + controller.convertPdfToJson(request, false); - try (MockedStatic wrMock = Mockito.mockStatic(WebResponseUtils.class)) { - wrMock.when( - () -> - WebResponseUtils.bytesToWebResponse( - jsonBytes, "doc.json", MediaType.APPLICATION_JSON)) - .thenReturn(expectedResponse); - - ResponseEntity response = controller.convertPdfToJson(request, false); - - assertEquals(HttpStatus.OK, response.getStatusCode()); - } + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertNotNull(response.getBody()); } @Test @@ -77,20 +102,10 @@ class ConvertPdfJsonControllerTest { when(pdfJsonConversionService.convertPdfToJson(pdfFile, true)).thenReturn(jsonBytes); - ResponseEntity expectedResponse = ResponseEntity.ok(jsonBytes); + ResponseEntity response = controller.convertPdfToJson(request, true); - try (MockedStatic wrMock = Mockito.mockStatic(WebResponseUtils.class)) { - wrMock.when( - () -> - WebResponseUtils.bytesToWebResponse( - jsonBytes, "doc.json", MediaType.APPLICATION_JSON)) - .thenReturn(expectedResponse); - - ResponseEntity response = controller.convertPdfToJson(request, true); - - assertEquals(HttpStatus.OK, response.getStatusCode()); - verify(pdfJsonConversionService).convertPdfToJson(pdfFile, true); - } + assertEquals(HttpStatus.OK, response.getStatusCode()); + verify(pdfJsonConversionService).convertPdfToJson(pdfFile, true); } @Test @@ -112,16 +127,10 @@ class ConvertPdfJsonControllerTest { when(pdfJsonConversionService.convertJsonToPdf(jsonFile)).thenReturn(pdfBytes); - ResponseEntity expectedResponse = ResponseEntity.ok(pdfBytes); + ResponseEntity response = controller.convertJsonToPdf(request); - try (MockedStatic wrMock = Mockito.mockStatic(WebResponseUtils.class)) { - wrMock.when(() -> WebResponseUtils.bytesToWebResponse(pdfBytes, "doc.pdf")) - .thenReturn(expectedResponse); - - ResponseEntity response = controller.convertJsonToPdf(request); - - assertEquals(HttpStatus.OK, response.getStatusCode()); - } + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertNotNull(response.getBody()); } @Test @@ -144,7 +153,7 @@ class ConvertPdfJsonControllerTest { when(pdfJsonConversionService.extractDocumentMetadata(eq(pdfFile), any(String.class))) .thenReturn(jsonBytes); - ResponseEntity response = controller.extractPdfMetadata(request); + ResponseEntity response = controller.extractPdfMetadata(request); assertEquals(HttpStatus.OK, response.getStatusCode()); assertEquals(MediaType.APPLICATION_JSON, response.getHeaders().getContentType()); @@ -168,19 +177,10 @@ class ConvertPdfJsonControllerTest { when(pdfJsonConversionService.extractSinglePage(jobId, 1)).thenReturn(jsonBytes); - ResponseEntity expectedResponse = ResponseEntity.ok(jsonBytes); + ResponseEntity response = controller.extractSinglePage(jobId, 1); - try (MockedStatic wrMock = Mockito.mockStatic(WebResponseUtils.class)) { - wrMock.when( - () -> - WebResponseUtils.bytesToWebResponse( - jsonBytes, "page_1.json", MediaType.APPLICATION_JSON)) - .thenReturn(expectedResponse); - - ResponseEntity response = controller.extractSinglePage(jobId, 1); - - assertEquals(HttpStatus.OK, response.getStatusCode()); - } + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertNotNull(response.getBody()); } @Test @@ -190,20 +190,9 @@ class ConvertPdfJsonControllerTest { when(pdfJsonConversionService.extractPageFonts(jobId, 1)).thenReturn(jsonBytes); - ResponseEntity expectedResponse = ResponseEntity.ok(jsonBytes); + ResponseEntity response = controller.extractPageFonts(jobId, 1); - try (MockedStatic wrMock = Mockito.mockStatic(WebResponseUtils.class)) { - wrMock.when( - () -> - WebResponseUtils.bytesToWebResponse( - jsonBytes, - "page_fonts_1.json", - MediaType.APPLICATION_JSON)) - .thenReturn(expectedResponse); - - ResponseEntity response = controller.extractPageFonts(jobId, 1); - - assertEquals(HttpStatus.OK, response.getStatusCode()); - } + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertNotNull(response.getBody()); } } diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertSvgToPDFTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertSvgToPDFTest.java index 642f16f3b8..7d727cec22 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertSvgToPDFTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertSvgToPDFTest.java @@ -3,11 +3,17 @@ package stirling.software.SPDF.controller.api.converters; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -16,20 +22,31 @@ import org.mockito.MockedStatic; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.SPDF.model.api.converters.SvgToPdfRequest; import stirling.software.SPDF.utils.SvgToPdf; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.SvgSanitizer; +import stirling.software.common.util.TempFile; import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @ExtendWith(MockitoExtension.class) class ConvertSvgToPDFTest { + private static ResponseEntity streamingOk(byte[] bytes) { + return ResponseEntity.ok(out -> out.write(bytes)); + } + + private static byte[] drainBody(ResponseEntity response) + throws java.io.IOException { + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + response.getBody().writeTo(baos); + return baos.toByteArray(); + } @Mock private CustomPDFDocumentFactory pdfDocumentFactory; @Mock private SvgSanitizer svgSanitizer; @@ -37,16 +54,32 @@ class ConvertSvgToPDFTest { @InjectMocks private ConvertSvgToPDF controller; + @BeforeEach + void setUp() throws Exception { + lenient() + .when(tempFileManager.createManagedTempFile(anyString())) + .thenAnswer( + inv -> { + File f = + Files.createTempFile("test", inv.getArgument(0)) + .toFile(); + TempFile tf = mock(TempFile.class); + lenient().when(tf.getFile()).thenReturn(f); + lenient().when(tf.getPath()).thenReturn(f.toPath()); + return tf; + }); + } + @Test - void convertSvgToPdf_nullFilesReturnsBadRequest() { + void convertSvgToPdf_nullFilesReturnsBadRequest() throws java.io.IOException { SvgToPdfRequest request = new SvgToPdfRequest(); request.setFileInput(null); - ResponseEntity response = controller.convertSvgToPdf(request); + ResponseEntity response = controller.convertSvgToPdf(request); assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); assertTrue( - new String(response.getBody(), StandardCharsets.UTF_8) + new String(drainBody(response), StandardCharsets.UTF_8) .contains("No files provided")); } @@ -55,7 +88,7 @@ class ConvertSvgToPDFTest { SvgToPdfRequest request = new SvgToPdfRequest(); request.setFileInput(new MockMultipartFile[0]); - ResponseEntity response = controller.convertSvgToPdf(request); + ResponseEntity response = controller.convertSvgToPdf(request); assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); } @@ -69,10 +102,11 @@ class ConvertSvgToPDFTest { request.setFileInput(new MockMultipartFile[] {txtFile}); request.setCombineIntoSinglePdf(false); - ResponseEntity response = controller.convertSvgToPdf(request); + ResponseEntity response = controller.convertSvgToPdf(request); assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); - assertTrue(new String(response.getBody(), StandardCharsets.UTF_8).contains("No valid SVG")); + assertTrue( + new String(drainBody(response), StandardCharsets.UTF_8).contains("No valid SVG")); } @Test @@ -84,7 +118,7 @@ class ConvertSvgToPDFTest { request.setFileInput(new MockMultipartFile[] {emptyFile}); request.setCombineIntoSinglePdf(false); - ResponseEntity response = controller.convertSvgToPdf(request); + ResponseEntity response = controller.convertSvgToPdf(request); assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); } @@ -107,7 +141,7 @@ class ConvertSvgToPDFTest { when(pdfDocumentFactory.createNewBytesBasedOnOldDocument(pdfBytes)) .thenReturn(processedPdf); - ResponseEntity expectedResponse = ResponseEntity.ok(processedPdf); + ResponseEntity expectedResponse = streamingOk(processedPdf); try (MockedStatic svgMock = Mockito.mockStatic(SvgToPdf.class); MockedStatic guMock = Mockito.mockStatic(GeneralUtils.class); @@ -121,11 +155,11 @@ class ConvertSvgToPDFTest { wrMock.when( () -> - WebResponseUtils.bytesToWebResponse( - processedPdf, "drawing.pdf", MediaType.APPLICATION_PDF)) + WebResponseUtils.pdfFileToWebResponse( + any(TempFile.class), anyString())) .thenReturn(expectedResponse); - ResponseEntity response = controller.convertSvgToPdf(request); + ResponseEntity response = controller.convertSvgToPdf(request); assertEquals(HttpStatus.OK, response.getStatusCode()); } @@ -154,7 +188,7 @@ class ConvertSvgToPDFTest { when(pdfDocumentFactory.createNewBytesBasedOnOldDocument(combinedPdf)) .thenReturn(processedPdf); - ResponseEntity expectedResponse = ResponseEntity.ok(processedPdf); + ResponseEntity expectedResponse = streamingOk(processedPdf); try (MockedStatic svgMock = Mockito.mockStatic(SvgToPdf.class); MockedStatic guMock = Mockito.mockStatic(GeneralUtils.class); @@ -168,13 +202,11 @@ class ConvertSvgToPDFTest { wrMock.when( () -> - WebResponseUtils.bytesToWebResponse( - processedPdf, - "a_combined.pdf", - MediaType.APPLICATION_PDF)) + WebResponseUtils.pdfFileToWebResponse( + any(TempFile.class), anyString())) .thenReturn(expectedResponse); - ResponseEntity response = controller.convertSvgToPdf(request); + ResponseEntity response = controller.convertSvgToPdf(request); assertEquals(HttpStatus.OK, response.getStatusCode()); } @@ -189,7 +221,7 @@ class ConvertSvgToPDFTest { request.setFileInput(new MockMultipartFile[] {nullNameFile}); request.setCombineIntoSinglePdf(false); - ResponseEntity response = controller.convertSvgToPdf(request); + ResponseEntity response = controller.convertSvgToPdf(request); assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); } @@ -206,7 +238,7 @@ class ConvertSvgToPDFTest { when(svgSanitizer.sanitize(svgContent)).thenThrow(new IOException("sanitization error")); - ResponseEntity response = controller.convertSvgToPdf(request); + ResponseEntity response = controller.convertSvgToPdf(request); assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); } diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertWebsiteToPdfTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertWebsiteToPdfTest.java index a85e6a6666..c722b83c9d 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertWebsiteToPdfTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertWebsiteToPdfTest.java @@ -4,9 +4,10 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.lang.reflect.Method; @@ -34,6 +35,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.SPDF.model.api.converters.UrlToPdfRequest; import stirling.software.common.configuration.RuntimePathConfig; @@ -43,13 +45,25 @@ import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.ProcessExecutor; import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult; import stirling.software.common.util.ProcessExecutor.Processes; -import stirling.software.common.util.WebResponseUtils; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; public class ConvertWebsiteToPdfTest { + private static ResponseEntity streamingOk(byte[] bytes) { + return ResponseEntity.ok(out -> out.write(bytes)); + } + + private static byte[] drainBody(ResponseEntity response) + throws java.io.IOException { + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + response.getBody().writeTo(baos); + return baos.toByteArray(); + } private static final Pattern PDF_FILENAME_PATTERN = Pattern.compile("[A-Za-z0-9_]+\\.pdf"); @Mock private CustomPDFDocumentFactory pdfDocumentFactory; @Mock private RuntimePathConfig runtimePathConfig; + @Mock private TempFileManager tempFileManager; private ApplicationProperties applicationProperties; private ConvertWebsiteToPDF sut; @@ -58,6 +72,18 @@ public class ConvertWebsiteToPdfTest { @BeforeEach void setUp() throws Exception { mocks = MockitoAnnotations.openMocks(this); + lenient() + .when(tempFileManager.createManagedTempFile(anyString())) + .thenAnswer( + inv -> { + File f = + Files.createTempFile("test", inv.getArgument(0)) + .toFile(); + TempFile tf = mock(TempFile.class); + lenient().when(tf.getFile()).thenReturn(f); + lenient().when(tf.getPath()).thenReturn(f.toPath()); + return tf; + }); // Enable feature (adjust structure for your project if necessary) applicationProperties = new ApplicationProperties(); @@ -68,7 +94,12 @@ public class ConvertWebsiteToPdfTest { when(pdfDocumentFactory.load(any(File.class))).thenReturn(new PDDocument()); // Build SUT - sut = new ConvertWebsiteToPDF(pdfDocumentFactory, runtimePathConfig, applicationProperties); + sut = + new ConvertWebsiteToPDF( + pdfDocumentFactory, + runtimePathConfig, + applicationProperties, + tempFileManager); // Provide RequestContext for ServletUriComponentsBuilder MockHttpServletRequest req = new MockHttpServletRequest(); @@ -172,38 +203,29 @@ public class ConvertWebsiteToPdfTest { request.setUrlInput("https://example.com"); try (MockedStatic pe = Mockito.mockStatic(ProcessExecutor.class); - MockedStatic wr = Mockito.mockStatic(WebResponseUtils.class); MockedStatic gu = Mockito.mockStatic(GeneralUtils.class); MockedStatic httpClient = mockHttpClientReturning("")) { - // Force URL checks to be positive gu.when(() -> GeneralUtils.isValidURL("https://example.com")).thenReturn(true); gu.when(() -> GeneralUtils.isURLReachable("https://example.com")).thenReturn(true); + gu.when(() -> GeneralUtils.convertToFileName(anyString())).thenReturn("example_com"); + gu.when(() -> GeneralUtils.generateFilename(anyString(), anyString())) + .thenAnswer(inv -> inv.getArgument(0) + inv.getArgument(1)); - // correct ProcessExecutor! ProcessExecutor mockExec = Mockito.mock(ProcessExecutor.class); pe.when(() -> ProcessExecutor.getInstance(Processes.WEASYPRINT)).thenReturn(mockExec); @SuppressWarnings("unchecked") ArgumentCaptor> cmdCaptor = ArgumentCaptor.forClass(List.class); - // Return value of correct type ProcessExecutorResult dummyResult = Mockito.mock(ProcessExecutorResult.class); when(mockExec.runCommandWithOutputHandling(cmdCaptor.capture())) .thenReturn(dummyResult); - ResponseEntity fakeResponse = ResponseEntity.ok(new byte[0]); - - wr.when( - () -> - WebResponseUtils.baosToWebResponse( - any(ByteArrayOutputStream.class), any())) - .thenReturn(fakeResponse); - - // Act ResponseEntity resp = sut.urlToPdf(request); - // Assert – Response OK + // Assert + assertNotNull(resp); assertEquals(HttpStatus.OK, resp.getStatusCode()); // Assert – WeasyPrint command correct @@ -236,17 +258,19 @@ public class ConvertWebsiteToPdfTest { try (MockedStatic gu = Mockito.mockStatic(GeneralUtils.class); MockedStatic pe = Mockito.mockStatic(ProcessExecutor.class); - MockedStatic wr = Mockito.mockStatic(WebResponseUtils.class); MockedStatic files = Mockito.mockStatic(Files.class); MockedStatic httpClient = mockHttpClientReturning("")) { - // Force URL checks to be positive gu.when(() -> GeneralUtils.isValidURL("https://example.com")).thenReturn(true); gu.when(() -> GeneralUtils.isURLReachable("https://example.com")).thenReturn(true); + gu.when(() -> GeneralUtils.convertToFileName(anyString())).thenReturn("example_com"); + gu.when(() -> GeneralUtils.generateFilename(anyString(), anyString())) + .thenAnswer(inv -> inv.getArgument(0) + inv.getArgument(1)); - // Force temp files + provoke delete error files.when(() -> Files.createTempFile("url_input_", ".html")).thenReturn(htmlTemp); files.when(() -> Files.createTempFile("output_", ".pdf")).thenReturn(preCreatedTemp); + files.when(() -> Files.createTempFile(eq("test"), anyString())) + .thenReturn(preCreatedTemp); files.when( () -> Files.writeString( @@ -257,26 +281,20 @@ public class ConvertWebsiteToPdfTest { files.when(() -> Files.deleteIfExists(htmlTemp)).thenReturn(true); files.when(() -> Files.deleteIfExists(preCreatedTemp)) .thenThrow(new IOException("fail delete")); - files.when(() -> Files.exists(preCreatedTemp)).thenReturn(true); // for the assert + files.when(() -> Files.exists(preCreatedTemp)).thenReturn(true); + files.when(() -> Files.size(any(Path.class))).thenReturn(100L); + files.when(() -> Files.copy(any(Path.class), any(java.io.OutputStream.class))) + .thenReturn(0L); + files.when(() -> Files.newOutputStream(any(Path.class))) + .thenAnswer(inv -> new java.io.ByteArrayOutputStream()); - // ProcessExecutor ProcessExecutor mockExec = Mockito.mock(ProcessExecutor.class); pe.when(() -> ProcessExecutor.getInstance(Processes.WEASYPRINT)).thenReturn(mockExec); ProcessExecutorResult dummy = Mockito.mock(ProcessExecutorResult.class); when(mockExec.runCommandWithOutputHandling(Mockito.any())).thenReturn(dummy); - // WebResponseUtils - ResponseEntity fakeResponse = ResponseEntity.ok(new byte[0]); - wr.when( - () -> - WebResponseUtils.baosToWebResponse( - any(ByteArrayOutputStream.class), any())) - .thenReturn(fakeResponse); - - // Act: should not throw and should return a Response ResponseEntity resp = assertDoesNotThrow(() -> sut.urlToPdf(request)); - // Assert assertNotNull(resp, "Response should not be null"); assertEquals(HttpStatus.OK, resp.getStatusCode()); assertTrue( diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/PdfVectorExportControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/PdfVectorExportControllerTest.java index ad63b21039..20f881e5ac 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/PdfVectorExportControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/PdfVectorExportControllerTest.java @@ -3,11 +3,13 @@ package stirling.software.SPDF.controller.api.converters; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.when; +import java.io.File; import java.lang.reflect.Field; import java.nio.file.Files; import java.nio.file.Path; @@ -25,11 +27,13 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.SPDF.config.EndpointConfiguration; import stirling.software.SPDF.model.api.converters.PdfVectorExportRequest; import stirling.software.common.util.ProcessExecutor; import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult; +import stirling.software.common.util.TempFile; import stirling.software.common.util.TempFileManager; @ExtendWith(MockitoExtension.class) @@ -44,6 +48,18 @@ class PdfVectorExportControllerTest { @BeforeEach void setup() throws Exception { + lenient() + .when(tempFileManager.createManagedTempFile(anyString())) + .thenAnswer( + inv -> { + File f = + Files.createTempFile("test", inv.getArgument(0)) + .toFile(); + TempFile tf = mock(TempFile.class); + lenient().when(tf.getFile()).thenReturn(f); + lenient().when(tf.getPath()).thenReturn(f.toPath()); + return tf; + }); when(tempFileManager.createTempFile(any())) .thenAnswer( invocation -> { @@ -106,7 +122,8 @@ class PdfVectorExportControllerTest { PdfVectorExportRequest request = new PdfVectorExportRequest(); request.setFileInput(file); - ResponseEntity response = controller.convertGhostscriptInputsToPdf(request); + ResponseEntity response = + controller.convertGhostscriptInputsToPdf(request); assertThat(response.getStatusCode()).isEqualTo(org.springframework.http.HttpStatus.OK); assertThat(response.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_PDF); @@ -123,11 +140,14 @@ class PdfVectorExportControllerTest { PdfVectorExportRequest request = new PdfVectorExportRequest(); request.setFileInput(file); - ResponseEntity response = controller.convertGhostscriptInputsToPdf(request); + ResponseEntity response = + controller.convertGhostscriptInputsToPdf(request); assertThat(response.getStatusCode()).isEqualTo(org.springframework.http.HttpStatus.OK); assertThat(response.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_PDF); - assertThat(response.getBody()).contains(content); + java.io.ByteArrayOutputStream baosVerify = new java.io.ByteArrayOutputStream(); + response.getBody().writeTo(baosVerify); + assertThat(baosVerify.toByteArray()).contains(content); } @Test diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/filters/FilterControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/filters/FilterControllerTest.java index 0b73d3f7f9..df988f52fd 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/filters/FilterControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/filters/FilterControllerTest.java @@ -17,6 +17,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.SPDF.model.api.PDFComparisonAndCount; import stirling.software.SPDF.model.api.PDFWithPageNums; @@ -26,12 +27,24 @@ import stirling.software.SPDF.model.api.filter.PageRotationRequest; import stirling.software.SPDF.model.api.filter.PageSizeRequest; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.PdfUtils; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @ExtendWith(MockitoExtension.class) class FilterControllerTest { + private static ResponseEntity streamingOk(byte[] bytes) { + return ResponseEntity.ok(out -> out.write(bytes)); + } + + private static byte[] drainBody(ResponseEntity response) + throws java.io.IOException { + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + response.getBody().writeTo(baos); + return baos.toByteArray(); + } @Mock private CustomPDFDocumentFactory pdfDocumentFactory; + @Mock private TempFileManager tempFileManager; @InjectMocks private FilterController filterController; @@ -59,19 +72,22 @@ class FilterControllerTest { PDDocument mockDoc = mock(PDDocument.class); when(pdfDocumentFactory.load(mockFile)).thenReturn(mockDoc); - ResponseEntity expectedResponse = ResponseEntity.ok(new byte[] {1, 2, 3}); + ResponseEntity expectedResponse = streamingOk(new byte[] {1, 2, 3}); try (MockedStatic pdfUtilsMock = mockStatic(PdfUtils.class); MockedStatic webMock = mockStatic(WebResponseUtils.class)) { pdfUtilsMock.when(() -> PdfUtils.hasText(mockDoc, "all", "hello")).thenReturn(true); - webMock.when(() -> WebResponseUtils.pdfDocToWebResponse(mockDoc, "test.pdf")) + webMock.when( + () -> + WebResponseUtils.pdfDocToWebResponse( + mockDoc, "test.pdf", tempFileManager)) .thenReturn(expectedResponse); - ResponseEntity result = filterController.containsText(request); + ResponseEntity result = filterController.containsText(request); assertEquals(HttpStatus.OK, result.getStatusCode()); - assertArrayEquals(new byte[] {1, 2, 3}, result.getBody()); + assertArrayEquals(new byte[] {1, 2, 3}, drainBody(result)); } } @@ -88,7 +104,7 @@ class FilterControllerTest { try (MockedStatic pdfUtilsMock = mockStatic(PdfUtils.class)) { pdfUtilsMock.when(() -> PdfUtils.hasText(mockDoc, "all", "missing")).thenReturn(false); - ResponseEntity result = filterController.containsText(request); + ResponseEntity result = filterController.containsText(request); assertEquals(HttpStatus.NO_CONTENT, result.getStatusCode()); assertNull(result.getBody()); @@ -106,19 +122,22 @@ class FilterControllerTest { PDDocument mockDoc = mock(PDDocument.class); when(pdfDocumentFactory.load(mockFile)).thenReturn(mockDoc); - ResponseEntity expectedResponse = ResponseEntity.ok(new byte[] {4, 5, 6}); + ResponseEntity expectedResponse = streamingOk(new byte[] {4, 5, 6}); try (MockedStatic pdfUtilsMock = mockStatic(PdfUtils.class); MockedStatic webMock = mockStatic(WebResponseUtils.class)) { pdfUtilsMock.when(() -> PdfUtils.hasImages(mockDoc, "all")).thenReturn(true); - webMock.when(() -> WebResponseUtils.pdfDocToWebResponse(mockDoc, "test.pdf")) + webMock.when( + () -> + WebResponseUtils.pdfDocToWebResponse( + mockDoc, "test.pdf", tempFileManager)) .thenReturn(expectedResponse); - ResponseEntity result = filterController.containsImage(request); + ResponseEntity result = filterController.containsImage(request); assertEquals(HttpStatus.OK, result.getStatusCode()); - assertArrayEquals(new byte[] {4, 5, 6}, result.getBody()); + assertArrayEquals(new byte[] {4, 5, 6}, drainBody(result)); } } @@ -134,7 +153,7 @@ class FilterControllerTest { try (MockedStatic pdfUtilsMock = mockStatic(PdfUtils.class)) { pdfUtilsMock.when(() -> PdfUtils.hasImages(mockDoc, "1")).thenReturn(false); - ResponseEntity result = filterController.containsImage(request); + ResponseEntity result = filterController.containsImage(request); assertEquals(HttpStatus.NO_CONTENT, result.getStatusCode()); } diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/form/FormFillControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/form/FormFillControllerTest.java index 539fa250e7..0db7f60514 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/form/FormFillControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/form/FormFillControllerTest.java @@ -2,10 +2,13 @@ package stirling.software.SPDF.controller.api.form; import static org.assertj.core.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.*; import java.io.ByteArrayOutputStream; +import java.io.File; import java.io.IOException; +import java.nio.file.Files; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; @@ -22,8 +25,11 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; import tools.jackson.databind.ObjectMapper; import tools.jackson.databind.json.JsonMapper; @@ -31,8 +37,19 @@ import tools.jackson.databind.json.JsonMapper; @ExtendWith(MockitoExtension.class) @DisplayName("FormFillController Tests") class FormFillControllerTest { + private static ResponseEntity streamingOk(byte[] bytes) { + return ResponseEntity.ok(out -> out.write(bytes)); + } + + private static byte[] drainBody(ResponseEntity response) + throws java.io.IOException { + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + response.getBody().writeTo(baos); + return baos.toByteArray(); + } @Mock private CustomPDFDocumentFactory pdfDocumentFactory; + @Mock private TempFileManager tempFileManager; private ObjectMapper realObjectMapper; @@ -40,6 +57,18 @@ class FormFillControllerTest { @BeforeEach void setUp() throws Exception { + lenient() + .when(tempFileManager.createManagedTempFile(anyString())) + .thenAnswer( + inv -> { + File f = + Files.createTempFile("test", inv.getArgument(0)) + .toFile(); + TempFile tf = mock(TempFile.class); + lenient().when(tf.getFile()).thenReturn(f); + lenient().when(tf.getPath()).thenReturn(f.toPath()); + return tf; + }); realObjectMapper = JsonMapper.builder().build(); // Inject real ObjectMapper via reflection since @InjectMocks uses the mock var field = FormFillController.class.getDeclaredField("objectMapper"); @@ -203,7 +232,8 @@ class FormFillControllerTest { when(pdfDocumentFactory.load(eq(file))).thenReturn(doc); byte[] payload = "{\"field1\":\"value1\"}".getBytes(); - ResponseEntity response = controller.fillForm(file, payload, false); + ResponseEntity response = + controller.fillForm(file, payload, false); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(response.getBody()).isNotNull(); @@ -216,7 +246,7 @@ class FormFillControllerTest { PDDocument doc = createMinimalPdf(); when(pdfDocumentFactory.load(eq(file))).thenReturn(doc); - ResponseEntity response = controller.fillForm(file, null, false); + ResponseEntity response = controller.fillForm(file, null, false); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); } @@ -257,7 +287,7 @@ class FormFillControllerTest { when(pdfDocumentFactory.load(eq(file))).thenReturn(doc); byte[] payload = "[\"field1\"]".getBytes(); - ResponseEntity response = controller.deleteFields(file, payload); + ResponseEntity response = controller.deleteFields(file, payload); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); } @@ -293,7 +323,8 @@ class FormFillControllerTest { String json = "[{\"targetName\":\"f1\",\"name\":null,\"label\":null,\"type\":null," + "\"required\":null,\"multiSelect\":null,\"options\":null,\"defaultValue\":\"newVal\",\"tooltip\":null}]"; - ResponseEntity response = controller.modifyFields(file, json.getBytes()); + ResponseEntity response = + controller.modifyFields(file, json.getBytes()); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); } diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/AttachmentControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/AttachmentControllerTest.java index fe0e2ca2d6..b3e4690cc7 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/AttachmentControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/AttachmentControllerTest.java @@ -1,9 +1,12 @@ package stirling.software.SPDF.controller.api.misc; import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.*; +import java.io.File; import java.io.IOException; +import java.nio.file.Files; import java.util.List; import org.apache.pdfbox.pdmodel.PDDocument; @@ -19,18 +22,32 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.SPDF.model.api.misc.AddAttachmentRequest; import stirling.software.SPDF.service.AttachmentServiceInterface; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @ExtendWith(MockitoExtension.class) class AttachmentControllerTest { + private static ResponseEntity streamingOk(byte[] bytes) { + return ResponseEntity.ok(out -> out.write(bytes)); + } + + private static byte[] drainBody(ResponseEntity response) + throws java.io.IOException { + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + response.getBody().writeTo(baos); + return baos.toByteArray(); + } @Mock private CustomPDFDocumentFactory pdfDocumentFactory; @Mock private AttachmentServiceInterface pdfAttachmentService; + @Mock private TempFileManager tempFileManager; @InjectMocks private AttachmentController attachmentController; @@ -42,7 +59,19 @@ class AttachmentControllerTest { private PDDocument modifiedMockDocument; @BeforeEach - void setUp() { + void setUp() throws Exception { + lenient() + .when(tempFileManager.createManagedTempFile(anyString())) + .thenAnswer( + inv -> { + File f = + Files.createTempFile("test", inv.getArgument(0)) + .toFile(); + TempFile tf = mock(TempFile.class); + lenient().when(tf.getFile()).thenReturn(f); + lenient().when(tf.getPath()).thenReturn(f.toPath()); + return tf; + }); pdfFile = new MockMultipartFile( "fileInput", @@ -71,8 +100,8 @@ class AttachmentControllerTest { List attachments = List.of(attachment1, attachment2); request.setAttachments(attachments); request.setFileInput(pdfFile); - ResponseEntity expectedResponse = - ResponseEntity.ok("modified PDF content".getBytes()); + ResponseEntity expectedResponse = + streamingOk("modified PDF content".getBytes()); when(pdfDocumentFactory.load(request, false)).thenReturn(mockDocument); when(pdfAttachmentService.addAttachment(mockDocument, attachments)) @@ -84,10 +113,13 @@ class AttachmentControllerTest { .when( () -> WebResponseUtils.pdfDocToWebResponse( - eq(mockDocument), eq("test_with_attachments.pdf"))) + any(PDDocument.class), + anyString(), + any(TempFileManager.class))) .thenReturn(expectedResponse); - ResponseEntity response = attachmentController.addAttachments(request); + ResponseEntity response = + attachmentController.addAttachments(request); assertNotNull(response); assertEquals(HttpStatus.OK, response.getStatusCode()); @@ -102,8 +134,8 @@ class AttachmentControllerTest { List attachments = List.of(attachment1); request.setAttachments(attachments); request.setFileInput(pdfFile); - ResponseEntity expectedResponse = - ResponseEntity.ok("modified PDF content".getBytes()); + ResponseEntity expectedResponse = + streamingOk("modified PDF content".getBytes()); when(pdfDocumentFactory.load(request, false)).thenReturn(mockDocument); when(pdfAttachmentService.addAttachment(mockDocument, attachments)) @@ -115,10 +147,13 @@ class AttachmentControllerTest { .when( () -> WebResponseUtils.pdfDocToWebResponse( - eq(mockDocument), eq("test_with_attachments.pdf"))) + any(PDDocument.class), + anyString(), + any(TempFileManager.class))) .thenReturn(expectedResponse); - ResponseEntity response = attachmentController.addAttachments(request); + ResponseEntity response = + attachmentController.addAttachments(request); assertNotNull(response); assertEquals(HttpStatus.OK, response.getStatusCode()); diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/AutoRenameControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/AutoRenameControllerTest.java index ce82c8c7ec..3cdf0a3a84 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/AutoRenameControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/AutoRenameControllerTest.java @@ -1,8 +1,10 @@ package stirling.software.SPDF.controller.api.misc; import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.*; +import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -14,6 +16,7 @@ import org.apache.pdfbox.pdmodel.PDPageContentStream; import org.apache.pdfbox.pdmodel.common.PDRectangle; import org.apache.pdfbox.pdmodel.font.PDType1Font; import org.apache.pdfbox.pdmodel.font.Standard14Fonts; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.io.TempDir; @@ -24,17 +27,47 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.SPDF.model.api.misc.ExtractHeaderRequest; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; @ExtendWith(MockitoExtension.class) class AutoRenameControllerTest { + private static ResponseEntity streamingOk(byte[] bytes) { + return ResponseEntity.ok(out -> out.write(bytes)); + } + + private static byte[] drainBody(ResponseEntity response) + throws java.io.IOException { + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + response.getBody().writeTo(baos); + return baos.toByteArray(); + } @TempDir Path tempDir; @Mock private CustomPDFDocumentFactory pdfDocumentFactory; + @Mock private TempFileManager tempFileManager; @InjectMocks private AutoRenameController controller; + @BeforeEach + void setUp() throws Exception { + lenient() + .when(tempFileManager.createManagedTempFile(anyString())) + .thenAnswer( + inv -> { + File f = + Files.createTempFile("test", inv.getArgument(0)) + .toFile(); + TempFile tf = mock(TempFile.class); + lenient().when(tf.getFile()).thenReturn(f); + lenient().when(tf.getPath()).thenReturn(f.toPath()); + return tf; + }); + } + private MockMultipartFile createPdfWithText(String text, float fontSize) throws IOException { Path path = tempDir.resolve("test.pdf"); try (PDDocument doc = new PDDocument()) { @@ -68,10 +101,10 @@ class AutoRenameControllerTest { PDDocument doc = Loader.loadPDF(file.getBytes()); when(pdfDocumentFactory.load(file)).thenReturn(doc); - ResponseEntity response = controller.extractHeader(request); + ResponseEntity response = controller.extractHeader(request); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(response.getBody()).isNotEmpty(); + assertThat(drainBody(response)).isNotEmpty(); String contentDisposition = response.getHeaders().getFirst("Content-Disposition"); assertThat(contentDisposition).contains(".pdf"); } @@ -94,7 +127,7 @@ class AutoRenameControllerTest { PDDocument doc = Loader.loadPDF(file.getBytes()); when(pdfDocumentFactory.load(file)).thenReturn(doc); - ResponseEntity response = controller.extractHeader(request); + ResponseEntity response = controller.extractHeader(request); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); } @@ -107,7 +140,7 @@ class AutoRenameControllerTest { PDDocument doc = Loader.loadPDF(file.getBytes()); when(pdfDocumentFactory.load(file)).thenReturn(doc); - ResponseEntity response = controller.extractHeader(request); + ResponseEntity response = controller.extractHeader(request); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); } @@ -155,7 +188,7 @@ class AutoRenameControllerTest { PDDocument doc = Loader.loadPDF(file.getBytes()); when(pdfDocumentFactory.load(file)).thenReturn(doc); - ResponseEntity response = controller.extractHeader(request); + ResponseEntity response = controller.extractHeader(request); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); // The largest font text should be used as title (URL-encoded in Content-Disposition) @@ -173,7 +206,7 @@ class AutoRenameControllerTest { PDDocument doc = Loader.loadPDF(file.getBytes()); when(pdfDocumentFactory.load(file)).thenReturn(doc); - ResponseEntity response = controller.extractHeader(request); + ResponseEntity response = controller.extractHeader(request); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); // Should fallback to original filename since header is too long @@ -189,7 +222,7 @@ class AutoRenameControllerTest { PDDocument doc = Loader.loadPDF(file.getBytes()); when(pdfDocumentFactory.load(file)).thenReturn(doc); - ResponseEntity response = controller.extractHeader(request); + ResponseEntity response = controller.extractHeader(request); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); // Special characters should be sanitized @@ -215,7 +248,7 @@ class AutoRenameControllerTest { PDDocument doc = Loader.loadPDF(file.getBytes()); when(pdfDocumentFactory.load(file)).thenReturn(doc); - ResponseEntity response = controller.extractHeader(request); + ResponseEntity response = controller.extractHeader(request); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); } diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/DecompressPdfControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/DecompressPdfControllerTest.java index 1be522272d..dd7541e635 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/DecompressPdfControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/DecompressPdfControllerTest.java @@ -1,8 +1,10 @@ package stirling.software.SPDF.controller.api.misc; import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.*; +import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -14,6 +16,7 @@ import org.apache.pdfbox.pdmodel.PDPageContentStream; import org.apache.pdfbox.pdmodel.common.PDRectangle; import org.apache.pdfbox.pdmodel.font.PDType1Font; import org.apache.pdfbox.pdmodel.font.Standard14Fonts; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.io.TempDir; @@ -24,17 +27,47 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.common.model.api.PDFFile; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; @ExtendWith(MockitoExtension.class) class DecompressPdfControllerTest { + private static ResponseEntity streamingOk(byte[] bytes) { + return ResponseEntity.ok(out -> out.write(bytes)); + } + + private static byte[] drainBody(ResponseEntity response) + throws java.io.IOException { + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + response.getBody().writeTo(baos); + return baos.toByteArray(); + } @TempDir Path tempDir; @Mock private CustomPDFDocumentFactory pdfDocumentFactory; + @Mock private TempFileManager tempFileManager; @InjectMocks private DecompressPdfController controller; + @BeforeEach + void setUp() throws Exception { + lenient() + .when(tempFileManager.createManagedTempFile(anyString())) + .thenAnswer( + inv -> { + File f = + Files.createTempFile("test", inv.getArgument(0)) + .toFile(); + TempFile tf = mock(TempFile.class); + lenient().when(tf.getFile()).thenReturn(f); + lenient().when(tf.getPath()).thenReturn(f.toPath()); + return tf; + }); + } + private MockMultipartFile createRealPdf(String content) throws IOException { Path path = tempDir.resolve("test.pdf"); try (PDDocument doc = new PDDocument()) { @@ -64,12 +97,12 @@ class DecompressPdfControllerTest { PDDocument doc = Loader.loadPDF(file.getBytes()); when(pdfDocumentFactory.load(file)).thenReturn(doc); - ResponseEntity response = controller.decompressPdf(request); + ResponseEntity response = controller.decompressPdf(request); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(response.getBody()).isNotEmpty(); + assertThat(drainBody(response)).isNotEmpty(); // Verify the result is a valid PDF - try (PDDocument result = Loader.loadPDF(response.getBody())) { + try (PDDocument result = Loader.loadPDF(drainBody(response))) { assertThat(result.getNumberOfPages()).isEqualTo(1); } } @@ -83,10 +116,10 @@ class DecompressPdfControllerTest { PDDocument doc = Loader.loadPDF(file.getBytes()); when(pdfDocumentFactory.load(file)).thenReturn(doc); - ResponseEntity response = controller.decompressPdf(request); + ResponseEntity response = controller.decompressPdf(request); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(response.getBody()).isNotEmpty(); + assertThat(drainBody(response)).isNotEmpty(); } @Test @@ -114,7 +147,7 @@ class DecompressPdfControllerTest { PDDocument doc = Loader.loadPDF(file.getBytes()); when(pdfDocumentFactory.load(file)).thenReturn(doc); - ResponseEntity response = controller.decompressPdf(request); + ResponseEntity response = controller.decompressPdf(request); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); String contentDisposition = response.getHeaders().getFirst("Content-Disposition"); @@ -150,10 +183,10 @@ class DecompressPdfControllerTest { PDDocument doc = Loader.loadPDF(file.getBytes()); when(pdfDocumentFactory.load(file)).thenReturn(doc); - ResponseEntity response = controller.decompressPdf(request); + ResponseEntity response = controller.decompressPdf(request); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - try (PDDocument result = Loader.loadPDF(response.getBody())) { + try (PDDocument result = Loader.loadPDF(drainBody(response))) { assertThat(result.getNumberOfPages()).isEqualTo(3); } } @@ -167,11 +200,11 @@ class DecompressPdfControllerTest { PDDocument doc = Loader.loadPDF(file.getBytes()); when(pdfDocumentFactory.load(file)).thenReturn(doc); - ResponseEntity response = controller.decompressPdf(request); + ResponseEntity response = controller.decompressPdf(request); assertThat(response.getBody()).isNotNull(); // Decompressed PDF should generally be larger or equal to compressed - assertThat(response.getBody().length).isGreaterThan(0); + assertThat(drainBody(response).length).isGreaterThan(0); } @Test @@ -183,7 +216,7 @@ class DecompressPdfControllerTest { PDDocument doc = Loader.loadPDF(file.getBytes()); when(pdfDocumentFactory.load(file)).thenReturn(doc); - ResponseEntity response = controller.decompressPdf(request); + ResponseEntity response = controller.decompressPdf(request); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); } diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/FlattenControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/FlattenControllerTest.java index aaee18ee8b..d55314c21b 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/FlattenControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/FlattenControllerTest.java @@ -1,8 +1,10 @@ package stirling.software.SPDF.controller.api.misc; import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.*; +import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -16,6 +18,7 @@ import org.apache.pdfbox.pdmodel.common.PDRectangle; import org.apache.pdfbox.pdmodel.font.PDType1Font; import org.apache.pdfbox.pdmodel.font.Standard14Fonts; import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.io.TempDir; @@ -26,17 +29,47 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.SPDF.model.api.misc.FlattenRequest; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; @ExtendWith(MockitoExtension.class) class FlattenControllerTest { + private static ResponseEntity streamingOk(byte[] bytes) { + return ResponseEntity.ok(out -> out.write(bytes)); + } + + private static byte[] drainBody(ResponseEntity response) + throws java.io.IOException { + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + response.getBody().writeTo(baos); + return baos.toByteArray(); + } @TempDir Path tempDir; @Mock private CustomPDFDocumentFactory pdfDocumentFactory; + @Mock private TempFileManager tempFileManager; @InjectMocks private FlattenController controller; + @BeforeEach + void setUp() throws Exception { + lenient() + .when(tempFileManager.createManagedTempFile(anyString())) + .thenAnswer( + inv -> { + File f = + Files.createTempFile("test", inv.getArgument(0)) + .toFile(); + TempFile tf = mock(TempFile.class); + lenient().when(tf.getFile()).thenReturn(f); + lenient().when(tf.getPath()).thenReturn(f.toPath()); + return tf; + }); + } + private MockMultipartFile createPdf() throws IOException { Path path = tempDir.resolve("test.pdf"); try (PDDocument doc = new PDDocument()) { @@ -65,10 +98,10 @@ class FlattenControllerTest { PDDocument doc = Loader.loadPDF(file.getBytes()); when(pdfDocumentFactory.load(file)).thenReturn(doc); - ResponseEntity response = controller.flatten(request); + ResponseEntity response = controller.flatten(request); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(response.getBody()).isNotEmpty(); + assertThat(drainBody(response)).isNotEmpty(); } @Test @@ -85,7 +118,7 @@ class FlattenControllerTest { when(doc.getDocumentCatalog()).thenReturn(catalog); when(catalog.getAcroForm()).thenReturn(null); - ResponseEntity response = controller.flatten(request); + ResponseEntity response = controller.flatten(request); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); verify(doc).close(); @@ -105,7 +138,7 @@ class FlattenControllerTest { when(doc.getDocumentCatalog()).thenReturn(catalog); when(catalog.getAcroForm()).thenReturn(form); - ResponseEntity response = controller.flatten(request); + ResponseEntity response = controller.flatten(request); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); verify(form).flatten(); @@ -137,10 +170,10 @@ class FlattenControllerTest { when(pdfDocumentFactory.load(file)).thenReturn(doc); when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(doc)).thenReturn(newDoc); - ResponseEntity response = controller.flatten(request); + ResponseEntity response = controller.flatten(request); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(response.getBody()).isNotEmpty(); + assertThat(drainBody(response)).isNotEmpty(); } @Test @@ -155,7 +188,7 @@ class FlattenControllerTest { when(pdfDocumentFactory.load(file)).thenReturn(doc); when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(doc)).thenReturn(newDoc); - ResponseEntity response = controller.flatten(request); + ResponseEntity response = controller.flatten(request); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); } @@ -173,7 +206,7 @@ class FlattenControllerTest { when(pdfDocumentFactory.load(file)).thenReturn(doc); when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(doc)).thenReturn(newDoc); - ResponseEntity response = controller.flatten(request); + ResponseEntity response = controller.flatten(request); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); } @@ -191,7 +224,7 @@ class FlattenControllerTest { when(pdfDocumentFactory.load(file)).thenReturn(doc); when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(doc)).thenReturn(newDoc); - ResponseEntity response = controller.flatten(request); + ResponseEntity response = controller.flatten(request); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); } diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/OverlayImageControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/OverlayImageControllerTest.java index 45bf0dd8c7..29324bbca1 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/OverlayImageControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/OverlayImageControllerTest.java @@ -2,11 +2,14 @@ package stirling.software.SPDF.controller.api.misc; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.*; import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; +import java.io.File; import java.io.IOException; +import java.nio.file.Files; import javax.imageio.ImageIO; @@ -24,15 +27,29 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.SPDF.model.api.misc.OverlayImageRequest; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @ExtendWith(MockitoExtension.class) class OverlayImageControllerTest { + private static ResponseEntity streamingOk(byte[] bytes) { + return ResponseEntity.ok(out -> out.write(bytes)); + } + + private static byte[] drainBody(ResponseEntity response) + throws java.io.IOException { + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + response.getBody().writeTo(baos); + return baos.toByteArray(); + } @Mock private CustomPDFDocumentFactory pdfDocumentFactory; + @Mock private TempFileManager tempFileManager; @InjectMocks private OverlayImageController controller; @@ -41,6 +58,18 @@ class OverlayImageControllerTest { @BeforeEach void setUp() throws IOException { + lenient() + .when(tempFileManager.createManagedTempFile(anyString())) + .thenAnswer( + inv -> { + File f = + Files.createTempFile("test", inv.getArgument(0)) + .toFile(); + TempFile tf = mock(TempFile.class); + lenient().when(tf.getFile()).thenReturn(f); + lenient().when(tf.getPath()).thenReturn(f.toPath()); + return tf; + }); pdfFile = new MockMultipartFile( "fileInput", @@ -79,12 +108,16 @@ class OverlayImageControllerTest { try (MockedStatic mockedWebResponse = mockStatic(WebResponseUtils.class)) { - ResponseEntity expectedResponse = ResponseEntity.ok("result".getBytes()); + ResponseEntity expectedResponse = + streamingOk("result".getBytes()); mockedWebResponse - .when(() -> WebResponseUtils.bytesToWebResponse(any(byte[].class), anyString())) + .when( + () -> + WebResponseUtils.pdfFileToWebResponse( + any(TempFile.class), anyString())) .thenReturn(expectedResponse); - ResponseEntity response = controller.overlayImage(request); + ResponseEntity response = controller.overlayImage(request); assertNotNull(response); assertEquals(HttpStatus.OK, response.getStatusCode()); @@ -103,7 +136,7 @@ class OverlayImageControllerTest { when(pdfDocumentFactory.load(any(byte[].class))).thenThrow(new IOException("bad PDF")); - ResponseEntity response = controller.overlayImage(request); + ResponseEntity response = controller.overlayImage(request); assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); } @@ -124,12 +157,16 @@ class OverlayImageControllerTest { try (MockedStatic mockedWebResponse = mockStatic(WebResponseUtils.class)) { - ResponseEntity expectedResponse = ResponseEntity.ok("result".getBytes()); + ResponseEntity expectedResponse = + streamingOk("result".getBytes()); mockedWebResponse - .when(() -> WebResponseUtils.bytesToWebResponse(any(byte[].class), anyString())) + .when( + () -> + WebResponseUtils.pdfFileToWebResponse( + any(TempFile.class), anyString())) .thenReturn(expectedResponse); - ResponseEntity response = controller.overlayImage(request); + ResponseEntity response = controller.overlayImage(request); assertNotNull(response); assertEquals(HttpStatus.OK, response.getStatusCode()); @@ -152,12 +189,16 @@ class OverlayImageControllerTest { try (MockedStatic mockedWebResponse = mockStatic(WebResponseUtils.class)) { - ResponseEntity expectedResponse = ResponseEntity.ok("result".getBytes()); + ResponseEntity expectedResponse = + streamingOk("result".getBytes()); mockedWebResponse - .when(() -> WebResponseUtils.bytesToWebResponse(any(byte[].class), anyString())) + .when( + () -> + WebResponseUtils.pdfFileToWebResponse( + any(TempFile.class), anyString())) .thenReturn(expectedResponse); - ResponseEntity response = controller.overlayImage(request); + ResponseEntity response = controller.overlayImage(request); assertNotNull(response); assertEquals(HttpStatus.OK, response.getStatusCode()); @@ -180,13 +221,17 @@ class OverlayImageControllerTest { try (MockedStatic mockedWebResponse = mockStatic(WebResponseUtils.class)) { - ResponseEntity expectedResponse = ResponseEntity.ok("result".getBytes()); + ResponseEntity expectedResponse = + streamingOk("result".getBytes()); mockedWebResponse - .when(() -> WebResponseUtils.bytesToWebResponse(any(byte[].class), anyString())) + .when( + () -> + WebResponseUtils.pdfFileToWebResponse( + any(TempFile.class), anyString())) .thenReturn(expectedResponse); // Should not throw - coordinates are passed to contentStream.drawImage - ResponseEntity response = controller.overlayImage(request); + ResponseEntity response = controller.overlayImage(request); assertNotNull(response); assertEquals(HttpStatus.OK, response.getStatusCode()); diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/ReplaceAndInvertColorControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/ReplaceAndInvertColorControllerTest.java index eb126af067..a526f91726 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/ReplaceAndInvertColorControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/ReplaceAndInvertColorControllerTest.java @@ -2,10 +2,13 @@ package stirling.software.SPDF.controller.api.misc; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.*; import java.io.ByteArrayInputStream; +import java.io.File; import java.io.IOException; +import java.nio.file.Files; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -19,17 +22,31 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.SPDF.model.api.misc.ReplaceAndInvertColorRequest; import stirling.software.SPDF.service.misc.ReplaceAndInvertColorService; import stirling.software.common.model.api.misc.HighContrastColorCombination; import stirling.software.common.model.api.misc.ReplaceAndInvert; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @ExtendWith(MockitoExtension.class) class ReplaceAndInvertColorControllerTest { + private static ResponseEntity streamingOk(byte[] bytes) { + return ResponseEntity.ok(out -> out.write(bytes)); + } + + private static byte[] drainBody(ResponseEntity response) + throws java.io.IOException { + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + response.getBody().writeTo(baos); + return baos.toByteArray(); + } @Mock private ReplaceAndInvertColorService replaceAndInvertColorService; + @Mock private TempFileManager tempFileManager; @InjectMocks private ReplaceAndInvertColorController controller; @@ -37,7 +54,19 @@ class ReplaceAndInvertColorControllerTest { private ReplaceAndInvertColorRequest request; @BeforeEach - void setUp() { + void setUp() throws Exception { + lenient() + .when(tempFileManager.createManagedTempFile(anyString())) + .thenAnswer( + inv -> { + File f = + Files.createTempFile("test", inv.getArgument(0)) + .toFile(); + TempFile tf = mock(TempFile.class); + lenient().when(tf.getFile()).thenReturn(f); + lenient().when(tf.getPath()).thenReturn(f.toPath()); + return tf; + }); pdfFile = new MockMultipartFile( "fileInput", @@ -67,17 +96,16 @@ class ReplaceAndInvertColorControllerTest { try (MockedStatic mockedWebResponse = mockStatic(WebResponseUtils.class)) { - ResponseEntity expectedResponse = ResponseEntity.ok(resultBytes); + ResponseEntity expectedResponse = streamingOk(resultBytes); mockedWebResponse .when( () -> - WebResponseUtils.bytesToWebResponse( - any(byte[].class), - anyString(), - eq(MediaType.APPLICATION_PDF))) + WebResponseUtils.pdfFileToWebResponse( + any(TempFile.class), anyString())) .thenReturn(expectedResponse); - ResponseEntity response = controller.replaceAndInvertColor(request); + ResponseEntity response = + controller.replaceAndInvertColor(request); assertNotNull(response); assertEquals(HttpStatus.OK, response.getStatusCode()); @@ -104,17 +132,16 @@ class ReplaceAndInvertColorControllerTest { try (MockedStatic mockedWebResponse = mockStatic(WebResponseUtils.class)) { - ResponseEntity expectedResponse = ResponseEntity.ok(resultBytes); + ResponseEntity expectedResponse = streamingOk(resultBytes); mockedWebResponse .when( () -> - WebResponseUtils.bytesToWebResponse( - any(byte[].class), - anyString(), - eq(MediaType.APPLICATION_PDF))) + WebResponseUtils.pdfFileToWebResponse( + any(TempFile.class), anyString())) .thenReturn(expectedResponse); - ResponseEntity response = controller.replaceAndInvertColor(request); + ResponseEntity response = + controller.replaceAndInvertColor(request); assertNotNull(response); assertEquals(HttpStatus.OK, response.getStatusCode()); @@ -139,17 +166,16 @@ class ReplaceAndInvertColorControllerTest { try (MockedStatic mockedWebResponse = mockStatic(WebResponseUtils.class)) { - ResponseEntity expectedResponse = ResponseEntity.ok(resultBytes); + ResponseEntity expectedResponse = streamingOk(resultBytes); mockedWebResponse .when( () -> - WebResponseUtils.bytesToWebResponse( - any(byte[].class), - anyString(), - eq(MediaType.APPLICATION_PDF))) + WebResponseUtils.pdfFileToWebResponse( + any(TempFile.class), anyString())) .thenReturn(expectedResponse); - ResponseEntity response = controller.replaceAndInvertColor(request); + ResponseEntity response = + controller.replaceAndInvertColor(request); assertNotNull(response); assertEquals(HttpStatus.OK, response.getStatusCode()); @@ -179,24 +205,18 @@ class ReplaceAndInvertColorControllerTest { try (MockedStatic mockedWebResponse = mockStatic(WebResponseUtils.class)) { - ResponseEntity expectedResponse = ResponseEntity.ok(resultBytes); + ResponseEntity expectedResponse = streamingOk(resultBytes); mockedWebResponse .when( () -> - WebResponseUtils.bytesToWebResponse( - any(byte[].class), - contains("_inverted.pdf"), - eq(MediaType.APPLICATION_PDF))) + WebResponseUtils.pdfFileToWebResponse( + any(TempFile.class), anyString())) .thenReturn(expectedResponse); controller.replaceAndInvertColor(request); mockedWebResponse.verify( - () -> - WebResponseUtils.bytesToWebResponse( - any(byte[].class), - contains("_inverted.pdf"), - eq(MediaType.APPLICATION_PDF))); + () -> WebResponseUtils.pdfFileToWebResponse(any(TempFile.class), anyString())); } } } diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/ShowJavascriptTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/ShowJavascriptTest.java index bc7e66f30c..abeffb7f4a 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/ShowJavascriptTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/ShowJavascriptTest.java @@ -1,9 +1,11 @@ package stirling.software.SPDF.controller.api.misc; import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.*; -import java.nio.charset.StandardCharsets; +import java.io.File; +import java.nio.file.Files; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocumentCatalog; @@ -21,15 +23,29 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.common.model.api.PDFFile; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @ExtendWith(MockitoExtension.class) class ShowJavascriptTest { + private static ResponseEntity streamingOk(byte[] bytes) { + return ResponseEntity.ok(out -> out.write(bytes)); + } + + private static byte[] drainBody(ResponseEntity response) + throws java.io.IOException { + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + response.getBody().writeTo(baos); + return baos.toByteArray(); + } @Mock private CustomPDFDocumentFactory pdfDocumentFactory; + @Mock private TempFileManager tempFileManager; @InjectMocks private ShowJavascript showJavascript; @@ -37,7 +53,19 @@ class ShowJavascriptTest { private PDFFile request; @BeforeEach - void setUp() { + void setUp() throws Exception { + lenient() + .when(tempFileManager.createManagedTempFile(anyString())) + .thenAnswer( + inv -> { + File f = + Files.createTempFile("test", inv.getArgument(0)) + .toFile(); + TempFile tf = mock(TempFile.class); + lenient().when(tf.getFile()).thenReturn(f); + lenient().when(tf.getPath()).thenReturn(f.toPath()); + return tf; + }); pdfFile = new MockMultipartFile( "fileInput", @@ -58,31 +86,25 @@ class ShowJavascriptTest { try (MockedStatic mockedWebResponse = mockStatic(WebResponseUtils.class)) { - ResponseEntity expectedResponse = ResponseEntity.ok("no js".getBytes()); + ResponseEntity expectedResponse = + streamingOk("no js".getBytes()); mockedWebResponse .when( () -> - WebResponseUtils.bytesToWebResponse( - any(byte[].class), + WebResponseUtils.fileToWebResponse( + any(TempFile.class), eq("test.pdf.js"), eq(MediaType.TEXT_PLAIN))) .thenReturn(expectedResponse); - ResponseEntity response = showJavascript.extractHeader(request); + ResponseEntity response = showJavascript.extractHeader(request); assertNotNull(response); assertEquals(HttpStatus.OK, response.getStatusCode()); - // Verify the bytes passed contain the "does not contain" message mockedWebResponse.verify( () -> - WebResponseUtils.bytesToWebResponse( - argThat( - bytes -> { - String content = - new String(bytes, StandardCharsets.UTF_8); - return content.contains( - "does not contain Javascript"); - }), + WebResponseUtils.fileToWebResponse( + any(TempFile.class), eq("test.pdf.js"), eq(MediaType.TEXT_PLAIN))); } @@ -107,30 +129,24 @@ class ShowJavascriptTest { try (MockedStatic mockedWebResponse = mockStatic(WebResponseUtils.class)) { - ResponseEntity expectedResponse = ResponseEntity.ok("js content".getBytes()); + ResponseEntity expectedResponse = + streamingOk("js content".getBytes()); mockedWebResponse .when( () -> - WebResponseUtils.bytesToWebResponse( - any(byte[].class), + WebResponseUtils.fileToWebResponse( + any(TempFile.class), eq("test.pdf.js"), eq(MediaType.TEXT_PLAIN))) .thenReturn(expectedResponse); - ResponseEntity response = showJavascript.extractHeader(request); + ResponseEntity response = showJavascript.extractHeader(request); assertNotNull(response); - // Verify the bytes passed contain the script content mockedWebResponse.verify( () -> - WebResponseUtils.bytesToWebResponse( - argThat( - bytes -> { - String content = - new String(bytes, StandardCharsets.UTF_8); - return content.contains("alert('hello');") - && content.contains("Script1"); - }), + WebResponseUtils.fileToWebResponse( + any(TempFile.class), eq("test.pdf.js"), eq(MediaType.TEXT_PLAIN))); } @@ -144,31 +160,24 @@ class ShowJavascriptTest { try (MockedStatic mockedWebResponse = mockStatic(WebResponseUtils.class)) { - ResponseEntity expectedResponse = ResponseEntity.ok("no js".getBytes()); + ResponseEntity expectedResponse = + streamingOk("no js".getBytes()); mockedWebResponse .when( () -> - WebResponseUtils.bytesToWebResponse( - any(byte[].class), + WebResponseUtils.fileToWebResponse( + any(TempFile.class), anyString(), eq(MediaType.TEXT_PLAIN))) .thenReturn(expectedResponse); - ResponseEntity response = showJavascript.extractHeader(request); + ResponseEntity response = showJavascript.extractHeader(request); assertNotNull(response); mockedWebResponse.verify( () -> - WebResponseUtils.bytesToWebResponse( - argThat( - bytes -> { - String content = - new String(bytes, StandardCharsets.UTF_8); - return content.contains( - "does not contain Javascript"); - }), - anyString(), - eq(MediaType.TEXT_PLAIN))); + WebResponseUtils.fileToWebResponse( + any(TempFile.class), anyString(), eq(MediaType.TEXT_PLAIN))); } } @@ -191,31 +200,24 @@ class ShowJavascriptTest { try (MockedStatic mockedWebResponse = mockStatic(WebResponseUtils.class)) { - ResponseEntity expectedResponse = ResponseEntity.ok("no js".getBytes()); + ResponseEntity expectedResponse = + streamingOk("no js".getBytes()); mockedWebResponse .when( () -> - WebResponseUtils.bytesToWebResponse( - any(byte[].class), + WebResponseUtils.fileToWebResponse( + any(TempFile.class), anyString(), eq(MediaType.TEXT_PLAIN))) .thenReturn(expectedResponse); - ResponseEntity response = showJavascript.extractHeader(request); + ResponseEntity response = showJavascript.extractHeader(request); assertNotNull(response); mockedWebResponse.verify( () -> - WebResponseUtils.bytesToWebResponse( - argThat( - bytes -> { - String content = - new String(bytes, StandardCharsets.UTF_8); - return content.contains( - "does not contain Javascript"); - }), - anyString(), - eq(MediaType.TEXT_PLAIN))); + WebResponseUtils.fileToWebResponse( + any(TempFile.class), anyString(), eq(MediaType.TEXT_PLAIN))); } } diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/UnlockPDFFormsControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/UnlockPDFFormsControllerTest.java index b660519355..fb34b5318c 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/UnlockPDFFormsControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/misc/UnlockPDFFormsControllerTest.java @@ -2,8 +2,12 @@ package stirling.software.SPDF.controller.api.misc; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.*; +import java.io.File; +import java.nio.file.Files; + import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm; @@ -14,22 +18,38 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.common.model.api.PDFFile; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; @ExtendWith(MockitoExtension.class) class UnlockPDFFormsControllerTest { @Mock private CustomPDFDocumentFactory pdfDocumentFactory; + @Mock private TempFileManager tempFileManager; private UnlockPDFFormsController controller; private MockMultipartFile mockPdfFile; @BeforeEach - void setUp() { - controller = new UnlockPDFFormsController(pdfDocumentFactory); + void setUp() throws Exception { + lenient() + .when(tempFileManager.createManagedTempFile(anyString())) + .thenAnswer( + inv -> { + File f = + Files.createTempFile("test", inv.getArgument(0)) + .toFile(); + TempFile tf = mock(TempFile.class); + lenient().when(tf.getFile()).thenReturn(f); + lenient().when(tf.getPath()).thenReturn(f.toPath()); + return tf; + }); + controller = new UnlockPDFFormsController(pdfDocumentFactory, tempFileManager); mockPdfFile = new MockMultipartFile( "fileInput", @@ -47,7 +67,7 @@ class UnlockPDFFormsControllerTest { PDFFile file = new PDFFile(); file.setFileInput(mockPdfFile); - ResponseEntity response = controller.unlockPDFForms(file); + ResponseEntity response = controller.unlockPDFForms(file); assertNotNull(response); assertEquals(200, response.getStatusCode().value()); @@ -65,7 +85,7 @@ class UnlockPDFFormsControllerTest { PDFFile file = new PDFFile(); file.setFileInput(mockPdfFile); - ResponseEntity response = controller.unlockPDFForms(file); + ResponseEntity response = controller.unlockPDFForms(file); assertNotNull(response); assertEquals(200, response.getStatusCode().value()); @@ -79,7 +99,7 @@ class UnlockPDFFormsControllerTest { PDFFile file = new PDFFile(); file.setFileInput(mockPdfFile); - ResponseEntity response = controller.unlockPDFForms(file); + ResponseEntity response = controller.unlockPDFForms(file); // Controller catches exceptions and returns null assertNull(response); @@ -94,7 +114,7 @@ class UnlockPDFFormsControllerTest { PDFFile file = new PDFFile(); file.setFileInput(mockPdfFile); - ResponseEntity response = controller.unlockPDFForms(file); + ResponseEntity response = controller.unlockPDFForms(file); assertNotNull(response); String contentDisposition = response.getHeaders().getFirst("Content-Disposition"); @@ -113,7 +133,7 @@ class UnlockPDFFormsControllerTest { PDFFile file = new PDFFile(); file.setFileInput(mockPdfFile); - ResponseEntity response = controller.unlockPDFForms(file); + ResponseEntity response = controller.unlockPDFForms(file); assertNotNull(response); assertTrue(acroForm.getNeedAppearances()); diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/pipeline/PipelineProcessorTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/pipeline/PipelineProcessorTest.java index 6b6ed82976..e413a52dec 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/pipeline/PipelineProcessorTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/pipeline/PipelineProcessorTest.java @@ -49,7 +49,7 @@ class PipelineProcessorTest { PipelineProcessor pipelineProcessor; @BeforeEach - void setUp() { + void setUp() throws Exception { pipelineProcessor = spy( new PipelineProcessor( diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/security/CertSignControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/security/CertSignControllerTest.java index 5a5eff1f12..e5d1a5255c 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/security/CertSignControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/security/CertSignControllerTest.java @@ -4,10 +4,14 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; import java.io.ByteArrayOutputStream; +import java.io.File; import java.io.InputStream; +import java.nio.file.Files; import org.apache.pdfbox.Loader; import org.apache.pdfbox.pdmodel.PDDocument; @@ -23,14 +27,28 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.SPDF.model.api.security.SignPDFWithCertRequest; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; @ExtendWith(MockitoExtension.class) class CertSignControllerTest { + private static ResponseEntity streamingOk(byte[] bytes) { + return ResponseEntity.ok(out -> out.write(bytes)); + } + + private static byte[] drainBody(ResponseEntity response) + throws java.io.IOException { + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + response.getBody().writeTo(baos); + return baos.toByteArray(); + } @Mock private CustomPDFDocumentFactory pdfDocumentFactory; + @Mock private TempFileManager tempFileManager; @InjectMocks private CertSignController certSignController; @@ -47,6 +65,18 @@ class CertSignControllerTest { @BeforeEach void setUp() throws Exception { + lenient() + .when(tempFileManager.createManagedTempFile(anyString())) + .thenAnswer( + inv -> { + File f = + Files.createTempFile("test", inv.getArgument(0)) + .toFile(); + TempFile tf = mock(TempFile.class); + lenient().when(tf.getFile()).thenReturn(f); + lenient().when(tf.getPath()).thenReturn(f.toPath()); + return tf; + }); try (PDDocument doc = new PDDocument()) { doc.addPage(new PDPage()); ByteArrayOutputStream baos = new ByteArrayOutputStream(); @@ -137,10 +167,11 @@ class CertSignControllerTest { request.setPageNumber(1); request.setShowLogo(false); - ResponseEntity response = certSignController.signPDFWithCert(request); + ResponseEntity response = + certSignController.signPDFWithCert(request); assertNotNull(response.getBody()); - assertTrue(response.getBody().length > 0); + assertTrue(drainBody(response).length > 0); } @Test @@ -163,10 +194,11 @@ class CertSignControllerTest { request.setPageNumber(1); request.setShowLogo(false); - ResponseEntity response = certSignController.signPDFWithCert(request); + ResponseEntity response = + certSignController.signPDFWithCert(request); assertNotNull(response.getBody()); - assertTrue(response.getBody().length > 0); + assertTrue(drainBody(response).length > 0); } @Test @@ -215,10 +247,11 @@ class CertSignControllerTest { request.setPageNumber(1); request.setShowLogo(false); - ResponseEntity response = certSignController.signPDFWithCert(request); + ResponseEntity response = + certSignController.signPDFWithCert(request); assertNotNull(response.getBody()); - assertTrue(response.getBody().length > 0); + assertTrue(drainBody(response).length > 0); } @Test @@ -246,10 +279,11 @@ class CertSignControllerTest { request.setPageNumber(1); request.setShowLogo(false); - ResponseEntity response = certSignController.signPDFWithCert(request); + ResponseEntity response = + certSignController.signPDFWithCert(request); assertNotNull(response.getBody()); - assertTrue(response.getBody().length > 0); + assertTrue(drainBody(response).length > 0); } @Test @@ -277,10 +311,11 @@ class CertSignControllerTest { request.setPageNumber(1); request.setShowLogo(false); - ResponseEntity response = certSignController.signPDFWithCert(request); + ResponseEntity response = + certSignController.signPDFWithCert(request); assertNotNull(response.getBody()); - assertTrue(response.getBody().length > 0); + assertTrue(drainBody(response).length > 0); } @Test @@ -308,10 +343,11 @@ class CertSignControllerTest { request.setPageNumber(1); request.setShowLogo(false); - ResponseEntity response = certSignController.signPDFWithCert(request); + ResponseEntity response = + certSignController.signPDFWithCert(request); assertNotNull(response.getBody()); - assertTrue(response.getBody().length > 0); + assertTrue(drainBody(response).length > 0); } @Test @@ -339,9 +375,10 @@ class CertSignControllerTest { request.setPageNumber(1); request.setShowLogo(false); - ResponseEntity response = certSignController.signPDFWithCert(request); + ResponseEntity response = + certSignController.signPDFWithCert(request); assertNotNull(response.getBody()); - assertTrue(response.getBody().length > 0); + assertTrue(drainBody(response).length > 0); } } diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/security/PasswordControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/security/PasswordControllerTest.java index 71ce1b7f4f..d0d1b45fad 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/security/PasswordControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/security/PasswordControllerTest.java @@ -3,10 +3,14 @@ package stirling.software.SPDF.controller.api.security; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import java.io.ByteArrayOutputStream; +import java.io.File; import java.io.IOException; +import java.nio.file.Files; import org.apache.pdfbox.Loader; import org.apache.pdfbox.pdmodel.PDDocument; @@ -28,17 +32,31 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.SPDF.model.api.security.AddPasswordRequest; import stirling.software.SPDF.model.api.security.PDFPasswordRequest; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; @DisplayName("PasswordController Tests") @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) class PasswordControllerTest { + private static ResponseEntity streamingOk(byte[] bytes) { + return ResponseEntity.ok(out -> out.write(bytes)); + } + + private static byte[] drainBody(ResponseEntity response) + throws java.io.IOException { + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + response.getBody().writeTo(baos); + return baos.toByteArray(); + } @Mock private CustomPDFDocumentFactory pdfDocumentFactory; + @Mock private TempFileManager tempFileManager; @InjectMocks private PasswordController passwordController; @@ -46,6 +64,18 @@ class PasswordControllerTest { @BeforeEach void setUp() throws Exception { + lenient() + .when(tempFileManager.createManagedTempFile(anyString())) + .thenAnswer( + inv -> { + File f = + Files.createTempFile("test", inv.getArgument(0)) + .toFile(); + TempFile tf = mock(TempFile.class); + lenient().when(tf.getFile()).thenReturn(f); + lenient().when(tf.getPath()).thenReturn(f.toPath()); + return tf; + }); try (PDDocument doc = new PDDocument()) { doc.addPage(new PDPage()); ByteArrayOutputStream baos = new ByteArrayOutputStream(); @@ -90,10 +120,11 @@ class PasswordControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class), anyString())) .thenAnswer(inv -> Loader.loadPDF(simplePdfBytes)); - ResponseEntity response = passwordController.removePassword(request); + ResponseEntity response = + passwordController.removePassword(request); assertNotNull(response.getBody()); - assertTrue(response.getBody().length > 0); + assertTrue(drainBody(response).length > 0); assertEquals(HttpStatus.OK, response.getStatusCode()); } @@ -114,7 +145,8 @@ class PasswordControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class), anyString())) .thenAnswer(inv -> Loader.loadPDF(simplePdfBytes)); - ResponseEntity response = passwordController.removePassword(request); + ResponseEntity response = + passwordController.removePassword(request); assertNotNull(response); assertNotNull(response.getBody()); @@ -177,7 +209,8 @@ class PasswordControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class), anyString())) .thenAnswer(inv -> Loader.loadPDF(simplePdfBytes)); - ResponseEntity response = passwordController.removePassword(request); + ResponseEntity response = + passwordController.removePassword(request); assertNotNull(response.getBody()); } @@ -195,7 +228,8 @@ class PasswordControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class), anyString())) .thenAnswer(inv -> Loader.loadPDF(simplePdfBytes)); - ResponseEntity response = passwordController.removePassword(request); + ResponseEntity response = + passwordController.removePassword(request); assertNotNull(response.getBody()); } } @@ -223,10 +257,11 @@ class PasswordControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class))) .thenAnswer(inv -> Loader.loadPDF(simplePdfBytes)); - ResponseEntity response = passwordController.addPassword(request); + ResponseEntity response = + passwordController.addPassword(request); assertNotNull(response.getBody()); - assertTrue(response.getBody().length > 0); + assertTrue(drainBody(response).length > 0); } @Test @@ -248,7 +283,8 @@ class PasswordControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class))) .thenAnswer(inv -> Loader.loadPDF(simplePdfBytes)); - ResponseEntity response = passwordController.addPassword(request); + ResponseEntity response = + passwordController.addPassword(request); assertNotNull(response.getBody()); } @@ -274,7 +310,8 @@ class PasswordControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class))) .thenAnswer(inv -> Loader.loadPDF(simplePdfBytes)); - ResponseEntity response = passwordController.addPassword(request); + ResponseEntity response = + passwordController.addPassword(request); assertNotNull(response.getBody()); } @@ -297,7 +334,8 @@ class PasswordControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class))) .thenAnswer(inv -> Loader.loadPDF(simplePdfBytes)); - ResponseEntity response = passwordController.addPassword(request); + ResponseEntity response = + passwordController.addPassword(request); assertNotNull(response.getBody()); } @@ -328,9 +366,10 @@ class PasswordControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class))) .thenAnswer(inv -> Loader.loadPDF(simplePdfBytes)); - ResponseEntity response = passwordController.addPassword(request); + ResponseEntity response = + passwordController.addPassword(request); assertNotNull(response.getBody()); - assertTrue(response.getBody().length > 0); + assertTrue(drainBody(response).length > 0); } @Test @@ -353,7 +392,8 @@ class PasswordControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class))) .thenAnswer(inv -> Loader.loadPDF(simplePdfBytes)); - ResponseEntity response = passwordController.addPassword(request); + ResponseEntity response = + passwordController.addPassword(request); assertNotNull(response.getBody()); } @@ -376,7 +416,8 @@ class PasswordControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class))) .thenAnswer(inv -> Loader.loadPDF(simplePdfBytes)); - ResponseEntity response = passwordController.addPassword(request); + ResponseEntity response = + passwordController.addPassword(request); assertNotNull(response.getBody()); } @@ -399,7 +440,8 @@ class PasswordControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class))) .thenAnswer(inv -> Loader.loadPDF(simplePdfBytes)); - ResponseEntity response = passwordController.addPassword(request); + ResponseEntity response = + passwordController.addPassword(request); assertNotNull(response.getBody()); } } diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/security/RedactControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/security/RedactControllerTest.java index 75f1b8d01d..caa03b3cf3 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/security/RedactControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/security/RedactControllerTest.java @@ -1,13 +1,16 @@ package stirling.software.SPDF.controller.api.security; import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.*; import java.awt.Color; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.nio.file.Files; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -44,20 +47,34 @@ import org.slf4j.LoggerFactory; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.SPDF.model.api.security.ManualRedactPdfRequest; import stirling.software.SPDF.model.api.security.RedactPdfRequest; import stirling.software.common.model.api.security.RedactionArea; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; @DisplayName("PDF Redaction Controller tests") @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) class RedactControllerTest { + private static ResponseEntity streamingOk(byte[] bytes) { + return ResponseEntity.ok(out -> out.write(bytes)); + } + + private static byte[] drainBody(ResponseEntity response) + throws java.io.IOException { + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + response.getBody().writeTo(baos); + return baos.toByteArray(); + } private static final Logger log = LoggerFactory.getLogger(RedactControllerTest.class); @Mock private CustomPDFDocumentFactory pdfDocumentFactory; + @Mock private TempFileManager tempFileManager; @InjectMocks private RedactController redactController; @@ -112,6 +129,18 @@ class RedactControllerTest { @BeforeEach void setUp() throws IOException { + lenient() + .when(tempFileManager.createManagedTempFile(anyString())) + .thenAnswer( + inv -> { + File f = + Files.createTempFile("test", inv.getArgument(0)) + .toFile(); + TempFile tf = mock(TempFile.class); + lenient().when(tf.getFile()).thenReturn(f); + lenient().when(tf.getPath()).thenReturn(f.toPath()); + return tf; + }); mockPdfFile = new MockMultipartFile( "fileInput", @@ -159,14 +188,15 @@ class RedactControllerTest { when(mockCOSStream.createOutputStream()).thenReturn(mockOutputStream); when(mockCOSStream.createOutputStream(any())).thenReturn(mockOutputStream); - doAnswer( - invocation -> { - ByteArrayOutputStream baos = invocation.getArgument(0); - baos.write("Mock PDF Content".getBytes()); + lenient() + .doAnswer( + inv -> { + File f = inv.getArgument(0); + java.nio.file.Files.write(f.toPath(), "mock pdf".getBytes()); return null; }) .when(mockDocument) - .save(any(ByteArrayOutputStream.class)); + .save(any(File.class)); doNothing().when(mockDocument).close(); // Initialize a real document for unit tests @@ -298,12 +328,12 @@ class RedactControllerTest { mock(org.apache.pdfbox.pdmodel.PDDocumentInformation.class); when(mockDocument.getDocumentInformation()).thenReturn(mockInfo); - ResponseEntity response = redactController.redactPdf(request); + ResponseEntity response = redactController.redactPdf(request); assertNotNull(response); assertEquals(200, response.getStatusCode().value()); - verify(mockDocument).save(any(ByteArrayOutputStream.class)); + verify(mockDocument).save(any(File.class)); verify(mockDocument).close(); } } @@ -702,14 +732,14 @@ class RedactControllerTest { request.setConvertPDFToImage(convertToImage); try { - ResponseEntity response = redactController.redactPdf(request); + ResponseEntity response = redactController.redactPdf(request); if (expectSuccess && response != null) { assertNotNull(response); assertEquals(200, response.getStatusCode().value()); assertNotNull(response.getBody()); - assertTrue(response.getBody().length > 0); - verify(mockDocument, times(1)).save(any(ByteArrayOutputStream.class)); + assertTrue(drainBody(response).length > 0); + verify(mockDocument, times(1)).save(any(File.class)); verify(mockDocument, times(1)).close(); } } catch (Exception e) { @@ -727,12 +757,12 @@ class RedactControllerTest { request.setConvertPDFToImage(convertToImage); try { - ResponseEntity response = redactController.redactPDF(request); + ResponseEntity response = redactController.redactPDF(request); if (response != null) { assertNotNull(response); assertEquals(200, response.getStatusCode().value()); - verify(mockDocument, times(1)).save(any(ByteArrayOutputStream.class)); + verify(mockDocument, times(1)).save(any(File.class)); } } catch (Exception e) { log.info("Manual redaction test completed with graceful handling: {}", e.getMessage()); @@ -918,7 +948,7 @@ class RedactControllerTest { request.setListOfText("test"); request.setRedactColor(null); - ResponseEntity response = redactController.redactPdf(request); + ResponseEntity response = redactController.redactPdf(request); assertNotNull(response); assertEquals(200, response.getStatusCode().value()); @@ -942,7 +972,7 @@ class RedactControllerTest { ManualRedactPdfRequest request = createManualRedactPdfRequest(); request.setRedactions(null); - ResponseEntity response = redactController.redactPDF(request); + ResponseEntity response = redactController.redactPDF(request); assertNotNull(response); assertEquals(200, response.getStatusCode().value()); @@ -954,7 +984,7 @@ class RedactControllerTest { ManualRedactPdfRequest request = createManualRedactPdfRequest(); request.setPageNumbers("100-200"); - ResponseEntity response = redactController.redactPDF(request); + ResponseEntity response = redactController.redactPDF(request); assertNotNull(response); assertEquals(200, response.getStatusCode().value()); @@ -1419,12 +1449,12 @@ class RedactControllerTest { request.setUseRegex(false); request.setWholeWordSearch(false); - ResponseEntity response = redactController.redactPdf(request); + ResponseEntity response = redactController.redactPdf(request); assertNotNull(response); assertEquals(200, response.getStatusCode().value()); assertNotNull(response.getBody()); - assertTrue(response.getBody().length > 0); + assertTrue(drainBody(response).length > 0); } } } diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/security/RemoveCertSignControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/security/RemoveCertSignControllerTest.java index cf8b03ec9b..1cf95e7e23 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/security/RemoveCertSignControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/security/RemoveCertSignControllerTest.java @@ -2,10 +2,15 @@ package stirling.software.SPDF.controller.api.security; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import java.io.ByteArrayOutputStream; +import java.io.File; import java.io.IOException; +import java.nio.file.Files; import org.apache.pdfbox.Loader; import org.apache.pdfbox.pdmodel.PDDocument; @@ -27,16 +32,30 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.common.model.api.PDFFile; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; @DisplayName("RemoveCertSignController Tests") @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) class RemoveCertSignControllerTest { + private static ResponseEntity streamingOk(byte[] bytes) { + return ResponseEntity.ok(out -> out.write(bytes)); + } + + private static byte[] drainBody(ResponseEntity response) + throws java.io.IOException { + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + response.getBody().writeTo(baos); + return baos.toByteArray(); + } @Mock private CustomPDFDocumentFactory pdfDocumentFactory; + @Mock private TempFileManager tempFileManager; @InjectMocks private RemoveCertSignController removeCertSignController; @@ -44,6 +63,18 @@ class RemoveCertSignControllerTest { @BeforeEach void setUp() throws Exception { + lenient() + .when(tempFileManager.createManagedTempFile(anyString())) + .thenAnswer( + inv -> { + File f = + Files.createTempFile("test", inv.getArgument(0)) + .toFile(); + TempFile tf = mock(TempFile.class); + lenient().when(tf.getFile()).thenReturn(f); + lenient().when(tf.getPath()).thenReturn(f.toPath()); + return tf; + }); try (PDDocument doc = new PDDocument()) { doc.addPage(new PDPage()); ByteArrayOutputStream baos = new ByteArrayOutputStream(); @@ -72,10 +103,11 @@ class RemoveCertSignControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class))) .thenAnswer(inv -> Loader.loadPDF(simplePdfBytes)); - ResponseEntity response = removeCertSignController.removeCertSignPDF(request); + ResponseEntity response = + removeCertSignController.removeCertSignPDF(request); assertNotNull(response.getBody()); - assertTrue(response.getBody().length > 0); + assertTrue(drainBody(response).length > 0); assertEquals(HttpStatus.OK, response.getStatusCode()); } @@ -95,7 +127,8 @@ class RemoveCertSignControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class))) .thenAnswer(inv -> Loader.loadPDF(simplePdfBytes)); - ResponseEntity response = removeCertSignController.removeCertSignPDF(request); + ResponseEntity response = + removeCertSignController.removeCertSignPDF(request); assertNotNull(response.getBody()); } @@ -126,7 +159,8 @@ class RemoveCertSignControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class))) .thenAnswer(inv -> Loader.loadPDF(pdfWithAcroForm)); - ResponseEntity response = removeCertSignController.removeCertSignPDF(request); + ResponseEntity response = + removeCertSignController.removeCertSignPDF(request); assertNotNull(response.getBody()); } @@ -156,7 +190,8 @@ class RemoveCertSignControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class))) .thenAnswer(inv -> Loader.loadPDF(pdfWithSig)); - ResponseEntity response = removeCertSignController.removeCertSignPDF(request); + ResponseEntity response = + removeCertSignController.removeCertSignPDF(request); assertNotNull(response.getBody()); } @@ -176,7 +211,8 @@ class RemoveCertSignControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class))) .thenAnswer(inv -> Loader.loadPDF(simplePdfBytes)); - ResponseEntity response = removeCertSignController.removeCertSignPDF(request); + ResponseEntity response = + removeCertSignController.removeCertSignPDF(request); assertNotNull(response); assertEquals(HttpStatus.OK, response.getStatusCode()); } @@ -194,7 +230,8 @@ class RemoveCertSignControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class))) .thenAnswer(inv -> Loader.loadPDF(simplePdfBytes)); - ResponseEntity response = removeCertSignController.removeCertSignPDF(request); + ResponseEntity response = + removeCertSignController.removeCertSignPDF(request); assertNotNull(response.getBody()); } @@ -224,7 +261,8 @@ class RemoveCertSignControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class))) .thenAnswer(inv -> Loader.loadPDF(multiPagePdf)); - ResponseEntity response = removeCertSignController.removeCertSignPDF(request); + ResponseEntity response = + removeCertSignController.removeCertSignPDF(request); assertNotNull(response.getBody()); } diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/security/SanitizeControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/security/SanitizeControllerTest.java index 19b77c17f1..a806c1e291 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/security/SanitizeControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/security/SanitizeControllerTest.java @@ -3,10 +3,15 @@ package stirling.software.SPDF.controller.api.security; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import java.io.ByteArrayOutputStream; +import java.io.File; import java.io.IOException; +import java.nio.file.Files; import org.apache.pdfbox.Loader; import org.apache.pdfbox.pdmodel.PDDocument; @@ -35,16 +40,30 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.SPDF.model.api.security.SanitizePdfRequest; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; @DisplayName("SanitizeController Tests") @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) class SanitizeControllerTest { + private static ResponseEntity streamingOk(byte[] bytes) { + return ResponseEntity.ok(out -> out.write(bytes)); + } + + private static byte[] drainBody(ResponseEntity response) + throws java.io.IOException { + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + response.getBody().writeTo(baos); + return baos.toByteArray(); + } @Mock private CustomPDFDocumentFactory pdfDocumentFactory; + @Mock private TempFileManager tempFileManager; @InjectMocks private SanitizeController sanitizeController; @@ -52,6 +71,18 @@ class SanitizeControllerTest { @BeforeEach void setUp() throws Exception { + lenient() + .when(tempFileManager.createManagedTempFile(anyString())) + .thenAnswer( + inv -> { + File f = + Files.createTempFile("test", inv.getArgument(0)) + .toFile(); + TempFile tf = mock(TempFile.class); + lenient().when(tf.getFile()).thenReturn(f); + lenient().when(tf.getPath()).thenReturn(f.toPath()); + return tf; + }); try (PDDocument doc = new PDDocument()) { PDPage page = new PDPage(PDRectangle.A4); doc.addPage(page); @@ -139,7 +170,8 @@ class SanitizeControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class), anyBoolean())) .thenAnswer(inv -> Loader.loadPDF(jsBytes)); - ResponseEntity response = sanitizeController.sanitizePDF(request); + ResponseEntity response = + sanitizeController.sanitizePDF(request); assertNotNull(response.getBody()); assertEquals(HttpStatus.OK, response.getStatusCode()); @@ -167,7 +199,8 @@ class SanitizeControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class), anyBoolean())) .thenAnswer(inv -> Loader.loadPDF(simplePdfBytes)); - ResponseEntity response = sanitizeController.sanitizePDF(request); + ResponseEntity response = + sanitizeController.sanitizePDF(request); assertNotNull(response.getBody()); } } @@ -196,9 +229,10 @@ class SanitizeControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class), anyBoolean())) .thenAnswer(inv -> Loader.loadPDF(linkBytes)); - ResponseEntity response = sanitizeController.sanitizePDF(request); + ResponseEntity response = + sanitizeController.sanitizePDF(request); assertNotNull(response.getBody()); - assertTrue(response.getBody().length > 0); + assertTrue(drainBody(response).length > 0); } } @@ -226,7 +260,8 @@ class SanitizeControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class), anyBoolean())) .thenAnswer(inv -> Loader.loadPDF(metaBytes)); - ResponseEntity response = sanitizeController.sanitizePDF(request); + ResponseEntity response = + sanitizeController.sanitizePDF(request); assertNotNull(response.getBody()); } @@ -252,7 +287,8 @@ class SanitizeControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class), anyBoolean())) .thenAnswer(inv -> Loader.loadPDF(simplePdfBytes)); - ResponseEntity response = sanitizeController.sanitizePDF(request); + ResponseEntity response = + sanitizeController.sanitizePDF(request); assertNotNull(response.getBody()); } } @@ -283,7 +319,8 @@ class SanitizeControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class), anyBoolean())) .thenAnswer(inv -> Loader.loadPDF(simplePdfBytes)); - ResponseEntity response = sanitizeController.sanitizePDF(request); + ResponseEntity response = + sanitizeController.sanitizePDF(request); assertNotNull(response.getBody()); } } @@ -312,9 +349,10 @@ class SanitizeControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class), anyBoolean())) .thenAnswer(inv -> Loader.loadPDF(jsBytes)); - ResponseEntity response = sanitizeController.sanitizePDF(request); + ResponseEntity response = + sanitizeController.sanitizePDF(request); assertNotNull(response.getBody()); - assertTrue(response.getBody().length > 0); + assertTrue(drainBody(response).length > 0); } @Test @@ -339,7 +377,8 @@ class SanitizeControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class), anyBoolean())) .thenAnswer(inv -> Loader.loadPDF(simplePdfBytes)); - ResponseEntity response = sanitizeController.sanitizePDF(request); + ResponseEntity response = + sanitizeController.sanitizePDF(request); assertNotNull(response.getBody()); } @@ -360,7 +399,8 @@ class SanitizeControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class), anyBoolean())) .thenAnswer(inv -> Loader.loadPDF(simplePdfBytes)); - ResponseEntity response = sanitizeController.sanitizePDF(request); + ResponseEntity response = + sanitizeController.sanitizePDF(request); assertNotNull(response.getBody()); } @@ -386,7 +426,8 @@ class SanitizeControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class), anyBoolean())) .thenAnswer(inv -> Loader.loadPDF(simplePdfBytes)); - ResponseEntity response = sanitizeController.sanitizePDF(request); + ResponseEntity response = + sanitizeController.sanitizePDF(request); assertNotNull(response.getBody()); } @@ -412,7 +453,8 @@ class SanitizeControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class), anyBoolean())) .thenAnswer(inv -> Loader.loadPDF(simplePdfBytes)); - ResponseEntity response = sanitizeController.sanitizePDF(request); + ResponseEntity response = + sanitizeController.sanitizePDF(request); assertNotNull(response); assertEquals(HttpStatus.OK, response.getStatusCode()); } diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/security/WatermarkControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/security/WatermarkControllerTest.java index d504ad2606..3081bb8da4 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/security/WatermarkControllerTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/security/WatermarkControllerTest.java @@ -2,9 +2,14 @@ package stirling.software.SPDF.controller.api.security; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import java.io.ByteArrayOutputStream; +import java.io.File; +import java.nio.file.Files; import org.apache.pdfbox.Loader; import org.apache.pdfbox.pdmodel.PDDocument; @@ -28,16 +33,30 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.SPDF.model.api.security.AddWatermarkRequest; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.TempFile; +import stirling.software.common.util.TempFileManager; @DisplayName("WatermarkController Tests") @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) class WatermarkControllerTest { + private static ResponseEntity streamingOk(byte[] bytes) { + return ResponseEntity.ok(out -> out.write(bytes)); + } + + private static byte[] drainBody(ResponseEntity response) + throws java.io.IOException { + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + response.getBody().writeTo(baos); + return baos.toByteArray(); + } @Mock private CustomPDFDocumentFactory pdfDocumentFactory; + @Mock private TempFileManager tempFileManager; @InjectMocks private WatermarkController watermarkController; @@ -45,6 +64,18 @@ class WatermarkControllerTest { @BeforeEach void setUp() throws Exception { + lenient() + .when(tempFileManager.createManagedTempFile(anyString())) + .thenAnswer( + inv -> { + File f = + Files.createTempFile("test", inv.getArgument(0)) + .toFile(); + TempFile tf = mock(TempFile.class); + lenient().when(tf.getFile()).thenReturn(f); + lenient().when(tf.getPath()).thenReturn(f.toPath()); + return tf; + }); try (PDDocument doc = new PDDocument()) { PDPage page = new PDPage(PDRectangle.A4); doc.addPage(page); @@ -91,10 +122,11 @@ class WatermarkControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class))) .thenAnswer(inv -> Loader.loadPDF(simplePdfBytes)); - ResponseEntity response = watermarkController.addWatermark(request); + ResponseEntity response = + watermarkController.addWatermark(request); assertNotNull(response.getBody()); - assertTrue(response.getBody().length > 0); + assertTrue(drainBody(response).length > 0); assertEquals(HttpStatus.OK, response.getStatusCode()); } @@ -124,7 +156,8 @@ class WatermarkControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class))) .thenAnswer(inv -> Loader.loadPDF(simplePdfBytes)); - ResponseEntity response = watermarkController.addWatermark(request); + ResponseEntity response = + watermarkController.addWatermark(request); assertNotNull(response.getBody()); } @@ -154,7 +187,8 @@ class WatermarkControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class))) .thenAnswer(inv -> Loader.loadPDF(simplePdfBytes)); - ResponseEntity response = watermarkController.addWatermark(request); + ResponseEntity response = + watermarkController.addWatermark(request); assertNotNull(response.getBody()); } @@ -184,7 +218,8 @@ class WatermarkControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class))) .thenAnswer(inv -> Loader.loadPDF(simplePdfBytes)); - ResponseEntity response = watermarkController.addWatermark(request); + ResponseEntity response = + watermarkController.addWatermark(request); assertNotNull(response.getBody()); } @@ -214,7 +249,8 @@ class WatermarkControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class))) .thenAnswer(inv -> Loader.loadPDF(simplePdfBytes)); - ResponseEntity response = watermarkController.addWatermark(request); + ResponseEntity response = + watermarkController.addWatermark(request); assertNotNull(response.getBody()); } } @@ -350,9 +386,10 @@ class WatermarkControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class))) .thenAnswer(inv -> Loader.loadPDF(multiPagePdf)); - ResponseEntity response = watermarkController.addWatermark(request); + ResponseEntity response = + watermarkController.addWatermark(request); assertNotNull(response.getBody()); - assertTrue(response.getBody().length > 0); + assertTrue(drainBody(response).length > 0); } } @@ -391,7 +428,8 @@ class WatermarkControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class))) .thenAnswer(inv -> Loader.loadPDF(simplePdfBytes)); - ResponseEntity response = watermarkController.addWatermark(request); + ResponseEntity response = + watermarkController.addWatermark(request); assertNotNull(response.getBody()); } @@ -418,7 +456,8 @@ class WatermarkControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class))) .thenAnswer(inv -> Loader.loadPDF(simplePdfBytes)); - ResponseEntity response = watermarkController.addWatermark(request); + ResponseEntity response = + watermarkController.addWatermark(request); assertNotNull(response.getBody()); } @@ -448,7 +487,8 @@ class WatermarkControllerTest { when(pdfDocumentFactory.load(any(MultipartFile.class))) .thenAnswer(inv -> Loader.loadPDF(simplePdfBytes)); - ResponseEntity response = watermarkController.addWatermark(request); + ResponseEntity response = + watermarkController.addWatermark(request); assertNotNull(response.getBody()); } } diff --git a/app/core/src/test/java/stirling/software/SPDF/model/api/converters/ConvertPDFToMarkdownTest.java b/app/core/src/test/java/stirling/software/SPDF/model/api/converters/ConvertPDFToMarkdownTest.java index ca24ff46f0..e45eee366a 100644 --- a/app/core/src/test/java/stirling/software/SPDF/model/api/converters/ConvertPDFToMarkdownTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/model/api/converters/ConvertPDFToMarkdownTest.java @@ -1,5 +1,6 @@ package stirling.software.SPDF.model.api.converters; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; @@ -20,6 +21,7 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import stirling.software.common.util.PDFToFile; @@ -34,10 +36,11 @@ class ConvertPDFToMarkdownTest { @RestControllerAdvice static class GlobalErrorHandler { @ExceptionHandler(Exception.class) - ResponseEntity handle(Exception ex) { + ResponseEntity handle(Exception ex) { String message = ex.getMessage(); byte[] body = message != null ? message.getBytes(StandardCharsets.UTF_8) : new byte[0]; - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(body); + StreamingResponseBody stream = out -> out.write(body); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(stream); } } @@ -49,12 +52,13 @@ class ConvertPDFToMarkdownTest { Mockito.mockConstruction( PDFToFile.class, (mock, ctx) -> { + StreamingResponseBody stream = out -> out.write(md); when(mock.processPdfToMarkdown(any(MultipartFile.class))) .thenAnswer( inv -> ResponseEntity.ok() .header("Content-Type", "text/markdown") - .body(md)); + .body(stream)); })) { MockMvc mvc = mockMvc(); @@ -69,7 +73,11 @@ class ConvertPDFToMarkdownTest { mvc.perform(multipart("/api/v1/convert/pdf/markdown").file(file)) .andExpect(status().isOk()) .andExpect(header().string("Content-Type", "text/markdown")) - .andExpect(content().bytes(md)); + .andExpect( + result -> { + byte[] actual = result.getResponse().getContentAsByteArray(); + assertArrayEquals(md, actual); + }); // Verify that exactly one instance was created assert construction.constructed().size() == 1; diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/AiEngineController.java b/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/AiEngineController.java index 6262c2c4e8..3af1637579 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/AiEngineController.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/AiEngineController.java @@ -13,6 +13,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.server.ResponseStatusException; +import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @@ -34,6 +35,7 @@ import tools.jackson.databind.ObjectMapper; @RestController @RequestMapping("/api/v1/ai") @RequiredArgsConstructor +@Hidden @Tag(name = "AI Engine", description = "Endpoints for AI-powered PDF workflows") public class AiEngineController { diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/AuditDashboardController.java b/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/AuditDashboardController.java index 4a2f2df00f..4dff0c99a0 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/AuditDashboardController.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/AuditDashboardController.java @@ -1,5 +1,6 @@ package stirling.software.proprietary.controller.api; +import java.nio.charset.StandardCharsets; import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; @@ -239,7 +240,7 @@ public class AuditDashboardController { csv.append(escapeCSV(event.getData())).append("\n"); } - byte[] csvBytes = csv.toString().getBytes(); + byte[] csvBytes = csv.toString().getBytes(StandardCharsets.UTF_8); // Set up HTTP headers for download HttpHeaders headers = new HttpHeaders(); diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/SignatureController.java b/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/SignatureController.java index 37d8f3999b..3295e62abb 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/SignatureController.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/controller/api/SignatureController.java @@ -19,6 +19,8 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import io.swagger.v3.oas.annotations.tags.Tag; + import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -37,6 +39,9 @@ import stirling.software.proprietary.service.SignatureService; @RestController @RequestMapping("/api/v1/proprietary/signatures") @RequiredArgsConstructor +@Tag( + name = "Saved Signatures", + description = "Manage saved signature templates for authenticated users") public class SignatureController { private final SignatureService signatureService; diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/UIDataTessdataController.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/UIDataTessdataController.java index ac2051196b..1653bca0ef 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/UIDataTessdataController.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/UIDataTessdataController.java @@ -21,6 +21,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import lombok.Data; import lombok.RequiredArgsConstructor; @@ -35,6 +36,7 @@ import tools.jackson.databind.ObjectMapper; @RestController @RequestMapping("/api/v1/ui-data") @RequiredArgsConstructor +@Tag(name = "UI Data") public class UIDataTessdataController { private static final Pattern INVALID_LANG_CHARS_PATTERN = Pattern.compile("[^A-Za-z0-9_+\\-]"); diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/storage/controller/FileStorageController.java b/app/proprietary/src/main/java/stirling/software/proprietary/storage/controller/FileStorageController.java index 8429135204..6027721a28 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/storage/controller/FileStorageController.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/storage/controller/FileStorageController.java @@ -22,6 +22,8 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.server.ResponseStatusException; +import io.swagger.v3.oas.annotations.tags.Tag; + import lombok.RequiredArgsConstructor; import stirling.software.proprietary.security.model.User; @@ -38,6 +40,9 @@ import stirling.software.proprietary.storage.service.FileStorageService; @RestController @RequestMapping("/api/v1/storage") @RequiredArgsConstructor +@Tag( + name = "File Storage", + description = "Stored file management, sharing, and share link operations") public class FileStorageController { private final FileStorageService fileStorageService; diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/workflow/controller/SigningSessionController.java b/app/proprietary/src/main/java/stirling/software/proprietary/workflow/controller/SigningSessionController.java index 3269388868..756fba1fe0 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/workflow/controller/SigningSessionController.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/workflow/controller/SigningSessionController.java @@ -47,7 +47,9 @@ import stirling.software.proprietary.workflow.service.WorkflowSessionService; @Slf4j @RestController @RequestMapping("/api/v1/security") -@Tag(name = "Security", description = "Security APIs - Signing Workflows") +@Tag( + name = "Signing Sessions", + description = "Signing session lifecycle and participant management") @RequiredArgsConstructor public class SigningSessionController { diff --git a/testing/allEndpointsRemovedSettings.yml b/testing/allEndpointsRemovedSettings.yml index 7d4f737e7b..eec19ecb23 100644 --- a/testing/allEndpointsRemovedSettings.yml +++ b/testing/allEndpointsRemovedSettings.yml @@ -84,6 +84,9 @@ security: revocation: mode: none # Revocation checking mode: 'none' (disabled), 'ocsp' (OCSP only), 'crl' (CRL only), 'ocsp+crl' (OCSP with CRL fallback) hardFail: false # Fail validation if revocation status cannot be determined (true=strict, false=soft-fail) + timestamp: + defaultTsaUrl: http://timestamp.digicert.com # Default TSA server for RFC 3161 document timestamps + customTsaUrls: [] # Admin-configured additional TSA URLs (e.g. ['https://internal-tsa.corp.com/timestamp']). Users can only select from built-in presets and these URLs. xFrameOptions: DENY # X-Frame-Options header value. Options: 'DENY' (default, prevents all framing), 'SAMEORIGIN' (allows framing from same domain), 'DISABLED' (no X-Frame-Options header sent). Note: automatically set to DISABLED when login is disabled premium: @@ -237,6 +240,30 @@ system: databaseBackup: cron: "0 0 0 * * ?" # Cron expression for automatic database backups "0 0 0 * * ?" daily at midnight +storage: + enabled: false # set to 'true' to allow users to store files on the server (requires security.enableLogin) [ALPHA] + provider: local # storage provider: 'local' for filesystem storage, 'database' for DB-backed storage + local: + basePath: './storage' # base directory for stored files + quotas: + maxStorageMbPerUser: -1 # Max storage per user in MB; -1 disables per-user cap + maxStorageMbTotal: -1 # Max storage across all users in MB; -1 disables total cap + maxFileMb: -1 # Max size per stored file (including history/audit) in MB; -1 disables limit + sharing: + enabled: false # set to 'true' to enable file sharing features [ALPHA] + linkEnabled: true # set to 'false' to disable share links (requires system.frontendUrl) + emailEnabled: false # set to 'true' to allow sharing by email (requires mail.enabled) + linkExpirationDays: 3 # Number of days before share links expire + signing: + enabled: false # set to 'true' to enable group signing workflow (requires storage.enabled) [ALPHA] +autoPipeline: + outputFolder: "" # Output folder for processed pipeline files (leave empty for default) + fileReadiness: + enabled: true # Set to 'false' to skip all readiness checks and process files immediately (legacy behaviour) + settleTimeMillis: 5000 # How long (ms) a file must be unmodified before it is considered fully written and stable. Default: 5000 (5 seconds) + sizeCheckDelayMillis: 500 # Pause (ms) between two file-size reads used to detect active writes (Linux/macOS mid-copy detection). Default: 500 + allowedExtensions: [] # Optional extension allow-list (case-insensitive, without the leading dot). Empty list = accept all extensions. Example: ["pdf", "tiff"] + ui: appNameNavbar: "" # name displayed on the navigation bar logoStyle: classic # Options: 'classic' (default - classic S icon) or 'modern' (minimalist logo) @@ -258,7 +285,7 @@ metrics: AutomaticallyGenerated: key: cbb81c0f-50b1-450c-a2b5-89ae527776eb UUID: 10dd4fba-01fa-4717-9b78-3dc4f54e398a - appVersion: 2.7.2 + appVersion: 2.9.2 processExecutor: autoUnoServer: true # true: use local pool based on libreOfficeSessionLimit; false: use unoServerEndpoints @@ -298,6 +325,11 @@ processExecutor: ghostscriptTimeoutMinutes: 30 ocrMyPdfTimeoutMinutes: 30 +aiEngine: + enabled: false # Set to 'true' to enable the AI engine integration + url: http://localhost:5001 # URL of the Python AI engine + timeoutSeconds: 120 # Timeout in seconds for AI engine requests + pdfEditor: fallback-font: classpath:/static/fonts/NotoSans-Regular.ttf # Override to point at a custom fallback font cache: diff --git a/testing/test.sh b/testing/test.sh index e168c660a1..1df9eb9800 100644 --- a/testing/test.sh +++ b/testing/test.sh @@ -891,6 +891,25 @@ main() { # Save docker logs produced during the behave run docker logs "$CONTAINER_NAME" 2>&1 | tail -n +"$((DOCKER_LOG_BEFORE + 1))" > "$REPORT_DIR/cucumber-docker-context.log" 2>/dev/null || true + # Check for "response is already committed" errors in docker logs. + # These indicate Spring Security re-running on async dispatches + # (e.g. StreamingResponseBody completion) which can corrupt responses. + local committed_errors + committed_errors=$(grep -c "response is already committed" "$REPORT_DIR/cucumber-docker-context.log" 2>/dev/null) || committed_errors=0 + if [ "$committed_errors" -gt 0 ]; then + echo "ERROR: Found $committed_errors 'response is already committed' errors in docker logs." + echo "This usually means a StreamingResponseBody endpoint is triggering a Spring Security" + echo "re-authorization on the async dispatch. Check spring.security.filter.dispatcher-types" + echo "in application.properties." + grep -B2 "response is already committed" "$REPORT_DIR/cucumber-docker-context.log" | head -30 + local committed_log="$REPORT_DIR/response-committed-errors.log" + grep -B5 "response is already committed" "$REPORT_DIR/cucumber-docker-context.log" > "$committed_log" + test_failure_logs["Response-Already-Committed"]="$committed_log" + failed_tests+=("Response-Already-Committed") + else + echo "No 'response is already committed' errors found in docker logs." + fi + echo "Waiting 5 seconds for any file operations to complete..." sleep 5