From 47bce86ae280969a012fc359208d1dd3659039c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Sz=C3=BCcs?= <127139797+balazs-szucs@users.noreply.github.com> Date: Sat, 6 Sep 2025 10:00:17 +0200 Subject: [PATCH] fix: try-with-resources for Streams interacting with Files to ensure proper resource management (#4404) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Description of Changes The Javadoc recommends wrapping Files.list(), Files.walk(), Files.find(), and Files.lines() in try-with-resources so the stream’s close() is called as soon as the terminal operation completes. This is because when Stream interact with files, Java can ONLY close the Stream during garbage-collection finalization, which is not guaranteed to run promptly or at all before the JVM exits, creating a memory leak. Direct quote: > Streams have a [BaseStream.close()](https://docs.oracle.com/javase/8/docs/api/java/util/stream/BaseStream.html#close--) method and implement [AutoCloseable](https://docs.oracle.com/javase/8/docs/api/java/lang/AutoCloseable.html), but nearly all stream instances do not actually need to be closed after use. Generally, only streams whose source is an IO channel (such as those returned by [Files.lines(Path, Charset)](https://docs.oracle.com/javase/8/docs/api/java/nio/file/Files.html#lines-java.nio.file.Path-java.nio.charset.Charset-)) will require closing. Most streams are backed by collections, arrays, or generating functions, which require no special resource management. (If a stream does require closing, it can be declared as a resource in a try-with-resources statement.) > A DirectoryStream is opened upon creation and is closed by invoking the close method. Closing a directory stream releases any resources associated with the stream. Failure to close the stream may result in a resource leak. The try-with-resources statement provides a useful construct to ensure that the stream is closed: Sources: https://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/nio/file/DirectoryStream.html https://stackoverflow.com/questions/79078272/using-try-with-resources-for-a-java-files-walk-stream-created-in-a-separate-meth https://stackoverflow.com/questions/36990053/resource-leak-in-files-listpath-dir-when-stream-is-not-explicitly-closed --- ## Checklist ### General - [x] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [x] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [x] I have performed a self-review of my own code - [x] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [x] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --------- Signed-off-by: Balázs Szücs --- .../software/common/util/FileToPdf.java | 29 ++++++++++--------- .../converters/ConvertImgPDFController.java | 10 ++++--- .../api/filters/FilterController.java | 10 ++++--- .../api/misc/ExtractImageScansController.java | 6 +++- .../SPDF/service/SignatureService.java | 10 ++++--- 5 files changed, 38 insertions(+), 27 deletions(-) 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 799f91e05..9d208e971 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 @@ -124,20 +124,21 @@ public class FileToPdf { private static void zipDirectory(Path sourceDir, Path zipFilePath) throws IOException { try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFilePath.toFile()))) { - Files.walk(sourceDir) - .filter(path -> !Files.isDirectory(path)) - .forEach( - path -> { - ZipEntry zipEntry = - new ZipEntry(sourceDir.relativize(path).toString()); - try { - zos.putNextEntry(zipEntry); - Files.copy(path, zos); - zos.closeEntry(); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - }); + try (Stream walk = Files.walk(sourceDir)) { + walk.filter(path -> !Files.isDirectory(path)) + .forEach( + path -> { + ZipEntry zipEntry = + new ZipEntry(sourceDir.relativize(path).toString()); + try { + zos.putNextEntry(zipEntry); + Files.copy(path, zos); + zos.closeEntry(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); + } } } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertImgPDFController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertImgPDFController.java index be2e55df7..09a605156 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertImgPDFController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertImgPDFController.java @@ -9,6 +9,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; +import java.util.stream.Stream; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; @@ -150,10 +151,11 @@ public class ConvertImgPDFController { .runCommandWithOutputHandling(command); // Find all WebP files in the output directory - List webpFiles = - Files.walk(tempOutputDir) - .filter(path -> path.toString().endsWith(".webp")) - .toList(); + List webpFiles; + try (Stream walkStream = Files.walk(tempOutputDir)) { + webpFiles = + walkStream.filter(path -> path.toString().endsWith(".webp")).toList(); + } if (webpFiles.isEmpty()) { log.error("No WebP files were created in: {}", tempOutputDir.toString()); 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 cf60b3d95..7455ad1de 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 @@ -48,10 +48,12 @@ public class FilterController { String text = request.getText(); String pageNumber = request.getPageNumbers(); - PDDocument pdfDocument = pdfDocumentFactory.load(inputFile); - if (PdfUtils.hasText(pdfDocument, pageNumber, text)) - return WebResponseUtils.pdfDocToWebResponse( - pdfDocument, Filenames.toSimpleFileName(inputFile.getOriginalFilename())); + try (PDDocument pdfDocument = pdfDocumentFactory.load(inputFile)) { + if (PdfUtils.hasText(pdfDocument, pageNumber, text)) { + return WebResponseUtils.pdfDocToWebResponse( + pdfDocument, Filenames.toSimpleFileName(inputFile.getOriginalFilename())); + } + } return null; } 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 ba3bc5f39..bb4490163 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 @@ -8,6 +8,7 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.stream.Stream; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; @@ -142,7 +143,10 @@ public class ExtractImageScansController { .runCommandWithOutputHandling(command); // Read the output photos in temp directory - List tempOutputFiles = Files.list(tempDir).sorted().toList(); + List tempOutputFiles; + try (Stream listStream = Files.list(tempDir)) { + tempOutputFiles = listStream.sorted().toList(); + } for (Path tempOutputFile : tempOutputFiles) { byte[] imageBytes = Files.readAllBytes(tempOutputFile); processedImageBytes.add(imageBytes); diff --git a/app/core/src/main/java/stirling/software/SPDF/service/SignatureService.java b/app/core/src/main/java/stirling/software/SPDF/service/SignatureService.java index 1d25f409f..fd27439ed 100644 --- a/app/core/src/main/java/stirling/software/SPDF/service/SignatureService.java +++ b/app/core/src/main/java/stirling/software/SPDF/service/SignatureService.java @@ -7,6 +7,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; +import java.util.stream.Stream; import org.springframework.stereotype.Service; import org.thymeleaf.util.StringUtils; @@ -66,10 +67,11 @@ public class SignatureService { private List getSignaturesFromFolder(Path folder, String category) throws IOException { - return Files.list(folder) - .filter(path -> isImageFile(path)) - .map(path -> new SignatureFile(path.getFileName().toString(), category)) - .toList(); + try (Stream stream = Files.list(folder)) { + return stream.filter(this::isImageFile) + .map(path -> new SignatureFile(path.getFileName().toString(), category)) + .toList(); + } } public byte[] getSignatureBytes(String username, String fileName) throws IOException {