fix: try-with-resources for Streams interacting with Files to ensure proper resource management (#4404)

# 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

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## 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 <bszucs1209@gmail.com>
This commit is contained in:
Balázs Szücs 2025-09-06 10:00:17 +02:00 committed by GitHub
parent 5e72dce0de
commit 47bce86ae2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 38 additions and 27 deletions

View File

@ -124,20 +124,21 @@ public class FileToPdf {
private static void zipDirectory(Path sourceDir, Path zipFilePath) throws IOException { private static void zipDirectory(Path sourceDir, Path zipFilePath) throws IOException {
try (ZipOutputStream zos = try (ZipOutputStream zos =
new ZipOutputStream(new FileOutputStream(zipFilePath.toFile()))) { new ZipOutputStream(new FileOutputStream(zipFilePath.toFile()))) {
Files.walk(sourceDir) try (Stream<Path> walk = Files.walk(sourceDir)) {
.filter(path -> !Files.isDirectory(path)) walk.filter(path -> !Files.isDirectory(path))
.forEach( .forEach(
path -> { path -> {
ZipEntry zipEntry = ZipEntry zipEntry =
new ZipEntry(sourceDir.relativize(path).toString()); new ZipEntry(sourceDir.relativize(path).toString());
try { try {
zos.putNextEntry(zipEntry); zos.putNextEntry(zipEntry);
Files.copy(path, zos); Files.copy(path, zos);
zos.closeEntry(); zos.closeEntry();
} catch (IOException e) { } catch (IOException e) {
throw new UncheckedIOException(e); throw new UncheckedIOException(e);
} }
}); });
}
} }
} }

View File

@ -9,6 +9,7 @@ import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.stream.Stream;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream; import java.util.zip.ZipOutputStream;
@ -150,10 +151,11 @@ public class ConvertImgPDFController {
.runCommandWithOutputHandling(command); .runCommandWithOutputHandling(command);
// Find all WebP files in the output directory // Find all WebP files in the output directory
List<Path> webpFiles = List<Path> webpFiles;
Files.walk(tempOutputDir) try (Stream<Path> walkStream = Files.walk(tempOutputDir)) {
.filter(path -> path.toString().endsWith(".webp")) webpFiles =
.toList(); walkStream.filter(path -> path.toString().endsWith(".webp")).toList();
}
if (webpFiles.isEmpty()) { if (webpFiles.isEmpty()) {
log.error("No WebP files were created in: {}", tempOutputDir.toString()); log.error("No WebP files were created in: {}", tempOutputDir.toString());

View File

@ -48,10 +48,12 @@ public class FilterController {
String text = request.getText(); String text = request.getText();
String pageNumber = request.getPageNumbers(); String pageNumber = request.getPageNumbers();
PDDocument pdfDocument = pdfDocumentFactory.load(inputFile); try (PDDocument pdfDocument = pdfDocumentFactory.load(inputFile)) {
if (PdfUtils.hasText(pdfDocument, pageNumber, text)) if (PdfUtils.hasText(pdfDocument, pageNumber, text)) {
return WebResponseUtils.pdfDocToWebResponse( return WebResponseUtils.pdfDocToWebResponse(
pdfDocument, Filenames.toSimpleFileName(inputFile.getOriginalFilename())); pdfDocument, Filenames.toSimpleFileName(inputFile.getOriginalFilename()));
}
}
return null; return null;
} }

View File

@ -8,6 +8,7 @@ import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.stream.Stream;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream; import java.util.zip.ZipOutputStream;
@ -142,7 +143,10 @@ public class ExtractImageScansController {
.runCommandWithOutputHandling(command); .runCommandWithOutputHandling(command);
// Read the output photos in temp directory // Read the output photos in temp directory
List<Path> tempOutputFiles = Files.list(tempDir).sorted().toList(); List<Path> tempOutputFiles;
try (Stream<Path> listStream = Files.list(tempDir)) {
tempOutputFiles = listStream.sorted().toList();
}
for (Path tempOutputFile : tempOutputFiles) { for (Path tempOutputFile : tempOutputFiles) {
byte[] imageBytes = Files.readAllBytes(tempOutputFile); byte[] imageBytes = Files.readAllBytes(tempOutputFile);
processedImageBytes.add(imageBytes); processedImageBytes.add(imageBytes);

View File

@ -7,6 +7,7 @@ import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.stream.Stream;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thymeleaf.util.StringUtils; import org.thymeleaf.util.StringUtils;
@ -66,10 +67,11 @@ public class SignatureService {
private List<SignatureFile> getSignaturesFromFolder(Path folder, String category) private List<SignatureFile> getSignaturesFromFolder(Path folder, String category)
throws IOException { throws IOException {
return Files.list(folder) try (Stream<Path> stream = Files.list(folder)) {
.filter(path -> isImageFile(path)) return stream.filter(this::isImageFile)
.map(path -> new SignatureFile(path.getFileName().toString(), category)) .map(path -> new SignatureFile(path.getFileName().toString(), category))
.toList(); .toList();
}
} }
public byte[] getSignatureBytes(String username, String fileName) throws IOException { public byte[] getSignatureBytes(String username, String fileName) throws IOException {