diff --git a/common/src/main/java/stirling/software/common/configuration/AppConfig.java b/common/src/main/java/stirling/software/common/configuration/AppConfig.java index b983769a8..f611f42ca 100644 --- a/common/src/main/java/stirling/software/common/configuration/AppConfig.java +++ b/common/src/main/java/stirling/software/common/configuration/AppConfig.java @@ -10,7 +10,6 @@ import java.util.Properties; import java.util.function.Predicate; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -151,9 +150,8 @@ public class AppConfig { @Bean(name = "activeSecurity") public boolean missingActiveSecurity() { return ClassUtils.isPresent( - "stirling.software.proprietary.security.configuration.SecurityConfiguration", - this.getClass().getClassLoader() - ); + "stirling.software.proprietary.security.configuration.SecurityConfiguration", + this.getClass().getClassLoader()); } @Bean(name = "directoryFilter") diff --git a/common/src/main/java/stirling/software/common/model/job/JobResult.java b/common/src/main/java/stirling/software/common/model/job/JobResult.java index bdc92ffdb..1aa66d1a8 100644 --- a/common/src/main/java/stirling/software/common/model/job/JobResult.java +++ b/common/src/main/java/stirling/software/common/model/job/JobResult.java @@ -59,7 +59,6 @@ public class JobResult { .build(); } - /** * Mark this job as complete with a general result * @@ -101,13 +100,15 @@ public class JobResult { * @param contentType The content type of the file * @param fileSize The size of the file in bytes */ - public void completeWithSingleFile(String fileId, String fileName, String contentType, long fileSize) { - ResultFile resultFile = ResultFile.builder() - .fileId(fileId) - .fileName(fileName) - .contentType(contentType) - .fileSize(fileSize) - .build(); + public void completeWithSingleFile( + String fileId, String fileName, String contentType, long fileSize) { + ResultFile resultFile = + ResultFile.builder() + .fileId(fileId) + .fileName(fileName) + .contentType(contentType) + .fileSize(fileSize) + .build(); completeWithFiles(List.of(resultFile)); } diff --git a/common/src/main/java/stirling/software/common/model/job/ResultFile.java b/common/src/main/java/stirling/software/common/model/job/ResultFile.java index 0354e2904..da51b1d6c 100644 --- a/common/src/main/java/stirling/software/common/model/job/ResultFile.java +++ b/common/src/main/java/stirling/software/common/model/job/ResultFile.java @@ -23,4 +23,4 @@ public class ResultFile { /** Size of the file in bytes */ private long fileSize; -} \ No newline at end of file +} diff --git a/common/src/main/java/stirling/software/common/service/FileStorage.java b/common/src/main/java/stirling/software/common/service/FileStorage.java index 52931b1bb..320b97865 100644 --- a/common/src/main/java/stirling/software/common/service/FileStorage.java +++ b/common/src/main/java/stirling/software/common/service/FileStorage.java @@ -140,11 +140,11 @@ public class FileStorage { */ public long getFileSize(String fileId) throws IOException { Path filePath = getFilePath(fileId); - + if (!Files.exists(filePath)) { throw new IOException("File not found with ID: " + fileId); } - + return Files.size(filePath); } @@ -153,7 +153,8 @@ public class FileStorage { * * @param fileId The ID of the file * @return The path to the file - * @throws IllegalArgumentException if fileId contains path traversal characters or resolves outside base directory + * @throws IllegalArgumentException if fileId contains path traversal characters or resolves + * outside base directory */ private Path getFilePath(String fileId) { // Validate fileId to prevent path traversal diff --git a/common/src/main/java/stirling/software/common/service/TaskManager.java b/common/src/main/java/stirling/software/common/service/TaskManager.java index 12d3d4b63..69eca5828 100644 --- a/common/src/main/java/stirling/software/common/service/TaskManager.java +++ b/common/src/main/java/stirling/software/common/service/TaskManager.java @@ -15,11 +15,10 @@ import java.util.concurrent.TimeUnit; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; -import org.springframework.http.MediaType; -import org.springframework.web.multipart.MultipartFile; - import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.MediaType; import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; import jakarta.annotation.PreDestroy; @@ -91,30 +90,36 @@ public class TaskManager { public void setFileResult( String jobId, String fileId, String originalFileName, String contentType) { JobResult jobResult = getOrCreateJobResult(jobId); - + // Check if this is a ZIP file that should be extracted if (isZipFile(contentType, originalFileName)) { try { - List extractedFiles = extractZipToIndividualFiles(fileId, originalFileName); + List extractedFiles = + extractZipToIndividualFiles(fileId, originalFileName); if (!extractedFiles.isEmpty()) { jobResult.completeWithFiles(extractedFiles); - log.debug("Set multiple file results for job ID: {} with {} files extracted from ZIP", - jobId, extractedFiles.size()); + log.debug( + "Set multiple file results for job ID: {} with {} files extracted from ZIP", + jobId, + extractedFiles.size()); return; } } catch (Exception e) { - log.warn("Failed to extract ZIP file for job {}: {}. Falling back to single file result.", - jobId, e.getMessage()); + log.warn( + "Failed to extract ZIP file for job {}: {}. Falling back to single file result.", + jobId, + e.getMessage()); } } - + // Handle as single file using new ResultFile approach try { long fileSize = fileStorage.getFileSize(fileId); jobResult.completeWithSingleFile(fileId, originalFileName, contentType, fileSize); log.debug("Set single file result for job ID: {} with file ID: {}", jobId, fileId); } catch (Exception e) { - log.warn("Failed to get file size for job {}: {}. Using size 0.", jobId, e.getMessage()); + log.warn( + "Failed to get file size for job {}: {}. Using size 0.", jobId, e.getMessage()); jobResult.completeWithSingleFile(fileId, originalFileName, contentType, 0); } } @@ -128,7 +133,10 @@ public class TaskManager { public void setMultipleFileResults(String jobId, List resultFiles) { JobResult jobResult = getOrCreateJobResult(jobId); jobResult.completeWithFiles(resultFiles); - log.debug("Set multiple file results for job ID: {} with {} files", jobId, resultFiles.size()); + log.debug( + "Set multiple file results for job ID: {} with {} files", + jobId, + resultFiles.size()); } /** @@ -329,32 +337,30 @@ public class TaskManager { } } - /** - * Check if a file is a ZIP file based on content type and filename - */ + /** Check if a file is a ZIP file based on content type and filename */ private boolean isZipFile(String contentType, String fileName) { - if (contentType != null && (contentType.equals("application/zip") || - contentType.equals("application/x-zip-compressed"))) { + if (contentType != null + && (contentType.equals("application/zip") + || contentType.equals("application/x-zip-compressed"))) { return true; } - + if (fileName != null && fileName.toLowerCase().endsWith(".zip")) { return true; } - + return false; } - /** - * Extract a ZIP file into individual files and store them - */ - private List extractZipToIndividualFiles(String zipFileId, String originalZipFileName) - throws IOException { + /** Extract a ZIP file into individual files and store them */ + private List extractZipToIndividualFiles( + String zipFileId, String originalZipFileName) throws IOException { List extractedFiles = new ArrayList<>(); - + MultipartFile zipFile = fileStorage.retrieveFile(zipFileId); - - try (ZipInputStream zipIn = new ZipInputStream(new ByteArrayInputStream(zipFile.getBytes()))) { + + try (ZipInputStream zipIn = + new ZipInputStream(new ByteArrayInputStream(zipFile.getBytes()))) { ZipEntry entry; while ((entry = zipIn.getNextEntry()) != null) { if (!entry.isDirectory()) { @@ -366,24 +372,28 @@ public class TaskManager { out.write(buffer, 0, bytesRead); } byte[] fileContent = out.toByteArray(); - + String contentType = determineContentType(entry.getName()); String individualFileId = fileStorage.storeBytes(fileContent, entry.getName()); - - ResultFile resultFile = ResultFile.builder() - .fileId(individualFileId) - .fileName(entry.getName()) - .contentType(contentType) - .fileSize(fileContent.length) - .build(); - + + ResultFile resultFile = + ResultFile.builder() + .fileId(individualFileId) + .fileName(entry.getName()) + .contentType(contentType) + .fileSize(fileContent.length) + .build(); + extractedFiles.add(resultFile); - log.debug("Extracted file: {} (size: {} bytes)", entry.getName(), fileContent.length); + log.debug( + "Extracted file: {} (size: {} bytes)", + entry.getName(), + fileContent.length); } zipIn.closeEntry(); } } - + // Clean up the original ZIP file after extraction try { fileStorage.deleteFile(zipFileId); @@ -391,18 +401,16 @@ public class TaskManager { } catch (Exception e) { log.warn("Failed to clean up original ZIP file {}: {}", zipFileId, e.getMessage()); } - + return extractedFiles; } - /** - * Determine content type based on file extension - */ + /** Determine content type based on file extension */ private String determineContentType(String fileName) { if (fileName == null) { return MediaType.APPLICATION_OCTET_STREAM_VALUE; } - + String lowerName = fileName.toLowerCase(); if (lowerName.endsWith(".pdf")) { return MediaType.APPLICATION_PDF_VALUE; @@ -421,9 +429,7 @@ public class TaskManager { } } - /** - * Clean up files associated with a job result - */ + /** Clean up files associated with a job result */ private void cleanupJobFiles(JobResult result, String jobId) { // Clean up all result files if (result.hasFiles()) { @@ -431,16 +437,17 @@ public class TaskManager { try { fileStorage.deleteFile(resultFile.getFileId()); } catch (Exception e) { - log.warn("Failed to delete file {} for job {}: {}", - resultFile.getFileId(), jobId, e.getMessage()); + log.warn( + "Failed to delete file {} for job {}: {}", + resultFile.getFileId(), + jobId, + e.getMessage()); } } } } - /** - * Find the ResultFile metadata for a given file ID by searching through all job results - */ + /** Find the ResultFile metadata for a given file ID by searching through all job results */ public ResultFile findResultFileByFileId(String fileId) { for (JobResult jobResult : jobResults.values()) { if (jobResult.hasFiles()) { diff --git a/common/src/test/java/stirling/software/common/service/TaskManagerTest.java b/common/src/test/java/stirling/software/common/service/TaskManagerTest.java index 4bed01143..b2cb26dd8 100644 --- a/common/src/test/java/stirling/software/common/service/TaskManagerTest.java +++ b/common/src/test/java/stirling/software/common/service/TaskManagerTest.java @@ -18,6 +18,7 @@ import org.springframework.test.util.ReflectionTestUtils; import stirling.software.common.model.job.JobResult; import stirling.software.common.model.job.JobStats; +import stirling.software.common.model.job.ResultFile; class TaskManagerTest { @@ -73,13 +74,17 @@ class TaskManagerTest { } @Test - void testSetFileResult() { + void testSetFileResult() throws Exception { // Arrange String jobId = UUID.randomUUID().toString(); taskManager.createTask(jobId); String fileId = "file-id"; String originalFileName = "test.pdf"; String contentType = "application/pdf"; + long fileSize = 1024L; + + // Mock the fileStorage.getFileSize() call + when(fileStorage.getFileSize(fileId)).thenReturn(fileSize); // Act taskManager.setFileResult(jobId, fileId, originalFileName, contentType); @@ -88,9 +93,17 @@ class TaskManagerTest { JobResult result = taskManager.getJobResult(jobId); assertNotNull(result); assertTrue(result.isComplete()); - assertEquals(fileId, result.getFileId()); - assertEquals(originalFileName, result.getOriginalFileName()); - assertEquals(contentType, result.getContentType()); + assertTrue(result.hasFiles()); + assertFalse(result.hasMultipleFiles()); + + var resultFiles = result.getAllResultFiles(); + assertEquals(1, resultFiles.size()); + + ResultFile resultFile = resultFiles.get(0); + assertEquals(fileId, resultFile.getFileId()); + assertEquals(originalFileName, resultFile.getFileName()); + assertEquals(contentType, resultFile.getContentType()); + assertEquals(fileSize, resultFile.getFileSize()); assertNotNull(result.getCompletedAt()); } @@ -163,8 +176,11 @@ class TaskManagerTest { } @Test - void testGetJobStats() { + void testGetJobStats() throws Exception { // Arrange + // Mock fileStorage.getFileSize for file operations + when(fileStorage.getFileSize("file-id")).thenReturn(1024L); + // 1. Create active job String activeJobId = "active-job"; taskManager.createTask(activeJobId); @@ -216,9 +232,15 @@ class TaskManagerTest { LocalDateTime oldTime = LocalDateTime.now().minusHours(1); ReflectionTestUtils.setField(oldJob, "completedAt", oldTime); ReflectionTestUtils.setField(oldJob, "complete", true); - ReflectionTestUtils.setField(oldJob, "fileId", "file-id"); - ReflectionTestUtils.setField(oldJob, "originalFileName", "test.pdf"); - ReflectionTestUtils.setField(oldJob, "contentType", "application/pdf"); + + // Create a ResultFile and set it using the new approach + ResultFile resultFile = ResultFile.builder() + .fileId("file-id") + .fileName("test.pdf") + .contentType("application/pdf") + .fileSize(1024L) + .build(); + ReflectionTestUtils.setField(oldJob, "resultFiles", java.util.List.of(resultFile)); when(fileStorage.deleteFile("file-id")).thenReturn(true); diff --git a/stirling-pdf/src/main/java/stirling/software/common/controller/JobController.java b/stirling-pdf/src/main/java/stirling/software/common/controller/JobController.java index f5fef96a3..44b15265b 100644 --- a/stirling-pdf/src/main/java/stirling/software/common/controller/JobController.java +++ b/stirling-pdf/src/main/java/stirling/software/common/controller/JobController.java @@ -87,11 +87,14 @@ public class JobController { if (result.hasMultipleFiles()) { return ResponseEntity.ok() .contentType(MediaType.APPLICATION_JSON) - .body(Map.of( - "jobId", jobId, - "hasMultipleFiles", true, - "files", result.getAllResultFiles() - )); + .body( + Map.of( + "jobId", + jobId, + "hasMultipleFiles", + true, + "files", + result.getAllResultFiles())); } // Handle single file (download directly) @@ -102,7 +105,9 @@ public class JobController { byte[] fileContent = fileStorage.retrieveBytes(singleFile.getFileId()); return ResponseEntity.ok() .header("Content-Type", singleFile.getContentType()) - .header("Content-Disposition", createContentDispositionHeader(singleFile.getFileName())) + .header( + "Content-Disposition", + createContentDispositionHeader(singleFile.getFileName())) .body(fileContent); } catch (Exception e) { log.error("Error retrieving file for job {}: {}", jobId, e.getMessage(), e); @@ -208,11 +213,11 @@ public class JobController { } List files = result.getAllResultFiles(); - return ResponseEntity.ok(Map.of( - "jobId", jobId, - "fileCount", files.size(), - "files", files - )); + return ResponseEntity.ok( + Map.of( + "jobId", jobId, + "fileCount", files.size(), + "files", files)); } /** @@ -231,18 +236,22 @@ public class JobController { // Find the file metadata from any job that contains this file ResultFile resultFile = taskManager.findResultFileByFileId(fileId); - + if (resultFile != null) { return ResponseEntity.ok(resultFile); } else { // File exists but no metadata found, get basic info efficiently long fileSize = fileStorage.getFileSize(fileId); - return ResponseEntity.ok(Map.of( - "fileId", fileId, - "fileName", "unknown", - "contentType", "application/octet-stream", - "fileSize", fileSize - )); + return ResponseEntity.ok( + Map.of( + "fileId", + fileId, + "fileName", + "unknown", + "contentType", + "application/octet-stream", + "fileSize", + fileSize)); } } catch (Exception e) { log.error("Error retrieving file metadata {}: {}", fileId, e.getMessage(), e); @@ -267,14 +276,15 @@ public class JobController { // Retrieve file content byte[] fileContent = fileStorage.retrieveBytes(fileId); - + // Find the file metadata from any job that contains this file // This is for getting the original filename and content type ResultFile resultFile = taskManager.findResultFileByFileId(fileId); - + String fileName = resultFile != null ? resultFile.getFileName() : "download"; - String contentType = resultFile != null ? resultFile.getContentType() : "application/octet-stream"; - + String contentType = + resultFile != null ? resultFile.getContentType() : "application/octet-stream"; + return ResponseEntity.ok() .header("Content-Type", contentType) .header("Content-Disposition", createContentDispositionHeader(fileName)) @@ -286,7 +296,6 @@ public class JobController { } } - /** * Create Content-Disposition header with UTF-8 filename support * @@ -295,8 +304,9 @@ public class JobController { */ private String createContentDispositionHeader(String fileName) { try { - String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8) - .replace("+", "%20"); // URLEncoder uses + for spaces, but we want %20 + String encodedFileName = + URLEncoder.encode(fileName, StandardCharsets.UTF_8) + .replace("+", "%20"); // URLEncoder uses + for spaces, but we want %20 return "attachment; filename=\"" + fileName + "\"; filename*=UTF-8''" + encodedFileName; } catch (Exception e) { // Fallback to basic filename if encoding fails diff --git a/stirling-pdf/src/test/java/stirling/software/common/controller/JobControllerTest.java b/stirling-pdf/src/test/java/stirling/software/common/controller/JobControllerTest.java index c183113d5..3ec041eb5 100644 --- a/stirling-pdf/src/test/java/stirling/software/common/controller/JobControllerTest.java +++ b/stirling-pdf/src/test/java/stirling/software/common/controller/JobControllerTest.java @@ -19,6 +19,7 @@ import jakarta.servlet.http.HttpSession; import stirling.software.common.model.job.JobResult; import stirling.software.common.model.job.JobStats; +import stirling.software.common.model.job.ResultFile; import stirling.software.common.service.FileStorage; import stirling.software.common.service.JobQueue; import stirling.software.common.service.TaskManager; @@ -138,7 +139,7 @@ class JobControllerTest { JobResult mockResult = new JobResult(); mockResult.setJobId(jobId); - mockResult.completeWithFile(fileId, originalFileName, contentType); + mockResult.completeWithSingleFile(fileId, originalFileName, contentType, fileContent.length); when(taskManager.getJobResult(jobId)).thenReturn(mockResult); when(fileStorage.retrieveBytes(fileId)).thenReturn(fileContent); @@ -215,7 +216,7 @@ class JobControllerTest { JobResult mockResult = new JobResult(); mockResult.setJobId(jobId); - mockResult.completeWithFile(fileId, originalFileName, contentType); + mockResult.completeWithSingleFile(fileId, originalFileName, contentType, 1024L); when(taskManager.getJobResult(jobId)).thenReturn(mockResult); when(fileStorage.retrieveBytes(fileId)).thenThrow(new RuntimeException("File not found"));