This is an automated email from the ASF dual-hosted git repository.

cstamas pushed a commit to branch maven-resolver-1.9.x
in repository https://gitbox.apache.org/repos/asf/maven-resolver.git


The following commit(s) were added to refs/heads/maven-resolver-1.9.x by this 
push:
     new 1b8ce7f9 [MRESOLVER-372] Rework the FileUtils collocated temp file 
(#365)
1b8ce7f9 is described below

commit 1b8ce7f95bd025024f5d0a4023b338147001040b
Author: Tamas Cservenak <ta...@cservenak.net>
AuthorDate: Fri Nov 17 13:27:18 2023 +0100

    [MRESOLVER-372] Rework the FileUtils collocated temp file (#365)
    
    Fixes:
    * move() call should NOT perform the move, as writer stream to tmp file may 
still be open
    * move the file move logic to close, make it happen only when closing 
collocated temp file
    * perform fsync before atomic move to ensure there is no OS dirty buffers 
related to newly written file
    * on windows go with old code that for some reason works (avoid NIO2)
    * on non-Win OS fsync the parent directory as well.
    
    ---
    
    https://issues.apache.org/jira/browse/MRESOLVER-372
    
    Backport to 1.9.x branch of the 
https://github.com/apache/maven-resolver/pull/364
---
 .../java/org/eclipse/aether/util/FileUtils.java    | 75 +++++++++++++++++++++-
 1 file changed, 72 insertions(+), 3 deletions(-)

diff --git 
a/maven-resolver-util/src/main/java/org/eclipse/aether/util/FileUtils.java 
b/maven-resolver-util/src/main/java/org/eclipse/aether/util/FileUtils.java
index dc260d76..0ec3880b 100644
--- a/maven-resolver-util/src/main/java/org/eclipse/aether/util/FileUtils.java
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/FileUtils.java
@@ -20,10 +20,16 @@ package org.eclipse.aether.util;
 
 import java.io.Closeable;
 import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.StandardCopyOption;
+import java.nio.file.StandardOpenOption;
 import java.util.concurrent.ThreadLocalRandom;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 import static java.util.Objects.requireNonNull;
 
@@ -33,6 +39,10 @@ import static java.util.Objects.requireNonNull;
  * @since 1.9.0
  */
 public final class FileUtils {
+    // Logic borrowed from Commons-Lang3: we really need only this, to decide 
do we fsync on directories or not
+    private static final boolean IS_WINDOWS =
+            System.getProperty("os.name", "unknown").startsWith("Windows");
+
     private FileUtils() {
         // hide constructor
     }
@@ -52,7 +62,10 @@ public final class FileUtils {
      */
     public interface CollocatedTempFile extends TempFile {
         /**
-         * Atomically moves temp file to target file it is collocated with.
+         * Upon close, atomically moves temp file to target file it is 
collocated with overwriting target (if exists).
+         * Invocation of this method merely signals that caller ultimately 
wants temp file to replace the target
+         * file, but when this method returns, the move operation did not yet 
happen, it will happen when this
+         * instance is closed.
          */
         void move() throws IOException;
     }
@@ -98,23 +111,79 @@ public final class FileUtils {
         Path tempFile = parent.resolve(file.getFileName() + "."
                 + 
Long.toUnsignedString(ThreadLocalRandom.current().nextLong()) + ".tmp");
         return new CollocatedTempFile() {
+            private final AtomicBoolean wantsMove = new AtomicBoolean(false);
+
             @Override
             public Path getPath() {
                 return tempFile;
             }
 
             @Override
-            public void move() throws IOException {
-                Files.move(tempFile, file, StandardCopyOption.ATOMIC_MOVE);
+            public void move() {
+                wantsMove.set(true);
             }
 
             @Override
             public void close() throws IOException {
+                if (wantsMove.get() && Files.isReadable(tempFile)) {
+                    if (IS_WINDOWS) {
+                        copy(tempFile, file);
+                    } else {
+                        fsyncFile(tempFile);
+                        Files.move(tempFile, file, 
StandardCopyOption.ATOMIC_MOVE);
+                        fsyncParent(tempFile);
+                    }
+                }
                 Files.deleteIfExists(tempFile);
             }
         };
     }
 
+    /**
+     * On Windows we use pre-NIO2 way to copy files, as for some reason it 
works. Beat me why.
+     */
+    private static void copy(Path source, Path target) throws IOException {
+        ByteBuffer buffer = ByteBuffer.allocate(1024 * 32);
+        byte[] array = buffer.array();
+        try (InputStream is = Files.newInputStream(source);
+                OutputStream os = Files.newOutputStream(target)) {
+            while (true) {
+                int bytes = is.read(array);
+                if (bytes < 0) {
+                    break;
+                }
+                os.write(array, 0, bytes);
+            }
+        }
+    }
+
+    /**
+     * Performs fsync: makes sure no OS "dirty buffers" exist for given file.
+     *
+     * @param target Path that must not be {@code null}, must exist as plain 
file.
+     */
+    private static void fsyncFile(Path target) throws IOException {
+        try (FileChannel file = FileChannel.open(target, 
StandardOpenOption.WRITE)) {
+            file.force(true);
+        }
+    }
+
+    /**
+     * Performs directory fsync: not usable on Windows, but some other OSes 
may also throw, hence thrown IO exception
+     * is just ignored.
+     *
+     * @param target Path that must not be {@code null}, must exist as plain 
file, and must have parent.
+     */
+    private static void fsyncParent(Path target) throws IOException {
+        try (FileChannel parent = FileChannel.open(target.getParent(), 
StandardOpenOption.READ)) {
+            try {
+                parent.force(true);
+            } catch (IOException e) {
+                // ignore
+            }
+        }
+    }
+
     /**
      * A file writer, that accepts a {@link Path} to write some content to. 
Note: the file denoted by path may exist,
      * hence implementation have to ensure it is able to achieve its goal 
("replace existing" option or equivalent

Reply via email to