You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by cs...@apache.org on 2022/11/14 10:31:36 UTC

[maven-resolver] branch master updated: [MRESOLVER-290] Expand use of atomic file ops (#218)

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

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


The following commit(s) were added to refs/heads/master by this push:
     new 7469aa25 [MRESOLVER-290] Expand use of atomic file ops (#218)
7469aa25 is described below

commit 7469aa25e781e30fbbb5158a1433108bc14956c3
Author: Tamas Cservenak <ta...@cservenak.net>
AuthorDate: Mon Nov 14 11:31:30 2022 +0100

    [MRESOLVER-290] Expand use of atomic file ops (#218)
    
    Drop old and legacy code doing smart(er) things, use common shared primitives instead.
    
    Also, make IO handling stricter, do not be "forgiving" by swallowing IOEx-es as they will inevitably lead to failures, it does not make resolver more robust, but actually makes it harder to figure out the problems, and could lead to incomplete downloads or uploads. Simply put, if reading or writing a file on disk fails with IOEx, do not assume "is fine" in any case. This probably shows some existing issue like permissions or whatever.
    
    ---
    
    https://issues.apache.org/jira/browse/MRESOLVER-290
---
 .../connector/basic/BasicRepositoryConnector.java  |   8 +-
 .../aether/connector/basic/ChecksumCalculator.java |   6 +-
 .../aether/connector/basic/ChecksumValidator.java  |   2 +
 .../aether/internal/impl/DefaultFileProcessor.java | 144 ++++-----------------
 .../internal/impl/DefaultTrackingFileManager.java  | 119 +++++++----------
 .../aether/internal/impl/TrackingFileManager.java  |   7 +
 .../SparseDirectoryTrustedChecksumsSource.java     |   4 +-
 .../checksum/ChecksumAlgorithmHelper.java          |   4 +-
 .../aether/spi/connector/transport/GetTask.java    |  14 +-
 .../aether/spi/connector/transport/PutTask.java    |   4 +-
 .../aether/transport/file/FileTransporter.java     |   7 +-
 .../aether/transport/http/HttpTransporter.java     |   4 +-
 .../aether/transport/wagon/WagonTransporter.java   |   2 +-
 .../java/org/eclipse/aether/util/FileUtils.java    |  49 ++++---
 14 files changed, 143 insertions(+), 231 deletions(-)

diff --git a/maven-resolver-connector-basic/src/main/java/org/eclipse/aether/connector/basic/BasicRepositoryConnector.java b/maven-resolver-connector-basic/src/main/java/org/eclipse/aether/connector/basic/BasicRepositoryConnector.java
index 4c6ab9f4..6dd3e680 100644
--- a/maven-resolver-connector-basic/src/main/java/org/eclipse/aether/connector/basic/BasicRepositoryConnector.java
+++ b/maven-resolver-connector-basic/src/main/java/org/eclipse/aether/connector/basic/BasicRepositoryConnector.java
@@ -25,6 +25,7 @@ import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.UncheckedIOException;
 import java.net.URI;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -455,9 +456,7 @@ final class BasicRepositoryConnector
         protected void runTask()
                 throws Exception
         {
-            fileProcessor.mkdirs( file.getParentFile() );
-
-            try ( FileUtils.TempFile tempFile = FileUtils.newTempFile( file.toPath() ) )
+            try ( FileUtils.CollocatedTempFile tempFile = FileUtils.newTempFile( file.toPath() ) )
             {
                 final File tmp = tempFile.getPath().toFile();
                 listener.setChecksumCalculator( checksumValidator.newChecksumCalculator( tmp ) );
@@ -489,7 +488,7 @@ final class BasicRepositoryConnector
                         }
                     }
                 }
-                fileProcessor.move( tmp, file );
+                tempFile.move();
                 if ( persistedChecksums )
                 {
                     checksumValidator.commit();
@@ -602,6 +601,7 @@ final class BasicRepositoryConnector
             catch ( IOException e )
             {
                 LOGGER.warn( "Failed to upload checksums for {}", file, e );
+                throw new UncheckedIOException( e );
             }
         }
 
diff --git a/maven-resolver-connector-basic/src/main/java/org/eclipse/aether/connector/basic/ChecksumCalculator.java b/maven-resolver-connector-basic/src/main/java/org/eclipse/aether/connector/basic/ChecksumCalculator.java
index ccf04310..b1b35368 100644
--- a/maven-resolver-connector-basic/src/main/java/org/eclipse/aether/connector/basic/ChecksumCalculator.java
+++ b/maven-resolver-connector-basic/src/main/java/org/eclipse/aether/connector/basic/ChecksumCalculator.java
@@ -21,11 +21,11 @@ package org.eclipse.aether.connector.basic;
 
 import java.io.BufferedInputStream;
 import java.io.File;
-import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.nio.Buffer;
 import java.nio.ByteBuffer;
+import java.nio.file.Files;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
@@ -126,11 +126,11 @@ final class ChecksumCalculator
             return;
         }
 
-        try ( InputStream in = new BufferedInputStream( new FileInputStream( targetFile ) ) )
+        try ( InputStream in = new BufferedInputStream( Files.newInputStream( targetFile.toPath() ) ) )
         {
             long total = 0;
             final byte[] buffer = new byte[ 1024 * 32 ];
-            for ( ; total < dataOffset; )
+            while ( total < dataOffset )
             {
                 int read = in.read( buffer );
                 if ( read < 0 )
diff --git a/maven-resolver-connector-basic/src/main/java/org/eclipse/aether/connector/basic/ChecksumValidator.java b/maven-resolver-connector-basic/src/main/java/org/eclipse/aether/connector/basic/ChecksumValidator.java
index 988cf54c..4835e986 100644
--- a/maven-resolver-connector-basic/src/main/java/org/eclipse/aether/connector/basic/ChecksumValidator.java
+++ b/maven-resolver-connector-basic/src/main/java/org/eclipse/aether/connector/basic/ChecksumValidator.java
@@ -21,6 +21,7 @@ package org.eclipse.aether.connector.basic;
 
 import java.io.File;
 import java.io.IOException;
+import java.io.UncheckedIOException;
 import java.net.URI;
 import java.util.Collection;
 import java.util.HashMap;
@@ -259,6 +260,7 @@ final class ChecksumValidator
             catch ( IOException e )
             {
                 LOGGER.debug( "Failed to write checksum file {}", checksumFile, e );
+                throw new UncheckedIOException( e );
             }
         }
         checksumExpectedValues.clear();
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultFileProcessor.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultFileProcessor.java
index 3aebd783..14177ac5 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultFileProcessor.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultFileProcessor.java
@@ -19,20 +19,23 @@ package org.eclipse.aether.internal.impl;
  * under the License.
  */
 
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
 import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.io.UncheckedIOException;
 import java.nio.ByteBuffer;
 import java.nio.charset.StandardCharsets;
-
-import javax.inject.Named;
-import javax.inject.Singleton;
+import java.nio.file.Files;
 
 import org.eclipse.aether.spi.io.FileProcessor;
 import org.eclipse.aether.util.ChecksumUtils;
+import org.eclipse.aether.util.FileUtils;
 
 /**
  * A utility class helping with file-based operations.
@@ -40,7 +43,7 @@ import org.eclipse.aether.util.ChecksumUtils;
 @Singleton
 @Named
 public class DefaultFileProcessor
-    implements FileProcessor
+        implements FileProcessor
 {
 
     /**
@@ -50,7 +53,7 @@ public class DefaultFileProcessor
      *
      * @param directory The directory to create, may be {@code null}.
      * @return {@code true} if and only if the directory was created, along with all necessary parent directories;
-     *         {@code false} otherwise
+     * {@code false} otherwise
      */
     public boolean mkdirs( File directory )
     {
@@ -75,7 +78,7 @@ public class DefaultFileProcessor
         }
         catch ( IOException e )
         {
-            return false;
+            throw new UncheckedIOException( e );
         }
 
         File parentDir = canonDir.getParentFile();
@@ -83,136 +86,41 @@ public class DefaultFileProcessor
     }
 
     public void write( File target, String data )
-        throws IOException
+            throws IOException
     {
-        mkdirs( target.getAbsoluteFile().getParentFile() );
-
-        OutputStream out = null;
-        try
-        {
-            out = new FileOutputStream( target );
-
-            if ( data != null )
-            {
-                out.write( data.getBytes( StandardCharsets.UTF_8 ) );
-            }
-
-            out.close();
-            out = null;
-        }
-        finally
-        {
-            try
-            {
-                if ( out != null )
-                {
-                    out.close();
-                }
-            }
-            catch ( final IOException e )
-            {
-                // Suppressed due to an exception already thrown in the try block.
-            }
-        }
+        FileUtils.writeFile( target.toPath(), p -> Files.write( p, data.getBytes( StandardCharsets.UTF_8 ) ) );
     }
 
     public void write( File target, InputStream source )
-        throws IOException
+            throws IOException
     {
-        mkdirs( target.getAbsoluteFile().getParentFile() );
-
-        OutputStream out = null;
-        try
-        {
-            out = new FileOutputStream( target );
-
-            copy( out, source, null );
-
-            out.close();
-            out = null;
-        }
-        finally
-        {
-            try
-            {
-                if ( out != null )
-                {
-                    out.close();
-                }
-            }
-            catch ( final IOException e )
-            {
-                // Suppressed due to an exception already thrown in the try block.
-            }
-        }
+        FileUtils.writeFile( target.toPath(), p -> Files.copy( source, p ) );
     }
 
     public void copy( File source, File target )
-        throws IOException
+            throws IOException
     {
         copy( source, target, null );
     }
 
     public long copy( File source, File target, ProgressListener listener )
-        throws IOException
+            throws IOException
     {
-        long total = 0L;
-
-        InputStream in = null;
-        OutputStream out = null;
-        try
+        try ( InputStream in = new BufferedInputStream( Files.newInputStream( source.toPath() ) );
+              FileUtils.CollocatedTempFile tempTarget = FileUtils.newTempFile( target.toPath() );
+              OutputStream out = new BufferedOutputStream( Files.newOutputStream( tempTarget.getPath() ) ) )
         {
-            in = new FileInputStream( source );
-
-            mkdirs( target.getAbsoluteFile().getParentFile() );
-
-            out = new FileOutputStream( target );
-
-            total = copy( out, in, listener );
-
-            out.close();
-            out = null;
-
-            in.close();
-            in = null;
+            long result = copy( out, in, listener );
+            tempTarget.move();
+            return result;
         }
-        finally
-        {
-            try
-            {
-                if ( out != null )
-                {
-                    out.close();
-                }
-            }
-            catch ( final IOException e )
-            {
-                // Suppressed due to an exception already thrown in the try block.
-            }
-            finally
-            {
-                try
-                {
-                    if ( in != null )
-                    {
-                        in.close();
-                    }
-                }
-                catch ( final IOException e )
-                {
-                    // Suppressed due to an exception already thrown in the try block.
-                }
-            }
-        }
-
-        return total;
     }
 
     private long copy( OutputStream os, InputStream is, ProgressListener listener )
-        throws IOException
+            throws IOException
     {
         long total = 0L;
-        byte[] buffer = new byte[ 1024 * 32 ];
+        byte[] buffer = new byte[1024 * 32];
         while ( true )
         {
             int bytes = is.read( buffer );
@@ -242,7 +150,7 @@ public class DefaultFileProcessor
     }
 
     public void move( File source, File target )
-        throws IOException
+            throws IOException
     {
         if ( !source.renameTo( target ) )
         {
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultTrackingFileManager.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultTrackingFileManager.java
index e6e2e653..4269ce98 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultTrackingFileManager.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultTrackingFileManager.java
@@ -19,19 +19,20 @@ package org.eclipse.aether.internal.impl;
  * under the License.
  */
 
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.Closeable;
+import javax.inject.Named;
+import javax.inject.Singleton;
+
 import java.io.File;
-import java.io.FileInputStream;
 import java.io.IOException;
-import java.io.RandomAccessFile;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.Map;
 import java.util.Properties;
 
-import javax.inject.Named;
-import javax.inject.Singleton;
-
+import org.eclipse.aether.util.FileUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -41,66 +42,55 @@ import org.slf4j.LoggerFactory;
 @Singleton
 @Named
 public final class DefaultTrackingFileManager
-    implements TrackingFileManager
+        implements TrackingFileManager
 {
     private static final Logger LOGGER = LoggerFactory.getLogger( DefaultTrackingFileManager.class );
 
     @Override
     public Properties read( File file )
     {
-        FileInputStream stream = null;
-        try
+        Path filePath = file.toPath();
+        if ( Files.isRegularFile( filePath ) )
         {
-            if ( !file.exists() )
+            try ( InputStream stream = Files.newInputStream( filePath ) )
             {
-                return null;
+                Properties props = new Properties();
+                props.load( stream );
+                return props;
+            }
+            catch ( IOException e )
+            {
+                LOGGER.warn( "Failed to read tracking file '{}'", file, e );
+                throw new UncheckedIOException( e );
             }
-
-            stream = new FileInputStream( file );
-
-            Properties props = new Properties();
-            props.load( stream );
-
-            return props;
-        }
-        catch ( IOException e )
-        {
-            LOGGER.warn( "Failed to read tracking file {}", file, e );
-        }
-        finally
-        {
-            close( stream, file );
         }
-
         return null;
     }
 
     @Override
     public Properties update( File file, Map<String, String> updates )
     {
+        Path filePath = file.toPath();
         Properties props = new Properties();
 
-        File directory = file.getParentFile();
-        if ( !directory.mkdirs() && !directory.exists() )
+        try
+        {
+            Files.createDirectories( filePath.getParent() );
+        }
+        catch ( IOException e )
         {
-            LOGGER.warn( "Failed to create parent directories for tracking file {}", file );
-            return props;
+            LOGGER.warn( "Failed to create tracking file parent '{}'", file, e );
+            throw new UncheckedIOException( e );
         }
 
-        RandomAccessFile raf = null;
         try
         {
-            raf = new RandomAccessFile( file, "rw" );
-
-            if ( file.canRead() )
+            if ( Files.isReadable( filePath ) )
             {
-                byte[] buffer = new byte[(int) raf.length()];
-
-                raf.readFully( buffer );
-
-                ByteArrayInputStream stream = new ByteArrayInputStream( buffer );
-
-                props.load( stream );
+                try ( InputStream stream = Files.newInputStream( filePath ) )
+                {
+                    props.load( stream );
+                }
             }
 
             for ( Map.Entry<String, String> update : updates.entrySet() )
@@ -115,41 +105,22 @@ public final class DefaultTrackingFileManager
                 }
             }
 
-            ByteArrayOutputStream stream = new ByteArrayOutputStream( 1024 * 2 );
-
-            LOGGER.debug( "Writing tracking file {}", file );
-            props.store( stream, "NOTE: This is a Maven Resolver internal implementation file"
-                + ", its format can be changed without prior notice." );
-
-            raf.seek( 0 );
-            raf.write( stream.toByteArray() );
-            raf.setLength( raf.getFilePointer() );
+            FileUtils.writeFile( filePath, p ->
+            {
+                try ( OutputStream stream = Files.newOutputStream( p ) )
+                {
+                    LOGGER.debug( "Writing tracking file '{}'", file );
+                    props.store( stream, "NOTE: This is a Maven Resolver internal implementation file"
+                            + ", its format can be changed without prior notice." );
+                }
+            } );
         }
         catch ( IOException e )
         {
-            LOGGER.warn( "Failed to write tracking file {}", file, e );
-        }
-        finally
-        {
-            close( raf, file );
+            LOGGER.warn( "Failed to write tracking file '{}'", file, e );
+            throw new UncheckedIOException( e );
         }
 
         return props;
     }
-
-    private void close( Closeable closeable, File file )
-    {
-        if ( closeable != null )
-        {
-            try
-            {
-                closeable.close();
-            }
-            catch ( IOException e )
-            {
-                LOGGER.warn( "Error closing tracking file {}", file, e );
-            }
-        }
-    }
-
 }
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/TrackingFileManager.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/TrackingFileManager.java
index 1bc5b3e7..c719d13b 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/TrackingFileManager.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/TrackingFileManager.java
@@ -28,7 +28,14 @@ import java.util.Properties;
  */
 public interface TrackingFileManager
 {
+    /**
+     * Reads up the specified properties file into {@link Properties}, if exists, otherwise {@code null} is returned.
+     */
     Properties read( File file );
 
+    /**
+     * Applies updates to specified properties file and returns resulting {@link Properties} with contents same
+     * as in updated file, never {@code null}.
+     */
     Properties update( File file, Map<String, String> updates );
 }
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/SparseDirectoryTrustedChecksumsSource.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/SparseDirectoryTrustedChecksumsSource.java
index 3c5af62e..0d6307bf 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/SparseDirectoryTrustedChecksumsSource.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/SparseDirectoryTrustedChecksumsSource.java
@@ -24,6 +24,7 @@ import javax.inject.Named;
 import javax.inject.Singleton;
 
 import java.io.IOException;
+import java.io.UncheckedIOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.HashMap;
@@ -111,9 +112,10 @@ public final class SparseDirectoryTrustedChecksumsSource
                 }
                 catch ( IOException e )
                 {
-                    // unexpected, log, skip
+                    // unexpected, log
                     LOGGER.warn( "Could not read artifact '{}' trusted checksum on path '{}'", artifact, checksumPath,
                             e );
+                    throw new UncheckedIOException( e );
                 }
             }
         }
diff --git a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/checksum/ChecksumAlgorithmHelper.java b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/checksum/ChecksumAlgorithmHelper.java
index 5238dbbf..df4be0e7 100644
--- a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/checksum/ChecksumAlgorithmHelper.java
+++ b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/checksum/ChecksumAlgorithmHelper.java
@@ -22,10 +22,10 @@ package org.eclipse.aether.spi.connector.checksum;
 import java.io.BufferedInputStream;
 import java.io.ByteArrayInputStream;
 import java.io.File;
-import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.nio.ByteBuffer;
+import java.nio.file.Files;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
@@ -72,7 +72,7 @@ public final class ChecksumAlgorithmHelper
     public static Map<String, String> calculate( File file, List<ChecksumAlgorithmFactory> factories )
             throws IOException
     {
-        try ( InputStream inputStream = new BufferedInputStream( new FileInputStream( file ) ) )
+        try ( InputStream inputStream = new BufferedInputStream( Files.newInputStream( file.toPath() ) ) )
         {
             return calculate( inputStream, factories );
         }
diff --git a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/GetTask.java b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/GetTask.java
index e6eb9a41..9d68dc01 100644
--- a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/GetTask.java
+++ b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/GetTask.java
@@ -21,11 +21,12 @@ package org.eclipse.aether.spi.connector.transport;
 
 import java.io.ByteArrayOutputStream;
 import java.io.File;
-import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.OutputStream;
 import java.net.URI;
 import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.StandardOpenOption;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
@@ -87,7 +88,16 @@ public final class GetTask
     {
         if ( dataFile != null )
         {
-            return new FileOutputStream( dataFile, this.resume && resume );
+            if ( this.resume && resume )
+            {
+                return Files.newOutputStream( dataFile.toPath(),
+                        StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.APPEND );
+            }
+            else
+            {
+                return Files.newOutputStream( dataFile.toPath(),
+                        StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING );
+            }
         }
         if ( dataBytes == null )
         {
diff --git a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/PutTask.java b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/PutTask.java
index 1c30e071..5ba659a5 100644
--- a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/PutTask.java
+++ b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/transport/PutTask.java
@@ -21,11 +21,11 @@ package org.eclipse.aether.spi.connector.transport;
 
 import java.io.ByteArrayInputStream;
 import java.io.File;
-import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.net.URI;
 import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
 
 /**
  * A task to upload a resource to the remote repository.
@@ -62,7 +62,7 @@ public final class PutTask
     {
         if ( dataFile != null )
         {
-            return new FileInputStream( dataFile );
+            return Files.newInputStream( dataFile.toPath() );
         }
         return new ByteArrayInputStream( dataBytes );
     }
diff --git a/maven-resolver-transport-file/src/main/java/org/eclipse/aether/transport/file/FileTransporter.java b/maven-resolver-transport-file/src/main/java/org/eclipse/aether/transport/file/FileTransporter.java
index 7da42f2d..d2c971cd 100644
--- a/maven-resolver-transport-file/src/main/java/org/eclipse/aether/transport/file/FileTransporter.java
+++ b/maven-resolver-transport-file/src/main/java/org/eclipse/aether/transport/file/FileTransporter.java
@@ -20,8 +20,7 @@ package org.eclipse.aether.transport.file;
  */
 
 import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
+import java.nio.file.Files;
 
 import org.eclipse.aether.repository.RemoteRepository;
 import org.eclipse.aether.spi.connector.transport.AbstractTransporter;
@@ -80,7 +79,7 @@ final class FileTransporter
         throws Exception
     {
         File file = getFile( task, true );
-        utilGet( task, new FileInputStream( file ), true, file.length(), false );
+        utilGet( task, Files.newInputStream( file.toPath() ), true, file.length(), false );
     }
 
     @Override
@@ -91,7 +90,7 @@ final class FileTransporter
         file.getParentFile().mkdirs();
         try
         {
-            utilPut( task, new FileOutputStream( file ), true );
+            utilPut( task, Files.newOutputStream( file.toPath() ), true );
         }
         catch ( Exception e )
         {
diff --git a/maven-resolver-transport-http/src/main/java/org/eclipse/aether/transport/http/HttpTransporter.java b/maven-resolver-transport-http/src/main/java/org/eclipse/aether/transport/http/HttpTransporter.java
index 32cb7388..09d7c169 100644
--- a/maven-resolver-transport-http/src/main/java/org/eclipse/aether/transport/http/HttpTransporter.java
+++ b/maven-resolver-transport-http/src/main/java/org/eclipse/aether/transport/http/HttpTransporter.java
@@ -611,7 +611,7 @@ final class HttpTransporter
             }
             else
             {
-                try ( FileUtils.TempFile tempFile = FileUtils.newTempFile( dataFile.toPath() ) )
+                try ( FileUtils.CollocatedTempFile tempFile = FileUtils.newTempFile( dataFile.toPath() ) )
                 {
                     task.setDataFile( tempFile.getPath().toFile(), resume );
                     if ( resume && Files.isRegularFile( dataFile.toPath() ) )
@@ -625,7 +625,7 @@ final class HttpTransporter
                     {
                         utilGet( task, is, true, length, resume );
                     }
-                    Files.move( tempFile.getPath(), dataFile.toPath(), StandardCopyOption.ATOMIC_MOVE );
+                    tempFile.move();
                 }
                 finally
                 {
diff --git a/maven-resolver-transport-wagon/src/main/java/org/eclipse/aether/transport/wagon/WagonTransporter.java b/maven-resolver-transport-wagon/src/main/java/org/eclipse/aether/transport/wagon/WagonTransporter.java
index 06126ab1..92f0971f 100644
--- a/maven-resolver-transport-wagon/src/main/java/org/eclipse/aether/transport/wagon/WagonTransporter.java
+++ b/maven-resolver-transport-wagon/src/main/java/org/eclipse/aether/transport/wagon/WagonTransporter.java
@@ -533,7 +533,7 @@ final class WagonTransporter
 
                     if ( file != null )
                     {
-                        Files.move( dst.toPath(), file.toPath(), StandardCopyOption.ATOMIC_MOVE );
+                        ( (FileUtils.CollocatedTempFile) tempFile ).move();
                     }
                     else
                     {
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 f82e4e1f..d350baf7 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
@@ -44,9 +44,23 @@ public final class FileUtils
      */
     public interface TempFile extends Closeable
     {
+        /**
+         * Returns the path of the created temp file.
+         */
         Path getPath();
     }
 
+    /**
+     * A collocated temporary file, that resides next to a "target" file, and is removed when closed.
+     */
+    public interface CollocatedTempFile extends TempFile
+    {
+        /**
+         * Atomically moves temp file to target file it is collocated with.
+         */
+        void move() throws IOException;
+    }
+
     /**
      * Creates a {@link TempFile}. It will be in the default temporary-file directory. Returned instance should be
      * handled in try-with-resource construct and created temp file is removed on close, if exists.
@@ -73,13 +87,16 @@ public final class FileUtils
     /**
      * Creates a {@link TempFile} for given file. It will be in same directory where given file is, and will reuse its
      * name for generated name. Returned instance should be handled in try-with-resource construct and created temp
-     * file is removed on close, if exists.
+     * file once ready can be moved to passed in {@code file} parameter place.
+     * <p>
+     * The {@code file} nor it's parent directories have to exist. The parent directories are created if needed.
      */
-    public static TempFile newTempFile( Path file ) throws IOException
+    public static CollocatedTempFile newTempFile( Path file ) throws IOException
     {
-        requireNonNull( file.getParent(), "file must have parent" );
-        Path tempFile = Files.createTempFile( file.getParent(), file.getFileName().toString(), "tmp" );
-        return new TempFile()
+        Path parent = requireNonNull( file.getParent(), "file must have parent" );
+        Files.createDirectories( parent );
+        Path tempFile = Files.createTempFile( parent, file.getFileName().toString(), "tmp" );
+        return new CollocatedTempFile()
         {
             @Override
             public Path getPath()
@@ -87,6 +104,12 @@ public final class FileUtils
                 return tempFile;
             }
 
+            @Override
+            public void move() throws IOException
+            {
+                Files.move( tempFile, file, StandardCopyOption.ATOMIC_MOVE );
+            }
+
             @Override
             public void close() throws IOException
             {
@@ -144,26 +167,16 @@ public final class FileUtils
         requireNonNull( target, "target is null" );
         requireNonNull( writer, "writer is null" );
         Path parent = requireNonNull( target.getParent(), "target must have parent" );
-        Path temp = null;
 
-        Files.createDirectories( parent );
-        try
+        try ( CollocatedTempFile tempFile = newTempFile( target ) )
         {
-            temp = Files.createTempFile( parent, "writer", "tmp" );
-            writer.write( temp );
+            writer.write( tempFile.getPath() );
             if ( doBackup && Files.isRegularFile( target ) )
             {
                 Files.copy( target, parent.resolve( target.getFileName() + ".bak" ),
                         StandardCopyOption.REPLACE_EXISTING );
             }
-            Files.move( temp, target, StandardCopyOption.ATOMIC_MOVE );
-        }
-        finally
-        {
-            if ( temp != null )
-            {
-                Files.deleteIfExists( temp );
-            }
+            tempFile.move();
         }
     }
 }