20
20
21
21
import java .io .Closeable ;
22
22
import java .io .IOException ;
23
+ import java .io .InputStream ;
24
+ import java .io .OutputStream ;
25
+ import java .nio .ByteBuffer ;
26
+ import java .nio .channels .FileChannel ;
23
27
import java .nio .file .Files ;
24
28
import java .nio .file .Path ;
25
29
import java .nio .file .StandardCopyOption ;
30
+ import java .nio .file .StandardOpenOption ;
26
31
import java .util .concurrent .ThreadLocalRandom ;
32
+ import java .util .concurrent .atomic .AtomicBoolean ;
27
33
28
34
import static java .util .Objects .requireNonNull ;
29
35
33
39
* @since 1.9.0
34
40
*/
35
41
public final class FileUtils {
42
+ // Logic borrowed from Commons-Lang3: we really need only this, to decide do we fsync on directories or not
43
+ private static final boolean IS_WINDOWS =
44
+ System .getProperty ("os.name" , "unknown" ).startsWith ("Windows" );
45
+
36
46
private FileUtils () {
37
47
// hide constructor
38
48
}
@@ -52,7 +62,10 @@ public interface TempFile extends Closeable {
52
62
*/
53
63
public interface CollocatedTempFile extends TempFile {
54
64
/**
55
- * Atomically moves temp file to target file it is collocated with.
65
+ * Upon close, atomically moves temp file to target file it is collocated with overwriting target (if exists).
66
+ * Invocation of this method merely signals that caller ultimately wants temp file to replace the target
67
+ * file, but when this method returns, the move operation did not yet happen, it will happen when this
68
+ * instance is closed.
56
69
*/
57
70
void move () throws IOException ;
58
71
}
@@ -98,23 +111,79 @@ public static CollocatedTempFile newTempFile(Path file) throws IOException {
98
111
Path tempFile = parent .resolve (file .getFileName () + "."
99
112
+ Long .toUnsignedString (ThreadLocalRandom .current ().nextLong ()) + ".tmp" );
100
113
return new CollocatedTempFile () {
114
+ private final AtomicBoolean wantsMove = new AtomicBoolean (false );
115
+
101
116
@ Override
102
117
public Path getPath () {
103
118
return tempFile ;
104
119
}
105
120
106
121
@ Override
107
- public void move () throws IOException {
108
- Files . move ( tempFile , file , StandardCopyOption . ATOMIC_MOVE );
122
+ public void move () {
123
+ wantsMove . set ( true );
109
124
}
110
125
111
126
@ Override
112
127
public void close () throws IOException {
128
+ if (wantsMove .get () && Files .isReadable (tempFile )) {
129
+ if (IS_WINDOWS ) {
130
+ copy (tempFile , file );
131
+ } else {
132
+ fsyncFile (tempFile );
133
+ Files .move (tempFile , file , StandardCopyOption .ATOMIC_MOVE );
134
+ fsyncParent (tempFile );
135
+ }
136
+ }
113
137
Files .deleteIfExists (tempFile );
114
138
}
115
139
};
116
140
}
117
141
142
+ /**
143
+ * On Windows we use pre-NIO2 way to copy files, as for some reason it works. Beat me why.
144
+ */
145
+ private static void copy (Path source , Path target ) throws IOException {
146
+ ByteBuffer buffer = ByteBuffer .allocate (1024 * 32 );
147
+ byte [] array = buffer .array ();
148
+ try (InputStream is = Files .newInputStream (source );
149
+ OutputStream os = Files .newOutputStream (target )) {
150
+ while (true ) {
151
+ int bytes = is .read (array );
152
+ if (bytes < 0 ) {
153
+ break ;
154
+ }
155
+ os .write (array , 0 , bytes );
156
+ }
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Performs fsync: makes sure no OS "dirty buffers" exist for given file.
162
+ *
163
+ * @param target Path that must not be {@code null}, must exist as plain file.
164
+ */
165
+ private static void fsyncFile (Path target ) throws IOException {
166
+ try (FileChannel file = FileChannel .open (target , StandardOpenOption .WRITE )) {
167
+ file .force (true );
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Performs directory fsync: not usable on Windows, but some other OSes may also throw, hence thrown IO exception
173
+ * is just ignored.
174
+ *
175
+ * @param target Path that must not be {@code null}, must exist as plain file, and must have parent.
176
+ */
177
+ private static void fsyncParent (Path target ) throws IOException {
178
+ try (FileChannel parent = FileChannel .open (target .getParent (), StandardOpenOption .READ )) {
179
+ try {
180
+ parent .force (true );
181
+ } catch (IOException e ) {
182
+ // ignore
183
+ }
184
+ }
185
+ }
186
+
118
187
/**
119
188
* A file writer, that accepts a {@link Path} to write some content to. Note: the file denoted by path may exist,
120
189
* hence implementation have to ensure it is able to achieve its goal ("replace existing" option or equivalent
0 commit comments