Skip to content

Commit 7002863

Browse files
committed
DeferredFileOutputStream now clears and deletes its temporary storage by
default. To get the old behavior back, call org.apache.commons.io.output.DeferredFileOutputStream.Builder.setDeleteTempFileOnClose(false) - Add DeferredFileOutputStream.Builder.setDeleteTempFileOnClose(boolean) - Add PathUtils.clear[IfExists](Path) - Add PathUtils.deleteIfExists(Path) - Update generic type of RandomAccessFileMode.accept(Path, IOConsumer) from IOConsumer<RandomAccessFile> to IOConsumer<IORandomAccessFile> - Harden tests for NPEs on failures, happy path unchanged.
1 parent c1184e0 commit 7002863

19 files changed

Lines changed: 428 additions & 112 deletions

src/main/java/org/apache/commons/io/IORandomAccessFile.java

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
import java.io.File;
2121
import java.io.FileNotFoundException;
22+
import java.io.IOException;
2223
import java.io.RandomAccessFile;
2324
import java.util.Objects;
2425

@@ -51,7 +52,7 @@ public IORandomAccessFile(final File file, final String mode) throws FileNotFoun
5152
/**
5253
* Constructs a new instance by calling {@link RandomAccessFile#RandomAccessFile(String, String)}.
5354
*
54-
* @param name the file object.
55+
* @param name the system-dependent file name.
5556
* @param mode the access mode, as described in {@link RandomAccessFile#RandomAccessFile(String, String)}.
5657
* @throws FileNotFoundException Thrown by {@link RandomAccessFile#RandomAccessFile(String, String)}.
5758
* @see RandomAccessFile#RandomAccessFile(String, String)
@@ -62,6 +63,26 @@ public IORandomAccessFile(final String name, final String mode) throws FileNotFo
6263
this.mode = mode;
6364
}
6465

66+
/**
67+
* Clears the contents of this existing file by filling it with NUL ({@code 0)} bytes.
68+
*
69+
* @return {@code this} instance.
70+
* @throws FileNotFoundException See {@link #IORandomAccessFile(File, String)}.
71+
* @throws IOException Thrown if an I/O error occurs.
72+
*/
73+
public IORandomAccessFile clear() throws IOException {
74+
final long length = length();
75+
final byte[] zeroBuffer = IOUtils.byteArray();
76+
long bytesWritten = 0;
77+
while (bytesWritten < length) {
78+
final long remaining = length - bytesWritten;
79+
final int toWrite = (int) Math.min(zeroBuffer.length, remaining);
80+
write(zeroBuffer, 0, toWrite);
81+
bytesWritten += toWrite;
82+
}
83+
return this;
84+
}
85+
6586
/**
6687
* Gets the file passed to {@link #IORandomAccessFile(File, String)}.
6788
*
@@ -81,7 +102,7 @@ public String getMode() {
81102
}
82103

83104
/**
84-
* Returns the pathname string of this abstract pathname. This is just the string returned by the {@link File#toString()} method.
105+
* Returns the path name string of this abstract pathname. This is just the string returned by the {@link File#toString()} method.
85106
*
86107
* @return The string form of the File's abstract pathname.
87108
* @see File#toString()
@@ -90,5 +111,4 @@ public String getMode() {
90111
public String toString() {
91112
return Objects.toString(file);
92113
}
93-
94114
}

src/main/java/org/apache/commons/io/RandomAccessFileMode.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -163,8 +163,8 @@ public static RandomAccessFileMode valueOfMode(final String mode) {
163163
* @throws IOException Thrown by the given function.
164164
* @since 2.18.0
165165
*/
166-
public void accept(final Path file, final IOConsumer<RandomAccessFile> consumer) throws IOException {
167-
try (RandomAccessFile raf = create(file)) {
166+
public void accept(final Path file, final IOConsumer<IORandomAccessFile> consumer) throws IOException {
167+
try (IORandomAccessFile raf = new IORandomAccessFile(file.toFile(), mode)) {
168168
consumer.accept(raf);
169169
}
170170
}
@@ -282,7 +282,7 @@ public boolean implies(final RandomAccessFileMode other) {
282282
/**
283283
* Constructs a random access file to read from, and optionally to write to, the file specified by the {@link File} argument.
284284
*
285-
* @param name the file object.
285+
* @param name the system-dependent file name.
286286
* @return a random access file.
287287
* @throws FileNotFoundException See {@link IORandomAccessFile#IORandomAccessFile(File, String)}.
288288
* @since 2.18.0

src/main/java/org/apache/commons/io/file/PathUtils.java

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
package org.apache.commons.io.file;
1919

2020
import java.io.File;
21+
import java.io.FileNotFoundException;
2122
import java.io.IOException;
2223
import java.io.InputStream;
2324
import java.io.OutputStream;
@@ -75,6 +76,7 @@
7576
import org.apache.commons.io.Charsets;
7677
import org.apache.commons.io.FileUtils;
7778
import org.apache.commons.io.FilenameUtils;
79+
import org.apache.commons.io.IORandomAccessFile;
7880
import org.apache.commons.io.IOUtils;
7981
import org.apache.commons.io.RandomAccessFileMode;
8082
import org.apache.commons.io.RandomAccessFiles;
@@ -310,6 +312,33 @@ public static PathCounters cleanDirectory(final Path directory, final DeleteOpti
310312
return visitFileTree(new CleaningPathVisitor(Counters.longPathCounters(), deleteOptions), directory).getPathCounters();
311313
}
312314

315+
/**
316+
* Clears the contents at an existing Path by filling it with NUL ({@code 0)} bytes.
317+
*
318+
* @param path The target path to an existing file.
319+
* @return The given path.
320+
* @throws FileNotFoundException See {@link IORandomAccessFile#IORandomAccessFile(File, String)}.
321+
* @throws IOException Thrown writing to the Path.
322+
* @since 2.23.0
323+
*/
324+
public static Path clear(final Path path) throws IOException {
325+
RandomAccessFileMode.READ_WRITE.accept(path, IORandomAccessFile::clear);
326+
return path;
327+
}
328+
329+
/**
330+
* Clears the contents at a Path by filling it with NUL ({@code 0)} bytes.
331+
*
332+
* @param path The target path, may not exist, may be null.
333+
* @return The given path.
334+
* @throws FileNotFoundException See {@link IORandomAccessFile#IORandomAccessFile(File, String)}.
335+
* @throws IOException Thrown writing to the Path.
336+
* @since 2.23.0
337+
*/
338+
public static Path clearIfExists(final Path path) throws IOException {
339+
return exists(path) ? clear(path) : path;
340+
}
341+
313342
/**
314343
* Compares the given {@link Path}'s last modified time to the given file time.
315344
*
@@ -698,6 +727,18 @@ public static PathCounters deleteFile(final Path file, final LinkOption[] linkOp
698727
return pathCounts;
699728
}
700729

730+
/**
731+
* A null-safe version of {@link Files#deleteIfExists(Path)}.
732+
*
733+
* @param path The path to the file to delete, may be {@code null}.
734+
* @return Same as {@link Files#deleteIfExists(Path)} if {@code path} is non-null, false otherwise.
735+
* @throws IOException Same as {@link Files#deleteIfExists(Path)}.
736+
* @since 2.23.0
737+
*/
738+
public static boolean deleteIfExists(final Path path) throws IOException {
739+
return path != null && Files.deleteIfExists(path);
740+
}
741+
701742
/**
702743
* Delegates to {@link File#deleteOnExit()}.
703744
*

src/main/java/org/apache/commons/io/output/DeferredFileOutputStream.java

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,13 @@
3030

3131
/**
3232
* An output stream which will retain data in memory until a specified threshold is reached, and only then commit it to disk. If the stream is closed before the
33-
* threshold is reached, the data will not be written to disk at all.
33+
* threshold is reached, the data will not be written to disk.
3434
* <p>
35-
* To build an instance, use {@link Builder}.
35+
* To build an instance, use the {@link Builder builder}.
3636
* </p>
3737
* <p>
38-
* The caller is responsible for deleting the output file ({@link #getFile()}, {@link #getPath()}) created by a DeferredFileOutputStream when the caller only
39-
* configured a prefix.
38+
* A temporary file created <em>only</em> when a prefix is configured (via {@link Builder#setPrefix(String)}) and is deleted automatically when {@link #close()}
39+
* is called.
4040
* </p>
4141
* <p>
4242
* The caller is responsible for deleting the output file passed to a constructor or builder through {@link Builder#setOutputFile(File)} or
@@ -82,6 +82,7 @@ public static class Builder extends AbstractStreamBuilder<DeferredFileOutputStre
8282
private String prefix;
8383
private String suffix;
8484
private Path directory;
85+
private boolean deleteTempFileOnClose = true;
8586

8687
/**
8788
* Constructs a new builder of {@link DeferredFileOutputStream}.
@@ -113,6 +114,24 @@ public DeferredFileOutputStream get() {
113114
return new DeferredFileOutputStream(this);
114115
}
115116

117+
/**
118+
* Sets whether to delete the temporary file when {@link DeferredFileOutputStream#close()} is called.
119+
* <p>
120+
* This only applies when a temporary file is created internally via a prefix/suffix configuration (i.e.,
121+
* {@link #setPrefix(String)} is used). It has no effect when an explicit output file is provided via
122+
* {@link #setOutputFile(File)} or {@link #setOutputFile(Path)}.
123+
* </p>
124+
*
125+
* @param deleteTempFileOnClose {@code true} to delete the temporary file on close (default), {@code false} to
126+
* keep it (the caller is then responsible for deleting it).
127+
* @return {@code this} instance.
128+
* @since 2.23.0
129+
*/
130+
public Builder setDeleteTempFileOnClose(final boolean deleteTempFileOnClose) {
131+
this.deleteTempFileOnClose = deleteTempFileOnClose;
132+
return this;
133+
}
134+
116135
/**
117136
* Sets the temporary file directory.
118137
*
@@ -254,17 +273,29 @@ private static Path toPath(final Path file, final Supplier<Path> defaultPathSupp
254273
*/
255274
private boolean closed;
256275

276+
/**
277+
* True when the output file was created internally as a temporary file by {@link #thresholdReached()}.
278+
* Such a file is deleted automatically when {@link #close()} is called (subject to {@link #deleteTempFileOnClose}).
279+
*/
280+
private boolean tempFile;
281+
282+
/**
283+
* Controls whether a temporary file created internally is deleted when {@link #close()} is called.
284+
*/
285+
private final boolean deleteTempFileOnClose;
286+
257287
/**
258288
* Constructs an instance of this class which will trigger an event at the specified threshold, and save data either to a file beyond that point.
259289
*
260-
* @param builder The construction data source.
290+
* @param builder The construction data source.
261291
*/
262292
private DeferredFileOutputStream(final Builder builder) {
263293
super(builder.threshold);
264294
this.outputPath = toPath(builder.outputFile, null);
265295
this.prefix = builder.prefix;
266296
this.suffix = builder.suffix;
267297
this.directory = toPath(builder.directory, PathUtils::getTempDirectory);
298+
this.deleteTempFileOnClose = builder.deleteTempFileOnClose;
268299
this.memoryOutputStream = new ByteArrayOutputStream(checkBufferSize(builder.getBufferSize()));
269300
this.currentOutputStream = memoryOutputStream;
270301
}
@@ -300,6 +331,7 @@ private DeferredFileOutputStream(final int threshold, final File outputFile, fin
300331
this.prefix = prefix;
301332
this.suffix = suffix;
302333
this.directory = toPath(directory, PathUtils::getTempDirectory);
334+
this.deleteTempFileOnClose = true;
303335
this.memoryOutputStream = new ByteArrayOutputStream(checkBufferSize(initialBufferSize));
304336
this.currentOutputStream = memoryOutputStream;
305337
}
@@ -351,14 +383,22 @@ public DeferredFileOutputStream(final int threshold, final String prefix, final
351383
}
352384

353385
/**
354-
* Closes underlying output stream, and mark this as closed
386+
* Closes underlying output stream, and marks this as closed. If the output was directed to a temporary file created
387+
* internally via a prefix/suffix configuration, and {@link Builder#setDeleteTempFileOnClose(boolean)} is
388+
* {@code true} (the default), that file is deleted.
355389
*
356390
* @throws IOException if an error occurs.
357391
*/
358392
@Override
359393
public void close() throws IOException {
360-
super.close();
361-
closed = true;
394+
try {
395+
super.close();
396+
} finally {
397+
closed = true;
398+
if (tempFile && deleteTempFileOnClose && outputPath != null) {
399+
PathUtils.deleteIfExists(PathUtils.clearIfExists(outputPath));
400+
}
401+
}
362402
}
363403

364404
/**
@@ -436,6 +476,7 @@ public boolean isInMemory() {
436476
protected void thresholdReached() throws IOException {
437477
if (prefix != null) {
438478
outputPath = Files.createTempFile(directory, prefix, suffix);
479+
tempFile = true;
439480
}
440481
PathUtils.createParentDirectories(outputPath, null, PathUtils.EMPTY_FILE_ATTRIBUTE_ARRAY);
441482
final OutputStream fos = Files.newOutputStream(outputPath);

src/test/java/org/apache/commons/io/FileUtilsTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2988,7 +2988,7 @@ void testSizeOfDirectory() throws Exception {
29882988
// Different result value depending on the Java version and operating system, but should not throw an exception or loop infinitely.
29892989
FileUtils.sizeOfDirectory(file);
29902990
} finally {
2991-
Files.deleteIfExists(linkPath);
2991+
PathUtils.deleteIfExists(linkPath);
29922992
}
29932993
}
29942994

@@ -3030,7 +3030,7 @@ void testSizeOfDirectoryAsBigInteger() throws Exception {
30303030
assertDelete(true, nonEmptyFile);
30313031
assertDelete(false, file);
30323032
} finally {
3033-
Files.deleteIfExists(linkPath);
3033+
PathUtils.deleteIfExists(linkPath);
30343034
}
30353035
}
30363036

src/test/java/org/apache/commons/io/FilenameUtilsTest.java

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ class FilenameUtilsTest {
5050
private static final boolean WINDOWS = File.separatorChar == '\\';
5151

5252
@TempDir
53-
public Path temporaryFolder;
53+
Path temporaryFolder;
5454

5555
private Path testFile1;
5656
private Path testFile2;
@@ -59,42 +59,33 @@ class FilenameUtilsTest {
5959
private int testFile2Size;
6060

6161
@BeforeEach
62-
public void setUp() throws Exception {
62+
void setUp() throws Exception {
6363
testFile1 = Files.createTempFile(temporaryFolder, "test", "1");
6464
testFile2 = Files.createTempFile(temporaryFolder, "test", "2");
65-
6665
testFile1Size = (int) Files.size(testFile1);
6766
testFile2Size = (int) Files.size(testFile2);
6867
if (!Files.exists(testFile1.getParent())) {
69-
throw new IOException("Cannot create file " + testFile1
70-
+ " as the parent directory does not exist");
68+
throw new IOException("Cannot create file " + testFile1 + " as the parent directory does not exist");
7169
}
72-
try (BufferedOutputStream output3 =
73-
new BufferedOutputStream(Files.newOutputStream(testFile1))) {
70+
try (BufferedOutputStream output3 = new BufferedOutputStream(Files.newOutputStream(testFile1))) {
7471
TestUtils.generateTestData(output3, testFile1Size);
7572
}
7673
if (!Files.exists(testFile2.getParent())) {
77-
throw new IOException("Cannot create file " + testFile2
78-
+ " as the parent directory does not exist");
74+
throw new IOException("Cannot create file " + testFile2 + " as the parent directory does not exist");
7975
}
80-
try (BufferedOutputStream output2 =
81-
new BufferedOutputStream(Files.newOutputStream(testFile2))) {
76+
try (BufferedOutputStream output2 = new BufferedOutputStream(Files.newOutputStream(testFile2))) {
8277
TestUtils.generateTestData(output2, testFile2Size);
8378
}
8479
if (!Files.exists(testFile1.getParent())) {
85-
throw new IOException("Cannot create file " + testFile1
86-
+ " as the parent directory does not exist");
80+
throw new IOException("Cannot create file " + testFile1 + " as the parent directory does not exist");
8781
}
88-
try (BufferedOutputStream output1 =
89-
new BufferedOutputStream(Files.newOutputStream(testFile1))) {
82+
try (BufferedOutputStream output1 = new BufferedOutputStream(Files.newOutputStream(testFile1))) {
9083
TestUtils.generateTestData(output1, testFile1Size);
9184
}
9285
if (!Files.exists(testFile2.getParent())) {
93-
throw new IOException("Cannot create file " + testFile2
94-
+ " as the parent directory does not exist");
86+
throw new IOException("Cannot create file " + testFile2 + " as the parent directory does not exist");
9587
}
96-
try (BufferedOutputStream output =
97-
new BufferedOutputStream(Files.newOutputStream(testFile2))) {
88+
try (BufferedOutputStream output = new BufferedOutputStream(Files.newOutputStream(testFile2))) {
9889
TestUtils.generateTestData(output, testFile2Size);
9990
}
10091
}

src/test/java/org/apache/commons/io/IORandomAccessFileTest.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,13 @@
1717

1818
package org.apache.commons.io;
1919

20+
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
2021
import static org.junit.jupiter.api.Assertions.assertEquals;
22+
import static org.junit.jupiter.api.Assertions.assertSame;
2123

2224
import java.io.File;
2325
import java.io.IOException;
26+
import java.nio.file.Files;
2427

2528
import org.apache.commons.io.build.AbstractOriginTest;
2629
import org.junit.jupiter.api.Test;
@@ -40,6 +43,32 @@ private File newFileFixture() throws IOException {
4043
return file;
4144
}
4245

46+
/**
47+
* Tests {@link IORandomAccessFile#clear()} by writing known non-zero bytes to the file, calling {@code clear()},
48+
* and verifying that every byte in the file is {@code 0}.
49+
*
50+
* @throws IOException Thrown on a test failure.
51+
*/
52+
@Test
53+
void testClear() throws IOException {
54+
final File file = newFileFixture();
55+
final byte[] originalData = { 1, 2, 3, 4, 5, 6, 7, 8 };
56+
// Write non-zero data into the file first
57+
Files.write(file.toPath(), originalData);
58+
try (IORandomAccessFile raf = new IORandomAccessFile(file, "rw")) {
59+
assertEquals(originalData.length, raf.length());
60+
// clear() should overwrite every byte with 0 and return 'this'
61+
@SuppressWarnings("resource")
62+
final IORandomAccessFile echoRaf = raf.clear();
63+
assertSame(raf, echoRaf);
64+
// Seek back to the start and read the contents
65+
raf.seek(0);
66+
final byte[] result = new byte[originalData.length];
67+
raf.readFully(result);
68+
assertArrayEquals(new byte[originalData.length], result, "All bytes should be 0 after clear()");
69+
}
70+
}
71+
4372
@ParameterizedTest
4473
@EnumSource(RandomAccessFileMode.class)
4574
void testFile(final RandomAccessFileMode mode) throws IOException {

0 commit comments

Comments
 (0)