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 2024/02/20 16:35:24 UTC

(maven-resolver) branch master updated: [MRESOLVER-496] Paths everywhere (#430)

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 aafe7e71 [MRESOLVER-496] Paths everywhere (#430)
aafe7e71 is described below

commit aafe7e71e704977a9133dd96c49d0d8e67cdacfd
Author: Tamas Cservenak <ta...@cservenak.net>
AuthorDate: Tue Feb 20 17:35:19 2024 +0100

    [MRESOLVER-496] Paths everywhere (#430)
    
    Internally, stop using java.io.File and use NIO2 Paths instead. From outside, this should not make a lot of difference (and is enforced by japicmp as [documented here](https://maven.apache.org/resolver/api-compatibility.html)).
    
    But, as a consequence: if Resolver used on FileSystem like Google JIMFS, whatever code touches File method (that will do Path.toFile() under the hood), it will explode. Hence, we need to make sure this call never happens internally, and leave it to consumer projects. Resolver alone should never invoke it.
    
    Most notable changes:
    * change internals to use NIO2 Paths.
    * in resolver now everything is "resolved" from local repository `basePath`, hence, if you put your local repository onto non-default FileSystem, it will work just fine (as proved by Demo Snippets). Naturally, if anything touches `File` returning method, in case of non-default FileSystem, it explodes (Google JIMFs Path.toFile() simply throws).
    * change transport-file to support any (and not only default) FileSystem, covered by tests. This makes possible to use any FileSystem hosted repositories as "remote file repositories". Covered by UTs that now runs as parameterized two times: using default and using JIMFS FileSystem (as it is simple to set up, very handy for testing).
    * change file locking to support any (and not only default) FileSystem. Before this change the implicit requirement that "lock name is fully qualified absolute file system path", but now it changes to "lock name is proper string representation of an URI". The key change is in BasedirNameMapper, where one can now use custom `Path` (for example created with JIMFS) and use that to name locks (use `file:` for default FS or any other URIs, ie `jimfs:` ones).
    
    ---
    
    https://issues.apache.org/jira/browse/MRESOLVER-496
---
 .../java/org/eclipse/aether/RepositoryEvent.java   |  40 ++-
 .../eclipse/aether/RepositorySystemSession.java    |  56 ++++-
 .../eclipse/aether/artifact/AbstractArtifact.java  |  31 ++-
 .../java/org/eclipse/aether/artifact/Artifact.java |  23 ++
 .../eclipse/aether/artifact/DefaultArtifact.java   |  49 +++-
 .../eclipse/aether/metadata/AbstractMetadata.java  |  26 +-
 .../eclipse/aether/metadata/DefaultMetadata.java   |  80 +++++-
 .../eclipse/aether/metadata/MergeableMetadata.java |  16 ++
 .../java/org/eclipse/aether/metadata/Metadata.java |  22 ++
 .../aether/repository/LocalArtifactResult.java     |  36 ++-
 .../aether/repository/LocalMetadataResult.java     |  34 ++-
 .../eclipse/aether/repository/LocalRepository.java |  67 ++++-
 .../NoLocalRepositoryManagerException.java         |   2 +-
 .../aether/repository/RepositoryUriUtils.java      |  38 ++-
 .../eclipse/aether/repository/WorkspaceReader.java |  13 +
 .../resolution/ArtifactResolutionException.java    |   2 +-
 .../eclipse/aether/resolution/ArtifactResult.java  |   6 +-
 .../eclipse/aether/resolution/MetadataResult.java  |   4 +-
 .../aether/transfer/MetadataNotFoundException.java |   2 +-
 .../eclipse/aether/transfer/TransferResource.java  |  45 +++-
 .../aether/repository/LocalRepositoryTest.java     |  45 ++--
 .../eclipse/aether/transfer/TransferEventTest.java |   3 +-
 .../connector/basic/BasicRepositoryConnector.java  |  56 ++---
 .../basic/BasicRepositoryConnectorFactory.java     |  10 +-
 .../aether/connector/basic/ChecksumCalculator.java |  10 +-
 .../aether/connector/basic/ChecksumValidator.java  |  40 +--
 .../connector/basic/ChecksumCalculatorTest.java    |   2 +-
 .../connector/basic/ChecksumValidatorTest.java     |  26 +-
 .../maven-resolver-demo-snippets/pom.xml           |   4 +
 .../maven/resolver/examples/DeployArtifacts.java   |   4 +-
 .../maven/resolver/examples/InstallArtifacts.java  |   4 +-
 .../maven/resolver/examples/ResolveArtifact.java   |  15 +-
 .../examples/ResolveTransitiveDependencies.java    |   6 +-
 .../resolver/examples/ReverseDependencyTree.java   |  15 +-
 .../supplier/SupplierRepositorySystemFactory.java  |   4 +-
 .../maven/resolver/examples/util/Booter.java       |  17 +-
 .../java/org/eclipse/aether/impl/UpdateCheck.java  |  32 ++-
 .../internal/impl/DefaultArtifactResolver.java     |  75 +++---
 .../internal/impl/DefaultChecksumProcessor.java    |  88 +++++++
 .../aether/internal/impl/DefaultDeployer.java      |  68 +++---
 .../aether/internal/impl/DefaultFileProcessor.java |   3 +
 .../aether/internal/impl/DefaultInstaller.java     |  57 ++---
 .../impl/DefaultLocalRepositoryProvider.java       |   4 +-
 .../internal/impl/DefaultMetadataResolver.java     |  87 ++++---
 .../aether/internal/impl/DefaultPathProcessor.java |  95 ++++++++
 .../internal/impl/DefaultTrackingFileManager.java  | 106 ++++----
 .../internal/impl/DefaultUpdateCheckManager.java   |  99 ++++----
 .../impl/EnhancedLocalRepositoryManager.java       |  55 +++--
 .../EnhancedLocalRepositoryManagerFactory.java     |   2 +-
 .../impl/SimpleLocalRepositoryManager.java         |  29 +--
 .../impl/SimpleLocalRepositoryManagerFactory.java  |   2 +-
 .../aether/internal/impl/TrackingFileManager.java  |  18 ++
 .../SparseDirectoryTrustedChecksumsSource.java     |  13 +-
 ...stedChecksumsArtifactResolverPostProcessor.java |  10 +-
 .../impl/session/DefaultCloseableSession.java      |  22 +-
 .../impl/session/DefaultSessionBuilder.java        |  69 ++++--
 .../impl/synccontext/named/BasedirNameMapper.java  |  31 ++-
 .../named/DiscriminatingNameMapper.java            |   4 +-
 .../impl/synccontext/named/GAVNameMapper.java      |   2 +-
 .../impl/synccontext/named/NameMapper.java         |   5 +
 .../internal/impl/DefaultArtifactResolverTest.java |  12 +-
 .../impl/DefaultChecksumPolicyProviderTest.java    |   4 +-
 .../aether/internal/impl/DefaultDeployerTest.java  |  17 +-
 .../aether/internal/impl/DefaultInstallerTest.java |  12 +-
 .../internal/impl/DefaultMetadataResolverTest.java |   6 +-
 .../impl/DefaultUpdateCheckManagerTest.java        |   5 +-
 .../impl/EnhancedLocalRepositoryManagerTest.java   |   2 +-
 .../EnhancedSplitLocalRepositoryManagerTest.java   |   2 +-
 .../internal/impl/FailChecksumPolicyTest.java      |   4 +-
 .../impl/RecordingRepositoryConnector.java         |   9 +-
 .../impl/SimpleLocalRepositoryManagerTest.java     |   2 +-
 .../internal/impl/WarnChecksumPolicyTest.java      |   4 +-
 .../SparseDirectoryTrustedChecksumsSourceTest.java |   6 +-
 .../synccontext/named/BasedirNameMapperTest.java   |  24 +-
 .../synccontext/named/NameMapperTestSupport.java   |   7 +-
 .../named/providers/FileLockNamedLockFactory.java  |   8 +-
 .../named/FileLockNamedLockFactorySupportTest.java |   5 +-
 .../aether/spi/connector/ArtifactDownload.java     |  29 ++-
 .../aether/spi/connector/ArtifactTransfer.java     |  34 ++-
 .../aether/spi/connector/ArtifactUpload.java       |  24 +-
 .../aether/spi/connector/MetadataDownload.java     |  27 +-
 .../aether/spi/connector/MetadataTransfer.java     |  34 ++-
 .../aether/spi/connector/MetadataUpload.java       |  24 +-
 .../checksum/ChecksumAlgorithmHelper.java          |  18 +-
 .../aether/spi/connector/transport/GetTask.java    |  75 +++++-
 .../aether/spi/connector/transport/PutTask.java    |  52 +++-
 .../eclipse/aether/spi/io/ChecksumProcessor.java   |  28 ++-
 .../org/eclipse/aether/spi/io/FileProcessor.java   |   5 +-
 .../io/{FileProcessor.java => PathProcessor.java}  |  73 +++---
 .../aether/supplier/RepositorySystemSupplier.java  |  46 ++--
 .../supplier/RepositorySystemSupplierTest.java     |  12 +-
 .../test/util/http/HttpTransporterTest.java        |  24 +-
 .../internal/test/util/TestChecksumProcessor.java  |  32 +--
 .../internal/test/util/TestPathProcessor.java      |  59 +++++
 .../aether/transport/apache/ApacheTransporter.java |  27 +-
 maven-resolver-transport-file/pom.xml              |  10 +
 .../aether/transport/file/FileTransporter.java     |  44 ++--
 .../aether/transport/file/FileTransporterTest.java | 271 ++++++++++++++-------
 .../aether/transport/jdk/JdkTransporter.java       |  32 ++-
 .../transport/jdk/JdkTransporterFactory.java       |   8 +-
 .../aether/transport/jdk/JdkTransporterTest.java   |   5 +-
 .../transport/jdk/JdkTransporterFactory.java       |   3 +-
 .../aether/transport/jetty/JettyTransporter.java   |  24 +-
 .../transport/jetty/PutTaskRequestContent.java     |   4 +-
 .../org/eclipse/aether/util/DirectoryUtils.java    |   9 +-
 .../java/org/eclipse/aether/util/FileUtils.java    |   2 +-
 .../aether/util/artifact/DelegatingArtifact.java   |  27 ++
 .../eclipse/aether/util/artifact/SubArtifact.java  |  76 +++++-
 .../util/graph/visitor/NodeListGenerator.java      |  30 ++-
 .../repository/ChainedLocalRepositoryManager.java  |  11 +-
 .../aether/util/artifact/SubArtifactTest.java      |   2 +-
 pom.xml                                            |   8 +-
 112 files changed, 2279 insertions(+), 868 deletions(-)

diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/RepositoryEvent.java b/maven-resolver-api/src/main/java/org/eclipse/aether/RepositoryEvent.java
index a75bfa5e..a1c00dcb 100644
--- a/maven-resolver-api/src/main/java/org/eclipse/aether/RepositoryEvent.java
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/RepositoryEvent.java
@@ -19,6 +19,7 @@
 package org.eclipse.aether;
 
 import java.io.File;
+import java.nio.file.Path;
 import java.util.Collections;
 import java.util.List;
 
@@ -149,7 +150,7 @@ public final class RepositoryEvent {
 
     private final ArtifactRepository repository;
 
-    private final File file;
+    private final Path path;
 
     private final List<Exception> exceptions;
 
@@ -161,7 +162,7 @@ public final class RepositoryEvent {
         artifact = builder.artifact;
         metadata = builder.metadata;
         repository = builder.repository;
-        file = builder.file;
+        path = builder.path;
         exceptions = builder.exceptions;
         trace = builder.trace;
     }
@@ -206,9 +207,21 @@ public final class RepositoryEvent {
      * Gets the file involved in the event (if any).
      *
      * @return The involved file or {@code null} if none.
+     * @deprecated Use {@link #getPath()} instead.
      */
+    @Deprecated
     public File getFile() {
-        return file;
+        return path != null ? path.toFile() : null;
+    }
+
+    /**
+     * Gets the file involved in the event (if any).
+     *
+     * @return The involved file or {@code null} if none.
+     * @since 2.0.0
+     */
+    public Path getPath() {
+        return path;
     }
 
     /**
@@ -260,8 +273,8 @@ public final class RepositoryEvent {
         if (getMetadata() != null) {
             buffer.append(" ").append(getMetadata());
         }
-        if (getFile() != null) {
-            buffer.append(" (").append(getFile()).append(")");
+        if (getPath() != null) {
+            buffer.append(" (").append(getPath()).append(")");
         }
         if (getRepository() != null) {
             buffer.append(" @ ").append(getRepository());
@@ -284,7 +297,7 @@ public final class RepositoryEvent {
 
         ArtifactRepository repository;
 
-        File file;
+        Path path;
 
         List<Exception> exceptions = Collections.emptyList();
 
@@ -339,9 +352,22 @@ public final class RepositoryEvent {
          *
          * @param file The involved file, may be {@code null}.
          * @return This event builder for chaining, never {@code null}.
+         * @deprecated Use {@link #setPath(Path)} instead.
          */
+        @Deprecated
         public Builder setFile(File file) {
-            this.file = file;
+            return setPath(file != null ? file.toPath() : null);
+        }
+
+        /**
+         * Sets the file involved in the event.
+         *
+         * @param path The involved file, may be {@code null}.
+         * @return This event builder for chaining, never {@code null}.
+         * @since 2.0.0
+         */
+        public Builder setPath(Path path) {
+            this.path = path;
             return this;
         }
 
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/RepositorySystemSession.java b/maven-resolver-api/src/main/java/org/eclipse/aether/RepositorySystemSession.java
index e85780a4..d2abbda3 100644
--- a/maven-resolver-api/src/main/java/org/eclipse/aether/RepositorySystemSession.java
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/RepositorySystemSession.java
@@ -19,8 +19,8 @@
 package org.eclipse.aether;
 
 import java.io.Closeable;
-import java.io.File;
-import java.util.List;
+import java.nio.file.Path;
+import java.util.Collection;
 import java.util.Map;
 import java.util.function.Supplier;
 
@@ -397,6 +397,14 @@ public interface RepositorySystemSession {
          */
         SessionBuilder setSystemScopeHandler(SystemScopeHandler systemScopeHandler);
 
+        /**
+         * Adds on session ended handler to be immediately registered when this builder creates session.
+         *
+         * @param handler The on session ended handler, may not be {@code null}.
+         * @return The session for chaining, never {@code null}.
+         */
+        SessionBuilder addOnSessionEndedHandler(Runnable handler);
+
         /**
          * Sets the custom session data supplier associated with this session.
          * Note: The supplier will be used for every built session out of this builder instance, so if supplier supplies
@@ -419,25 +427,25 @@ public interface RepositorySystemSession {
 
         /**
          * Shortcut method to set up local repository manager directly onto builder. There must be at least one non-null
-         * {@link File} passed in this method. In case multiple files, session builder will use chained local repository
+         * {@link Path} passed in this method. In case multiple files, session builder will use chained local repository
          * manager.
          *
          * @param baseDirectories The local repository base directories.
          * @return This session for chaining, never {@code null}.
          * @see #withLocalRepositories(LocalRepository...)
          */
-        SessionBuilder withLocalRepositoryBaseDirectories(File... baseDirectories);
+        SessionBuilder withLocalRepositoryBaseDirectories(Path... baseDirectories);
 
         /**
          * Shortcut method to set up local repository manager directly onto builder. There must be at least one non-null
-         * {@link File} present in passed in list. In case multiple files, session builder will use chained local
+         * {@link Path} present in passed in list. In case multiple files, session builder will use chained local
          * repository manager.
          *
          * @param baseDirectories The local repository base directories.
          * @return This session for chaining, never {@code null}.
-         * @see #withLocalRepositories(List)
+         * @see #withLocalRepositories(Collection)
          */
-        SessionBuilder withLocalRepositoryBaseDirectories(List<File> baseDirectories);
+        SessionBuilder withLocalRepositoryBaseDirectories(Collection<Path> baseDirectories);
 
         /**
          * Shortcut method to set up local repository manager directly onto builder. There must be at least one non-null
@@ -457,7 +465,39 @@ public interface RepositorySystemSession {
          * @param localRepositories The local repositories.
          * @return This session for chaining, never {@code null}.
          */
-        SessionBuilder withLocalRepositories(List<LocalRepository> localRepositories);
+        SessionBuilder withLocalRepositories(Collection<LocalRepository> localRepositories);
+
+        /**
+         * Adds the listeners to be notified of actions in the repository system.
+         *
+         * @param repositoryListeners The repository listeners, never {@code null}.
+         * @return This session for chaining, never {@code null}.
+         */
+        SessionBuilder withRepositoryListener(RepositoryListener... repositoryListeners);
+
+        /**
+         * Adds the listeners to be notified of actions in the repository system.
+         *
+         * @param repositoryListeners The repository listeners, never {@code null}.
+         * @return This session for chaining, never {@code null}.
+         */
+        SessionBuilder withRepositoryListener(Collection<RepositoryListener> repositoryListeners);
+
+        /**
+         * Adds the listener to be notified of uploads/downloads by the repository system.
+         *
+         * @param transferListeners The transfer listeners, never {@code null}.
+         * @return This session for chaining, never {@code null}.
+         */
+        SessionBuilder withTransferListener(TransferListener... transferListeners);
+
+        /**
+         * Adds the listener to be notified of uploads/downloads by the repository system.
+         *
+         * @param transferListeners The transfer listeners, never {@code null}.
+         * @return This session for chaining, never {@code null}.
+         */
+        SessionBuilder withTransferListener(Collection<TransferListener> transferListeners);
 
         /**
          * Shortcut method to shallow-copy passed in session into current builder.
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/artifact/AbstractArtifact.java b/maven-resolver-api/src/main/java/org/eclipse/aether/artifact/AbstractArtifact.java
index 92e59f23..b204069f 100644
--- a/maven-resolver-api/src/main/java/org/eclipse/aether/artifact/AbstractArtifact.java
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/artifact/AbstractArtifact.java
@@ -19,6 +19,7 @@
 package org.eclipse.aether.artifact;
 
 import java.io.File;
+import java.nio.file.Path;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
@@ -35,6 +36,7 @@ public abstract class AbstractArtifact implements Artifact {
 
     private static final Pattern SNAPSHOT_TIMESTAMP = Pattern.compile("^(.*-)?([0-9]{8}\\.[0-9]{6}-[0-9]+)$");
 
+    @Override
     public boolean isSnapshot() {
         return isSnapshot(getVersion());
     }
@@ -43,6 +45,7 @@ public abstract class AbstractArtifact implements Artifact {
         return version.endsWith(SNAPSHOT) || SNAPSHOT_TIMESTAMP.matcher(version).matches();
     }
 
+    @Override
     public String getBaseVersion() {
         return toBaseVersion(getVersion());
     }
@@ -76,28 +79,36 @@ public abstract class AbstractArtifact implements Artifact {
      * @param version The version of the artifact, may be {@code null}.
      * @param properties The properties of the artifact, may be {@code null} if none. The method may assume immutability
      *            of the supplied map, i.e. need not copy it.
-     * @param file The resolved file of the artifact, may be {@code null}.
+     * @param path The resolved file of the artifact, may be {@code null}.
      * @return The new artifact instance, never {@code null}.
      */
-    private Artifact newInstance(String version, Map<String, String> properties, File file) {
+    private Artifact newInstance(String version, Map<String, String> properties, Path path) {
         return new DefaultArtifact(
-                getGroupId(), getArtifactId(), getClassifier(), getExtension(), version, file, properties);
+                getGroupId(), getArtifactId(), getClassifier(), getExtension(), version, path, properties);
     }
 
+    @Override
     public Artifact setVersion(String version) {
         String current = getVersion();
         if (current.equals(version) || (version == null && current.isEmpty())) {
             return this;
         }
-        return newInstance(version, getProperties(), getFile());
+        return newInstance(version, getProperties(), getPath());
     }
 
+    @Deprecated
+    @Override
     public Artifact setFile(File file) {
-        File current = getFile();
-        if (Objects.equals(current, file)) {
+        return setPath(file != null ? file.toPath() : null);
+    }
+
+    @Override
+    public Artifact setPath(Path path) {
+        Path current = getPath();
+        if (Objects.equals(current, path)) {
             return this;
         }
-        return newInstance(getVersion(), getProperties(), file);
+        return newInstance(getVersion(), getProperties(), path);
     }
 
     public Artifact setProperties(Map<String, String> properties) {
@@ -105,7 +116,7 @@ public abstract class AbstractArtifact implements Artifact {
         if (current.equals(properties) || (properties == null && current.isEmpty())) {
             return this;
         }
-        return newInstance(getVersion(), copyProperties(properties), getFile());
+        return newInstance(getVersion(), copyProperties(properties), getPath());
     }
 
     public String getProperty(String key, String defaultValue) {
@@ -163,7 +174,7 @@ public abstract class AbstractArtifact implements Artifact {
                 && Objects.equals(getVersion(), that.getVersion())
                 && Objects.equals(getExtension(), that.getExtension())
                 && Objects.equals(getClassifier(), that.getClassifier())
-                && Objects.equals(getFile(), that.getFile())
+                && Objects.equals(getPath(), that.getPath())
                 && Objects.equals(getProperties(), that.getProperties());
     }
 
@@ -180,7 +191,7 @@ public abstract class AbstractArtifact implements Artifact {
         hash = hash * 31 + getExtension().hashCode();
         hash = hash * 31 + getClassifier().hashCode();
         hash = hash * 31 + getVersion().hashCode();
-        hash = hash * 31 + hash(getFile());
+        hash = hash * 31 + hash(getPath());
         return hash;
     }
 
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/artifact/Artifact.java b/maven-resolver-api/src/main/java/org/eclipse/aether/artifact/Artifact.java
index d9736c12..7f920113 100644
--- a/maven-resolver-api/src/main/java/org/eclipse/aether/artifact/Artifact.java
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/artifact/Artifact.java
@@ -19,6 +19,7 @@
 package org.eclipse.aether.artifact;
 
 import java.io.File;
+import java.nio.file.Path;
 import java.util.Map;
 
 /**
@@ -97,17 +98,39 @@ public interface Artifact {
      * callers must not assume any relationship between an artifact's filename and its coordinates.
      *
      * @return The file or {@code null} if the artifact isn't resolved.
+     * @deprecated Use {@link #getPath()} instead.
      */
+    @Deprecated
     File getFile();
 
+    /**
+     * Gets the file of this artifact. Note that only resolved artifacts have a file associated with them. In general,
+     * callers must not assume any relationship between an artifact's filename and its coordinates.
+     *
+     * @return The file or {@code null} if the artifact isn't resolved.
+     * @since 2.0.0
+     */
+    Path getPath();
+
     /**
      * Sets the file of the artifact.
      *
      * @param file The file of the artifact, may be {@code null}
      * @return The new artifact, never {@code null}.
+     * @deprecated Use {@link #setPath(Path)} instead.
      */
+    @Deprecated
     Artifact setFile(File file);
 
+    /**
+     * Sets the file of the artifact.
+     *
+     * @param path The file of the artifact, may be {@code null}
+     * @return The new artifact, never {@code null}.
+     * @since 2.0.0
+     */
+    Artifact setPath(Path path);
+
     /**
      * Gets the specified property.
      *
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/artifact/DefaultArtifact.java b/maven-resolver-api/src/main/java/org/eclipse/aether/artifact/DefaultArtifact.java
index 4e5b2105..9050da71 100644
--- a/maven-resolver-api/src/main/java/org/eclipse/aether/artifact/DefaultArtifact.java
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/artifact/DefaultArtifact.java
@@ -19,6 +19,7 @@
 package org.eclipse.aether.artifact;
 
 import java.io.File;
+import java.nio.file.Path;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
@@ -43,7 +44,7 @@ public final class DefaultArtifact extends AbstractArtifact {
 
     private final String extension;
 
-    private final File file;
+    private final Path path;
 
     private final Map<String, String> properties;
 
@@ -116,7 +117,7 @@ public final class DefaultArtifact extends AbstractArtifact {
         extension = get(m.group(4), type == null ? "jar" : type.getExtension());
         classifier = get(m.group(6), type == null ? "" : type.getClassifier());
         this.version = emptify(m.group(7));
-        this.file = null;
+        this.path = null;
         this.properties = merge(properties, (type != null) ? type.getProperties() : null);
     }
 
@@ -205,7 +206,7 @@ public final class DefaultArtifact extends AbstractArtifact {
             this.extension = emptify(type.getExtension());
         }
         this.version = emptify(version);
-        this.file = null;
+        this.path = null;
         this.properties = merge(properties, (type != null) ? type.getProperties() : null);
     }
 
@@ -253,7 +254,36 @@ public final class DefaultArtifact extends AbstractArtifact {
         this.classifier = emptify(classifier);
         this.extension = emptify(extension);
         this.version = emptify(version);
-        this.file = file;
+        this.path = file != null ? file.toPath() : null;
+        this.properties = copyProperties(properties);
+    }
+
+    /**
+     * Creates a new artifact with the specified coordinates, properties and file. Passing {@code null} for any of the
+     * coordinates is equivalent to specifying an empty string.
+     *
+     * @param groupId The group identifier of the artifact, may be {@code null}.
+     * @param artifactId The artifact identifier of the artifact, may be {@code null}.
+     * @param classifier The classifier of the artifact, may be {@code null}.
+     * @param extension The file extension of the artifact, may be {@code null}.
+     * @param version The version of the artifact, may be {@code null}.
+     * @param properties The properties of the artifact, may be {@code null} if none.
+     * @param path The resolved file of the artifact, may be {@code null}.
+     */
+    public DefaultArtifact(
+            String groupId,
+            String artifactId,
+            String classifier,
+            String extension,
+            String version,
+            Map<String, String> properties,
+            Path path) {
+        this.groupId = emptify(groupId);
+        this.artifactId = emptify(artifactId);
+        this.classifier = emptify(classifier);
+        this.extension = emptify(extension);
+        this.version = emptify(version);
+        this.path = path;
         this.properties = copyProperties(properties);
     }
 
@@ -263,7 +293,7 @@ public final class DefaultArtifact extends AbstractArtifact {
             String classifier,
             String extension,
             String version,
-            File file,
+            Path path,
             Map<String, String> properties) {
         // NOTE: This constructor assumes immutability of the provided properties, for internal use only
         this.groupId = emptify(groupId);
@@ -271,7 +301,7 @@ public final class DefaultArtifact extends AbstractArtifact {
         this.classifier = emptify(classifier);
         this.extension = emptify(extension);
         this.version = emptify(version);
-        this.file = file;
+        this.path = path;
         this.properties = properties;
     }
 
@@ -299,8 +329,13 @@ public final class DefaultArtifact extends AbstractArtifact {
         return extension;
     }
 
+    @Deprecated
     public File getFile() {
-        return file;
+        return path != null ? path.toFile() : null;
+    }
+
+    public Path getPath() {
+        return path;
     }
 
     public Map<String, String> getProperties() {
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/metadata/AbstractMetadata.java b/maven-resolver-api/src/main/java/org/eclipse/aether/metadata/AbstractMetadata.java
index 486f0cfd..8d83e091 100644
--- a/maven-resolver-api/src/main/java/org/eclipse/aether/metadata/AbstractMetadata.java
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/metadata/AbstractMetadata.java
@@ -19,6 +19,7 @@
 package org.eclipse.aether.metadata;
 
 import java.io.File;
+import java.nio.file.Path;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
@@ -29,27 +30,40 @@ import java.util.Objects;
  */
 public abstract class AbstractMetadata implements Metadata {
 
-    private Metadata newInstance(Map<String, String> properties, File file) {
+    private Metadata newInstance(Map<String, String> properties, Path path) {
         return new DefaultMetadata(
-                getGroupId(), getArtifactId(), getVersion(), getType(), getNature(), file, properties);
+                getGroupId(), getArtifactId(), getVersion(), getType(), getNature(), path, properties);
     }
 
+    @Deprecated
+    @Override
     public Metadata setFile(File file) {
         File current = getFile();
         if (Objects.equals(current, file)) {
             return this;
         }
-        return newInstance(getProperties(), file);
+        return newInstance(getProperties(), file != null ? file.toPath() : null);
+    }
+
+    @Override
+    public Metadata setPath(Path path) {
+        Path current = getPath();
+        if (Objects.equals(current, path)) {
+            return this;
+        }
+        return newInstance(getProperties(), path);
     }
 
+    @Override
     public Metadata setProperties(Map<String, String> properties) {
         Map<String, String> current = getProperties();
         if (current.equals(properties) || (properties == null && current.isEmpty())) {
             return this;
         }
-        return newInstance(copyProperties(properties), getFile());
+        return newInstance(copyProperties(properties), getPath());
     }
 
+    @Override
     public String getProperty(String key, String defaultValue) {
         String value = getProperties().get(key);
         return (value != null) ? value : defaultValue;
@@ -108,7 +122,7 @@ public abstract class AbstractMetadata implements Metadata {
                 && Objects.equals(getVersion(), that.getVersion())
                 && Objects.equals(getType(), that.getType())
                 && Objects.equals(getNature(), that.getNature())
-                && Objects.equals(getFile(), that.getFile())
+                && Objects.equals(getPath(), that.getPath())
                 && Objects.equals(getProperties(), that.getProperties());
     }
 
@@ -125,7 +139,7 @@ public abstract class AbstractMetadata implements Metadata {
         hash = hash * 31 + getType().hashCode();
         hash = hash * 31 + getNature().hashCode();
         hash = hash * 31 + getVersion().hashCode();
-        hash = hash * 31 + hash(getFile());
+        hash = hash * 31 + hash(getPath());
         return hash;
     }
 
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/metadata/DefaultMetadata.java b/maven-resolver-api/src/main/java/org/eclipse/aether/metadata/DefaultMetadata.java
index 036ced33..5eeed674 100644
--- a/maven-resolver-api/src/main/java/org/eclipse/aether/metadata/DefaultMetadata.java
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/metadata/DefaultMetadata.java
@@ -19,6 +19,7 @@
 package org.eclipse.aether.metadata;
 
 import java.io.File;
+import java.nio.file.Path;
 import java.util.Map;
 
 import static java.util.Objects.requireNonNull;
@@ -39,7 +40,7 @@ public final class DefaultMetadata extends AbstractMetadata {
 
     private final Nature nature;
 
-    private final File file;
+    private final Path path;
 
     private final Map<String, String> properties;
 
@@ -50,7 +51,7 @@ public final class DefaultMetadata extends AbstractMetadata {
      * @param nature The nature of the metadata, must not be {@code null}.
      */
     public DefaultMetadata(String type, Nature nature) {
-        this("", "", "", type, nature, null, (File) null);
+        this("", "", "", type, nature, null, (Path) null);
     }
 
     /**
@@ -61,7 +62,7 @@ public final class DefaultMetadata extends AbstractMetadata {
      * @param nature The nature of the metadata, must not be {@code null}.
      */
     public DefaultMetadata(String groupId, String type, Nature nature) {
-        this(groupId, "", "", type, nature, null, (File) null);
+        this(groupId, "", "", type, nature, null, (Path) null);
     }
 
     /**
@@ -73,7 +74,7 @@ public final class DefaultMetadata extends AbstractMetadata {
      * @param nature The nature of the metadata, must not be {@code null}.
      */
     public DefaultMetadata(String groupId, String artifactId, String type, Nature nature) {
-        this(groupId, artifactId, "", type, nature, null, (File) null);
+        this(groupId, artifactId, "", type, nature, null, (Path) null);
     }
 
     /**
@@ -86,7 +87,7 @@ public final class DefaultMetadata extends AbstractMetadata {
      * @param nature The nature of the metadata, must not be {@code null}.
      */
     public DefaultMetadata(String groupId, String artifactId, String version, String type, Nature nature) {
-        this(groupId, artifactId, version, type, nature, null, (File) null);
+        this(groupId, artifactId, version, type, nature, null, (Path) null);
     }
 
     /**
@@ -98,11 +99,28 @@ public final class DefaultMetadata extends AbstractMetadata {
      * @param type The type of the metadata, e.g. "maven-metadata.xml", may be {@code null}.
      * @param nature The nature of the metadata, must not be {@code null}.
      * @param file The resolved file of the metadata, may be {@code null}.
+     * @deprecated Use {@link #DefaultMetadata(String, String, String, String, Nature, Path)} instead.
      */
+    @Deprecated
     public DefaultMetadata(String groupId, String artifactId, String version, String type, Nature nature, File file) {
         this(groupId, artifactId, version, type, nature, null, file);
     }
 
+    /**
+     * Creates a new metadata for the groupId:artifactId:version level with the specific type and nature.
+     *
+     * @param groupId The group identifier to which this metadata applies, may be {@code null}.
+     * @param artifactId The artifact identifier to which this metadata applies, may be {@code null}.
+     * @param version The version to which this metadata applies, may be {@code null}.
+     * @param type The type of the metadata, e.g. "maven-metadata.xml", may be {@code null}.
+     * @param nature The nature of the metadata, must not be {@code null}.
+     * @param path The resolved file of the metadata, may be {@code null}.
+     * @since 2.0.0
+     */
+    public DefaultMetadata(String groupId, String artifactId, String version, String type, Nature nature, Path path) {
+        this(groupId, artifactId, version, type, nature, null, path);
+    }
+
     /**
      * Creates a new metadata for the groupId:artifactId:version level with the specific type and nature.
      *
@@ -113,7 +131,9 @@ public final class DefaultMetadata extends AbstractMetadata {
      * @param nature The nature of the metadata, must not be {@code null}.
      * @param properties The properties of the metadata, may be {@code null} if none.
      * @param file The resolved file of the metadata, may be {@code null}.
+     * @deprecated Use {@link #DefaultMetadata(String, String, String, String, Nature, Map, Path)} instead.
      */
+    @Deprecated
     public DefaultMetadata(
             String groupId,
             String artifactId,
@@ -127,7 +147,36 @@ public final class DefaultMetadata extends AbstractMetadata {
         this.version = emptify(version);
         this.type = emptify(type);
         this.nature = requireNonNull(nature, "metadata nature cannot be null");
-        this.file = file;
+        this.path = file != null ? file.toPath() : null;
+        this.properties = copyProperties(properties);
+    }
+
+    /**
+     * Creates a new metadata for the groupId:artifactId:version level with the specific type and nature.
+     *
+     * @param groupId The group identifier to which this metadata applies, may be {@code null}.
+     * @param artifactId The artifact identifier to which this metadata applies, may be {@code null}.
+     * @param version The version to which this metadata applies, may be {@code null}.
+     * @param type The type of the metadata, e.g. "maven-metadata.xml", may be {@code null}.
+     * @param nature The nature of the metadata, must not be {@code null}.
+     * @param properties The properties of the metadata, may be {@code null} if none.
+     * @param path The resolved file of the metadata, may be {@code null}.
+     * @since 2.0.0
+     */
+    public DefaultMetadata(
+            String groupId,
+            String artifactId,
+            String version,
+            String type,
+            Nature nature,
+            Map<String, String> properties,
+            Path path) {
+        this.groupId = emptify(groupId);
+        this.artifactId = emptify(artifactId);
+        this.version = emptify(version);
+        this.type = emptify(type);
+        this.nature = requireNonNull(nature, "metadata nature cannot be null");
+        this.path = path;
         this.properties = copyProperties(properties);
     }
 
@@ -137,7 +186,7 @@ public final class DefaultMetadata extends AbstractMetadata {
             String version,
             String type,
             Nature nature,
-            File file,
+            Path path,
             Map<String, String> properties) {
         // NOTE: This constructor assumes immutability of the provided properties, for internal use only
         this.groupId = emptify(groupId);
@@ -145,7 +194,7 @@ public final class DefaultMetadata extends AbstractMetadata {
         this.version = emptify(version);
         this.type = emptify(type);
         this.nature = nature;
-        this.file = file;
+        this.path = path;
         this.properties = properties;
     }
 
@@ -153,30 +202,43 @@ public final class DefaultMetadata extends AbstractMetadata {
         return (str == null) ? "" : str;
     }
 
+    @Override
     public String getGroupId() {
         return groupId;
     }
 
+    @Override
     public String getArtifactId() {
         return artifactId;
     }
 
+    @Override
     public String getVersion() {
         return version;
     }
 
+    @Override
     public String getType() {
         return type;
     }
 
+    @Override
     public Nature getNature() {
         return nature;
     }
 
+    @Deprecated
+    @Override
     public File getFile() {
-        return file;
+        return path != null ? path.toFile() : null;
+    }
+
+    @Override
+    public Path getPath() {
+        return path;
     }
 
+    @Override
     public Map<String, String> getProperties() {
         return properties;
     }
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/metadata/MergeableMetadata.java b/maven-resolver-api/src/main/java/org/eclipse/aether/metadata/MergeableMetadata.java
index 96ab2ee9..54c96a1f 100644
--- a/maven-resolver-api/src/main/java/org/eclipse/aether/metadata/MergeableMetadata.java
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/metadata/MergeableMetadata.java
@@ -19,6 +19,7 @@
 package org.eclipse.aether.metadata;
 
 import java.io.File;
+import java.nio.file.Path;
 
 import org.eclipse.aether.RepositoryException;
 
@@ -34,9 +35,24 @@ public interface MergeableMetadata extends Metadata {
      * @param current The path to the current metadata file, may not exist but must not be {@code null}.
      * @param result The path to the result file where the merged metadata should be stored, must not be {@code null}.
      * @throws RepositoryException If the metadata could not be merged.
+     * @deprecated Use {@link #merge(Path, Path)} instead.
      */
+    @Deprecated
     void merge(File current, File result) throws RepositoryException;
 
+    /**
+     * Merges this metadata into the current metadata (if any). Note that this method will be invoked regardless whether
+     * metadata currently exists or not.
+     *
+     * @param current The path to the current metadata file, may not exist but must not be {@code null}.
+     * @param result The path to the result file where the merged metadata should be stored, must not be {@code null}.
+     * @throws RepositoryException If the metadata could not be merged.
+     * @since 2.0.0
+     */
+    default void merge(Path current, Path result) throws RepositoryException {
+        merge(current.toFile(), result.toFile());
+    }
+
     /**
      * Indicates whether this metadata has been merged.
      *
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/metadata/Metadata.java b/maven-resolver-api/src/main/java/org/eclipse/aether/metadata/Metadata.java
index e957919a..fa9f84d1 100644
--- a/maven-resolver-api/src/main/java/org/eclipse/aether/metadata/Metadata.java
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/metadata/Metadata.java
@@ -19,6 +19,7 @@
 package org.eclipse.aether.metadata;
 
 import java.io.File;
+import java.nio.file.Path;
 import java.util.Map;
 
 /**
@@ -96,17 +97,38 @@ public interface Metadata {
      * Gets the file of this metadata. Note that only resolved metadata has a file associated with it.
      *
      * @return The file or {@code null} if none.
+     * @deprecated Use {@link #getPath()} instead.
      */
+    @Deprecated
     File getFile();
 
+    /**
+     * Gets the file of this metadata. Note that only resolved metadata has a file associated with it.
+     *
+     * @return The file or {@code null} if none.
+     * @since 2.0.0
+     */
+    Path getPath();
+
     /**
      * Sets the file of the metadata.
      *
      * @param file The file of the metadata, may be {@code null}
      * @return The new metadata, never {@code null}.
+     * @deprecated Use {@link #setPath(Path)} instead.
      */
+    @Deprecated
     Metadata setFile(File file);
 
+    /**
+     * Sets the file of the metadata.
+     *
+     * @param path The file of the metadata, may be {@code null}
+     * @return The new metadata, never {@code null}.
+     * @since 2.0.0
+     */
+    Metadata setPath(Path path);
+
     /**
      * Gets the specified property.
      *
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/repository/LocalArtifactResult.java b/maven-resolver-api/src/main/java/org/eclipse/aether/repository/LocalArtifactResult.java
index aa1636b3..02869c15 100644
--- a/maven-resolver-api/src/main/java/org/eclipse/aether/repository/LocalArtifactResult.java
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/repository/LocalArtifactResult.java
@@ -19,6 +19,7 @@
 package org.eclipse.aether.repository;
 
 import java.io.File;
+import java.nio.file.Path;
 
 import static java.util.Objects.requireNonNull;
 
@@ -31,7 +32,7 @@ public final class LocalArtifactResult {
 
     private final LocalArtifactRequest request;
 
-    private File file;
+    private Path path;
 
     private boolean available;
 
@@ -61,9 +62,23 @@ public final class LocalArtifactResult {
      * remote repository that is not part of the list of remote repositories used for the query.
      *
      * @return The file to the requested artifact or {@code null} if the artifact does not exist locally.
+     * @deprecated Use {@link #getPath()} instead.
      */
+    @Deprecated
     public File getFile() {
-        return file;
+        return path != null ? path.toFile() : null;
+    }
+
+    /**
+     * Gets the file to the requested artifact. Note that this file must not be used unless {@link #isAvailable()}
+     * returns {@code true}. An artifact file can be found but considered unavailable if the artifact was cached from a
+     * remote repository that is not part of the list of remote repositories used for the query.
+     *
+     * @return The file to the requested artifact or {@code null} if the artifact does not exist locally.
+     * @since 2.0.0
+     */
+    public Path getPath() {
+        return path;
     }
 
     /**
@@ -71,9 +86,22 @@ public final class LocalArtifactResult {
      *
      * @param file The artifact file, may be {@code null}.
      * @return This result for chaining, never {@code null}.
+     * @deprecated Use {@link #setPath(Path)} instead.
      */
+    @Deprecated
     public LocalArtifactResult setFile(File file) {
-        this.file = file;
+        return setPath(file != null ? file.toPath() : null);
+    }
+
+    /**
+     * Sets the file to requested artifact.
+     *
+     * @param path The artifact file, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     * @since 2.0.0
+     */
+    public LocalArtifactResult setPath(Path path) {
+        this.path = path;
         return this;
     }
 
@@ -126,6 +154,6 @@ public final class LocalArtifactResult {
 
     @Override
     public String toString() {
-        return getFile() + " (" + (isAvailable() ? "available" : "unavailable") + ")";
+        return getPath() + " (" + (isAvailable() ? "available" : "unavailable") + ")";
     }
 }
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/repository/LocalMetadataResult.java b/maven-resolver-api/src/main/java/org/eclipse/aether/repository/LocalMetadataResult.java
index a3ad6f47..7faf9fc2 100644
--- a/maven-resolver-api/src/main/java/org/eclipse/aether/repository/LocalMetadataResult.java
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/repository/LocalMetadataResult.java
@@ -19,6 +19,7 @@
 package org.eclipse.aether.repository;
 
 import java.io.File;
+import java.nio.file.Path;
 
 import static java.util.Objects.requireNonNull;
 
@@ -31,7 +32,7 @@ public final class LocalMetadataResult {
 
     private final LocalMetadataRequest request;
 
-    private File file;
+    private Path path;
 
     private boolean stale;
 
@@ -57,9 +58,21 @@ public final class LocalMetadataResult {
      * Gets the file to the requested metadata if the metadata is available in the local repository.
      *
      * @return The file to the requested metadata or {@code null}.
+     * @deprecated Use {@link #getPath()} instead.
      */
+    @Deprecated
     public File getFile() {
-        return file;
+        return path != null ? path.toFile() : null;
+    }
+
+    /**
+     * Gets the file to the requested metadata if the metadata is available in the local repository.
+     *
+     * @return The file to the requested metadata or {@code null}.
+     * @since 2.0.0
+     */
+    public Path getPath() {
+        return path;
     }
 
     /**
@@ -67,9 +80,22 @@ public final class LocalMetadataResult {
      *
      * @param file The metadata file, may be {@code null}.
      * @return This result for chaining, never {@code null}.
+     * @deprecated Use {@link #setPath(Path)} instead.
      */
+    @Deprecated
     public LocalMetadataResult setFile(File file) {
-        this.file = file;
+        return setPath(file != null ? file.toPath() : null);
+    }
+
+    /**
+     * Sets the file to requested metadata.
+     *
+     * @param path The metadata file, may be {@code null}.
+     * @return This result for chaining, never {@code null}.
+     * @since 2.0.0
+     */
+    public LocalMetadataResult setPath(Path path) {
+        this.path = path;
         return this;
     }
 
@@ -95,6 +121,6 @@ public final class LocalMetadataResult {
 
     @Override
     public String toString() {
-        return request.toString() + "(" + getFile() + ")";
+        return request.toString() + "(" + getPath() + ")";
     }
 }
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/repository/LocalRepository.java b/maven-resolver-api/src/main/java/org/eclipse/aether/repository/LocalRepository.java
index 04ec5ad8..44ae964b 100644
--- a/maven-resolver-api/src/main/java/org/eclipse/aether/repository/LocalRepository.java
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/repository/LocalRepository.java
@@ -19,6 +19,9 @@
 package org.eclipse.aether.repository;
 
 import java.io.File;
+import java.net.URI;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.util.Objects;
 
 /**
@@ -29,7 +32,7 @@ import java.util.Objects;
  */
 public final class LocalRepository implements ArtifactRepository {
 
-    private final File basedir;
+    private final Path basePath;
 
     private final String type;
 
@@ -39,33 +42,71 @@ public final class LocalRepository implements ArtifactRepository {
      * @param basedir The base directory of the repository, may be {@code null}.
      */
     public LocalRepository(String basedir) {
-        this((basedir != null) ? new File(basedir) : null, "");
+        this.basePath = Paths.get(RepositoryUriUtils.toUri(basedir)).toAbsolutePath();
+        this.type = "";
     }
 
     /**
      * Creates a new local repository with the specified base directory and unknown type.
      *
      * @param basedir The base directory of the repository, may be {@code null}.
+     * @since 2.0.0
      */
+    public LocalRepository(URI basedir) {
+        this((basedir != null) ? Paths.get(basedir) : null, "");
+    }
+
+    /**
+     * Creates a new local repository with the specified base directory and unknown type.
+     *
+     * @param basedir The base directory of the repository, may be {@code null}.
+     * @deprecated Use {@link #LocalRepository(Path)} instead.
+     */
+    @Deprecated
     public LocalRepository(File basedir) {
         this(basedir, "");
     }
 
+    /**
+     * Creates a new local repository with the specified base directory and unknown type.
+     *
+     * @param basePath The base directory of the repository, may be {@code null}.
+     * @since 2.0.0
+     */
+    public LocalRepository(Path basePath) {
+        this(basePath, "");
+    }
+
     /**
      * Creates a new local repository with the specified properties.
      *
      * @param basedir The base directory of the repository, may be {@code null}.
      * @param type The type of the repository, may be {@code null}.
+     * @deprecated Use {@link #LocalRepository(Path, String)} instead.
      */
+    @Deprecated
     public LocalRepository(File basedir, String type) {
-        this.basedir = basedir;
+        this(basedir != null ? basedir.toPath() : null, type);
+    }
+
+    /**
+     * Creates a new local repository with the specified properties.
+     *
+     * @param basePath The base directory of the repository, may be {@code null}.
+     * @param type The type of the repository, may be {@code null}.
+     * @since 2.0.0
+     */
+    public LocalRepository(Path basePath, String type) {
+        this.basePath = basePath;
         this.type = (type != null) ? type : "";
     }
 
+    @Override
     public String getContentType() {
         return type;
     }
 
+    @Override
     public String getId() {
         return "local";
     }
@@ -74,14 +115,26 @@ public final class LocalRepository implements ArtifactRepository {
      * Gets the base directory of the repository.
      *
      * @return The base directory or {@code null} if none.
+     * @deprecated Use {@link #getBasePath()} instead.
      */
+    @Deprecated
     public File getBasedir() {
-        return basedir;
+        return basePath != null ? basePath.toFile() : null;
+    }
+
+    /**
+     * Gets the base directory of the repository.
+     *
+     * @return The base directory or {@code null} if none.
+     * @since 2.0.0
+     */
+    public Path getBasePath() {
+        return basePath;
     }
 
     @Override
     public String toString() {
-        return getBasedir() + " (" + getContentType() + ")";
+        return getBasePath() + " (" + getContentType() + ")";
     }
 
     @Override
@@ -95,13 +148,13 @@ public final class LocalRepository implements ArtifactRepository {
 
         LocalRepository that = (LocalRepository) obj;
 
-        return Objects.equals(basedir, that.basedir) && Objects.equals(type, that.type);
+        return Objects.equals(basePath, that.basePath) && Objects.equals(type, that.type);
     }
 
     @Override
     public int hashCode() {
         int hash = 17;
-        hash = hash * 31 + hash(basedir);
+        hash = hash * 31 + hash(basePath);
         hash = hash * 31 + hash(type);
         return hash;
     }
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/repository/NoLocalRepositoryManagerException.java b/maven-resolver-api/src/main/java/org/eclipse/aether/repository/NoLocalRepositoryManagerException.java
index 66c34873..05580406 100644
--- a/maven-resolver-api/src/main/java/org/eclipse/aether/repository/NoLocalRepositoryManagerException.java
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/repository/NoLocalRepositoryManagerException.java
@@ -72,7 +72,7 @@ public class NoLocalRepositoryManagerException extends RepositoryException {
     private static String toMessage(LocalRepository repository) {
         if (repository != null) {
             return "No manager available for local repository ("
-                    + repository.getBasedir().getAbsolutePath() + ") of type " + repository.getContentType();
+                    + repository.getBasePath().toAbsolutePath() + ") of type " + repository.getContentType();
         } else {
             return "No manager available for local repository";
         }
diff --git a/maven-resolver-transport-file/src/main/java/org/eclipse/aether/transport/file/PathUtils.java b/maven-resolver-api/src/main/java/org/eclipse/aether/repository/RepositoryUriUtils.java
similarity index 79%
rename from maven-resolver-transport-file/src/main/java/org/eclipse/aether/transport/file/PathUtils.java
rename to maven-resolver-api/src/main/java/org/eclipse/aether/repository/RepositoryUriUtils.java
index 5b0813b6..6fa08526 100644
--- a/maven-resolver-transport-file/src/main/java/org/eclipse/aether/transport/file/PathUtils.java
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/repository/RepositoryUriUtils.java
@@ -16,23 +16,39 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.eclipse.aether.transport.file;
+package org.eclipse.aether.repository;
+
+import java.io.File;
+import java.net.URI;
 
 /**
  * URL handling for file URLs. Based on org.apache.maven.wagon.PathUtils.
+ *
+ * @since 2.0.0
  */
-final class PathUtils {
+public final class RepositoryUriUtils {
+
+    private RepositoryUriUtils() {}
 
-    private PathUtils() {}
+    public static URI toUri(String repositoryUrl) {
+        final String protocol = protocol(repositoryUrl);
+        if ("file".equals(protocol)
+                || protocol.isEmpty()
+                || protocol.length() == 1
+                        && (Character.isLetter(protocol.charAt(0)) && Character.isUpperCase(protocol.charAt(0)))) {
+            return new File(basedir(repositoryUrl)).toURI();
+        } else {
+            return URI.create(repositoryUrl);
+        }
+    }
 
     /**
-     * Return the protocol name. <br/>
-     * E.g: for input <code>http://www.codehause.org</code> this method will return <code>http</code>
+     * Return the protocol name.
      *
      * @param url the url
-     * @return the host name
+     * @return the protocol or empty string.
      */
-    public static String protocol(final String url) {
+    private static String protocol(final String url) {
         final int pos = url.indexOf(":");
 
         if (pos == -1) {
@@ -47,10 +63,10 @@ final class PathUtils {
      * @param url the file-repository URL
      * @return the basedir of the repository
      */
-    public static String basedir(String url) {
-        String protocol = PathUtils.protocol(url);
+    private static String basedir(String url) {
+        String protocol = protocol(url);
 
-        String retValue = null;
+        String retValue;
 
         if (!protocol.isEmpty()) {
             retValue = url.substring(protocol.length() + 1);
@@ -97,7 +113,7 @@ final class PathUtils {
      * @param url The URL to decode, may be <code>null</code>.
      * @return The decoded URL or <code>null</code> if the input was <code>null</code>.
      */
-    static String decode(String url) {
+    private static String decode(String url) {
         String decoded = url;
         if (url != null) {
             int pos = -1;
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/repository/WorkspaceReader.java b/maven-resolver-api/src/main/java/org/eclipse/aether/repository/WorkspaceReader.java
index 92127bbe..5a55e5f1 100644
--- a/maven-resolver-api/src/main/java/org/eclipse/aether/repository/WorkspaceReader.java
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/repository/WorkspaceReader.java
@@ -19,6 +19,7 @@
 package org.eclipse.aether.repository;
 
 import java.io.File;
+import java.nio.file.Path;
 import java.util.List;
 
 import org.eclipse.aether.artifact.Artifact;
@@ -45,6 +46,18 @@ public interface WorkspaceReader {
      */
     File findArtifact(Artifact artifact);
 
+    /**
+     * Locates the specified artifact.
+     *
+     * @param artifact The artifact to locate, must not be {@code null}.
+     * @return The path to the artifact or {@code null} if the artifact is not available.
+     * @since 2.0.0
+     */
+    default Path findArtifactPath(Artifact artifact) {
+        File file = findArtifact(artifact);
+        return file != null ? file.toPath() : null;
+    }
+
     /**
      * Determines all available versions of the specified artifact.
      *
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/ArtifactResolutionException.java b/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/ArtifactResolutionException.java
index a45ea579..0ec971b2 100644
--- a/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/ArtifactResolutionException.java
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/ArtifactResolutionException.java
@@ -102,7 +102,7 @@ public class ArtifactResolutionException extends RepositoryException {
                 LocalArtifactResult localResult = result.getLocalArtifactResult();
                 if (localResult != null) {
                     buffer.append(" (");
-                    if (localResult.getFile() != null) {
+                    if (localResult.getPath() != null) {
                         buffer.append("present");
                         if (!localResult.isAvailable()) {
                             buffer.append(", but unavailable");
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/ArtifactResult.java b/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/ArtifactResult.java
index 3ee3d046..d155d09d 100644
--- a/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/ArtifactResult.java
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/ArtifactResult.java
@@ -34,7 +34,7 @@ import static java.util.Objects.requireNonNull;
  * The result of an artifact resolution request.
  *
  * @see RepositorySystem#resolveArtifacts(org.eclipse.aether.RepositorySystemSession, java.util.Collection)
- * @see Artifact#getFile()
+ * @see Artifact#getPath()
  */
 public final class ArtifactResult {
 
@@ -217,10 +217,10 @@ public final class ArtifactResult {
      * of the specified remote repositories.
      *
      * @return {@code true} if the artifact was resolved, {@code false} otherwise.
-     * @see Artifact#getFile()
+     * @see Artifact#getPath()
      */
     public boolean isResolved() {
-        return getArtifact() != null && getArtifact().getFile() != null;
+        return getArtifact() != null && getArtifact().getPath() != null;
     }
 
     /**
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/MetadataResult.java b/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/MetadataResult.java
index ddb648c3..f9155414 100644
--- a/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/MetadataResult.java
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/resolution/MetadataResult.java
@@ -127,10 +127,10 @@ public final class MetadataResult {
      * refetch the metadata from the remote repository.
      *
      * @return {@code true} if the metadata was resolved, {@code false} otherwise.
-     * @see Metadata#getFile()
+     * @see Metadata#getPath()
      */
     public boolean isResolved() {
-        return getMetadata() != null && getMetadata().getFile() != null;
+        return getMetadata() != null && getMetadata().getPath() != null;
     }
 
     /**
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/transfer/MetadataNotFoundException.java b/maven-resolver-api/src/main/java/org/eclipse/aether/transfer/MetadataNotFoundException.java
index f7f9c2d9..fdc2093f 100644
--- a/maven-resolver-api/src/main/java/org/eclipse/aether/transfer/MetadataNotFoundException.java
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/transfer/MetadataNotFoundException.java
@@ -41,7 +41,7 @@ public class MetadataNotFoundException extends MetadataTransferException {
         if (repository == null) {
             return "";
         } else {
-            return prefix + repository.getId() + " (" + repository.getBasedir() + ")";
+            return prefix + repository.getId() + " (" + repository.getBasePath().toAbsolutePath() + ")";
         }
     }
 
diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/transfer/TransferResource.java b/maven-resolver-api/src/main/java/org/eclipse/aether/transfer/TransferResource.java
index e2abb67c..824c40af 100644
--- a/maven-resolver-api/src/main/java/org/eclipse/aether/transfer/TransferResource.java
+++ b/maven-resolver-api/src/main/java/org/eclipse/aether/transfer/TransferResource.java
@@ -19,6 +19,7 @@
 package org.eclipse.aether.transfer;
 
 import java.io.File;
+import java.nio.file.Path;
 
 import org.eclipse.aether.RequestTrace;
 
@@ -33,7 +34,7 @@ public final class TransferResource {
 
     private final String resourceName;
 
-    private final File file;
+    private final Path path;
 
     private final long startTime;
 
@@ -56,9 +57,30 @@ public final class TransferResource {
      * @param trace The trace information, may be {@code null}.
      *
      * @since 1.1.0
+     * @deprecated Use {@link TransferResource(String, String, String, Path, RequestTrace)} instead.
      */
+    @Deprecated
     public TransferResource(
             String repositoryId, String repositoryUrl, String resourceName, File file, RequestTrace trace) {
+        this(repositoryId, repositoryUrl, resourceName, file != null ? file.toPath() : null, trace);
+    }
+
+    /**
+     * Creates a new transfer resource with the specified properties.
+     *
+     * @param repositoryId The ID of the repository used to transfer the resource, may be {@code null} or
+     *                     empty if unknown.
+     * @param repositoryUrl The base URL of the repository, may be {@code null} or empty if unknown. If not empty, a
+     *            trailing slash will automatically be added if missing.
+     * @param resourceName The relative path to the resource within the repository, may be {@code null}. A leading slash
+     *            (if any) will be automatically removed.
+     * @param path The source/target file involved in the transfer, may be {@code null}.
+     * @param trace The trace information, may be {@code null}.
+     *
+     * @since 2.0.0
+     */
+    public TransferResource(
+            String repositoryId, String repositoryUrl, String resourceName, Path path, RequestTrace trace) {
         if (repositoryId == null || repositoryId.isEmpty()) {
             this.repositoryId = "";
         } else {
@@ -81,7 +103,7 @@ public final class TransferResource {
             this.resourceName = resourceName;
         }
 
-        this.file = file;
+        this.path = path;
 
         this.trace = trace;
 
@@ -100,7 +122,7 @@ public final class TransferResource {
     }
 
     /**
-     * The base URL of the repository, e.g. "http://repo1.maven.org/maven2/". Unless the URL is unknown, it will be
+     * The base URL of the repository, e.g. "https://repo1.maven.org/maven2/". Unless the URL is unknown, it will be
      * terminated by a trailing slash.
      *
      * @return The base URL of the repository or an empty string if unknown, never {@code null}.
@@ -123,9 +145,22 @@ public final class TransferResource {
      * remote resource, no local file will be involved in the transfer.
      *
      * @return The source/target file involved in the transfer or {@code null} if none.
+     * @deprecated Use {@link #getPath()} instead.
      */
+    @Deprecated
     public File getFile() {
-        return file;
+        return path != null ? path.toFile() : null;
+    }
+
+    /**
+     * Gets the local file being uploaded or downloaded. When the repository system merely checks for the existence of a
+     * remote resource, no local file will be involved in the transfer.
+     *
+     * @return The source/target file involved in the transfer or {@code null} if none.
+     * @since 2.0.0
+     */
+    public Path getPath() {
+        return path;
     }
 
     /**
@@ -195,6 +230,6 @@ public final class TransferResource {
 
     @Override
     public String toString() {
-        return getRepositoryUrl() + getResourceName() + " <> " + getFile();
+        return getRepositoryUrl() + getResourceName() + " <> " + getPath();
     }
 }
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/TrackingFileManager.java b/maven-resolver-api/src/test/java/org/eclipse/aether/repository/LocalRepositoryTest.java
similarity index 53%
copy from maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/TrackingFileManager.java
copy to maven-resolver-api/src/test/java/org/eclipse/aether/repository/LocalRepositoryTest.java
index 5e922bf5..ce45c8cb 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/TrackingFileManager.java
+++ b/maven-resolver-api/src/test/java/org/eclipse/aether/repository/LocalRepositoryTest.java
@@ -16,24 +16,37 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.eclipse.aether.internal.impl;
+package org.eclipse.aether.repository;
 
 import java.io.File;
-import java.util.Map;
-import java.util.Properties;
+import java.nio.file.FileSystems;
 
-/**
- * Manages access to a properties file.
- */
-public interface TrackingFileManager {
-    /**
-     * Reads up the specified properties file into {@link Properties}, if exists, otherwise {@code null} is returned.
-     */
-    Properties read(File file);
+import org.junit.jupiter.api.Test;
+
+public class LocalRepositoryTest {
+    @Test
+    void cwd() {
+        doIt(new File("."));
+    }
+
+    @Test
+    void allRoots() {
+        FileSystems.getDefault().getRootDirectories().forEach(p -> doIt(new File(p.toUri())));
+    }
+
+    @Test
+    void root() {
+        doIt(new File(File.separator));
+    }
+
+    @Test
+    void allSomePaths() {
+        FileSystems.getDefault()
+                .getRootDirectories()
+                .forEach(p -> doIt(new File(p.resolve("some/path").toUri())));
+    }
 
-    /**
-     * 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);
+    private void doIt(File file) {
+        new LocalRepository(file.toPath().toUri().toASCIIString());
+    }
 }
diff --git a/maven-resolver-api/src/test/java/org/eclipse/aether/transfer/TransferEventTest.java b/maven-resolver-api/src/test/java/org/eclipse/aether/transfer/TransferEventTest.java
index d9fd517f..6b4c0fe6 100644
--- a/maven-resolver-api/src/test/java/org/eclipse/aether/transfer/TransferEventTest.java
+++ b/maven-resolver-api/src/test/java/org/eclipse/aether/transfer/TransferEventTest.java
@@ -19,6 +19,7 @@
 package org.eclipse.aether.transfer;
 
 import java.nio.ByteBuffer;
+import java.nio.file.Path;
 
 import org.eclipse.aether.RepositorySystemSession;
 import org.junit.jupiter.api.Test;
@@ -30,7 +31,7 @@ import static org.mockito.Mockito.mock;
  */
 public class TransferEventTest {
 
-    private static final TransferResource res = new TransferResource("none", "file://nil", "void", null, null);
+    private static final TransferResource res = new TransferResource("none", "file://nil", "void", (Path) null, null);
 
     private static final RepositorySystemSession session = mock(RepositorySystemSession.class);
 
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 b699ffa7..572f2a62 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
@@ -18,10 +18,10 @@
  */
 package org.eclipse.aether.connector.basic;
 
-import java.io.File;
 import java.io.IOException;
 import java.io.UncheckedIOException;
 import java.net.URI;
+import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -51,7 +51,7 @@ import org.eclipse.aether.spi.connector.transport.PeekTask;
 import org.eclipse.aether.spi.connector.transport.PutTask;
 import org.eclipse.aether.spi.connector.transport.Transporter;
 import org.eclipse.aether.spi.connector.transport.TransporterProvider;
-import org.eclipse.aether.spi.io.FileProcessor;
+import org.eclipse.aether.spi.io.ChecksumProcessor;
 import org.eclipse.aether.transfer.ChecksumFailureException;
 import org.eclipse.aether.transfer.NoRepositoryConnectorException;
 import org.eclipse.aether.transfer.NoRepositoryLayoutException;
@@ -83,7 +83,7 @@ final class BasicRepositoryConnector implements RepositoryConnector {
 
     private final Map<String, ProvidedChecksumsSource> providedChecksumsSources;
 
-    private final FileProcessor fileProcessor;
+    private final ChecksumProcessor checksumProcessor;
 
     private final RemoteRepository repository;
 
@@ -113,7 +113,7 @@ final class BasicRepositoryConnector implements RepositoryConnector {
             TransporterProvider transporterProvider,
             RepositoryLayoutProvider layoutProvider,
             ChecksumPolicyProvider checksumPolicyProvider,
-            FileProcessor fileProcessor,
+            ChecksumProcessor checksumProcessor,
             Map<String, ProvidedChecksumsSource> providedChecksumsSources)
             throws NoRepositoryConnectorException {
         try {
@@ -130,7 +130,7 @@ final class BasicRepositoryConnector implements RepositoryConnector {
 
         this.session = session;
         this.repository = repository;
-        this.fileProcessor = fileProcessor;
+        this.checksumProcessor = checksumProcessor;
         this.providedChecksumsSources = providedChecksumsSources;
         this.closed = new AtomicBoolean(false);
 
@@ -191,7 +191,7 @@ final class BasicRepositoryConnector implements RepositoryConnector {
         for (MetadataDownload transfer : safeMetadataDownloads) {
             URI location = layout.getLocation(transfer.getMetadata(), false);
 
-            TransferResource resource = newTransferResource(location, transfer.getFile(), transfer.getTrace());
+            TransferResource resource = newTransferResource(location, transfer.getPath(), transfer.getTrace());
             TransferEvent.Builder builder = newEventBuilder(resource, false, false);
             MetadataTransportListener listener = new MetadataTransportListener(transfer, repository, builder);
 
@@ -203,7 +203,7 @@ final class BasicRepositoryConnector implements RepositoryConnector {
 
             Runnable task = new GetTaskRunner(
                     location,
-                    transfer.getFile(),
+                    transfer.getPath(),
                     checksumPolicy,
                     checksumAlgorithmFactories,
                     checksumLocations,
@@ -231,7 +231,7 @@ final class BasicRepositoryConnector implements RepositoryConnector {
 
             URI location = layout.getLocation(transfer.getArtifact(), false);
 
-            TransferResource resource = newTransferResource(location, transfer.getFile(), transfer.getTrace());
+            TransferResource resource = newTransferResource(location, transfer.getPath(), transfer.getTrace());
             TransferEvent.Builder builder = newEventBuilder(resource, false, transfer.isExistenceCheck());
             ArtifactTransportListener listener = new ArtifactTransportListener(transfer, repository, builder);
 
@@ -247,7 +247,7 @@ final class BasicRepositoryConnector implements RepositoryConnector {
 
                 task = new GetTaskRunner(
                         location,
-                        transfer.getFile(),
+                        transfer.getPath(),
                         checksumPolicy,
                         checksumAlgorithmFactories,
                         checksumLocations,
@@ -282,14 +282,14 @@ final class BasicRepositoryConnector implements RepositoryConnector {
         for (ArtifactUpload transfer : safeArtifactUploads) {
             URI location = layout.getLocation(transfer.getArtifact(), true);
 
-            TransferResource resource = newTransferResource(location, transfer.getFile(), transfer.getTrace());
+            TransferResource resource = newTransferResource(location, transfer.getPath(), transfer.getTrace());
             TransferEvent.Builder builder = newEventBuilder(resource, true, false);
             ArtifactTransportListener listener = new ArtifactTransportListener(transfer, repository, builder);
 
             List<RepositoryLayout.ChecksumLocation> checksumLocations =
                     layout.getChecksumLocations(transfer.getArtifact(), true, location);
 
-            Runnable task = new PutTaskRunner(location, transfer.getFile(), checksumLocations, listener);
+            Runnable task = new PutTaskRunner(location, transfer.getPath(), checksumLocations, listener);
             if (first) {
                 task.run();
                 first = false;
@@ -304,14 +304,14 @@ final class BasicRepositoryConnector implements RepositoryConnector {
             for (MetadataUpload transfer : transferGroup) {
                 URI location = layout.getLocation(transfer.getMetadata(), true);
 
-                TransferResource resource = newTransferResource(location, transfer.getFile(), transfer.getTrace());
+                TransferResource resource = newTransferResource(location, transfer.getPath(), transfer.getTrace());
                 TransferEvent.Builder builder = newEventBuilder(resource, true, false);
                 MetadataTransportListener listener = new MetadataTransportListener(transfer, repository, builder);
 
                 List<RepositoryLayout.ChecksumLocation> checksumLocations =
                         layout.getChecksumLocations(transfer.getMetadata(), true, location);
 
-                Runnable task = new PutTaskRunner(location, transfer.getFile(), checksumLocations, listener);
+                Runnable task = new PutTaskRunner(location, transfer.getPath(), checksumLocations, listener);
                 if (first) {
                     task.run();
                     first = false;
@@ -368,7 +368,7 @@ final class BasicRepositoryConnector implements RepositoryConnector {
         return (items != null) ? items : Collections.emptyList();
     }
 
-    private TransferResource newTransferResource(URI path, File file, RequestTrace trace) {
+    private TransferResource newTransferResource(URI path, Path file, RequestTrace trace) {
         return new TransferResource(repository.getId(), repository.getUrl(), path.toString(), file, trace);
     }
 
@@ -432,13 +432,13 @@ final class BasicRepositoryConnector implements RepositoryConnector {
 
     class GetTaskRunner extends TaskRunner implements ChecksumValidator.ChecksumFetcher {
 
-        private final File file;
+        private final Path file;
 
         private final ChecksumValidator checksumValidator;
 
         GetTaskRunner(
                 URI path,
-                File file,
+                Path file,
                 ChecksumPolicy checksumPolicy,
                 List<ChecksumAlgorithmFactory> checksumAlgorithmFactories,
                 List<RepositoryLayout.ChecksumLocation> checksumLocations,
@@ -449,7 +449,7 @@ final class BasicRepositoryConnector implements RepositoryConnector {
             checksumValidator = new ChecksumValidator(
                     file,
                     checksumAlgorithmFactories,
-                    fileProcessor,
+                    checksumProcessor,
                     this,
                     checksumPolicy,
                     providedChecksums,
@@ -457,9 +457,9 @@ final class BasicRepositoryConnector implements RepositoryConnector {
         }
 
         @Override
-        public boolean fetchChecksum(URI remote, File local) throws Exception {
+        public boolean fetchChecksum(URI remote, Path local) throws Exception {
             try {
-                transporter.get(new GetTask(remote).setDataFile(local));
+                transporter.get(new GetTask(remote).setDataPath(local));
             } catch (Exception e) {
                 if (transporter.classify(e) == Transporter.ERROR_NOT_FOUND) {
                     return false;
@@ -471,11 +471,11 @@ final class BasicRepositoryConnector implements RepositoryConnector {
 
         @Override
         protected void runTask() throws Exception {
-            try (FileUtils.CollocatedTempFile tempFile = FileUtils.newTempFile(file.toPath())) {
-                final File tmp = tempFile.getPath().toFile();
+            try (FileUtils.CollocatedTempFile tempFile = FileUtils.newTempFile(file)) {
+                final Path tmp = tempFile.getPath();
                 listener.setChecksumCalculator(checksumValidator.newChecksumCalculator(tmp));
                 for (int firstTrial = 0, lastTrial = 1, trial = firstTrial; ; trial++) {
-                    GetTask task = new GetTask(path).setDataFile(tmp, false).setListener(listener);
+                    GetTask task = new GetTask(path).setDataPath(tmp, false).setListener(listener);
                     transporter.get(task);
                     try {
                         checksumValidator.validate(
@@ -504,13 +504,13 @@ final class BasicRepositoryConnector implements RepositoryConnector {
 
     class PutTaskRunner extends TaskRunner {
 
-        private final File file;
+        private final Path file;
 
         private final Collection<RepositoryLayout.ChecksumLocation> checksumLocations;
 
         PutTaskRunner(
                 URI path,
-                File file,
+                Path file,
                 List<RepositoryLayout.ChecksumLocation> checksumLocations,
                 TransferTransportListener<?> listener) {
             super(path, listener);
@@ -521,15 +521,15 @@ final class BasicRepositoryConnector implements RepositoryConnector {
         @SuppressWarnings("checkstyle:innerassignment")
         @Override
         protected void runTask() throws Exception {
-            transporter.put(new PutTask(path).setDataFile(file).setListener(listener));
+            transporter.put(new PutTask(path).setDataPath(file).setListener(listener));
             uploadChecksums(file, null);
         }
 
         /**
-         * @param file  source
+         * @param path  source
          * @param bytes transformed data from file or {@code null}
          */
-        private void uploadChecksums(File file, byte[] bytes) {
+        private void uploadChecksums(Path path, byte[] bytes) {
             if (checksumLocations.isEmpty()) {
                 return;
             }
@@ -543,7 +543,7 @@ final class BasicRepositoryConnector implements RepositoryConnector {
                 if (bytes != null) {
                     sumsByAlgo = ChecksumAlgorithmHelper.calculate(bytes, algorithms);
                 } else {
-                    sumsByAlgo = ChecksumAlgorithmHelper.calculate(file, algorithms);
+                    sumsByAlgo = ChecksumAlgorithmHelper.calculate(path, algorithms);
                 }
 
                 for (RepositoryLayout.ChecksumLocation checksumLocation : checksumLocations) {
diff --git a/maven-resolver-connector-basic/src/main/java/org/eclipse/aether/connector/basic/BasicRepositoryConnectorFactory.java b/maven-resolver-connector-basic/src/main/java/org/eclipse/aether/connector/basic/BasicRepositoryConnectorFactory.java
index 82699519..b16610e8 100644
--- a/maven-resolver-connector-basic/src/main/java/org/eclipse/aether/connector/basic/BasicRepositoryConnectorFactory.java
+++ b/maven-resolver-connector-basic/src/main/java/org/eclipse/aether/connector/basic/BasicRepositoryConnectorFactory.java
@@ -31,7 +31,7 @@ import org.eclipse.aether.spi.connector.RepositoryConnectorFactory;
 import org.eclipse.aether.spi.connector.checksum.ChecksumPolicyProvider;
 import org.eclipse.aether.spi.connector.layout.RepositoryLayoutProvider;
 import org.eclipse.aether.spi.connector.transport.TransporterProvider;
-import org.eclipse.aether.spi.io.FileProcessor;
+import org.eclipse.aether.spi.io.ChecksumProcessor;
 import org.eclipse.aether.transfer.NoRepositoryConnectorException;
 
 import static java.util.Objects.requireNonNull;
@@ -50,7 +50,7 @@ public final class BasicRepositoryConnectorFactory implements RepositoryConnecto
 
     private final ChecksumPolicyProvider checksumPolicyProvider;
 
-    private final FileProcessor fileProcessor;
+    private final ChecksumProcessor checksumProcessor;
 
     private final Map<String, ProvidedChecksumsSource> providedChecksumsSources;
 
@@ -61,12 +61,12 @@ public final class BasicRepositoryConnectorFactory implements RepositoryConnecto
             TransporterProvider transporterProvider,
             RepositoryLayoutProvider layoutProvider,
             ChecksumPolicyProvider checksumPolicyProvider,
-            FileProcessor fileProcessor,
+            ChecksumProcessor checksumProcessor,
             Map<String, ProvidedChecksumsSource> providedChecksumsSources) {
         this.transporterProvider = requireNonNull(transporterProvider, "transporter provider cannot be null");
         this.layoutProvider = requireNonNull(layoutProvider, "repository layout provider cannot be null");
         this.checksumPolicyProvider = requireNonNull(checksumPolicyProvider, "checksum policy provider cannot be null");
-        this.fileProcessor = requireNonNull(fileProcessor, "file processor cannot be null");
+        this.checksumProcessor = requireNonNull(checksumProcessor, "checksum processor cannot be null");
         this.providedChecksumsSources =
                 requireNonNull(providedChecksumsSources, "provided checksum sources cannot be null");
     }
@@ -99,7 +99,7 @@ public final class BasicRepositoryConnectorFactory implements RepositoryConnecto
                 transporterProvider,
                 layoutProvider,
                 checksumPolicyProvider,
-                fileProcessor,
+                checksumProcessor,
                 providedChecksumsSources);
     }
 }
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 94fe01fe..aa892121 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
@@ -19,12 +19,12 @@
 package org.eclipse.aether.connector.basic;
 
 import java.io.BufferedInputStream;
-import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 import java.nio.Buffer;
 import java.nio.ByteBuffer;
 import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
@@ -78,17 +78,17 @@ final class ChecksumCalculator {
 
     private final List<Checksum> checksums;
 
-    private final File targetFile;
+    private final Path targetFile;
 
     public static ChecksumCalculator newInstance(
-            File targetFile, Collection<ChecksumAlgorithmFactory> checksumAlgorithmFactories) {
+            Path targetFile, Collection<ChecksumAlgorithmFactory> checksumAlgorithmFactories) {
         if (checksumAlgorithmFactories == null || checksumAlgorithmFactories.isEmpty()) {
             return null;
         }
         return new ChecksumCalculator(targetFile, checksumAlgorithmFactories);
     }
 
-    private ChecksumCalculator(File targetFile, Collection<ChecksumAlgorithmFactory> checksumAlgorithmFactories) {
+    private ChecksumCalculator(Path targetFile, Collection<ChecksumAlgorithmFactory> checksumAlgorithmFactories) {
         this.checksums = new ArrayList<>();
         Set<String> algos = new HashSet<>();
         for (ChecksumAlgorithmFactory checksumAlgorithmFactory : checksumAlgorithmFactories) {
@@ -107,7 +107,7 @@ final class ChecksumCalculator {
             return;
         }
 
-        try (InputStream in = new BufferedInputStream(Files.newInputStream(targetFile.toPath()))) {
+        try (InputStream in = new BufferedInputStream(Files.newInputStream(targetFile))) {
             long total = 0;
             final byte[] buffer = new byte[1024 * 32];
             while (total < dataOffset) {
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 6cfb425d..24fdbc3a 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
@@ -18,10 +18,10 @@
  */
 package org.eclipse.aether.connector.basic;
 
-import java.io.File;
 import java.io.IOException;
 import java.io.UncheckedIOException;
 import java.net.URI;
+import java.nio.file.Path;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.Map;
@@ -30,7 +30,7 @@ import org.eclipse.aether.spi.connector.checksum.ChecksumAlgorithmFactory;
 import org.eclipse.aether.spi.connector.checksum.ChecksumPolicy;
 import org.eclipse.aether.spi.connector.checksum.ChecksumPolicy.ChecksumKind;
 import org.eclipse.aether.spi.connector.layout.RepositoryLayout.ChecksumLocation;
-import org.eclipse.aether.spi.io.FileProcessor;
+import org.eclipse.aether.spi.io.ChecksumProcessor;
 import org.eclipse.aether.transfer.ChecksumFailureException;
 import org.eclipse.aether.util.FileUtils;
 import org.slf4j.Logger;
@@ -47,16 +47,16 @@ final class ChecksumValidator {
          * Fetches the checksums from remote location into provided local file. The checksums fetched in this way
          * are of kind {@link ChecksumKind#REMOTE_EXTERNAL}.
          */
-        boolean fetchChecksum(URI remote, File local) throws Exception;
+        boolean fetchChecksum(URI remote, Path local) throws Exception;
     }
 
     private static final Logger LOGGER = LoggerFactory.getLogger(ChecksumValidator.class);
 
-    private final File dataFile;
+    private final Path dataPath;
 
     private final Collection<ChecksumAlgorithmFactory> checksumAlgorithmFactories;
 
-    private final FileProcessor fileProcessor;
+    private final ChecksumProcessor checksumProcessor;
 
     private final ChecksumFetcher checksumFetcher;
 
@@ -66,19 +66,19 @@ final class ChecksumValidator {
 
     private final Collection<ChecksumLocation> checksumLocations;
 
-    private final Map<File, String> checksumExpectedValues;
+    private final Map<Path, String> checksumExpectedValues;
 
     ChecksumValidator(
-            File dataFile,
+            Path dataPath,
             Collection<ChecksumAlgorithmFactory> checksumAlgorithmFactories,
-            FileProcessor fileProcessor,
+            ChecksumProcessor checksumProcessor,
             ChecksumFetcher checksumFetcher,
             ChecksumPolicy checksumPolicy,
             Map<String, String> providedChecksums,
             Collection<ChecksumLocation> checksumLocations) {
-        this.dataFile = dataFile;
+        this.dataPath = dataPath;
         this.checksumAlgorithmFactories = checksumAlgorithmFactories;
-        this.fileProcessor = fileProcessor;
+        this.checksumProcessor = checksumProcessor;
         this.checksumFetcher = checksumFetcher;
         this.checksumPolicy = checksumPolicy;
         this.providedChecksums = providedChecksums;
@@ -86,7 +86,7 @@ final class ChecksumValidator {
         this.checksumExpectedValues = new HashMap<>();
     }
 
-    public ChecksumCalculator newChecksumCalculator(File targetFile) {
+    public ChecksumCalculator newChecksumCalculator(Path targetFile) {
         if (checksumPolicy != null) {
             return ChecksumCalculator.newInstance(targetFile, checksumAlgorithmFactories);
         }
@@ -131,7 +131,7 @@ final class ChecksumValidator {
 
             String actual = String.valueOf(calculated);
             String expected = entry.getValue().toString();
-            checksumExpectedValues.put(getChecksumFile(checksumAlgorithmFactory), expected);
+            checksumExpectedValues.put(getChecksumPath(checksumAlgorithmFactory), expected);
 
             if (!isEqualChecksum(expected, actual)) {
                 checksumPolicy.onChecksumMismatch(
@@ -155,9 +155,9 @@ final class ChecksumValidator {
                                 calculated));
                 continue;
             }
-            File checksumFile = getChecksumFile(checksumLocation.getChecksumAlgorithmFactory());
+            Path checksumFile = getChecksumPath(checksumLocation.getChecksumAlgorithmFactory());
             try (FileUtils.TempFile tempFile = FileUtils.newTempFile()) {
-                File tmp = tempFile.getPath().toFile();
+                Path tmp = tempFile.getPath();
                 try {
                     if (!checksumFetcher.fetchChecksum(checksumLocation.getLocation(), tmp)) {
                         continue;
@@ -169,7 +169,7 @@ final class ChecksumValidator {
                 }
 
                 String actual = String.valueOf(calculated);
-                String expected = fileProcessor.readChecksum(tmp);
+                String expected = checksumProcessor.readChecksum(tmp);
                 checksumExpectedValues.put(checksumFile, expected);
 
                 if (!isEqualChecksum(expected, actual)) {
@@ -192,8 +192,8 @@ final class ChecksumValidator {
         return expected.equalsIgnoreCase(actual);
     }
 
-    private File getChecksumFile(ChecksumAlgorithmFactory factory) {
-        return new File(dataFile.getPath() + '.' + factory.getFileExtension());
+    private Path getChecksumPath(ChecksumAlgorithmFactory factory) {
+        return dataPath.getParent().resolve(dataPath.getFileName() + "." + factory.getFileExtension());
     }
 
     public void retry() {
@@ -206,10 +206,10 @@ final class ChecksumValidator {
     }
 
     public void commit() {
-        for (Map.Entry<File, String> entry : checksumExpectedValues.entrySet()) {
-            File checksumFile = entry.getKey();
+        for (Map.Entry<Path, String> entry : checksumExpectedValues.entrySet()) {
+            Path checksumFile = entry.getKey();
             try {
-                fileProcessor.writeChecksum(checksumFile, entry.getValue());
+                checksumProcessor.writeChecksum(checksumFile, entry.getValue());
             } catch (IOException e) {
                 LOGGER.debug("Failed to write checksum file {}", checksumFile, e);
                 throw new UncheckedIOException(e);
diff --git a/maven-resolver-connector-basic/src/test/java/org/eclipse/aether/connector/basic/ChecksumCalculatorTest.java b/maven-resolver-connector-basic/src/test/java/org/eclipse/aether/connector/basic/ChecksumCalculatorTest.java
index 9641bfc1..6ccfe0ed 100644
--- a/maven-resolver-connector-basic/src/test/java/org/eclipse/aether/connector/basic/ChecksumCalculatorTest.java
+++ b/maven-resolver-connector-basic/src/test/java/org/eclipse/aether/connector/basic/ChecksumCalculatorTest.java
@@ -48,7 +48,7 @@ public class ChecksumCalculatorTest {
         for (String algo : algos) {
             checksumAlgorithmFactories.add(selector.select(algo));
         }
-        return ChecksumCalculator.newInstance(file, checksumAlgorithmFactories);
+        return ChecksumCalculator.newInstance(file.toPath(), checksumAlgorithmFactories);
     }
 
     private ByteBuffer toBuffer(String data) {
diff --git a/maven-resolver-connector-basic/src/test/java/org/eclipse/aether/connector/basic/ChecksumValidatorTest.java b/maven-resolver-connector-basic/src/test/java/org/eclipse/aether/connector/basic/ChecksumValidatorTest.java
index 27afc216..6a896086 100644
--- a/maven-resolver-connector-basic/src/test/java/org/eclipse/aether/connector/basic/ChecksumValidatorTest.java
+++ b/maven-resolver-connector-basic/src/test/java/org/eclipse/aether/connector/basic/ChecksumValidatorTest.java
@@ -21,6 +21,8 @@ package org.eclipse.aether.connector.basic;
 import java.io.File;
 import java.io.IOException;
 import java.net.URI;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
@@ -28,7 +30,7 @@ import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 
-import org.eclipse.aether.internal.test.util.TestFileProcessor;
+import org.eclipse.aether.internal.test.util.TestChecksumProcessor;
 import org.eclipse.aether.internal.test.util.TestFileUtils;
 import org.eclipse.aether.spi.connector.checksum.ChecksumAlgorithmFactory;
 import org.eclipse.aether.spi.connector.checksum.ChecksumPolicy;
@@ -113,12 +115,12 @@ public class ChecksumValidatorTest {
 
         HashMap<URI, Object> checksums = new HashMap<>();
 
-        ArrayList<File> checksumFiles = new ArrayList<>();
+        ArrayList<Path> checksumFiles = new ArrayList<>();
 
         private final ArrayList<URI> fetchedFiles = new ArrayList<>();
 
         @Override
-        public boolean fetchChecksum(URI remote, File local) throws Exception {
+        public boolean fetchChecksum(URI remote, Path local) throws Exception {
             fetchedFiles.add(remote);
             Object checksum = checksums.get(remote);
             if (checksum == null) {
@@ -127,7 +129,7 @@ public class ChecksumValidatorTest {
             if (checksum instanceof Exception) {
                 throw (Exception) checksum;
             }
-            TestFileUtils.writeString(local, checksum.toString());
+            TestFileUtils.writeString(local.toFile(), checksum.toString());
             checksumFiles.add(local);
             return true;
         }
@@ -185,9 +187,9 @@ public class ChecksumValidatorTest {
     private ChecksumValidator newValidator(Map<String, String> providedChecksums, String... factories) {
         List<ChecksumAlgorithmFactory> checksumAlgorithmFactories = newChecksumAlgorithmFactories(factories);
         return new ChecksumValidator(
-                dataFile,
+                dataFile.toPath(),
                 checksumAlgorithmFactories,
-                new TestFileProcessor(),
+                new TestChecksumProcessor(),
                 fetcher,
                 policy,
                 providedChecksums,
@@ -368,8 +370,8 @@ public class ChecksumValidatorTest {
         fetcher.assertFetchedFiles(SHA1);
         assertEquals(1, fetcher.checksumFiles.size());
         validator.retry();
-        for (File file : fetcher.checksumFiles) {
-            assertFalse(file.exists(), file.getAbsolutePath());
+        for (Path file : fetcher.checksumFiles) {
+            assertFalse(Files.exists(file), file.toAbsolutePath().toString());
         }
     }
 
@@ -387,8 +389,8 @@ public class ChecksumValidatorTest {
         checksumFile = new File(dataFile.getPath() + ".md5");
         assertTrue(checksumFile.isFile(), checksumFile.getAbsolutePath());
         assertEquals("bar", TestFileUtils.readString(checksumFile));
-        for (File file : fetcher.checksumFiles) {
-            assertFalse(file.exists(), file.getAbsolutePath());
+        for (Path file : fetcher.checksumFiles) {
+            assertFalse(Files.exists(file), file.toAbsolutePath().toString());
         }
     }
 
@@ -399,8 +401,8 @@ public class ChecksumValidatorTest {
         validator.validate(checksums(SHA1, "foo"), null);
         fetcher.assertFetchedFiles(SHA1);
         assertEquals(1, fetcher.checksumFiles.size());
-        for (File file : fetcher.checksumFiles) {
-            assertFalse(file.exists(), file.getAbsolutePath());
+        for (Path file : fetcher.checksumFiles) {
+            assertFalse(Files.exists(file), file.toAbsolutePath().toString());
         }
     }
 }
diff --git a/maven-resolver-demos/maven-resolver-demo-snippets/pom.xml b/maven-resolver-demos/maven-resolver-demo-snippets/pom.xml
index 3cd6eb8f..1e3c53e6 100644
--- a/maven-resolver-demos/maven-resolver-demo-snippets/pom.xml
+++ b/maven-resolver-demos/maven-resolver-demo-snippets/pom.xml
@@ -97,6 +97,10 @@
       <groupId>org.eclipse.sisu</groupId>
       <artifactId>org.eclipse.sisu.inject</artifactId>
     </dependency>
+    <dependency>
+      <groupId>com.google.jimfs</groupId>
+      <artifactId>jimfs</artifactId>
+    </dependency>
     <dependency>
       <groupId>com.google.inject</groupId>
       <artifactId>guice</artifactId>
diff --git a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/DeployArtifacts.java b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/DeployArtifacts.java
index e06f7b6e..fcdd9964 100644
--- a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/DeployArtifacts.java
+++ b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/DeployArtifacts.java
@@ -48,10 +48,10 @@ public class DeployArtifacts {
                         Booter.newRepositorySystemSession(system).build()) {
             Artifact jarArtifact =
                     new DefaultArtifact("test", "org.apache.maven.aether.examples", "", "jar", "0.1-SNAPSHOT");
-            jarArtifact = jarArtifact.setFile(new File("src/main/data/demo.jar"));
+            jarArtifact = jarArtifact.setPath(new File("src/main/data/demo.jar").toPath());
 
             Artifact pomArtifact = new SubArtifact(jarArtifact, "", "pom");
-            pomArtifact = pomArtifact.setFile(new File("pom.xml"));
+            pomArtifact = pomArtifact.setPath(new File("pom.xml").toPath());
 
             RemoteRepository distRepo = new RemoteRepository.Builder(
                             "org.apache.maven.aether.examples",
diff --git a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/InstallArtifacts.java b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/InstallArtifacts.java
index b06c34e1..d285f10b 100644
--- a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/InstallArtifacts.java
+++ b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/InstallArtifacts.java
@@ -47,10 +47,10 @@ public class InstallArtifacts {
                         Booter.newRepositorySystemSession(system).build()) {
             Artifact jarArtifact =
                     new DefaultArtifact("test", "org.apache.maven.resolver.examples", "", "jar", "0.1-SNAPSHOT");
-            jarArtifact = jarArtifact.setFile(new File("src/main/data/demo.jar"));
+            jarArtifact = jarArtifact.setPath(new File("src/main/data/demo.jar").toPath());
 
             Artifact pomArtifact = new SubArtifact(jarArtifact, "", "pom");
-            pomArtifact = pomArtifact.setFile(new File("pom.xml"));
+            pomArtifact = pomArtifact.setPath(new File("pom.xml").toPath());
 
             InstallRequest installRequest = new InstallRequest();
             installRequest.addArtifact(jarArtifact).addArtifact(pomArtifact);
diff --git a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/ResolveArtifact.java b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/ResolveArtifact.java
index cbc81d08..b41a10ab 100644
--- a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/ResolveArtifact.java
+++ b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/ResolveArtifact.java
@@ -20,7 +20,6 @@ package org.apache.maven.resolver.examples;
 
 import org.apache.maven.resolver.examples.util.Booter;
 import org.eclipse.aether.RepositorySystem;
-import org.eclipse.aether.RepositorySystemSession;
 import org.eclipse.aether.RepositorySystemSession.CloseableSession;
 import org.eclipse.aether.artifact.Artifact;
 import org.eclipse.aether.artifact.DefaultArtifact;
@@ -43,12 +42,12 @@ public class ResolveArtifact {
         System.out.println(ResolveArtifact.class.getSimpleName());
 
         try (RepositorySystem system = Booter.newRepositorySystem(Booter.selectFactory(args))) {
-            RepositorySystemSession.SessionBuilder sessionBuilder = Booter.newRepositorySystemSession(system);
             Artifact artifact;
             ArtifactRequest artifactRequest;
             ArtifactResult artifactResult;
 
-            try (CloseableSession session = sessionBuilder.build()) {
+            try (CloseableSession session =
+                    Booter.newRepositorySystemSession(system).build()) {
                 artifact = new DefaultArtifact("org.apache.maven.resolver:maven-resolver-util:1.3.3");
 
                 artifactRequest = new ArtifactRequest();
@@ -59,13 +58,13 @@ public class ResolveArtifact {
 
                 artifact = artifactResult.getArtifact();
 
-                System.out.println(artifact + " resolved to  " + artifact.getFile());
+                System.out.println(artifact + " resolved to  " + artifact.getPath());
             }
 
             // signature
-            sessionBuilder.setChecksumPolicy(RepositoryPolicy.CHECKSUM_POLICY_FAIL);
-
-            try (CloseableSession session = sessionBuilder.build()) {
+            try (CloseableSession session = Booter.newRepositorySystemSession(system)
+                    .setChecksumPolicy(RepositoryPolicy.CHECKSUM_POLICY_FAIL)
+                    .build()) {
                 artifact = new DefaultArtifact("org.apache.maven.resolver:maven-resolver-util:jar.asc:1.3.3");
 
                 artifactRequest = new ArtifactRequest();
@@ -76,7 +75,7 @@ public class ResolveArtifact {
 
                 artifact = artifactResult.getArtifact();
 
-                System.out.println(artifact + " resolved signature to  " + artifact.getFile());
+                System.out.println(artifact + " resolved signature to  " + artifact.getPath());
             }
         }
     }
diff --git a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/ResolveTransitiveDependencies.java b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/ResolveTransitiveDependencies.java
index 35c215b2..a606c352 100644
--- a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/ResolveTransitiveDependencies.java
+++ b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/ResolveTransitiveDependencies.java
@@ -52,20 +52,20 @@ public class ResolveTransitiveDependencies {
                         Booter.newRepositorySystemSession(system).build()) {
             Artifact artifact = new DefaultArtifact("org.apache.maven.resolver:maven-resolver-impl:1.3.3");
 
-            DependencyFilter classpathFlter = DependencyFilterUtils.classpathFilter(JavaScopes.COMPILE);
+            DependencyFilter classpathFilter = DependencyFilterUtils.classpathFilter(JavaScopes.COMPILE);
 
             CollectRequest collectRequest = new CollectRequest();
             collectRequest.setRoot(new Dependency(artifact, JavaScopes.COMPILE));
             collectRequest.setRepositories(Booter.newRepositories(system, session));
 
-            DependencyRequest dependencyRequest = new DependencyRequest(collectRequest, classpathFlter);
+            DependencyRequest dependencyRequest = new DependencyRequest(collectRequest, classpathFilter);
 
             List<ArtifactResult> artifactResults =
                     system.resolveDependencies(session, dependencyRequest).getArtifactResults();
 
             for (ArtifactResult artifactResult : artifactResults) {
                 System.out.println(artifactResult.getArtifact() + " resolved to "
-                        + artifactResult.getArtifact().getFile());
+                        + artifactResult.getArtifact().getPath());
             }
         }
     }
diff --git a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/ReverseDependencyTree.java b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/ReverseDependencyTree.java
index 7a9d95f2..b730e428 100644
--- a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/ReverseDependencyTree.java
+++ b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/ReverseDependencyTree.java
@@ -22,7 +22,6 @@ import org.apache.maven.resolver.examples.util.Booter;
 import org.apache.maven.resolver.examples.util.ReverseTreeRepositoryListener;
 import org.eclipse.aether.RepositorySystem;
 import org.eclipse.aether.RepositorySystemSession.CloseableSession;
-import org.eclipse.aether.RepositorySystemSession.SessionBuilder;
 import org.eclipse.aether.artifact.Artifact;
 import org.eclipse.aether.artifact.DefaultArtifact;
 import org.eclipse.aether.collection.CollectRequest;
@@ -30,7 +29,6 @@ import org.eclipse.aether.resolution.ArtifactDescriptorRequest;
 import org.eclipse.aether.resolution.ArtifactDescriptorResult;
 import org.eclipse.aether.util.graph.manager.DependencyManagerUtils;
 import org.eclipse.aether.util.graph.transformer.ConflictResolver;
-import org.eclipse.aether.util.listener.ChainedRepositoryListener;
 
 /**
  * Example of building reverse dependency tree using custom {@link ReverseTreeRepositoryListener}.
@@ -47,14 +45,11 @@ public class ReverseDependencyTree {
         System.out.println(ReverseDependencyTree.class.getSimpleName());
 
         try (RepositorySystem system = Booter.newRepositorySystem(Booter.selectFactory(args))) {
-            SessionBuilder sessionBuilder = Booter.newRepositorySystemSession(system);
-            try (CloseableSession session = sessionBuilder.build()) {
-                sessionBuilder.setRepositoryListener(new ChainedRepositoryListener(
-                        session.getRepositoryListener(), new ReverseTreeRepositoryListener()));
-            }
-            sessionBuilder.setConfigProperty(ConflictResolver.CONFIG_PROP_VERBOSE, true);
-            sessionBuilder.setConfigProperty(DependencyManagerUtils.CONFIG_PROP_VERBOSE, true);
-            try (CloseableSession session = sessionBuilder.build()) {
+            try (CloseableSession session = Booter.newRepositorySystemSession(system)
+                    .withRepositoryListener(new ReverseTreeRepositoryListener())
+                    .setConfigProperty(ConflictResolver.CONFIG_PROP_VERBOSE, true)
+                    .setConfigProperty(DependencyManagerUtils.CONFIG_PROP_VERBOSE, true)
+                    .build()) {
                 Artifact artifact = new DefaultArtifact("org.apache.maven:maven-resolver-provider:3.6.1");
 
                 ArtifactDescriptorRequest descriptorRequest = new ArtifactDescriptorRequest();
diff --git a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/supplier/SupplierRepositorySystemFactory.java b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/supplier/SupplierRepositorySystemFactory.java
index 3980a9fb..bc69a836 100644
--- a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/supplier/SupplierRepositorySystemFactory.java
+++ b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/supplier/SupplierRepositorySystemFactory.java
@@ -35,7 +35,9 @@ public class SupplierRepositorySystemFactory {
             @Override
             protected Map<String, TransporterFactory> createTransporterFactories() {
                 Map<String, TransporterFactory> result = super.createTransporterFactories();
-                result.put(JdkTransporterFactory.NAME, new JdkTransporterFactory(getChecksumExtractor()));
+                result.put(
+                        JdkTransporterFactory.NAME,
+                        new JdkTransporterFactory(getChecksumExtractor(), getPathProcessor()));
                 result.put(JettyTransporterFactory.NAME, new JettyTransporterFactory(getChecksumExtractor()));
                 return result;
             }
diff --git a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/util/Booter.java b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/util/Booter.java
index 4ec87581..c9301855 100644
--- a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/util/Booter.java
+++ b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/util/Booter.java
@@ -18,7 +18,7 @@
  */
 package org.apache.maven.resolver.examples.util;
 
-import java.io.File;
+import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
@@ -61,11 +61,22 @@ public class Booter {
     }
 
     public static SessionBuilder newRepositorySystemSession(RepositorySystem system) {
-        return new SessionBuilderSupplier(system)
+        // FileSystem fs = Jimfs.newFileSystem(Configuration.unix());
+        SessionBuilder result = new SessionBuilderSupplier(system)
                 .get()
-                .withLocalRepositoryBaseDirectories(new File("target/local-repo"))
+                //        .withLocalRepositoryBaseDirectories(fs.getPath("local-repo"))
+                .withLocalRepositoryBaseDirectories(Paths.get("target/local-repo"))
                 .setRepositoryListener(new ConsoleRepositoryListener())
                 .setTransferListener(new ConsoleTransferListener());
+        result.setConfigProperty("aether.syncContext.named.factory", "noop");
+        // result.addOnSessionEndedHandler(() -> {
+        //     try {
+        //         fs.close();
+        //     } catch (IOException e) {
+        //         throw new UncheckedIOException(e);
+        //     }
+        // });
+        return result;
         // uncomment to generate dirty trees
         // session.setDependencyGraphTransformer( null );
     }
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/UpdateCheck.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/UpdateCheck.java
index 2f991a71..2fa4635f 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/UpdateCheck.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/impl/UpdateCheck.java
@@ -19,6 +19,7 @@
 package org.eclipse.aether.impl;
 
 import java.io.File;
+import java.nio.file.Path;
 
 import org.eclipse.aether.RepositoryException;
 import org.eclipse.aether.repository.RemoteRepository;
@@ -37,7 +38,7 @@ public final class UpdateCheck<T, E extends RepositoryException> {
 
     private T item;
 
-    private File file;
+    private Path path;
 
     private boolean fileValid = true;
 
@@ -108,9 +109,21 @@ public final class UpdateCheck<T, E extends RepositoryException> {
      * Returns the local file of the item.
      *
      * @return The local file of the item.
+     * @deprecated Use {@link #getPath()} instead.
      */
+    @Deprecated
     public File getFile() {
-        return file;
+        return path != null ? path.toFile() : null;
+    }
+
+    /**
+     * Returns the local file of the item.
+     *
+     * @return The local file of the item.
+     * @since 2.0.0
+     */
+    public Path getPath() {
+        return path;
     }
 
     /**
@@ -118,9 +131,22 @@ public final class UpdateCheck<T, E extends RepositoryException> {
      *
      * @param file The file of the item, never {@code null} .
      * @return This object for chaining.
+     * @deprecated Use {@link #setPath(Path)} instead.
      */
+    @Deprecated
     public UpdateCheck<T, E> setFile(File file) {
-        this.file = file;
+        return setPath(file != null ? file.toPath() : null);
+    }
+
+    /**
+     * Sets the local file of the item.
+     *
+     * @param path The file of the item, never {@code null} .
+     * @return This object for chaining.
+     * @since 2.0.0
+     */
+    public UpdateCheck<T, E> setPath(Path path) {
+        this.path = path;
         return this;
     }
 
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultArtifactResolver.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultArtifactResolver.java
index f0980f68..ca27ab8e 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultArtifactResolver.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultArtifactResolver.java
@@ -22,8 +22,11 @@ import javax.inject.Inject;
 import javax.inject.Named;
 import javax.inject.Singleton;
 
-import java.io.File;
 import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.attribute.FileTime;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -59,7 +62,7 @@ import org.eclipse.aether.resolution.VersionResult;
 import org.eclipse.aether.spi.connector.ArtifactDownload;
 import org.eclipse.aether.spi.connector.RepositoryConnector;
 import org.eclipse.aether.spi.connector.filter.RemoteRepositoryFilter;
-import org.eclipse.aether.spi.io.FileProcessor;
+import org.eclipse.aether.spi.io.PathProcessor;
 import org.eclipse.aether.spi.resolution.ArtifactResolverPostProcessor;
 import org.eclipse.aether.spi.synccontext.SyncContextFactory;
 import org.eclipse.aether.transfer.ArtifactFilteredOutException;
@@ -110,7 +113,7 @@ public class DefaultArtifactResolver implements ArtifactResolver {
 
     private static final Logger LOGGER = LoggerFactory.getLogger(DefaultArtifactResolver.class);
 
-    private final FileProcessor fileProcessor;
+    private final PathProcessor pathProcessor;
 
     private final RepositoryEventDispatcher repositoryEventDispatcher;
 
@@ -133,7 +136,7 @@ public class DefaultArtifactResolver implements ArtifactResolver {
     @SuppressWarnings("checkstyle:parameternumber")
     @Inject
     public DefaultArtifactResolver(
-            FileProcessor fileProcessor,
+            PathProcessor pathProcessor,
             RepositoryEventDispatcher repositoryEventDispatcher,
             VersionResolver versionResolver,
             UpdateCheckManager updateCheckManager,
@@ -143,7 +146,7 @@ public class DefaultArtifactResolver implements ArtifactResolver {
             OfflineController offlineController,
             Map<String, ArtifactResolverPostProcessor> artifactResolverPostProcessors,
             RemoteRepositoryFilterManager remoteRepositoryFilterManager) {
-        this.fileProcessor = requireNonNull(fileProcessor, "file processor cannot be null");
+        this.pathProcessor = requireNonNull(pathProcessor, "path processor cannot be null");
         this.repositoryEventDispatcher =
                 requireNonNull(repositoryEventDispatcher, "repository event dispatcher cannot be null");
         this.versionResolver = requireNonNull(versionResolver, "version resolver cannot be null");
@@ -227,13 +230,13 @@ public class DefaultArtifactResolver implements ArtifactResolver {
                     String localPath = session.getSystemScopeHandler().getSystemPath(artifact);
                     if (localPath != null) {
                         // unhosted artifact, just validate file
-                        File file = new File(localPath);
-                        if (!file.isFile()) {
+                        Path path = Paths.get(localPath);
+                        if (!Files.isRegularFile(path)) {
                             failures = true;
                             result.addException(
                                     ArtifactResult.NO_REPOSITORY, new ArtifactNotFoundException(artifact, localPath));
                         } else {
-                            artifact = artifact.setFile(file);
+                            artifact = artifact.setPath(path);
                             result.setArtifact(artifact);
                             artifactResolved(session, trace, artifact, null, result.getExceptions());
                         }
@@ -282,9 +285,9 @@ public class DefaultArtifactResolver implements ArtifactResolver {
                     }
 
                     if (workspace != null) {
-                        File file = workspace.findArtifact(artifact);
-                        if (file != null) {
-                            artifact = artifact.setFile(file);
+                        Path path = workspace.findArtifactPath(artifact);
+                        if (path != null) {
+                            artifact = artifact.setPath(path);
                             result.setArtifact(artifact);
                             result.setRepository(workspace.getRepository());
                             artifactResolved(session, trace, artifact, result.getRepository(), null);
@@ -309,7 +312,7 @@ public class DefaultArtifactResolver implements ArtifactResolver {
                         }
 
                         try {
-                            artifact = artifact.setFile(getFile(session, artifact, local.getFile()));
+                            artifact = artifact.setPath(getPath(session, artifact, local.getPath()));
                             result.setArtifact(artifact);
                             artifactResolved(session, trace, artifact, result.getRepository(), null);
                         } catch (ArtifactTransferException e) {
@@ -328,7 +331,7 @@ public class DefaultArtifactResolver implements ArtifactResolver {
                         continue;
                     }
 
-                    if (local.getFile() != null) {
+                    if (local.getPath() != null) {
                         LOGGER.info(
                                 "Artifact {} is present in the local repository, but cached from a remote repository ID that is unavailable in current build context, verifying that is downloadable from {}",
                                 artifact,
@@ -393,7 +396,7 @@ public class DefaultArtifactResolver implements ArtifactResolver {
                     ArtifactRequest request = result.getRequest();
 
                     Artifact artifact = result.getArtifact();
-                    if (artifact == null || artifact.getFile() == null) {
+                    if (artifact == null || artifact.getPath() == null) {
                         failures = true;
                         if (result.getExceptions().isEmpty()) {
                             Exception exception =
@@ -420,7 +423,7 @@ public class DefaultArtifactResolver implements ArtifactResolver {
         if (lar.isAvailable()) {
             return true;
         }
-        if (lar.getFile() != null) {
+        if (lar.getPath() != null) {
             // resolution of version range found locally installed artifact
             if (vr.getRepository() instanceof LocalRepository) {
                 // resolution of (snapshot) version found locally installed artifact
@@ -433,29 +436,31 @@ public class DefaultArtifactResolver implements ArtifactResolver {
         return false;
     }
 
-    private File getFile(RepositorySystemSession session, Artifact artifact, File file)
+    private Path getPath(RepositorySystemSession session, Artifact artifact, Path path)
             throws ArtifactTransferException {
         if (artifact.isSnapshot()
                 && !artifact.getVersion().equals(artifact.getBaseVersion())
                 && ConfigUtils.getBoolean(
                         session, DEFAULT_SNAPSHOT_NORMALIZATION, CONFIG_PROP_SNAPSHOT_NORMALIZATION)) {
-            String name = file.getName().replace(artifact.getVersion(), artifact.getBaseVersion());
-            File dst = new File(file.getParent(), name);
-
-            boolean copy = dst.length() != file.length() || dst.lastModified() != file.lastModified();
-            if (copy) {
-                try {
-                    fileProcessor.copy(file, dst);
-                    dst.setLastModified(file.lastModified());
-                } catch (IOException e) {
-                    throw new ArtifactTransferException(artifact, null, e);
+            String name = path.getFileName().toString().replace(artifact.getVersion(), artifact.getBaseVersion());
+            Path dst = path.getParent().resolve(name);
+
+            try {
+                long pathLastModified = pathProcessor.lastModified(path, 0L);
+                boolean copy = pathProcessor.size(dst, 0L) != pathProcessor.size(path, 0L)
+                        || pathProcessor.lastModified(dst, 0L) != pathLastModified;
+                if (copy) {
+                    pathProcessor.copy(path, dst);
+                    Files.setLastModifiedTime(dst, FileTime.fromMillis(pathLastModified));
                 }
+            } catch (IOException e) {
+                throw new ArtifactTransferException(artifact, null, e);
             }
 
-            file = dst;
+            path = dst;
         }
 
-        return file;
+        return path;
     }
 
     private void performDownloads(RepositorySystemSession session, ResolutionGroup group) {
@@ -499,13 +504,13 @@ public class DefaultArtifactResolver implements ArtifactResolver {
             download.setRequestContext(item.request.getRequestContext());
             download.setListener(SafeTransferListener.wrap(session));
             download.setTrace(item.trace);
-            if (item.local.getFile() != null) {
-                download.setFile(item.local.getFile());
+            if (item.local.getPath() != null) {
+                download.setPath(item.local.getPath());
                 download.setExistenceCheck(true);
             } else {
                 String path =
                         lrm.getPathForRemoteArtifact(artifact, group.repository, item.request.getRequestContext());
-                download.setFile(new File(lrm.getRepository().getBasedir(), path));
+                download.setPath(lrm.getRepository().getBasePath().resolve(path));
             }
 
             boolean snapshot = artifact.isSnapshot();
@@ -515,7 +520,7 @@ public class DefaultArtifactResolver implements ArtifactResolver {
             if ((errorPolicy & ResolutionErrorPolicy.CACHE_ALL) != 0) {
                 UpdateCheck<Artifact, ArtifactTransferException> check = new UpdateCheck<>();
                 check.setItem(artifact);
-                check.setFile(download.getFile());
+                check.setPath(download.getPath());
                 check.setFileValid(false);
                 check.setRepository(group.repository);
                 check.setArtifactPolicy(policy.getArtifactUpdatePolicy());
@@ -551,7 +556,7 @@ public class DefaultArtifactResolver implements ArtifactResolver {
                 item.resolved.set(true);
                 item.result.setRepository(group.repository);
                 try {
-                    artifact = artifact.setFile(getFile(session, artifact, download.getFile()));
+                    artifact = artifact.setPath(getPath(session, artifact, download.getPath()));
                     item.result.setArtifact(artifact);
 
                     lrm.add(
@@ -601,7 +606,7 @@ public class DefaultArtifactResolver implements ArtifactResolver {
         event.setRepository(repository);
         event.setExceptions(exceptions != null ? new ArrayList<>(exceptions) : null);
         if (artifact != null) {
-            event.setFile(artifact.getFile());
+            event.setPath(artifact.getPath());
         }
 
         repositoryEventDispatcher.dispatch(event.build());
@@ -629,7 +634,7 @@ public class DefaultArtifactResolver implements ArtifactResolver {
         event.setRepository(repository);
         event.setException(exception);
         if (artifact != null) {
-            event.setFile(artifact.getFile());
+            event.setPath(artifact.getPath());
         }
 
         repositoryEventDispatcher.dispatch(event.build());
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultChecksumProcessor.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultChecksumProcessor.java
new file mode 100644
index 00000000..cc1a3f69
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultChecksumProcessor.java
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.eclipse.aether.internal.impl;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import org.eclipse.aether.spi.io.ChecksumProcessor;
+import org.eclipse.aether.spi.io.PathProcessor;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * A utility class helping with file-based operations.
+ */
+@Singleton
+@Named
+public class DefaultChecksumProcessor implements ChecksumProcessor {
+    private final PathProcessor pathProcessor;
+
+    @Inject
+    public DefaultChecksumProcessor(PathProcessor pathProcessor) {
+        this.pathProcessor = requireNonNull(pathProcessor);
+    }
+
+    @Override
+    public String readChecksum(final Path checksumPath) throws IOException {
+        // for now do exactly same as happened before, but FileProcessor is a component and can be replaced
+        String checksum = "";
+        try (BufferedReader br = new BufferedReader(
+                new InputStreamReader(Files.newInputStream(checksumPath), StandardCharsets.UTF_8), 512)) {
+            while (true) {
+                String line = br.readLine();
+                if (line == null) {
+                    break;
+                }
+                line = line.trim();
+                if (!line.isEmpty()) {
+                    checksum = line;
+                    break;
+                }
+            }
+        }
+
+        if (checksum.matches(".+= [0-9A-Fa-f]+")) {
+            int lastSpacePos = checksum.lastIndexOf(' ');
+            checksum = checksum.substring(lastSpacePos + 1);
+        } else {
+            int spacePos = checksum.indexOf(' ');
+
+            if (spacePos != -1) {
+                checksum = checksum.substring(0, spacePos);
+            }
+        }
+
+        return checksum;
+    }
+
+    @Override
+    public void writeChecksum(Path target, String checksum) throws IOException {
+        // for now do exactly same as happened before, but FileProcessor is a component and can be replaced
+        pathProcessor.write(target, checksum);
+    }
+}
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultDeployer.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultDeployer.java
index 20c13407..ede10657 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultDeployer.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultDeployer.java
@@ -22,8 +22,9 @@ import javax.inject.Inject;
 import javax.inject.Named;
 import javax.inject.Singleton;
 
-import java.io.File;
 import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -60,7 +61,7 @@ import org.eclipse.aether.spi.connector.ArtifactUpload;
 import org.eclipse.aether.spi.connector.MetadataDownload;
 import org.eclipse.aether.spi.connector.MetadataUpload;
 import org.eclipse.aether.spi.connector.RepositoryConnector;
-import org.eclipse.aether.spi.io.FileProcessor;
+import org.eclipse.aether.spi.io.PathProcessor;
 import org.eclipse.aether.spi.synccontext.SyncContextFactory;
 import org.eclipse.aether.transfer.ArtifactTransferException;
 import org.eclipse.aether.transfer.MetadataNotFoundException;
@@ -77,7 +78,7 @@ import static java.util.Objects.requireNonNull;
 @Singleton
 @Named
 public class DefaultDeployer implements Deployer {
-    private final FileProcessor fileProcessor;
+    private final PathProcessor pathProcessor;
 
     private final RepositoryEventDispatcher repositoryEventDispatcher;
 
@@ -96,7 +97,7 @@ public class DefaultDeployer implements Deployer {
     @SuppressWarnings("checkstyle:parameternumber")
     @Inject
     public DefaultDeployer(
-            FileProcessor fileProcessor,
+            PathProcessor pathProcessor,
             RepositoryEventDispatcher repositoryEventDispatcher,
             RepositoryConnectorProvider repositoryConnectorProvider,
             RemoteRepositoryManager remoteRepositoryManager,
@@ -104,7 +105,7 @@ public class DefaultDeployer implements Deployer {
             Map<String, MetadataGeneratorFactory> metadataFactories,
             SyncContextFactory syncContextFactory,
             OfflineController offlineController) {
-        this.fileProcessor = requireNonNull(fileProcessor, "file processor cannot be null");
+        this.pathProcessor = requireNonNull(pathProcessor, "path processor cannot be null");
         this.repositoryEventDispatcher =
                 requireNonNull(repositoryEventDispatcher, "repository event dispatcher cannot be null");
         this.repositoryConnectorProvider =
@@ -179,7 +180,7 @@ public class DefaultDeployer implements Deployer {
 
                 iterator.set(artifact);
 
-                ArtifactUpload upload = new ArtifactUpload(artifact, artifact.getFile());
+                ArtifactUpload upload = new ArtifactUpload(artifact, artifact.getPath());
                 upload.setTrace(trace);
                 upload.setListener(new ArtifactUploadListener(catapult, upload));
                 artifactUploads.add(upload);
@@ -257,9 +258,9 @@ public class DefaultDeployer implements Deployer {
             EventCatapult catapult)
             throws DeploymentException {
         LocalRepositoryManager lrm = session.getLocalRepositoryManager();
-        File basedir = lrm.getRepository().getBasedir();
+        Path basePath = lrm.getRepository().getBasePath();
 
-        File dstFile = new File(basedir, lrm.getPathForRemoteMetadata(metadata, repository, ""));
+        Path dstPath = basePath.resolve(lrm.getPathForRemoteMetadata(metadata, repository, ""));
 
         if (metadata instanceof MergeableMetadata) {
             if (!((MergeableMetadata) metadata).isMerged()) {
@@ -278,7 +279,7 @@ public class DefaultDeployer implements Deployer {
                 RepositoryPolicy policy = getPolicy(session, repository, metadata.getNature());
                 MetadataDownload download = new MetadataDownload();
                 download.setMetadata(metadata);
-                download.setFile(dstFile);
+                download.setPath(dstPath);
                 download.setChecksumPolicy(policy.getChecksumPolicy());
                 download.setListener(SafeTransferListener.wrap(session));
                 download.setTrace(catapult.getTrace());
@@ -287,7 +288,12 @@ public class DefaultDeployer implements Deployer {
                 Exception error = download.getException();
 
                 if (error instanceof MetadataNotFoundException) {
-                    dstFile.delete();
+                    try {
+                        Files.deleteIfExists(dstPath);
+                    } catch (IOException e) {
+                        throw new DeploymentException(
+                                "Failed to delete cached metadata " + metadata + ": " + e.getMessage(), e);
+                    }
                 }
 
                 event = new RepositoryEvent.Builder(session, EventType.METADATA_DOWNLOADED);
@@ -295,7 +301,7 @@ public class DefaultDeployer implements Deployer {
                 event.setMetadata(metadata);
                 event.setRepository(repository);
                 event.setException(error);
-                event.setFile(dstFile);
+                event.setPath(dstPath);
                 repositoryEventDispatcher.dispatch(event.build());
 
                 event = new RepositoryEvent.Builder(session, EventType.METADATA_RESOLVED);
@@ -303,7 +309,7 @@ public class DefaultDeployer implements Deployer {
                 event.setMetadata(metadata);
                 event.setRepository(repository);
                 event.setException(error);
-                event.setFile(dstFile);
+                event.setPath(dstPath);
                 repositoryEventDispatcher.dispatch(event.build());
 
                 if (error != null && !(error instanceof MetadataNotFoundException)) {
@@ -313,16 +319,16 @@ public class DefaultDeployer implements Deployer {
             }
 
             try {
-                ((MergeableMetadata) metadata).merge(dstFile, dstFile);
+                ((MergeableMetadata) metadata).merge(dstPath, dstPath);
             } catch (RepositoryException e) {
                 throw new DeploymentException("Failed to update metadata " + metadata + ": " + e.getMessage(), e);
             }
         } else {
-            if (metadata.getFile() == null) {
+            if (metadata.getPath() == null) {
                 throw new DeploymentException("Failed to update metadata " + metadata + ": No file attached.");
             }
             try {
-                fileProcessor.copy(metadata.getFile(), dstFile);
+                pathProcessor.copy(metadata.getPath(), dstPath);
             } catch (IOException e) {
                 throw new DeploymentException("Failed to update metadata " + metadata + ": " + e.getMessage(), e);
             }
@@ -330,12 +336,12 @@ public class DefaultDeployer implements Deployer {
 
         UpdateCheck<Metadata, MetadataTransferException> check = new UpdateCheck<>();
         check.setItem(metadata);
-        check.setFile(dstFile);
+        check.setPath(dstPath);
         check.setRepository(repository);
         check.setAuthoritativeRepository(repository);
         updateCheckManager.touchMetadata(session, check);
 
-        MetadataUpload upload = new MetadataUpload(metadata, dstFile);
+        MetadataUpload upload = new MetadataUpload(metadata, dstPath);
         upload.setTrace(catapult.getTrace());
         upload.setListener(new MetadataUploadListener(catapult, upload));
         metadataUploads.add(upload);
@@ -377,43 +383,43 @@ public class DefaultDeployer implements Deployer {
             return trace;
         }
 
-        public void artifactDeploying(Artifact artifact, File file) {
+        public void artifactDeploying(Artifact artifact, Path path) {
             RepositoryEvent.Builder event = new RepositoryEvent.Builder(session, EventType.ARTIFACT_DEPLOYING);
             event.setTrace(trace);
             event.setArtifact(artifact);
             event.setRepository(repository);
-            event.setFile(file);
+            event.setPath(path);
 
             dispatcher.dispatch(event.build());
         }
 
-        public void artifactDeployed(Artifact artifact, File file, ArtifactTransferException exception) {
+        public void artifactDeployed(Artifact artifact, Path path, ArtifactTransferException exception) {
             RepositoryEvent.Builder event = new RepositoryEvent.Builder(session, EventType.ARTIFACT_DEPLOYED);
             event.setTrace(trace);
             event.setArtifact(artifact);
             event.setRepository(repository);
-            event.setFile(file);
+            event.setPath(path);
             event.setException(exception);
 
             dispatcher.dispatch(event.build());
         }
 
-        public void metadataDeploying(Metadata metadata, File file) {
+        public void metadataDeploying(Metadata metadata, Path path) {
             RepositoryEvent.Builder event = new RepositoryEvent.Builder(session, EventType.METADATA_DEPLOYING);
             event.setTrace(trace);
             event.setMetadata(metadata);
             event.setRepository(repository);
-            event.setFile(file);
+            event.setPath(path);
 
             dispatcher.dispatch(event.build());
         }
 
-        public void metadataDeployed(Metadata metadata, File file, Exception exception) {
+        public void metadataDeployed(Metadata metadata, Path path, Exception exception) {
             RepositoryEvent.Builder event = new RepositoryEvent.Builder(session, EventType.METADATA_DEPLOYED);
             event.setTrace(trace);
             event.setMetadata(metadata);
             event.setRepository(repository);
-            event.setFile(file);
+            event.setPath(path);
             event.setException(exception);
 
             dispatcher.dispatch(event.build());
@@ -436,21 +442,21 @@ public class DefaultDeployer implements Deployer {
         public void transferInitiated(TransferEvent event) throws TransferCancelledException {
             super.transferInitiated(event);
             requireNonNull(event, "event cannot be null");
-            catapult.artifactDeploying(transfer.getArtifact(), transfer.getFile());
+            catapult.artifactDeploying(transfer.getArtifact(), transfer.getPath());
         }
 
         @Override
         public void transferFailed(TransferEvent event) {
             super.transferFailed(event);
             requireNonNull(event, "event cannot be null");
-            catapult.artifactDeployed(transfer.getArtifact(), transfer.getFile(), transfer.getException());
+            catapult.artifactDeployed(transfer.getArtifact(), transfer.getPath(), transfer.getException());
         }
 
         @Override
         public void transferSucceeded(TransferEvent event) {
             super.transferSucceeded(event);
             requireNonNull(event, "event cannot be null");
-            catapult.artifactDeployed(transfer.getArtifact(), transfer.getFile(), null);
+            catapult.artifactDeployed(transfer.getArtifact(), transfer.getPath(), null);
         }
     }
 
@@ -470,21 +476,21 @@ public class DefaultDeployer implements Deployer {
         public void transferInitiated(TransferEvent event) throws TransferCancelledException {
             super.transferInitiated(event);
             requireNonNull(event, "event cannot be null");
-            catapult.metadataDeploying(transfer.getMetadata(), transfer.getFile());
+            catapult.metadataDeploying(transfer.getMetadata(), transfer.getPath());
         }
 
         @Override
         public void transferFailed(TransferEvent event) {
             super.transferFailed(event);
             requireNonNull(event, "event cannot be null");
-            catapult.metadataDeployed(transfer.getMetadata(), transfer.getFile(), transfer.getException());
+            catapult.metadataDeployed(transfer.getMetadata(), transfer.getPath(), transfer.getException());
         }
 
         @Override
         public void transferSucceeded(TransferEvent event) {
             super.transferSucceeded(event);
             requireNonNull(event, "event cannot be null");
-            catapult.metadataDeployed(transfer.getMetadata(), transfer.getFile(), null);
+            catapult.metadataDeployed(transfer.getMetadata(), transfer.getPath(), null);
         }
     }
 }
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 12bd4797..811960e5 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
@@ -32,7 +32,10 @@ import org.eclipse.aether.util.FileUtils;
 
 /**
  * A utility class helping with file-based operations.
+ *
+ * @deprecated
  */
+@Deprecated
 @Singleton
 @Named
 public class DefaultFileProcessor implements FileProcessor {
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultInstaller.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultInstaller.java
index f4c60bf3..78b63b9a 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultInstaller.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultInstaller.java
@@ -22,7 +22,8 @@ import javax.inject.Inject;
 import javax.inject.Named;
 import javax.inject.Singleton;
 
-import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.IdentityHashMap;
@@ -48,7 +49,7 @@ import org.eclipse.aether.metadata.Metadata;
 import org.eclipse.aether.repository.LocalArtifactRegistration;
 import org.eclipse.aether.repository.LocalMetadataRegistration;
 import org.eclipse.aether.repository.LocalRepositoryManager;
-import org.eclipse.aether.spi.io.FileProcessor;
+import org.eclipse.aether.spi.io.PathProcessor;
 import org.eclipse.aether.spi.synccontext.SyncContextFactory;
 
 import static java.util.Objects.requireNonNull;
@@ -58,7 +59,7 @@ import static java.util.Objects.requireNonNull;
 @Singleton
 @Named
 public class DefaultInstaller implements Installer {
-    private final FileProcessor fileProcessor;
+    private final PathProcessor pathProcessor;
 
     private final RepositoryEventDispatcher repositoryEventDispatcher;
 
@@ -68,11 +69,11 @@ public class DefaultInstaller implements Installer {
 
     @Inject
     public DefaultInstaller(
-            FileProcessor fileProcessor,
+            PathProcessor pathProcessor,
             RepositoryEventDispatcher repositoryEventDispatcher,
             Map<String, MetadataGeneratorFactory> metadataFactories,
             SyncContextFactory syncContextFactory) {
-        this.fileProcessor = requireNonNull(fileProcessor, "file processor cannot be null");
+        this.pathProcessor = requireNonNull(pathProcessor, "path processor cannot be null");
         this.repositoryEventDispatcher =
                 requireNonNull(repositoryEventDispatcher, "repository event dispatcher cannot be null");
         this.metadataFactories = Collections.unmodifiableMap(metadataFactories);
@@ -163,25 +164,25 @@ public class DefaultInstaller implements Installer {
     private void install(RepositorySystemSession session, RequestTrace trace, Artifact artifact)
             throws InstallationException {
         final LocalRepositoryManager lrm = session.getLocalRepositoryManager();
-        final File srcFile = artifact.getFile();
-        final File dstFile = new File(lrm.getRepository().getBasedir(), lrm.getPathForLocalArtifact(artifact));
+        final Path srcPath = artifact.getPath();
+        final Path dstPath = lrm.getRepository().getBasePath().resolve(lrm.getPathForLocalArtifact(artifact));
 
-        artifactInstalling(session, trace, artifact, dstFile);
+        artifactInstalling(session, trace, artifact, dstPath);
 
         Exception exception = null;
         try {
-            if (dstFile.equals(srcFile)) {
-                throw new IllegalStateException("cannot install " + dstFile + " to same path");
+            if (dstPath.equals(srcPath)) {
+                throw new IllegalStateException("cannot install " + dstPath + " to same path");
             }
 
-            fileProcessor.copy(srcFile, dstFile);
-            dstFile.setLastModified(srcFile.lastModified());
+            pathProcessor.copy(srcPath, dstPath);
+            Files.setLastModifiedTime(dstPath, Files.getLastModifiedTime(srcPath));
             lrm.add(session, new LocalArtifactRegistration(artifact));
         } catch (Exception e) {
             exception = e;
             throw new InstallationException("Failed to install artifact " + artifact + ": " + e.getMessage(), e);
         } finally {
-            artifactInstalled(session, trace, artifact, dstFile, exception);
+            artifactInstalled(session, trace, artifact, dstPath, exception);
         }
     }
 
@@ -189,19 +190,19 @@ public class DefaultInstaller implements Installer {
             throws InstallationException {
         LocalRepositoryManager lrm = session.getLocalRepositoryManager();
 
-        File dstFile = new File(lrm.getRepository().getBasedir(), lrm.getPathForLocalMetadata(metadata));
+        Path dstPath = lrm.getRepository().getBasePath().resolve(lrm.getPathForLocalMetadata(metadata));
 
-        metadataInstalling(session, trace, metadata, dstFile);
+        metadataInstalling(session, trace, metadata, dstPath);
 
         Exception exception = null;
         try {
             if (metadata instanceof MergeableMetadata) {
-                ((MergeableMetadata) metadata).merge(dstFile, dstFile);
+                ((MergeableMetadata) metadata).merge(dstPath, dstPath);
             } else {
-                if (dstFile.equals(metadata.getFile())) {
-                    throw new IllegalStateException("cannot install " + dstFile + " to same path");
+                if (dstPath.equals(metadata.getPath())) {
+                    throw new IllegalStateException("cannot install " + dstPath + " to same path");
                 }
-                fileProcessor.copy(metadata.getFile(), dstFile);
+                pathProcessor.copy(metadata.getPath(), dstPath);
             }
 
             lrm.add(session, new LocalMetadataRegistration(metadata));
@@ -209,51 +210,51 @@ public class DefaultInstaller implements Installer {
             exception = e;
             throw new InstallationException("Failed to install metadata " + metadata + ": " + e.getMessage(), e);
         } finally {
-            metadataInstalled(session, trace, metadata, dstFile, exception);
+            metadataInstalled(session, trace, metadata, dstPath, exception);
         }
     }
 
     private void artifactInstalling(
-            RepositorySystemSession session, RequestTrace trace, Artifact artifact, File dstFile) {
+            RepositorySystemSession session, RequestTrace trace, Artifact artifact, Path dstPath) {
         RepositoryEvent.Builder event = new RepositoryEvent.Builder(session, EventType.ARTIFACT_INSTALLING);
         event.setTrace(trace);
         event.setArtifact(artifact);
         event.setRepository(session.getLocalRepositoryManager().getRepository());
-        event.setFile(dstFile);
+        event.setPath(dstPath);
 
         repositoryEventDispatcher.dispatch(event.build());
     }
 
     private void artifactInstalled(
-            RepositorySystemSession session, RequestTrace trace, Artifact artifact, File dstFile, Exception exception) {
+            RepositorySystemSession session, RequestTrace trace, Artifact artifact, Path dstPath, Exception exception) {
         RepositoryEvent.Builder event = new RepositoryEvent.Builder(session, EventType.ARTIFACT_INSTALLED);
         event.setTrace(trace);
         event.setArtifact(artifact);
         event.setRepository(session.getLocalRepositoryManager().getRepository());
-        event.setFile(dstFile);
+        event.setPath(dstPath);
         event.setException(exception);
 
         repositoryEventDispatcher.dispatch(event.build());
     }
 
     private void metadataInstalling(
-            RepositorySystemSession session, RequestTrace trace, Metadata metadata, File dstFile) {
+            RepositorySystemSession session, RequestTrace trace, Metadata metadata, Path dstPath) {
         RepositoryEvent.Builder event = new RepositoryEvent.Builder(session, EventType.METADATA_INSTALLING);
         event.setTrace(trace);
         event.setMetadata(metadata);
         event.setRepository(session.getLocalRepositoryManager().getRepository());
-        event.setFile(dstFile);
+        event.setPath(dstPath);
 
         repositoryEventDispatcher.dispatch(event.build());
     }
 
     private void metadataInstalled(
-            RepositorySystemSession session, RequestTrace trace, Metadata metadata, File dstFile, Exception exception) {
+            RepositorySystemSession session, RequestTrace trace, Metadata metadata, Path dstPath, Exception exception) {
         RepositoryEvent.Builder event = new RepositoryEvent.Builder(session, EventType.METADATA_INSTALLED);
         event.setTrace(trace);
         event.setMetadata(metadata);
         event.setRepository(session.getLocalRepositoryManager().getRepository());
-        event.setFile(dstFile);
+        event.setPath(dstPath);
         event.setException(exception);
 
         repositoryEventDispatcher.dispatch(event.build());
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultLocalRepositoryProvider.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultLocalRepositoryProvider.java
index a72c83d3..e2c23e65 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultLocalRepositoryProvider.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultLocalRepositoryProvider.java
@@ -72,7 +72,7 @@ public class DefaultLocalRepositoryProvider implements LocalRepositoryProvider {
                     buffer.append("Using manager ").append(manager.getClass().getSimpleName());
                     Utils.appendClassLoader(buffer, manager);
                     buffer.append(" with priority ").append(factory.getPriority());
-                    buffer.append(" for ").append(repository.getBasedir());
+                    buffer.append(" for ").append(repository.getBasePath());
 
                     LOGGER.debug(buffer.toString());
                 }
@@ -93,7 +93,7 @@ public class DefaultLocalRepositoryProvider implements LocalRepositoryProvider {
         if (factories.isEmpty()) {
             buffer.append("No local repository managers registered");
         } else {
-            buffer.append("Cannot access ").append(repository.getBasedir());
+            buffer.append("Cannot access ").append(repository.getBasePath());
             buffer.append(" with type ").append(repository.getContentType());
             buffer.append(" using the available factories ");
             factories.list(buffer);
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultMetadataResolver.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultMetadataResolver.java
index 4bbe30cd..28a43ae1 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultMetadataResolver.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultMetadataResolver.java
@@ -22,7 +22,10 @@ import javax.inject.Inject;
 import javax.inject.Named;
 import javax.inject.Singleton;
 
-import java.io.File;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -59,6 +62,7 @@ import org.eclipse.aether.resolution.MetadataResult;
 import org.eclipse.aether.spi.connector.MetadataDownload;
 import org.eclipse.aether.spi.connector.RepositoryConnector;
 import org.eclipse.aether.spi.connector.filter.RemoteRepositoryFilter;
+import org.eclipse.aether.spi.io.PathProcessor;
 import org.eclipse.aether.spi.synccontext.SyncContextFactory;
 import org.eclipse.aether.transfer.MetadataNotFoundException;
 import org.eclipse.aether.transfer.MetadataTransferException;
@@ -102,6 +106,9 @@ public class DefaultMetadataResolver implements MetadataResolver {
 
     private final RemoteRepositoryFilterManager remoteRepositoryFilterManager;
 
+    private final PathProcessor pathProcessor;
+
+    @SuppressWarnings("checkstyle:parameternumber")
     @Inject
     public DefaultMetadataResolver(
             RepositoryEventDispatcher repositoryEventDispatcher,
@@ -110,7 +117,8 @@ public class DefaultMetadataResolver implements MetadataResolver {
             RemoteRepositoryManager remoteRepositoryManager,
             SyncContextFactory syncContextFactory,
             OfflineController offlineController,
-            RemoteRepositoryFilterManager remoteRepositoryFilterManager) {
+            RemoteRepositoryFilterManager remoteRepositoryFilterManager,
+            PathProcessor pathProcessor) {
         this.repositoryEventDispatcher =
                 requireNonNull(repositoryEventDispatcher, "repository event dispatcher cannot be null");
         this.updateCheckManager = requireNonNull(updateCheckManager, "update check manager cannot be null");
@@ -122,6 +130,7 @@ public class DefaultMetadataResolver implements MetadataResolver {
         this.offlineController = requireNonNull(offlineController, "offline controller cannot be null");
         this.remoteRepositoryFilterManager =
                 requireNonNull(remoteRepositoryFilterManager, "remote repository filter manager cannot be null");
+        this.pathProcessor = requireNonNull(pathProcessor, "path processor cannot be null");
     }
 
     @Override
@@ -154,7 +163,7 @@ public class DefaultMetadataResolver implements MetadataResolver {
 
                 final List<MetadataResult> results = new ArrayList<>(requests.size());
                 final List<ResolveTask> tasks = new ArrayList<>(requests.size());
-                final Map<File, Long> localLastUpdates = new HashMap<>();
+                final Map<Path, Long> localLastUpdates = new HashMap<>();
                 final RemoteRepositoryFilter remoteRepositoryFilter =
                         remoteRepositoryFilterManager.getRemoteRepositoryFilter(session);
 
@@ -173,10 +182,10 @@ public class DefaultMetadataResolver implements MetadataResolver {
 
                         metadataResolving(session, trace, metadata, localRepo);
 
-                        File localFile = getLocalFile(session, metadata);
+                        Path localFile = getLocalFile(session, metadata);
 
                         if (localFile != null) {
-                            metadata = metadata.setFile(localFile);
+                            metadata = metadata.setPath(localFile);
                             result.setMetadata(metadata);
                         } else {
                             result.setException(new MetadataNotFoundException(metadata, localRepo));
@@ -209,13 +218,13 @@ public class DefaultMetadataResolver implements MetadataResolver {
                             new LocalMetadataRequest(metadata, repository, request.getRequestContext());
                     LocalMetadataResult lrmResult = lrm.find(session, localRequest);
 
-                    File metadataFile = lrmResult.getFile();
+                    Path metadataPath = lrmResult.getPath();
 
                     try {
                         Utils.checkOffline(session, offlineController, repository);
                     } catch (RepositoryOfflineException e) {
-                        if (metadataFile != null) {
-                            metadata = metadata.setFile(metadataFile);
+                        if (metadataPath != null) {
+                            metadata = metadata.setPath(metadataPath);
                             result.setMetadata(metadata);
                         } else {
                             String msg = "Cannot access " + repository.getId() + " (" + repository.getUrl()
@@ -230,11 +239,11 @@ public class DefaultMetadataResolver implements MetadataResolver {
 
                     Long localLastUpdate = null;
                     if (request.isFavorLocalRepository()) {
-                        File localFile = getLocalFile(session, metadata);
-                        localLastUpdate = localLastUpdates.get(localFile);
+                        Path localPath = getLocalFile(session, metadata);
+                        localLastUpdate = localLastUpdates.get(localPath);
                         if (localLastUpdate == null) {
-                            localLastUpdate = localFile != null ? localFile.lastModified() : 0;
-                            localLastUpdates.put(localFile, localLastUpdate);
+                            localLastUpdate = localPath != null ? pathProcessor.lastModified(localPath, 0L) : 0L;
+                            localLastUpdates.put(localPath, localLastUpdate);
                         }
                     }
 
@@ -248,11 +257,11 @@ public class DefaultMetadataResolver implements MetadataResolver {
                         check.setItem(metadata);
 
                         // use 'main' installation file for the check (-> use requested repository)
-                        File checkFile = new File(
-                                session.getLocalRepository().getBasedir(),
-                                session.getLocalRepositoryManager()
+                        Path checkPath = session.getLocalRepository()
+                                .getBasePath()
+                                .resolve(session.getLocalRepositoryManager()
                                         .getPathForRemoteMetadata(metadata, repository, request.getRequestContext()));
-                        check.setFile(checkFile);
+                        check.setPath(checkPath);
                         check.setRepository(repository);
                         check.setAuthoritativeRepository(repo);
                         check.setArtifactPolicy(policy.getArtifactUpdatePolicy());
@@ -274,19 +283,19 @@ public class DefaultMetadataResolver implements MetadataResolver {
                         RepositoryPolicy policy = getPolicy(session, repository, metadata.getNature());
 
                         // install path may be different from lookup path
-                        File installFile = new File(
-                                session.getLocalRepository().getBasedir(),
-                                session.getLocalRepositoryManager()
+                        Path installPath = session.getLocalRepository()
+                                .getBasePath()
+                                .resolve(session.getLocalRepositoryManager()
                                         .getPathForRemoteMetadata(
                                                 metadata, request.getRepository(), request.getRequestContext()));
 
                         ResolveTask task = new ResolveTask(
-                                session, trace, result, installFile, checks, policy.getChecksumPolicy());
+                                session, trace, result, installPath, checks, policy.getChecksumPolicy());
                         tasks.add(task);
                     } else {
                         result.setException(exception);
-                        if (metadataFile != null) {
-                            metadata = metadata.setFile(metadataFile);
+                        if (metadataPath != null) {
+                            metadata = metadata.setPath(metadataPath);
                             result.setMetadata(metadata);
                         }
                         metadataResolved(session, trace, metadata, repository, result.getException());
@@ -330,7 +339,7 @@ public class DefaultMetadataResolver implements MetadataResolver {
                                     task.trace,
                                     task.request.getMetadata(),
                                     task.request.getRepository(),
-                                    task.metadataFile,
+                                    task.metadataPath,
                                     task.exception);
 
                             task.result.setException(task.exception);
@@ -343,11 +352,11 @@ public class DefaultMetadataResolver implements MetadataResolver {
                         // re-lookup metadata for resolve
                         LocalMetadataRequest localRequest = new LocalMetadataRequest(
                                 metadata, task.request.getRepository(), task.request.getRequestContext());
-                        File metadataFile = session.getLocalRepositoryManager()
+                        Path metadataPath = session.getLocalRepositoryManager()
                                 .find(session, localRequest)
-                                .getFile();
-                        if (metadataFile != null) {
-                            metadata = metadata.setFile(metadataFile);
+                                .getPath();
+                        if (metadataPath != null) {
+                            metadata = metadata.setPath(metadataPath);
                             task.result.setMetadata(metadata);
                         }
                         if (task.result.getException() == null) {
@@ -369,10 +378,10 @@ public class DefaultMetadataResolver implements MetadataResolver {
         }
     }
 
-    private File getLocalFile(RepositorySystemSession session, Metadata metadata) {
+    private Path getLocalFile(RepositorySystemSession session, Metadata metadata) {
         LocalRepositoryManager lrm = session.getLocalRepositoryManager();
         LocalMetadataResult localResult = lrm.find(session, new LocalMetadataRequest(metadata, null, null));
-        return localResult.getFile();
+        return localResult.getPath();
     }
 
     private List<RemoteRepository> getEnabledSourceRepositories(RemoteRepository repository, Metadata.Nature nature) {
@@ -428,7 +437,7 @@ public class DefaultMetadataResolver implements MetadataResolver {
         event.setMetadata(metadata);
         event.setRepository(repository);
         event.setException(exception);
-        event.setFile(metadata.getFile());
+        event.setPath(metadata.getPath());
 
         repositoryEventDispatcher.dispatch(event.build());
     }
@@ -448,14 +457,14 @@ public class DefaultMetadataResolver implements MetadataResolver {
             RequestTrace trace,
             Metadata metadata,
             ArtifactRepository repository,
-            File file,
+            Path path,
             Exception exception) {
         RepositoryEvent.Builder event = new RepositoryEvent.Builder(session, EventType.METADATA_DOWNLOADED);
         event.setTrace(trace);
         event.setMetadata(metadata);
         event.setRepository(repository);
         event.setException(exception);
-        event.setFile(file);
+        event.setPath(path);
 
         repositoryEventDispatcher.dispatch(event.build());
     }
@@ -469,7 +478,7 @@ public class DefaultMetadataResolver implements MetadataResolver {
 
         final MetadataRequest request;
 
-        final File metadataFile;
+        final Path metadataPath;
 
         final String policy;
 
@@ -481,14 +490,14 @@ public class DefaultMetadataResolver implements MetadataResolver {
                 RepositorySystemSession session,
                 RequestTrace trace,
                 MetadataResult result,
-                File metadataFile,
+                Path metadataPath,
                 List<UpdateCheck<Metadata, MetadataTransferException>> checks,
                 String policy) {
             this.session = session;
             this.trace = trace;
             this.result = result;
             this.request = result.getRequest();
-            this.metadataFile = metadataFile;
+            this.metadataPath = metadataPath;
             this.policy = policy;
             this.checks = checks;
         }
@@ -506,7 +515,7 @@ public class DefaultMetadataResolver implements MetadataResolver {
                 MetadataDownload download = new MetadataDownload();
                 download.setMetadata(metadata);
                 download.setRequestContext(request.getRequestContext());
-                download.setFile(metadataFile);
+                download.setPath(metadataPath);
                 download.setChecksumPolicy(policy);
                 download.setRepositories(repositories);
                 download.setListener(SafeTransferListener.wrap(session));
@@ -527,7 +536,11 @@ public class DefaultMetadataResolver implements MetadataResolver {
 
                     session.getLocalRepositoryManager().add(session, registration);
                 } else if (request.isDeleteLocalCopyIfMissing() && exception instanceof MetadataNotFoundException) {
-                    download.getFile().delete();
+                    try {
+                        Files.deleteIfExists(download.getPath());
+                    } catch (IOException e) {
+                        throw new UncheckedIOException(e);
+                    }
                 }
             } catch (NoRepositoryConnectorException e) {
                 exception = new MetadataTransferException(metadata, requestRepository, e);
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultPathProcessor.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultPathProcessor.java
new file mode 100644
index 00000000..b3af35bb
--- /dev/null
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultPathProcessor.java
@@ -0,0 +1,95 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.eclipse.aether.internal.impl;
+
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+import java.io.*;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+
+import org.eclipse.aether.spi.io.PathProcessor;
+import org.eclipse.aether.util.FileUtils;
+
+/**
+ * A utility class helping with file-based operations.
+ */
+@Singleton
+@Named
+public class DefaultPathProcessor implements PathProcessor {
+    @Override
+    public void write(Path target, String data) throws IOException {
+        FileUtils.writeFile(target, p -> Files.write(p, data.getBytes(StandardCharsets.UTF_8)));
+    }
+
+    @Override
+    public void write(Path target, InputStream source) throws IOException {
+        FileUtils.writeFile(target, p -> Files.copy(source, p, StandardCopyOption.REPLACE_EXISTING));
+    }
+
+    @Override
+    public void copy(Path source, Path target) throws IOException {
+        copy(source, target, null);
+    }
+
+    @Override
+    public long copy(Path source, Path target, ProgressListener listener) throws IOException {
+        try (InputStream in = new BufferedInputStream(Files.newInputStream(source));
+                FileUtils.CollocatedTempFile tempTarget = FileUtils.newTempFile(target);
+                OutputStream out = new BufferedOutputStream(Files.newOutputStream(tempTarget.getPath()))) {
+            long result = copy(out, in, listener);
+            tempTarget.move();
+            return result;
+        }
+    }
+
+    private long copy(OutputStream os, InputStream is, ProgressListener listener) throws IOException {
+        long total = 0L;
+        byte[] buffer = new byte[1024 * 32];
+        while (true) {
+            int bytes = is.read(buffer);
+            if (bytes < 0) {
+                break;
+            }
+
+            os.write(buffer, 0, bytes);
+
+            total += bytes;
+
+            if (listener != null && bytes > 0) {
+                try {
+                    listener.progressed(ByteBuffer.wrap(buffer, 0, bytes));
+                } catch (Exception e) {
+                    // too bad
+                }
+            }
+        }
+
+        return total;
+    }
+
+    @Override
+    public void move(Path source, Path target) throws IOException {
+        Files.move(source, target, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES);
+    }
+}
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 8bef789a..a4e73130 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
@@ -21,17 +21,18 @@ package org.eclipse.aether.internal.impl;
 import javax.inject.Named;
 import javax.inject.Singleton;
 
-import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.File;
-import java.io.FileInputStream;
 import java.io.IOException;
-import java.io.RandomAccessFile;
 import java.io.UncheckedIOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.Channels;
 import java.nio.channels.FileChannel;
 import java.nio.channels.FileLock;
 import java.nio.channels.OverlappingFileLockException;
 import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
 import java.util.Map;
 import java.util.Properties;
 
@@ -51,17 +52,26 @@ import org.slf4j.LoggerFactory;
 public final class DefaultTrackingFileManager implements TrackingFileManager {
     private static final Logger LOGGER = LoggerFactory.getLogger(DefaultTrackingFileManager.class);
 
+    @Deprecated
     @Override
     public Properties read(File file) {
-        if (Files.isReadable(file.toPath())) {
-            synchronized (getMutex(file)) {
-                try (FileInputStream stream = new FileInputStream(file);
-                        FileLock unused = fileLock(stream.getChannel(), Math.max(1, file.length()), true)) {
-                    Properties props = new Properties();
-                    props.load(stream);
-                    return props;
+        return read(file.toPath());
+    }
+
+    @Override
+    public Properties read(Path path) {
+        if (Files.isReadable(path)) {
+            synchronized (getMutex(path)) {
+                try {
+                    long fileSize = Files.size(path);
+                    try (FileChannel fileChannel = FileChannel.open(path, StandardOpenOption.READ);
+                            FileLock unused = fileLock(fileChannel, Math.max(1, fileSize), true)) {
+                        Properties props = new Properties();
+                        props.load(Channels.newInputStream(fileChannel));
+                        return props;
+                    }
                 } catch (IOException e) {
-                    LOGGER.warn("Failed to read tracking file '{}'", file, e);
+                    LOGGER.warn("Failed to read tracking file '{}'", path, e);
                     throw new UncheckedIOException(e);
                 }
             }
@@ -69,45 +79,57 @@ public final class DefaultTrackingFileManager implements TrackingFileManager {
         return null;
     }
 
+    @Deprecated
     @Override
     public Properties update(File file, Map<String, String> updates) {
-        Properties props = new Properties();
+        return update(file.toPath(), updates);
+    }
 
+    @Override
+    public Properties update(Path path, Map<String, String> updates) {
+        Properties props = new Properties();
         try {
-            Files.createDirectories(file.getParentFile().toPath());
+            Files.createDirectories(path.getParent());
         } catch (IOException e) {
-            LOGGER.warn("Failed to create tracking file parent '{}'", file, e);
+            LOGGER.warn("Failed to create tracking file parent '{}'", path, e);
             throw new UncheckedIOException(e);
         }
 
-        synchronized (getMutex(file)) {
-            try (RandomAccessFile raf = new RandomAccessFile(file, "rw");
-                    FileLock unused = fileLock(raf.getChannel(), Math.max(1, raf.length()), false)) {
-                if (raf.length() > 0) {
-                    byte[] buffer = new byte[(int) raf.length()];
-                    raf.readFully(buffer);
-                    props.load(new ByteArrayInputStream(buffer));
+        synchronized (getMutex(path)) {
+            try {
+                long fileSize;
+                try {
+                    fileSize = Files.size(path);
+                } catch (IOException e) {
+                    fileSize = 0L;
                 }
+                try (FileChannel fileChannel = FileChannel.open(
+                                path, StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.CREATE);
+                        FileLock unused = fileLock(fileChannel, Math.max(1, fileSize), false)) {
+                    if (fileSize > 0) {
+                        props.load(Channels.newInputStream(fileChannel));
+                    }
 
-                for (Map.Entry<String, String> update : updates.entrySet()) {
-                    if (update.getValue() == null) {
-                        props.remove(update.getKey());
-                    } else {
-                        props.setProperty(update.getKey(), update.getValue());
+                    for (Map.Entry<String, String> update : updates.entrySet()) {
+                        if (update.getValue() == null) {
+                            props.remove(update.getKey());
+                        } else {
+                            props.setProperty(update.getKey(), update.getValue());
+                        }
                     }
-                }
 
-                LOGGER.debug("Writing tracking file '{}'", file);
-                ByteArrayOutputStream stream = new ByteArrayOutputStream(1024 * 2);
-                props.store(
-                        stream,
-                        "NOTE: This is a Maven Resolver internal implementation file"
-                                + ", its format can be changed without prior notice.");
-                raf.seek(0L);
-                raf.write(stream.toByteArray());
-                raf.setLength(raf.getFilePointer());
+                    LOGGER.debug("Writing tracking file '{}'", path);
+                    ByteArrayOutputStream stream = new ByteArrayOutputStream(1024 * 2);
+                    props.store(
+                            stream,
+                            "NOTE: This is a Maven Resolver internal implementation file"
+                                    + ", its format can be changed without prior notice.");
+                    fileChannel.position(0);
+                    int written = fileChannel.write(ByteBuffer.wrap(stream.toByteArray()));
+                    fileChannel.truncate(written);
+                }
             } catch (IOException e) {
-                LOGGER.warn("Failed to write tracking file '{}'", file, e);
+                LOGGER.warn("Failed to write tracking file '{}'", path, e);
                 throw new UncheckedIOException(e);
             }
         }
@@ -115,7 +137,7 @@ public final class DefaultTrackingFileManager implements TrackingFileManager {
         return props;
     }
 
-    private Object getMutex(File file) {
+    private Object getMutex(Path path) {
         // The interned string of path is (mis)used as mutex, to exclude different threads going for same file,
         // as JVM file locking happens on JVM not on Thread level. This is how original code did it  ¯\_(ツ)_/¯
         /*
@@ -123,13 +145,7 @@ public final class DefaultTrackingFileManager implements TrackingFileManager {
          * piece of code might have locked the same file (unlikely though) or the canonical path fails to capture file
          * identity sufficiently as is the case with Java 1.6 and symlinks on Windows.
          */
-        try {
-            return file.getCanonicalPath().intern();
-        } catch (IOException e) {
-            LOGGER.warn("Failed to canonicalize path {}", file, e);
-            // TODO This is code smell and deprecated
-            return file.getAbsolutePath().intern();
-        }
+        return path.toAbsolutePath().normalize().toString().intern();
     }
 
     @SuppressWarnings({"checkstyle:magicnumber"})
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultUpdateCheckManager.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultUpdateCheckManager.java
index 51c03ce4..2c5e1965 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultUpdateCheckManager.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultUpdateCheckManager.java
@@ -22,7 +22,10 @@ import javax.inject.Inject;
 import javax.inject.Named;
 import javax.inject.Singleton;
 
-import java.io.File;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
@@ -43,6 +46,7 @@ import org.eclipse.aether.repository.AuthenticationDigest;
 import org.eclipse.aether.repository.Proxy;
 import org.eclipse.aether.repository.RemoteRepository;
 import org.eclipse.aether.resolution.ResolutionErrorPolicy;
+import org.eclipse.aether.spi.io.PathProcessor;
 import org.eclipse.aether.transfer.ArtifactNotFoundException;
 import org.eclipse.aether.transfer.ArtifactTransferException;
 import org.eclipse.aether.transfer.MetadataNotFoundException;
@@ -118,11 +122,16 @@ public class DefaultUpdateCheckManager implements UpdateCheckManager {
 
     private final UpdatePolicyAnalyzer updatePolicyAnalyzer;
 
+    private final PathProcessor pathProcessor;
+
     @Inject
     public DefaultUpdateCheckManager(
-            TrackingFileManager trackingFileManager, UpdatePolicyAnalyzer updatePolicyAnalyzer) {
+            TrackingFileManager trackingFileManager,
+            UpdatePolicyAnalyzer updatePolicyAnalyzer,
+            PathProcessor pathProcessor) {
         this.trackingFileManager = requireNonNull(trackingFileManager, "tracking file manager cannot be null");
         this.updatePolicyAnalyzer = requireNonNull(updatePolicyAnalyzer, "update policy analyzer cannot be null");
+        this.pathProcessor = requireNonNull(pathProcessor, "path processor cannot be null");
     }
 
     @Override
@@ -141,15 +150,15 @@ public class DefaultUpdateCheckManager implements UpdateCheckManager {
         Artifact artifact = check.getItem();
         RemoteRepository repository = check.getRepository();
 
-        File artifactFile =
-                requireNonNull(check.getFile(), String.format("The artifact '%s' has no file attached", artifact));
+        Path artifactPath =
+                requireNonNull(check.getPath(), String.format("The artifact '%s' has no file attached", artifact));
 
-        boolean fileExists = check.isFileValid() && artifactFile.exists();
+        boolean fileExists = check.isFileValid() && Files.exists(artifactPath);
 
-        File touchFile = getArtifactTouchFile(artifactFile);
-        Properties props = read(touchFile);
+        Path touchPath = getArtifactTouchFile(artifactPath);
+        Properties props = read(touchPath);
 
-        String updateKey = getUpdateKey(session, artifactFile, repository);
+        String updateKey = getUpdateKey(session, artifactPath, repository);
         String dataKey = getDataKey(repository);
 
         String error = getError(props, dataKey);
@@ -158,7 +167,7 @@ public class DefaultUpdateCheckManager implements UpdateCheckManager {
         if (error == null) {
             if (fileExists) {
                 // last update was successful
-                lastUpdated = artifactFile.lastModified();
+                lastUpdated = pathProcessor.lastModified(artifactPath, 0L);
             } else {
                 // this is the first attempt ever
                 lastUpdated = TS_NEVER;
@@ -248,16 +257,16 @@ public class DefaultUpdateCheckManager implements UpdateCheckManager {
         Metadata metadata = check.getItem();
         RemoteRepository repository = check.getRepository();
 
-        File metadataFile =
-                requireNonNull(check.getFile(), String.format("The metadata '%s' has no file attached", metadata));
+        Path metadataPath =
+                requireNonNull(check.getPath(), String.format("The metadata '%s' has no file attached", metadata));
 
-        boolean fileExists = check.isFileValid() && metadataFile.exists();
+        boolean fileExists = check.isFileValid() && Files.exists(metadataPath);
 
-        File touchFile = getMetadataTouchFile(metadataFile);
-        Properties props = read(touchFile);
+        Path touchPath = getMetadataTouchFile(metadataPath);
+        Properties props = read(touchPath);
 
-        String updateKey = getUpdateKey(session, metadataFile, repository);
-        String dataKey = getDataKey(metadataFile);
+        String updateKey = getUpdateKey(session, metadataPath, repository);
+        String dataKey = getDataKey(metadataPath);
 
         String error = getError(props, dataKey);
 
@@ -275,7 +284,7 @@ public class DefaultUpdateCheckManager implements UpdateCheckManager {
             lastUpdated = getLastUpdated(props, dataKey);
         } else {
             // metadata could not be transferred
-            String transferKey = getTransferKey(session, metadataFile, repository);
+            String transferKey = getTransferKey(session, metadataPath, repository);
             lastUpdated = getLastUpdated(props, transferKey);
         }
 
@@ -344,12 +353,12 @@ public class DefaultUpdateCheckManager implements UpdateCheckManager {
         return props.getProperty(key + ERROR_KEY_SUFFIX);
     }
 
-    private File getArtifactTouchFile(File artifactFile) {
-        return new File(artifactFile.getPath() + UPDATED_KEY_SUFFIX);
+    private Path getArtifactTouchFile(Path artifactPath) {
+        return artifactPath.getParent().resolve(artifactPath.getFileName() + UPDATED_KEY_SUFFIX);
     }
 
-    private File getMetadataTouchFile(File metadataFile) {
-        return new File(metadataFile.getParent(), "resolver-status.properties");
+    private Path getMetadataTouchFile(Path metadataPath) {
+        return metadataPath.getParent().resolve("resolver-status.properties");
     }
 
     private String getDataKey(RemoteRepository repository) {
@@ -375,12 +384,12 @@ public class DefaultUpdateCheckManager implements UpdateCheckManager {
         return getRepoKey(session, repository);
     }
 
-    private String getDataKey(File metadataFile) {
-        return metadataFile.getName();
+    private String getDataKey(Path metadataPath) {
+        return metadataPath.getFileName().toString();
     }
 
-    private String getTransferKey(RepositorySystemSession session, File metadataFile, RemoteRepository repository) {
-        return metadataFile.getName() + '/' + getRepoKey(session, repository);
+    private String getTransferKey(RepositorySystemSession session, Path metadataPath, RemoteRepository repository) {
+        return metadataPath.getFileName().toString() + '/' + getRepoKey(session, repository);
     }
 
     private String getRepoKey(RepositorySystemSession session, RemoteRepository repository) {
@@ -409,8 +418,8 @@ public class DefaultUpdateCheckManager implements UpdateCheckManager {
         return result;
     }
 
-    private String getUpdateKey(RepositorySystemSession session, File file, RemoteRepository repository) {
-        return file.getAbsolutePath() + '|' + getRepoKey(session, repository);
+    private String getUpdateKey(RepositorySystemSession session, Path path, RemoteRepository repository) {
+        return path.toAbsolutePath() + "|" + getRepoKey(session, repository);
     }
 
     private int getSessionState(RepositorySystemSession session) {
@@ -453,8 +462,8 @@ public class DefaultUpdateCheckManager implements UpdateCheckManager {
         return updatePolicyAnalyzer.isUpdatedRequired(session, lastModified, policy);
     }
 
-    private Properties read(File touchFile) {
-        Properties props = trackingFileManager.read(touchFile);
+    private Properties read(Path touchPath) {
+        Properties props = trackingFileManager.read(touchPath);
         return (props != null) ? props : new Properties();
     }
 
@@ -462,18 +471,22 @@ public class DefaultUpdateCheckManager implements UpdateCheckManager {
     public void touchArtifact(RepositorySystemSession session, UpdateCheck<Artifact, ArtifactTransferException> check) {
         requireNonNull(session, "session cannot be null");
         requireNonNull(check, "check cannot be null");
-        File artifactFile = check.getFile();
-        File touchFile = getArtifactTouchFile(artifactFile);
+        Path artifactPath = check.getPath();
+        Path touchPath = getArtifactTouchFile(artifactPath);
 
-        String updateKey = getUpdateKey(session, artifactFile, check.getRepository());
+        String updateKey = getUpdateKey(session, artifactPath, check.getRepository());
         String dataKey = getDataKey(check.getAuthoritativeRepository());
         String transferKey = getTransferKey(session, check.getRepository());
 
         setUpdated(session, updateKey);
-        Properties props = write(touchFile, dataKey, transferKey, check.getException());
+        Properties props = write(touchPath, dataKey, transferKey, check.getException());
 
-        if (artifactFile.exists() && !hasErrors(props)) {
-            touchFile.delete();
+        if (Files.exists(artifactPath) && !hasErrors(props)) {
+            try {
+                Files.deleteIfExists(touchPath);
+            } catch (IOException e) {
+                throw new UncheckedIOException(e);
+            }
         }
     }
 
@@ -490,18 +503,18 @@ public class DefaultUpdateCheckManager implements UpdateCheckManager {
     public void touchMetadata(RepositorySystemSession session, UpdateCheck<Metadata, MetadataTransferException> check) {
         requireNonNull(session, "session cannot be null");
         requireNonNull(check, "check cannot be null");
-        File metadataFile = check.getFile();
-        File touchFile = getMetadataTouchFile(metadataFile);
+        Path metadataPath = check.getPath();
+        Path touchPath = getMetadataTouchFile(metadataPath);
 
-        String updateKey = getUpdateKey(session, metadataFile, check.getRepository());
-        String dataKey = getDataKey(metadataFile);
-        String transferKey = getTransferKey(session, metadataFile, check.getRepository());
+        String updateKey = getUpdateKey(session, metadataPath, check.getRepository());
+        String dataKey = getDataKey(metadataPath);
+        String transferKey = getTransferKey(session, metadataPath, check.getRepository());
 
         setUpdated(session, updateKey);
-        write(touchFile, dataKey, transferKey, check.getException());
+        write(touchPath, dataKey, transferKey, check.getException());
     }
 
-    private Properties write(File touchFile, String dataKey, String transferKey, Exception error) {
+    private Properties write(Path touchPath, String dataKey, String transferKey, Exception error) {
         Map<String, String> updates = new HashMap<>();
 
         String timestamp = Long.toString(System.currentTimeMillis());
@@ -524,6 +537,6 @@ public class DefaultUpdateCheckManager implements UpdateCheckManager {
             updates.put(transferKey + UPDATED_KEY_SUFFIX, timestamp);
         }
 
-        return trackingFileManager.update(touchFile, updates);
+        return trackingFileManager.update(touchPath, updates);
     }
 }
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManager.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManager.java
index 8191f0b0..a414c858 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManager.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManager.java
@@ -18,7 +18,8 @@
  */
 package org.eclipse.aether.internal.impl;
 
-import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
@@ -67,7 +68,7 @@ class EnhancedLocalRepositoryManager extends SimpleLocalRepositoryManager {
     private final LocalPathPrefixComposer localPathPrefixComposer;
 
     EnhancedLocalRepositoryManager(
-            File basedir,
+            Path basedir,
             LocalPathComposer localPathComposer,
             String trackingFilename,
             TrackingFileManager trackingFileManager,
@@ -119,21 +120,21 @@ class EnhancedLocalRepositoryManager extends SimpleLocalRepositoryManager {
         LocalArtifactResult result = new LocalArtifactResult(request);
 
         String path;
-        File file;
+        Path filePath;
 
         // Local repository CANNOT have timestamped installed, they are created only during deploy
         if (Objects.equals(artifact.getVersion(), artifact.getBaseVersion())) {
             path = getPathForLocalArtifact(artifact);
-            file = new File(getRepository().getBasedir(), path);
-            checkFind(file, result);
+            filePath = getRepository().getBasePath().resolve(path);
+            checkFind(filePath, result);
         }
 
         if (!result.isAvailable()) {
             for (RemoteRepository repository : request.getRepositories()) {
                 path = getPathForRemoteArtifact(artifact, repository, request.getContext());
-                file = new File(getRepository().getBasedir(), path);
+                filePath = getRepository().getBasePath().resolve(path);
 
-                checkFind(file, result);
+                checkFind(filePath, result);
 
                 if (result.isAvailable()) {
                     break;
@@ -144,19 +145,19 @@ class EnhancedLocalRepositoryManager extends SimpleLocalRepositoryManager {
         return result;
     }
 
-    private void checkFind(File file, LocalArtifactResult result) {
-        if (file.isFile()) {
-            result.setFile(file);
+    private void checkFind(Path path, LocalArtifactResult result) {
+        if (Files.isRegularFile(path)) {
+            result.setPath(path);
 
-            Properties props = readRepos(file);
+            Properties props = readRepos(path);
 
-            if (props.get(getKey(file, LOCAL_REPO_ID)) != null) {
+            if (props.get(getKey(path, LOCAL_REPO_ID)) != null) {
                 // artifact installed into the local repo is always accepted
                 result.setAvailable(true);
             } else {
                 String context = result.getRequest().getContext();
                 for (RemoteRepository repository : result.getRequest().getRepositories()) {
-                    if (props.get(getKey(file, getRepositoryKey(repository, context))) != null) {
+                    if (props.get(getKey(path, getRepositoryKey(repository, context))) != null) {
                         // artifact downloaded from remote repository is accepted only downloaded from request
                         // repositories
                         result.setAvailable(true);
@@ -164,7 +165,7 @@ class EnhancedLocalRepositoryManager extends SimpleLocalRepositoryManager {
                         break;
                     }
                 }
-                if (!result.isAvailable() && !isTracked(props, file)) {
+                if (!result.isAvailable() && !isTracked(props, path)) {
                     /*
                      * NOTE: The artifact is present but not tracked at all, for inter-op with simple local repo, assume
                      * the artifact was locally installed.
@@ -210,40 +211,40 @@ class EnhancedLocalRepositoryManager extends SimpleLocalRepositoryManager {
         String path = repository == null
                 ? getPathForLocalArtifact(artifact)
                 : getPathForRemoteArtifact(artifact, repository, context);
-        File file = new File(getRepository().getBasedir(), path);
+        Path file = getRepository().getBasePath().resolve(path);
         addRepo(file, repositories);
     }
 
-    private Properties readRepos(File artifactFile) {
-        File trackingFile = getTrackingFile(artifactFile);
+    private Properties readRepos(Path artifactPath) {
+        Path trackingFile = getTrackingFile(artifactPath);
 
         Properties props = trackingFileManager.read(trackingFile);
 
         return (props != null) ? props : new Properties();
     }
 
-    private void addRepo(File artifactFile, Collection<String> repositories) {
+    private void addRepo(Path artifactPath, Collection<String> repositories) {
         Map<String, String> updates = new HashMap<>();
         for (String repository : repositories) {
-            updates.put(getKey(artifactFile, repository), "");
+            updates.put(getKey(artifactPath, repository), "");
         }
 
-        File trackingFile = getTrackingFile(artifactFile);
+        Path trackingPath = getTrackingFile(artifactPath);
 
-        trackingFileManager.update(trackingFile, updates);
+        trackingFileManager.update(trackingPath, updates);
     }
 
-    private File getTrackingFile(File artifactFile) {
-        return new File(artifactFile.getParentFile(), trackingFilename);
+    private Path getTrackingFile(Path artifactPath) {
+        return artifactPath.getParent().resolve(trackingFilename);
     }
 
-    private String getKey(File file, String repository) {
-        return file.getName() + '>' + repository;
+    private String getKey(Path path, String repository) {
+        return path.getFileName() + ">" + repository;
     }
 
-    private boolean isTracked(Properties props, File file) {
+    private boolean isTracked(Properties props, Path path) {
         if (props != null) {
-            String keyPrefix = file.getName() + '>';
+            String keyPrefix = path.getFileName() + ">";
             for (Object key : props.keySet()) {
                 if (key.toString().startsWith(keyPrefix)) {
                     return true;
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManagerFactory.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManagerFactory.java
index d25ea85a..561540d1 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManagerFactory.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManagerFactory.java
@@ -91,7 +91,7 @@ public class EnhancedLocalRepositoryManagerFactory implements LocalRepositoryMan
 
         if ("".equals(repository.getContentType()) || "default".equals(repository.getContentType())) {
             return new EnhancedLocalRepositoryManager(
-                    repository.getBasedir(),
+                    repository.getBasePath(),
                     localPathComposer,
                     trackingFilename,
                     trackingFileManager,
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/SimpleLocalRepositoryManager.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/SimpleLocalRepositoryManager.java
index 82f6ddfe..ee393140 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/SimpleLocalRepositoryManager.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/SimpleLocalRepositoryManager.java
@@ -18,7 +18,8 @@
  */
 package org.eclipse.aether.internal.impl;
 
-import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.Objects;
 import java.util.SortedSet;
 import java.util.TreeSet;
@@ -48,9 +49,9 @@ class SimpleLocalRepositoryManager implements LocalRepositoryManager {
 
     private final LocalPathComposer localPathComposer;
 
-    SimpleLocalRepositoryManager(File basedir, String type, LocalPathComposer localPathComposer) {
-        requireNonNull(basedir, "base directory cannot be null");
-        repository = new LocalRepository(basedir.getAbsoluteFile(), type);
+    SimpleLocalRepositoryManager(Path basePath, String type, LocalPathComposer localPathComposer) {
+        requireNonNull(basePath, "base directory cannot be null");
+        repository = new LocalRepository(basePath.toAbsolutePath(), type);
         this.localPathComposer = requireNonNull(localPathComposer);
     }
 
@@ -132,14 +133,14 @@ class SimpleLocalRepositoryManager implements LocalRepositoryManager {
         LocalArtifactResult result = new LocalArtifactResult(request);
 
         String path;
-        File file;
+        Path filePath;
 
         // Local repository CANNOT have timestamped installed, they are created only during deploy
         if (Objects.equals(artifact.getVersion(), artifact.getBaseVersion())) {
             path = getPathForLocalArtifact(artifact);
-            file = new File(getRepository().getBasedir(), path);
-            if (file.isFile()) {
-                result.setFile(file);
+            filePath = getRepository().getBasePath().resolve(path);
+            if (Files.isRegularFile(filePath)) {
+                result.setPath(filePath);
                 result.setAvailable(true);
             }
         }
@@ -147,9 +148,9 @@ class SimpleLocalRepositoryManager implements LocalRepositoryManager {
         if (!result.isAvailable()) {
             for (RemoteRepository repository : request.getRepositories()) {
                 path = getPathForRemoteArtifact(artifact, repository, request.getContext());
-                file = new File(getRepository().getBasedir(), path);
-                if (file.isFile()) {
-                    result.setFile(file);
+                filePath = getRepository().getBasePath().resolve(path);
+                if (Files.isRegularFile(filePath)) {
+                    result.setPath(filePath);
                     result.setAvailable(true);
                     break;
                 }
@@ -184,9 +185,9 @@ class SimpleLocalRepositoryManager implements LocalRepositoryManager {
             path = getPathForLocalMetadata(metadata);
         }
 
-        File file = new File(getRepository().getBasedir(), path);
-        if (file.isFile()) {
-            result.setFile(file);
+        Path filePath = getRepository().getBasePath().resolve(path);
+        if (Files.isRegularFile(filePath)) {
+            result.setPath(filePath);
         }
 
         return result;
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/SimpleLocalRepositoryManagerFactory.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/SimpleLocalRepositoryManagerFactory.java
index 55ddb1e1..f4a3cc7f 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/SimpleLocalRepositoryManagerFactory.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/SimpleLocalRepositoryManagerFactory.java
@@ -60,7 +60,7 @@ public class SimpleLocalRepositoryManagerFactory implements LocalRepositoryManag
         requireNonNull(repository, "repository cannot be null");
 
         if ("".equals(repository.getContentType()) || "simple".equals(repository.getContentType())) {
-            return new SimpleLocalRepositoryManager(repository.getBasedir(), "simple", localPathComposer);
+            return new SimpleLocalRepositoryManager(repository.getBasePath(), "simple", localPathComposer);
         } else {
             throw new NoLocalRepositoryManagerException(repository);
         }
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 5e922bf5..d7a03ab8 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
@@ -19,6 +19,7 @@
 package org.eclipse.aether.internal.impl;
 
 import java.io.File;
+import java.nio.file.Path;
 import java.util.Map;
 import java.util.Properties;
 
@@ -28,12 +29,29 @@ import java.util.Properties;
 public interface TrackingFileManager {
     /**
      * Reads up the specified properties file into {@link Properties}, if exists, otherwise {@code null} is returned.
+     * @deprecated Use {@link #read(Path)} instead.
      */
+    @Deprecated
     Properties read(File file);
 
+    /**
+     * Reads up the specified properties file into {@link Properties}, if exists, otherwise {@code null} is returned.
+     * @since 2.0.0
+     */
+    Properties read(Path path);
+
     /**
      * Applies updates to specified properties file and returns resulting {@link Properties} with contents same
      * as in updated file, never {@code null}.
+     * @deprecated Use {@link #update(Path, Map)} instead.
      */
+    @Deprecated
     Properties update(File file, Map<String, String> updates);
+
+    /**
+     * Applies updates to specified properties file and returns resulting {@link Properties} with contents same
+     * as in updated file, never {@code null}.
+     * @since 2.0.0
+     */
+    Properties update(Path path, 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 66eb0dd9..5a4013df 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
@@ -35,7 +35,7 @@ import org.eclipse.aether.artifact.Artifact;
 import org.eclipse.aether.internal.impl.LocalPathComposer;
 import org.eclipse.aether.repository.ArtifactRepository;
 import org.eclipse.aether.spi.connector.checksum.ChecksumAlgorithmFactory;
-import org.eclipse.aether.spi.io.FileProcessor;
+import org.eclipse.aether.spi.io.ChecksumProcessor;
 import org.eclipse.aether.util.ConfigUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -98,13 +98,14 @@ public final class SparseDirectoryTrustedChecksumsSource extends FileTrustedChec
 
     private static final Logger LOGGER = LoggerFactory.getLogger(SparseDirectoryTrustedChecksumsSource.class);
 
-    private final FileProcessor fileProcessor;
+    private final ChecksumProcessor checksumProcessor;
 
     private final LocalPathComposer localPathComposer;
 
     @Inject
-    public SparseDirectoryTrustedChecksumsSource(FileProcessor fileProcessor, LocalPathComposer localPathComposer) {
-        this.fileProcessor = requireNonNull(fileProcessor);
+    public SparseDirectoryTrustedChecksumsSource(
+            ChecksumProcessor checksumProcessor, LocalPathComposer localPathComposer) {
+        this.checksumProcessor = requireNonNull(checksumProcessor);
         this.localPathComposer = requireNonNull(localPathComposer);
     }
 
@@ -141,7 +142,7 @@ public final class SparseDirectoryTrustedChecksumsSource extends FileTrustedChec
                 }
 
                 try {
-                    String checksum = fileProcessor.readChecksum(checksumPath.toFile());
+                    String checksum = checksumProcessor.readChecksum(checksumPath);
                     if (checksum != null) {
                         checksums.put(checksumAlgorithmFactory.getName(), checksum);
                     }
@@ -196,7 +197,7 @@ public final class SparseDirectoryTrustedChecksumsSource extends FileTrustedChec
                 Path checksumPath = basedir.resolve(
                         calculateArtifactPath(originAware, artifact, artifactRepository, checksumAlgorithmFactory));
                 String checksum = requireNonNull(trustedArtifactChecksums.get(checksumAlgorithmFactory.getName()));
-                fileProcessor.writeChecksum(checksumPath.toFile(), checksum);
+                checksumProcessor.writeChecksum(checksumPath, checksum);
             }
         }
     }
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/resolution/TrustedChecksumsArtifactResolverPostProcessor.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/resolution/TrustedChecksumsArtifactResolverPostProcessor.java
index 709ac446..95e59485 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/resolution/TrustedChecksumsArtifactResolverPostProcessor.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/resolution/TrustedChecksumsArtifactResolverPostProcessor.java
@@ -169,7 +169,7 @@ public final class TrustedChecksumsArtifactResolverPostProcessor extends Artifac
                 if (record) {
                     recordArtifactChecksums(session, artifactResult, checksumAlgorithms);
                 } else if (!validateArtifactChecksums(session, artifactResult, checksumAlgorithms, failIfMissing)) {
-                    artifactResult.setArtifact(artifactResult.getArtifact().setFile(null)); // make it unresolved
+                    artifactResult.setArtifact(artifactResult.getArtifact().setPath(null)); // make it unresolved
                 }
             }
         }
@@ -186,7 +186,7 @@ public final class TrustedChecksumsArtifactResolverPostProcessor extends Artifac
         ArtifactRepository artifactRepository = artifactResult.getRepository();
         try {
             final Map<String, String> calculatedChecksums =
-                    ChecksumAlgorithmHelper.calculate(artifact.getFile(), checksumAlgorithmFactories);
+                    ChecksumAlgorithmHelper.calculate(artifact.getPath(), checksumAlgorithmFactories);
 
             for (TrustedChecksumsSource trustedChecksumsSource : trustedChecksumsSources.values()) {
                 TrustedChecksumsSource.Writer writer =
@@ -197,12 +197,12 @@ public final class TrustedChecksumsArtifactResolverPostProcessor extends Artifac
                                 artifact, artifactRepository, checksumAlgorithmFactories, calculatedChecksums);
                     } catch (IOException e) {
                         throw new UncheckedIOException(
-                                "Could not write required checksums for " + artifact.getFile(), e);
+                                "Could not write required checksums for " + artifact.getPath(), e);
                     }
                 }
             }
         } catch (IOException e) {
-            throw new UncheckedIOException("Could not calculate required checksums for " + artifact.getFile(), e);
+            throw new UncheckedIOException("Could not calculate required checksums for " + artifact.getPath(), e);
         }
     }
 
@@ -222,7 +222,7 @@ public final class TrustedChecksumsArtifactResolverPostProcessor extends Artifac
         try {
             // full set: calculate all algorithms we were asked for
             final Map<String, String> calculatedChecksums =
-                    ChecksumAlgorithmHelper.calculate(artifact.getFile(), checksumAlgorithmFactories);
+                    ChecksumAlgorithmHelper.calculate(artifact.getPath(), checksumAlgorithmFactories);
 
             for (Map.Entry<String, TrustedChecksumsSource> entry : trustedChecksumsSources.entrySet()) {
                 final String trustedSourceName = entry.getKey();
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/session/DefaultCloseableSession.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/session/DefaultCloseableSession.java
index 4aca985d..dc808690 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/session/DefaultCloseableSession.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/session/DefaultCloseableSession.java
@@ -18,9 +18,7 @@
  */
 package org.eclipse.aether.internal.impl.session;
 
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
 import java.util.concurrent.atomic.AtomicBoolean;
 
 import org.eclipse.aether.*;
@@ -41,6 +39,8 @@ import org.eclipse.aether.repository.WorkspaceReader;
 import org.eclipse.aether.resolution.ArtifactDescriptorPolicy;
 import org.eclipse.aether.resolution.ResolutionErrorPolicy;
 import org.eclipse.aether.transfer.TransferListener;
+import org.eclipse.aether.util.listener.ChainedRepositoryListener;
+import org.eclipse.aether.util.listener.ChainedTransferListener;
 
 import static java.util.Objects.requireNonNull;
 
@@ -119,10 +119,10 @@ public final class DefaultCloseableSession implements CloseableSession {
             String artifactUpdatePolicy,
             String metadataUpdatePolicy,
             LocalRepositoryManager localRepositoryManager,
-            List<LocalRepository> localRepositories,
+            Collection<LocalRepository> localRepositories,
             WorkspaceReader workspaceReader,
-            RepositoryListener repositoryListener,
-            TransferListener transferListener,
+            Collection<RepositoryListener> repositoryListener,
+            Collection<TransferListener> transferListener,
             Map<String, String> systemProperties,
             Map<String, String> userProperties,
             Map<String, Object> configProperties,
@@ -138,6 +138,7 @@ public final class DefaultCloseableSession implements CloseableSession {
             SessionData data,
             RepositoryCache cache,
             SystemScopeHandler systemScopeHandler,
+            List<Runnable> onSessionEndedHandlers,
             RepositorySystem repositorySystem,
             RepositorySystemLifecycle repositorySystemLifecycle) {
         this.sessionId = requireNonNull(sessionId);
@@ -150,8 +151,8 @@ public final class DefaultCloseableSession implements CloseableSession {
         this.artifactUpdatePolicy = artifactUpdatePolicy;
         this.metadataUpdatePolicy = metadataUpdatePolicy;
         this.workspaceReader = workspaceReader;
-        this.repositoryListener = repositoryListener;
-        this.transferListener = transferListener;
+        this.repositoryListener = new ChainedRepositoryListener(repositoryListener);
+        this.transferListener = new ChainedTransferListener(transferListener);
         this.systemProperties = Collections.unmodifiableMap(systemProperties);
         this.userProperties = Collections.unmodifiableMap(userProperties);
         this.configProperties = Collections.unmodifiableMap(configProperties);
@@ -174,14 +175,15 @@ public final class DefaultCloseableSession implements CloseableSession {
         this.localRepositoryManager = getOrCreateLocalRepositoryManager(localRepositoryManager, localRepositories);
 
         repositorySystemLifecycle.sessionStarted(this);
+        onSessionEndedHandlers.forEach(this::addOnSessionEndedHandler);
     }
 
     private LocalRepositoryManager getOrCreateLocalRepositoryManager(
-            LocalRepositoryManager localRepositoryManager, List<LocalRepository> localRepositories) {
+            LocalRepositoryManager localRepositoryManager, Collection<LocalRepository> localRepositories) {
         if (localRepositoryManager != null) {
             return localRepositoryManager;
         } else if (localRepositories != null) {
-            return repositorySystem.newLocalRepositoryManager(this, localRepositories);
+            return repositorySystem.newLocalRepositoryManager(this, new ArrayList<>(localRepositories));
         } else {
             throw new IllegalStateException("No local repository manager or local repositories set on session");
         }
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/session/DefaultSessionBuilder.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/session/DefaultSessionBuilder.java
index 845a2f39..630fdf9e 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/session/DefaultSessionBuilder.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/session/DefaultSessionBuilder.java
@@ -18,16 +18,19 @@
  */
 package org.eclipse.aether.internal.impl.session;
 
-import java.io.File;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
+import java.nio.file.Path;
+import java.util.*;
 import java.util.function.Supplier;
 
-import org.eclipse.aether.*;
+import org.eclipse.aether.DefaultSessionData;
+import org.eclipse.aether.RepositoryCache;
+import org.eclipse.aether.RepositoryListener;
+import org.eclipse.aether.RepositorySystem;
+import org.eclipse.aether.RepositorySystemSession;
 import org.eclipse.aether.RepositorySystemSession.CloseableSession;
 import org.eclipse.aether.RepositorySystemSession.SessionBuilder;
+import org.eclipse.aether.SessionData;
+import org.eclipse.aether.SystemScopeHandler;
 import org.eclipse.aether.artifact.ArtifactTypeRegistry;
 import org.eclipse.aether.collection.DependencyGraphTransformer;
 import org.eclipse.aether.collection.DependencyManager;
@@ -87,13 +90,13 @@ public final class DefaultSessionBuilder implements SessionBuilder {
 
     private LocalRepositoryManager localRepositoryManager;
 
-    private List<LocalRepository> localRepositories;
+    private Collection<LocalRepository> localRepositories;
 
     private WorkspaceReader workspaceReader;
 
-    private RepositoryListener repositoryListener;
+    private final ArrayList<RepositoryListener> repositoryListener = new ArrayList<>();
 
-    private TransferListener transferListener;
+    private final ArrayList<TransferListener> transferListener = new ArrayList<>();
 
     private Map<String, String> systemProperties = new HashMap<>();
 
@@ -125,6 +128,8 @@ public final class DefaultSessionBuilder implements SessionBuilder {
 
     private SystemScopeHandler systemScopeHandler = SystemScopeHandler.LEGACY;
 
+    private final ArrayList<Runnable> onSessionCloseHandlers = new ArrayList<>();
+
     /**
      * Constructor for "top level" builders.
      */
@@ -200,13 +205,19 @@ public final class DefaultSessionBuilder implements SessionBuilder {
 
     @Override
     public DefaultSessionBuilder setRepositoryListener(RepositoryListener repositoryListener) {
-        this.repositoryListener = repositoryListener;
+        this.repositoryListener.clear();
+        if (repositoryListener != null) {
+            this.repositoryListener.add(repositoryListener);
+        }
         return this;
     }
 
     @Override
     public DefaultSessionBuilder setTransferListener(TransferListener transferListener) {
-        this.transferListener = transferListener;
+        this.transferListener.clear();
+        if (transferListener != null) {
+            this.transferListener.add(transferListener);
+        }
         return this;
     }
 
@@ -348,6 +359,13 @@ public final class DefaultSessionBuilder implements SessionBuilder {
         return this;
     }
 
+    @Override
+    public SessionBuilder addOnSessionEndedHandler(Runnable handler) {
+        requireNonNull(handler, "null handler");
+        onSessionCloseHandlers.add(handler);
+        return this;
+    }
+
     @Override
     public DefaultSessionBuilder setRepositoryCacheSupplier(Supplier<RepositoryCache> cacheSupplier) {
         requireNonNull(cacheSupplier, "null cacheSupplier");
@@ -356,12 +374,12 @@ public final class DefaultSessionBuilder implements SessionBuilder {
     }
 
     @Override
-    public SessionBuilder withLocalRepositoryBaseDirectories(File... baseDirectories) {
+    public SessionBuilder withLocalRepositoryBaseDirectories(Path... baseDirectories) {
         return withLocalRepositoryBaseDirectories(Arrays.asList(baseDirectories));
     }
 
     @Override
-    public SessionBuilder withLocalRepositoryBaseDirectories(List<File> baseDirectories) {
+    public SessionBuilder withLocalRepositoryBaseDirectories(Collection<Path> baseDirectories) {
         requireNonNull(baseDirectories, "null baseDirectories");
         return withLocalRepositories(
                 baseDirectories.stream().map(LocalRepository::new).collect(toList()));
@@ -373,12 +391,34 @@ public final class DefaultSessionBuilder implements SessionBuilder {
     }
 
     @Override
-    public SessionBuilder withLocalRepositories(List<LocalRepository> localRepositories) {
+    public SessionBuilder withLocalRepositories(Collection<LocalRepository> localRepositories) {
         requireNonNull(localRepositories, "null localRepositories");
         this.localRepositories = localRepositories;
         return this;
     }
 
+    @Override
+    public SessionBuilder withRepositoryListener(RepositoryListener... repositoryListeners) {
+        return withRepositoryListener(Arrays.asList(repositoryListeners));
+    }
+
+    @Override
+    public SessionBuilder withRepositoryListener(Collection<RepositoryListener> repositoryListeners) {
+        this.repositoryListener.addAll(repositoryListeners);
+        return this;
+    }
+
+    @Override
+    public SessionBuilder withTransferListener(TransferListener... transferListeners) {
+        return withTransferListener(Arrays.asList(transferListeners));
+    }
+
+    @Override
+    public SessionBuilder withTransferListener(Collection<TransferListener> transferListeners) {
+        this.transferListener.addAll(transferListeners);
+        return this;
+    }
+
     @Override
     public SessionBuilder withRepositorySystemSession(RepositorySystemSession session) {
         requireNonNull(session, "repository system session cannot be null");
@@ -442,6 +482,7 @@ public final class DefaultSessionBuilder implements SessionBuilder {
                 sessionDataSupplier.get(),
                 repositoryCacheSupplier.get(),
                 systemScopeHandler,
+                onSessionCloseHandlers,
                 repositorySystem,
                 repositorySystemLifecycle);
     }
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/BasedirNameMapper.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/BasedirNameMapper.java
index 8ae07dc2..c3b192a0 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/BasedirNameMapper.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/BasedirNameMapper.java
@@ -52,13 +52,33 @@ public class BasedirNameMapper implements NameMapper {
 
     private final NameMapper delegate;
 
+    private final Path basePath;
+
     public BasedirNameMapper(final NameMapper delegate) {
-        this.delegate = requireNonNull(delegate);
+        this(delegate, null);
+    }
+
+    /**
+     * Creates basedir name mapper with provided path as base.
+     *
+     * @param delegate The delegate to resolve against basedir, must not be {@code null}. The delegate must be
+     *                 "file system friendly", see {@link NameMapper#isFileSystemFriendly()} method.
+     * @param path The basedir, may be {@code null} in which case given session local repository root is used as
+     *             basedir.
+     * @since 2.0.0
+     */
+    public BasedirNameMapper(final NameMapper delegate, final Path path) {
+        requireNonNull(delegate);
+        if (!delegate.isFileSystemFriendly()) {
+            throw new IllegalArgumentException("delegate must be file-system friendly");
+        }
+        this.delegate = delegate;
+        this.basePath = path;
     }
 
     @Override
     public boolean isFileSystemFriendly() {
-        return delegate.isFileSystemFriendly();
+        return true; // it enforces delegate to be friendly, and itself produces friendly names
     }
 
     @Override
@@ -67,11 +87,12 @@ public class BasedirNameMapper implements NameMapper {
             final Collection<? extends Artifact> artifacts,
             final Collection<? extends Metadata> metadatas) {
         try {
-            final Path basedir =
-                    DirectoryUtils.resolveDirectory(session, DEFAULT_LOCKS_DIR, CONFIG_PROP_LOCKS_DIR, false);
+            final Path basedir = basePath != null
+                    ? basePath
+                    : DirectoryUtils.resolveDirectory(session, DEFAULT_LOCKS_DIR, CONFIG_PROP_LOCKS_DIR, false);
 
             return delegate.nameLocks(session, artifacts, metadatas).stream()
-                    .map(name -> basedir.resolve(name).toAbsolutePath().toString())
+                    .map(name -> basedir.resolve(name).toAbsolutePath().toUri().toASCIIString())
                     .collect(Collectors.toList());
         } catch (IOException e) {
             throw new UncheckedIOException(e);
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/DiscriminatingNameMapper.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/DiscriminatingNameMapper.java
index 37464e9d..84e9fae9 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/DiscriminatingNameMapper.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/DiscriminatingNameMapper.java
@@ -18,9 +18,9 @@
  */
 package org.eclipse.aether.internal.impl.synccontext.named;
 
-import java.io.File;
 import java.net.InetAddress;
 import java.net.UnknownHostException;
+import java.nio.file.Path;
 import java.util.Collection;
 
 import org.eclipse.aether.RepositorySystemSession;
@@ -110,7 +110,7 @@ public class DiscriminatingNameMapper implements NameMapper {
 
         if (discriminator == null || discriminator.isEmpty()) {
             String hostname = ConfigUtils.getString(session, this.hostname, CONFIG_PROP_HOSTNAME);
-            File basedir = session.getLocalRepository().getBasedir();
+            Path basedir = session.getLocalRepository().getBasePath();
             discriminator = hostname + ":" + basedir;
             try {
                 return StringDigestUtil.sha1(discriminator);
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/GAVNameMapper.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/GAVNameMapper.java
index 556a9f68..dfbf9740 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/GAVNameMapper.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/GAVNameMapper.java
@@ -29,7 +29,7 @@ import static java.util.Objects.requireNonNull;
 
 /**
  * Artifact GAV {@link NameMapper}, uses artifact and metadata coordinates to name their corresponding locks. Is not
- * considering local repository, only the artifact coordinates. May use custom prefixes and sufixes and separators,
+ * considering local repository, only the artifact coordinates. May use custom prefixes and suffixes and separators,
  * hence this instance may or may not be filesystem friendly (depends on strings used).
  */
 public class GAVNameMapper implements NameMapper {
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/NameMapper.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/NameMapper.java
index 2ec63c84..80b7b040 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/NameMapper.java
+++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/NameMapper.java
@@ -31,6 +31,11 @@ public interface NameMapper {
     /**
      * Returns {@code true} if lock names returned by this lock name mapper are file system friendly, can be used
      * as file names and paths.
+     * <p>
+     * <em>Note:</em> The fact that name mapper is "file system friendly" means ONLY that names it produces CAN be
+     * used as file names and paths. Still, it does not mean they will work with ANY file based locking, as for example
+     * {@link org.eclipse.aether.named.providers.FileLockNamedLockFactory} expects names as string encoded
+     * {@link java.net.URI}s. The only name mapper doing it is {@link BasedirNameMapper}.
      *
      * @since 1.9.0
      */
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultArtifactResolverTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultArtifactResolverTest.java
index afdb8aac..2098aba6 100644
--- a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultArtifactResolverTest.java
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultArtifactResolverTest.java
@@ -38,10 +38,7 @@ import org.eclipse.aether.impl.UpdateCheckManager;
 import org.eclipse.aether.impl.VersionResolver;
 import org.eclipse.aether.internal.impl.filter.DefaultRemoteRepositoryFilterManager;
 import org.eclipse.aether.internal.impl.filter.Filters;
-import org.eclipse.aether.internal.test.util.TestFileProcessor;
-import org.eclipse.aether.internal.test.util.TestFileUtils;
-import org.eclipse.aether.internal.test.util.TestLocalRepositoryManager;
-import org.eclipse.aether.internal.test.util.TestUtils;
+import org.eclipse.aether.internal.test.util.*;
 import org.eclipse.aether.metadata.Metadata;
 import org.eclipse.aether.repository.LocalArtifactRegistration;
 import org.eclipse.aether.repository.LocalArtifactRequest;
@@ -113,7 +110,7 @@ public class DefaultArtifactResolverTest {
     private DefaultArtifactResolver setupArtifactResolver(
             VersionResolver versionResolver, UpdateCheckManager updateCheckManager) {
         return new DefaultArtifactResolver(
-                new TestFileProcessor(),
+                new TestPathProcessor(),
                 new StubRepositoryEventDispatcher(),
                 versionResolver,
                 updateCheckManager,
@@ -365,7 +362,10 @@ public class DefaultArtifactResolverTest {
         repositoryConnectorProvider.setConnector(connector);
         resolver = setupArtifactResolver(
                 new StubVersionResolver(),
-                new DefaultUpdateCheckManager(new DefaultTrackingFileManager(), new DefaultUpdatePolicyAnalyzer()));
+                new DefaultUpdateCheckManager(
+                        new DefaultTrackingFileManager(),
+                        new DefaultUpdatePolicyAnalyzer(),
+                        new DefaultPathProcessor()));
 
         session.setResolutionErrorPolicy(new SimpleResolutionErrorPolicy(true, false));
         session.setUpdatePolicy(RepositoryPolicy.UPDATE_POLICY_NEVER);
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultChecksumPolicyProviderTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultChecksumPolicyProviderTest.java
index cba05a93..af434cc4 100644
--- a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultChecksumPolicyProviderTest.java
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultChecksumPolicyProviderTest.java
@@ -18,6 +18,8 @@
  */
 package org.eclipse.aether.internal.impl;
 
+import java.nio.file.Path;
+
 import org.eclipse.aether.DefaultRepositorySystemSession;
 import org.eclipse.aether.internal.test.util.TestUtils;
 import org.eclipse.aether.repository.RemoteRepository;
@@ -47,7 +49,7 @@ public class DefaultChecksumPolicyProviderTest {
         session = TestUtils.newSession();
         provider = new DefaultChecksumPolicyProvider();
         repository = new RemoteRepository.Builder("test", "default", "file:/void").build();
-        resource = new TransferResource(repository.getId(), repository.getUrl(), "file.txt", null, null);
+        resource = new TransferResource(repository.getId(), repository.getUrl(), "file.txt", (Path) null, null);
     }
 
     @AfterEach
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultDeployerTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultDeployerTest.java
index abe6e532..7dd083d6 100644
--- a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultDeployerTest.java
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultDeployerTest.java
@@ -20,6 +20,7 @@ package org.eclipse.aether.internal.impl;
 
 import java.io.File;
 import java.io.IOException;
+import java.nio.file.Path;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
@@ -34,8 +35,8 @@ import org.eclipse.aether.artifact.Artifact;
 import org.eclipse.aether.artifact.DefaultArtifact;
 import org.eclipse.aether.deployment.DeployRequest;
 import org.eclipse.aether.deployment.DeploymentException;
-import org.eclipse.aether.internal.test.util.TestFileProcessor;
 import org.eclipse.aether.internal.test.util.TestFileUtils;
+import org.eclipse.aether.internal.test.util.TestPathProcessor;
 import org.eclipse.aether.internal.test.util.TestUtils;
 import org.eclipse.aether.metadata.DefaultMetadata;
 import org.eclipse.aether.metadata.MergeableMetadata;
@@ -84,7 +85,7 @@ public class DefaultDeployerTest {
         connectorProvider = new StubRepositoryConnectorProvider();
 
         deployer = new DefaultDeployer(
-                new TestFileProcessor(),
+                new TestPathProcessor(),
                 new StubRepositoryEventDispatcher(),
                 connectorProvider,
                 new StubRemoteRepositoryManager(),
@@ -239,6 +240,10 @@ public class DefaultDeployerTest {
                 return this;
             }
 
+            public Metadata setPath(Path path) {
+                return this;
+            }
+
             public String getVersion() {
                 return "";
             }
@@ -259,6 +264,10 @@ public class DefaultDeployerTest {
                 return null;
             }
 
+            public Path getPath() {
+                return null;
+            }
+
             public String getArtifactId() {
                 return "aether";
             }
@@ -275,6 +284,10 @@ public class DefaultDeployerTest {
                 return defaultValue;
             }
 
+            public void merge(Path current, Path result) throws RepositoryException {
+                merge(current != null ? current.toFile() : null, result != null ? result.toFile() : null);
+            }
+
             public void merge(File current, File result) throws RepositoryException {
                 requireNonNull(current, "current cannot be null");
                 requireNonNull(result, "result cannot be null");
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultInstallerTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultInstallerTest.java
index 76f15b71..f279a3b6 100644
--- a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultInstallerTest.java
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultInstallerTest.java
@@ -20,6 +20,7 @@ package org.eclipse.aether.internal.impl;
 
 import java.io.File;
 import java.io.IOException;
+import java.nio.file.Path;
 import java.util.Collections;
 import java.util.List;
 
@@ -31,10 +32,7 @@ import org.eclipse.aether.artifact.DefaultArtifact;
 import org.eclipse.aether.installation.InstallRequest;
 import org.eclipse.aether.installation.InstallResult;
 import org.eclipse.aether.installation.InstallationException;
-import org.eclipse.aether.internal.test.util.TestFileProcessor;
-import org.eclipse.aether.internal.test.util.TestFileUtils;
-import org.eclipse.aether.internal.test.util.TestLocalRepositoryManager;
-import org.eclipse.aether.internal.test.util.TestUtils;
+import org.eclipse.aether.internal.test.util.*;
 import org.eclipse.aether.metadata.DefaultMetadata;
 import org.eclipse.aether.metadata.Metadata;
 import org.eclipse.aether.metadata.Metadata.Nature;
@@ -86,7 +84,7 @@ public class DefaultInstallerTest {
         localArtifactFile = new File(session.getLocalRepository().getBasedir(), localArtifactPath);
 
         installer = new DefaultInstaller(
-                new TestFileProcessor(),
+                new TestPathProcessor(),
                 new StubRepositoryEventDispatcher(),
                 Collections.emptyMap(),
                 new StubSyncContextFactory());
@@ -331,9 +329,9 @@ public class DefaultInstallerTest {
         installer.install(session, request);
 
         installer = new DefaultInstaller(
-                new DefaultFileProcessor() {
+                new DefaultPathProcessor() {
                     @Override
-                    public long copy(File src, File target, ProgressListener listener) throws IOException {
+                    public long copy(Path src, Path target, ProgressListener listener) throws IOException {
                         throw new IOException("copy called");
                     }
                 },
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultMetadataResolverTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultMetadataResolverTest.java
index 6a415c6b..688656c1 100644
--- a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultMetadataResolverTest.java
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultMetadataResolverTest.java
@@ -90,7 +90,8 @@ public class DefaultMetadataResolverTest {
                 new StubRemoteRepositoryManager(),
                 new StubSyncContextFactory(),
                 new DefaultOfflineController(),
-                remoteRepositoryFilterManager);
+                remoteRepositoryFilterManager,
+                new DefaultPathProcessor());
         repository = new RemoteRepository.Builder(
                         "test-DMRT",
                         "default",
@@ -267,7 +268,8 @@ public class DefaultMetadataResolverTest {
                 new StubRemoteRepositoryManager(),
                 new StubSyncContextFactory(),
                 new DefaultOfflineController(),
-                remoteRepositoryFilterManager);
+                remoteRepositoryFilterManager,
+                new DefaultPathProcessor());
 
         List<MetadataResult> results = resolver.resolveMetadata(session, Arrays.asList(request));
 
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultUpdateCheckManagerTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultUpdateCheckManagerTest.java
index 62540888..678ab634 100644
--- a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultUpdateCheckManagerTest.java
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultUpdateCheckManagerTest.java
@@ -64,7 +64,7 @@ public class DefaultUpdateCheckManagerTest {
 
     @BeforeEach
     void setup() throws Exception {
-        File dir = TestFileUtils.createTempFile("");
+        File dir = TestFileUtils.createTempDir("");
         TestFileUtils.deleteFile(dir);
 
         File metadataFile = new File(dir, "metadata.txt");
@@ -78,7 +78,8 @@ public class DefaultUpdateCheckManagerTest {
                         "default",
                         TestFileUtils.createTempDir().toURI().toURL().toString())
                 .build();
-        manager = new DefaultUpdateCheckManager(new DefaultTrackingFileManager(), new DefaultUpdatePolicyAnalyzer());
+        manager = new DefaultUpdateCheckManager(
+                new DefaultTrackingFileManager(), new DefaultUpdatePolicyAnalyzer(), new DefaultPathProcessor());
         metadata = new DefaultMetadata(
                 "gid", "aid", "ver", "maven-metadata.xml", Metadata.Nature.RELEASE_OR_SNAPSHOT, metadataFile);
         artifact = new DefaultArtifact("gid", "aid", "", "ext", "ver").setFile(artifactFile);
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManagerTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManagerTest.java
index be51eaa7..1b85146d 100644
--- a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManagerTest.java
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManagerTest.java
@@ -107,7 +107,7 @@ public class EnhancedLocalRepositoryManagerTest {
 
     protected EnhancedLocalRepositoryManager getManager() {
         return new EnhancedLocalRepositoryManager(
-                basedir,
+                basedir.toPath(),
                 new DefaultLocalPathComposer(),
                 "_remote.repositories",
                 trackingFileManager,
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/EnhancedSplitLocalRepositoryManagerTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/EnhancedSplitLocalRepositoryManagerTest.java
index 604db15a..1a500a0c 100644
--- a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/EnhancedSplitLocalRepositoryManagerTest.java
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/EnhancedSplitLocalRepositoryManagerTest.java
@@ -31,7 +31,7 @@ public class EnhancedSplitLocalRepositoryManagerTest extends EnhancedLocalReposi
     protected EnhancedLocalRepositoryManager getManager() {
         session.setConfigProperty(DefaultLocalPathPrefixComposerFactory.CONFIG_PROP_SPLIT, Boolean.TRUE.toString());
         return new EnhancedLocalRepositoryManager(
-                basedir,
+                basedir.toPath(),
                 new DefaultLocalPathComposer(),
                 "_remote.repositories",
                 trackingFileManager,
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/FailChecksumPolicyTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/FailChecksumPolicyTest.java
index 1eecc5d2..a357ea9b 100644
--- a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/FailChecksumPolicyTest.java
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/FailChecksumPolicyTest.java
@@ -18,6 +18,8 @@
  */
 package org.eclipse.aether.internal.impl;
 
+import java.nio.file.Path;
+
 import org.eclipse.aether.spi.connector.checksum.ChecksumPolicy.ChecksumKind;
 import org.eclipse.aether.transfer.ChecksumFailureException;
 import org.eclipse.aether.transfer.TransferResource;
@@ -34,7 +36,7 @@ public class FailChecksumPolicyTest {
 
     @BeforeEach
     void setup() {
-        policy = new FailChecksumPolicy(new TransferResource("null", "file:/dev/null", "file.txt", null, null));
+        policy = new FailChecksumPolicy(new TransferResource("null", "file:/dev/null", "file.txt", (Path) null, null));
         exception = new ChecksumFailureException("test");
     }
 
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/RecordingRepositoryConnector.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/RecordingRepositoryConnector.java
index 257b1e05..091015ce 100644
--- a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/RecordingRepositoryConnector.java
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/RecordingRepositoryConnector.java
@@ -18,6 +18,7 @@
  */
 package org.eclipse.aether.internal.impl;
 
+import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
@@ -163,8 +164,8 @@ class RecordingRepositoryConnector implements RepositoryConnector {
         if (listener == null) {
             return;
         }
-        TransferEvent.Builder event =
-                new TransferEvent.Builder(session, new TransferResource(null, null, null, null, transfer.getTrace()));
+        TransferEvent.Builder event = new TransferEvent.Builder(
+                session, new TransferResource(null, null, null, (Path) null, transfer.getTrace()));
         event.setType(TransferEvent.EventType.INITIATED);
         listener.transferInitiated(event.build());
     }
@@ -174,8 +175,8 @@ class RecordingRepositoryConnector implements RepositoryConnector {
         if (listener == null) {
             return;
         }
-        TransferEvent.Builder event =
-                new TransferEvent.Builder(session, new TransferResource(null, null, null, null, transfer.getTrace()));
+        TransferEvent.Builder event = new TransferEvent.Builder(
+                session, new TransferResource(null, null, null, (Path) null, transfer.getTrace()));
         event.setException(transfer.getException());
         if (transfer.getException() != null) {
             listener.transferFailed(
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/SimpleLocalRepositoryManagerTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/SimpleLocalRepositoryManagerTest.java
index 607e8a78..f17f315a 100644
--- a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/SimpleLocalRepositoryManagerTest.java
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/SimpleLocalRepositoryManagerTest.java
@@ -48,7 +48,7 @@ public class SimpleLocalRepositoryManagerTest {
     @BeforeEach
     void setup() throws IOException {
         basedir = TestFileUtils.createTempDir("simple-repo");
-        manager = new SimpleLocalRepositoryManager(basedir, "simple", new DefaultLocalPathComposer());
+        manager = new SimpleLocalRepositoryManager(basedir.toPath(), "simple", new DefaultLocalPathComposer());
         session = TestUtils.newSession();
     }
 
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/WarnChecksumPolicyTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/WarnChecksumPolicyTest.java
index 6550e2f6..fefea635 100644
--- a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/WarnChecksumPolicyTest.java
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/WarnChecksumPolicyTest.java
@@ -18,6 +18,8 @@
  */
 package org.eclipse.aether.internal.impl;
 
+import java.nio.file.Path;
+
 import org.eclipse.aether.spi.connector.checksum.ChecksumPolicy.ChecksumKind;
 import org.eclipse.aether.transfer.ChecksumFailureException;
 import org.eclipse.aether.transfer.TransferResource;
@@ -34,7 +36,7 @@ public class WarnChecksumPolicyTest {
 
     @BeforeEach
     void setup() {
-        policy = new WarnChecksumPolicy(new TransferResource("null", "file:/dev/null", "file.txt", null, null));
+        policy = new WarnChecksumPolicy(new TransferResource("null", "file:/dev/null", "file.txt", (Path) null, null));
         exception = new ChecksumFailureException("test");
     }
 
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/checksum/SparseDirectoryTrustedChecksumsSourceTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/checksum/SparseDirectoryTrustedChecksumsSourceTest.java
index 0dd0a070..83e812a5 100644
--- a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/checksum/SparseDirectoryTrustedChecksumsSourceTest.java
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/checksum/SparseDirectoryTrustedChecksumsSourceTest.java
@@ -20,13 +20,15 @@ package org.eclipse.aether.internal.impl.checksum;
 
 import org.eclipse.aether.DefaultRepositorySystemSession;
 import org.eclipse.aether.impl.RepositorySystemLifecycle;
-import org.eclipse.aether.internal.impl.DefaultFileProcessor;
+import org.eclipse.aether.internal.impl.DefaultChecksumProcessor;
 import org.eclipse.aether.internal.impl.DefaultLocalPathComposer;
+import org.eclipse.aether.internal.impl.DefaultPathProcessor;
 
 public class SparseDirectoryTrustedChecksumsSourceTest extends FileTrustedChecksumsSourceTestSupport {
     @Override
     protected FileTrustedChecksumsSourceSupport prepareSubject(RepositorySystemLifecycle lifecycle) {
-        return new SparseDirectoryTrustedChecksumsSource(new DefaultFileProcessor(), new DefaultLocalPathComposer());
+        return new SparseDirectoryTrustedChecksumsSource(
+                new DefaultChecksumProcessor(new DefaultPathProcessor()), new DefaultLocalPathComposer());
     }
 
     @Override
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/synccontext/named/BasedirNameMapperTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/synccontext/named/BasedirNameMapperTest.java
index 970c2f6f..95eeed10 100644
--- a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/synccontext/named/BasedirNameMapperTest.java
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/synccontext/named/BasedirNameMapperTest.java
@@ -33,7 +33,7 @@ import static java.util.Collections.singletonList;
 import static org.junit.jupiter.api.Assertions.*;
 
 public class BasedirNameMapperTest extends NameMapperTestSupport {
-    private final String PS = File.separator;
+    private final String PS = "/"; // we work with URIs now, not OS file paths
 
     BasedirNameMapper mapper = new BasedirNameMapper(new HashingNameMapper(GAVNameMapper.gav()));
 
@@ -62,7 +62,8 @@ public class BasedirNameMapperTest extends NameMapperTestSupport {
         Collection<String> names = mapper.nameLocks(session, singletonList(artifact), null);
         assertEquals(names.size(), 1);
         assertEquals(
-                names.iterator().next(), basedir + PS + ".locks" + PS + "46e98183d232f1e16f863025080c7f2b9797fd10");
+                names.iterator().next(),
+                basedir.toUri() + PS + ".locks" + PS + "46e98183d232f1e16f863025080c7f2b9797fd10");
     }
 
     @Test
@@ -74,13 +75,14 @@ public class BasedirNameMapperTest extends NameMapperTestSupport {
         assertEquals(names.size(), 1);
         assertEquals(
                 names.iterator().next(),
-                basedir + PS + "my" + PS + "locks" + PS + "46e98183d232f1e16f863025080c7f2b9797fd10");
+                basedir.toUri() + PS + "my" + PS + "locks" + PS + "46e98183d232f1e16f863025080c7f2b9797fd10");
     }
 
     @Test
     void absoluteLocksDir() throws IOException {
         String absoluteLocksDir = "/my/locks";
-        String customBaseDir = new File(absoluteLocksDir).getCanonicalPath();
+        String customBaseDir =
+                new File(absoluteLocksDir).getCanonicalFile().toPath().toUri().toASCIIString();
 
         configProperties.put("aether.syncContext.named.hashing.depth", "0");
         configProperties.put("aether.syncContext.named.basedir.locksDir", absoluteLocksDir);
@@ -98,7 +100,8 @@ public class BasedirNameMapperTest extends NameMapperTestSupport {
         Collection<String> names = mapper.nameLocks(session, singletonList(artifact), null);
         assertEquals(names.size(), 1);
         assertEquals(
-                names.iterator().next(), basedir + PS + ".locks" + PS + "46e98183d232f1e16f863025080c7f2b9797fd10");
+                names.iterator().next(),
+                basedir.toUri() + PS + ".locks" + PS + "46e98183d232f1e16f863025080c7f2b9797fd10");
     }
 
     @Test
@@ -110,7 +113,8 @@ public class BasedirNameMapperTest extends NameMapperTestSupport {
         Collection<String> names = mapper.nameLocks(session, null, singletonList(metadata));
         assertEquals(names.size(), 1);
         assertEquals(
-                names.iterator().next(), basedir + PS + ".locks" + PS + "293b3990971f4b4b02b220620d2538eaac5f221b");
+                names.iterator().next(),
+                basedir.toUri() + PS + ".locks" + PS + "293b3990971f4b4b02b220620d2538eaac5f221b");
     }
 
     @Test
@@ -126,7 +130,11 @@ public class BasedirNameMapperTest extends NameMapperTestSupport {
         Iterator<String> namesIterator = names.iterator();
 
         // they are sorted as well
-        assertEquals(namesIterator.next(), basedir + PS + ".locks" + PS + "d36504431d00d1c6e4d1c34258f2bf0a004de085");
-        assertEquals(namesIterator.next(), basedir + PS + ".locks" + PS + "fbcebba60d7eb931eca634f6ca494a8a1701b638");
+        assertEquals(
+                namesIterator.next(),
+                basedir.toUri() + PS + ".locks" + PS + "d36504431d00d1c6e4d1c34258f2bf0a004de085");
+        assertEquals(
+                namesIterator.next(),
+                basedir.toUri() + PS + ".locks" + PS + "fbcebba60d7eb931eca634f6ca494a8a1701b638");
     }
 }
diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/synccontext/named/NameMapperTestSupport.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/synccontext/named/NameMapperTestSupport.java
index 0daad74f..829513ae 100644
--- a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/synccontext/named/NameMapperTestSupport.java
+++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/synccontext/named/NameMapperTestSupport.java
@@ -20,6 +20,7 @@ package org.eclipse.aether.internal.impl.synccontext.named;
 
 import java.io.File;
 import java.io.IOException;
+import java.nio.file.Path;
 import java.util.HashMap;
 
 import org.eclipse.aether.RepositorySystemSession;
@@ -33,7 +34,7 @@ import static org.mockito.Mockito.when;
  * Simple support class for {@link NameMapper} implementation UTs.
  */
 public abstract class NameMapperTestSupport {
-    protected String basedir;
+    protected Path basedir;
 
     protected HashMap<String, Object> configProperties;
 
@@ -41,10 +42,10 @@ public abstract class NameMapperTestSupport {
 
     @BeforeEach
     void before() throws IOException {
-        basedir = new File("/home/maven/.m2/repository").getCanonicalPath();
+        basedir = new File("/home/maven/.m2/repository").getCanonicalFile().toPath();
         configProperties = new HashMap<>();
 
-        LocalRepository localRepository = new LocalRepository(new File(basedir));
+        LocalRepository localRepository = new LocalRepository(basedir);
         session = mock(RepositorySystemSession.class);
         when(session.getConfigProperties()).thenReturn(configProperties);
         when(session.getLocalRepository()).thenReturn(localRepository);
diff --git a/maven-resolver-named-locks/src/main/java/org/eclipse/aether/named/providers/FileLockNamedLockFactory.java b/maven-resolver-named-locks/src/main/java/org/eclipse/aether/named/providers/FileLockNamedLockFactory.java
index 86ce7749..9d4ef472 100644
--- a/maven-resolver-named-locks/src/main/java/org/eclipse/aether/named/providers/FileLockNamedLockFactory.java
+++ b/maven-resolver-named-locks/src/main/java/org/eclipse/aether/named/providers/FileLockNamedLockFactory.java
@@ -23,6 +23,7 @@ import javax.inject.Singleton;
 
 import java.io.IOException;
 import java.io.UncheckedIOException;
+import java.net.URI;
 import java.nio.channels.FileChannel;
 import java.nio.file.AccessDeniedException;
 import java.nio.file.Files;
@@ -39,8 +40,9 @@ import org.eclipse.aether.named.support.NamedLockSupport;
 import static org.eclipse.aether.named.support.Retry.retry;
 
 /**
- * Named locks factory of {@link FileLockNamedLock}s. This is a bit special implementation, as it
- * expects locks names to be fully qualified absolute file system paths.
+ * Named locks factory of {@link FileLockNamedLock}s. This is a bit of special implementation, as it
+ * expects locks names to be proper URI string representations (use {@code file:} protocol for default
+ * file system).
  *
  * @since 1.7.3
  */
@@ -98,7 +100,7 @@ public class FileLockNamedLockFactory extends NamedLockFactorySupport {
 
     @Override
     protected NamedLockSupport createLock(final String name) {
-        Path path = Paths.get(name);
+        Path path = Paths.get(URI.create(name));
         FileChannel fileChannel = fileChannels.computeIfAbsent(name, k -> {
             try {
                 Files.createDirectories(path.getParent());
diff --git a/maven-resolver-named-locks/src/test/java/org/eclipse/aether/named/FileLockNamedLockFactorySupportTest.java b/maven-resolver-named-locks/src/test/java/org/eclipse/aether/named/FileLockNamedLockFactorySupportTest.java
index b538ddcc..9f37ca58 100644
--- a/maven-resolver-named-locks/src/test/java/org/eclipse/aether/named/FileLockNamedLockFactorySupportTest.java
+++ b/maven-resolver-named-locks/src/test/java/org/eclipse/aether/named/FileLockNamedLockFactorySupportTest.java
@@ -39,7 +39,10 @@ public class FileLockNamedLockFactorySupportTest extends NamedLockFactoryTestSup
 
     @Override
     protected String lockName(TestInfo testInfo) {
-        return baseDir.resolve(testInfo.getDisplayName()).toAbsolutePath().toString();
+        return baseDir.resolve(testInfo.getDisplayName())
+                .toAbsolutePath()
+                .toUri()
+                .toASCIIString();
     }
 
     @BeforeAll
diff --git a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/ArtifactDownload.java b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/ArtifactDownload.java
index 36eb4c03..c76206d7 100644
--- a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/ArtifactDownload.java
+++ b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/ArtifactDownload.java
@@ -19,6 +19,7 @@
 package org.eclipse.aether.spi.connector;
 
 import java.io.File;
+import java.nio.file.Path;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
@@ -60,7 +61,9 @@ public final class ArtifactDownload extends ArtifactTransfer {
      * @param context The context in which this download is performed, may be {@code null}.
      * @param file The local file to download the artifact to, may be {@code null}.
      * @param checksumPolicy The checksum policy, may be {@code null}.
+     * @deprecated Use {@link ArtifactDownload(Artifact, String, Path, String)} instead.
      */
+    @Deprecated
     public ArtifactDownload(Artifact artifact, String context, File file, String checksumPolicy) {
         setArtifact(artifact);
         setRequestContext(context);
@@ -68,18 +71,42 @@ public final class ArtifactDownload extends ArtifactTransfer {
         setChecksumPolicy(checksumPolicy);
     }
 
+    /**
+     * Creates a new download with the specified properties.
+     *
+     * @param artifact The artifact to download, may be {@code null}.
+     * @param context The context in which this download is performed, may be {@code null}.
+     * @param path The local file to download the artifact to, may be {@code null}.
+     * @param checksumPolicy The checksum policy, may be {@code null}.
+     * @deprecated Use {@link ArtifactDownload(Artifact, String, Path, String)} instead.
+     * @since 2.0.0
+     */
+    public ArtifactDownload(Artifact artifact, String context, Path path, String checksumPolicy) {
+        setArtifact(artifact);
+        setRequestContext(context);
+        setPath(path);
+        setChecksumPolicy(checksumPolicy);
+    }
+
     @Override
     public ArtifactDownload setArtifact(Artifact artifact) {
         super.setArtifact(artifact);
         return this;
     }
 
+    @Deprecated
     @Override
     public ArtifactDownload setFile(File file) {
         super.setFile(file);
         return this;
     }
 
+    @Override
+    public ArtifactDownload setPath(Path path) {
+        super.setPath(path);
+        return this;
+    }
+
     /**
      * Indicates whether this transfer shall only verify the existence of the artifact in the remote repository rather
      * than actually downloading the file. Just like with an actual transfer, a connector is expected to signal the
@@ -222,6 +249,6 @@ public final class ArtifactDownload extends ArtifactTransfer {
 
     @Override
     public String toString() {
-        return getArtifact() + " - " + (isExistenceCheck() ? "?" : "") + getFile();
+        return getArtifact() + " - " + (isExistenceCheck() ? "?" : "") + getPath();
     }
 }
diff --git a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/ArtifactTransfer.java b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/ArtifactTransfer.java
index 979098b6..80fe7efe 100644
--- a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/ArtifactTransfer.java
+++ b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/ArtifactTransfer.java
@@ -19,6 +19,7 @@
 package org.eclipse.aether.spi.connector;
 
 import java.io.File;
+import java.nio.file.Path;
 
 import org.eclipse.aether.artifact.Artifact;
 import org.eclipse.aether.transfer.ArtifactTransferException;
@@ -32,7 +33,7 @@ public abstract class ArtifactTransfer extends Transfer {
 
     private Artifact artifact;
 
-    private File file;
+    private Path path;
 
     private ArtifactTransferException exception;
 
@@ -66,9 +67,23 @@ public abstract class ArtifactTransfer extends Transfer {
      * completed such that an interrupted/failed download does not corrupt the current file contents.
      *
      * @return The local file or {@code null} if not set.
+     * @deprecated Use {@link #getPath()} instead.
      */
+    @Deprecated
     public File getFile() {
-        return file;
+        return path != null ? path.toFile() : null;
+    }
+
+    /**
+     * Gets the local file the artifact is downloaded to or uploaded from. In case of a download, a connector should
+     * first transfer the bytes to a temporary file and only overwrite the target file once the entire download is
+     * completed such that an interrupted/failed download does not corrupt the current file contents.
+     *
+     * @return The local file or {@code null} if not set.
+     * @since 2.0.0
+     */
+    public Path getPath() {
+        return path;
     }
 
     /**
@@ -76,9 +91,22 @@ public abstract class ArtifactTransfer extends Transfer {
      *
      * @param file The local file, may be {@code null}.
      * @return This transfer for chaining, never {@code null}.
+     * @deprecated Use {@link #setPath(Path)} instead.
      */
+    @Deprecated
     public ArtifactTransfer setFile(File file) {
-        this.file = file;
+        return setPath(file != null ? file.toPath() : null);
+    }
+
+    /**
+     * Sets the local file the artifact is downloaded to or uploaded from.
+     *
+     * @param path The local file, may be {@code null}.
+     * @return This transfer for chaining, never {@code null}.
+     * @since 2.0.0
+     */
+    public ArtifactTransfer setPath(Path path) {
+        this.path = path;
         return this;
     }
 
diff --git a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/ArtifactUpload.java b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/ArtifactUpload.java
index 81530621..496dff9a 100644
--- a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/ArtifactUpload.java
+++ b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/ArtifactUpload.java
@@ -19,6 +19,7 @@
 package org.eclipse.aether.spi.connector;
 
 import java.io.File;
+import java.nio.file.Path;
 
 import org.eclipse.aether.RequestTrace;
 import org.eclipse.aether.artifact.Artifact;
@@ -42,24 +43,45 @@ public final class ArtifactUpload extends ArtifactTransfer {
      *
      * @param artifact The artifact to upload, may be {@code null}.
      * @param file The local file to upload the artifact from, may be {@code null}.
+     * @deprecated Use {@link #ArtifactUpload(Artifact, Path)} instead.
      */
+    @Deprecated
     public ArtifactUpload(Artifact artifact, File file) {
         setArtifact(artifact);
         setFile(file);
     }
 
+    /**
+     * Creates a new upload with the specified properties.
+     *
+     * @param artifact The artifact to upload, may be {@code null}.
+     * @param path The local file to upload the artifact from, may be {@code null}.
+     * @since 2.0.0
+     */
+    public ArtifactUpload(Artifact artifact, Path path) {
+        setArtifact(artifact);
+        setPath(path);
+    }
+
     @Override
     public ArtifactUpload setArtifact(Artifact artifact) {
         super.setArtifact(artifact);
         return this;
     }
 
+    @Deprecated
     @Override
     public ArtifactUpload setFile(File file) {
         super.setFile(file);
         return this;
     }
 
+    @Override
+    public ArtifactUpload setPath(Path path) {
+        super.setPath(path);
+        return this;
+    }
+
     @Override
     public ArtifactUpload setException(ArtifactTransferException exception) {
         super.setException(exception);
@@ -80,6 +102,6 @@ public final class ArtifactUpload extends ArtifactTransfer {
 
     @Override
     public String toString() {
-        return getArtifact() + " - " + getFile();
+        return getArtifact() + " - " + getPath();
     }
 }
diff --git a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/MetadataDownload.java b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/MetadataDownload.java
index cc77497f..5bb38a6b 100644
--- a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/MetadataDownload.java
+++ b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/MetadataDownload.java
@@ -19,6 +19,7 @@
 package org.eclipse.aether.spi.connector;
 
 import java.io.File;
+import java.nio.file.Path;
 import java.util.Collections;
 import java.util.List;
 
@@ -54,10 +55,25 @@ public final class MetadataDownload extends MetadataTransfer {
      * @param context The context in which this download is performed, may be {@code null}.
      * @param file The local file to download the metadata to, may be {@code null}.
      * @param checksumPolicy The checksum policy, may be {@code null}.
+     * @deprecated Use {@link #MetadataDownload(Metadata, String, Path, String)} instead.
      */
+    @Deprecated
     public MetadataDownload(Metadata metadata, String context, File file, String checksumPolicy) {
+        this(metadata, context, file != null ? file.toPath() : null, checksumPolicy);
+    }
+
+    /**
+     * Creates a new download with the specified properties.
+     *
+     * @param metadata The metadata to download, may be {@code null}.
+     * @param context The context in which this download is performed, may be {@code null}.
+     * @param path The local file to download the metadata to, may be {@code null}.
+     * @param checksumPolicy The checksum policy, may be {@code null}.
+     * @since 2.0.0
+     */
+    public MetadataDownload(Metadata metadata, String context, Path path, String checksumPolicy) {
         setMetadata(metadata);
-        setFile(file);
+        setPath(path);
         setChecksumPolicy(checksumPolicy);
         setRequestContext(context);
     }
@@ -68,12 +84,19 @@ public final class MetadataDownload extends MetadataTransfer {
         return this;
     }
 
+    @Deprecated
     @Override
     public MetadataDownload setFile(File file) {
         super.setFile(file);
         return this;
     }
 
+    @Override
+    public MetadataDownload setPath(Path path) {
+        super.setPath(path);
+        return this;
+    }
+
     /**
      * Gets the checksum policy for this transfer.
      *
@@ -160,6 +183,6 @@ public final class MetadataDownload extends MetadataTransfer {
 
     @Override
     public String toString() {
-        return getMetadata() + " - " + getFile();
+        return getMetadata() + " - " + getPath();
     }
 }
diff --git a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/MetadataTransfer.java b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/MetadataTransfer.java
index b1be3fab..c9976ebf 100644
--- a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/MetadataTransfer.java
+++ b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/MetadataTransfer.java
@@ -19,6 +19,7 @@
 package org.eclipse.aether.spi.connector;
 
 import java.io.File;
+import java.nio.file.Path;
 
 import org.eclipse.aether.metadata.Metadata;
 import org.eclipse.aether.transfer.MetadataTransferException;
@@ -32,7 +33,7 @@ public abstract class MetadataTransfer extends Transfer {
 
     private Metadata metadata;
 
-    private File file;
+    private Path path;
 
     private MetadataTransferException exception;
 
@@ -66,9 +67,23 @@ public abstract class MetadataTransfer extends Transfer {
      * completed such that an interrupted/failed download does not corrupt the current file contents.
      *
      * @return The local file or {@code null} if not set.
+     * @deprecated Use {@link #getPath()} instead.
      */
+    @Deprecated
     public File getFile() {
-        return file;
+        return path != null ? path.toFile() : null;
+    }
+
+    /**
+     * Gets the local file the metadata is downloaded to or uploaded from. In case of a download, a connector should
+     * first transfer the bytes to a temporary file and only overwrite the target file once the entire download is
+     * completed such that an interrupted/failed download does not corrupt the current file contents.
+     *
+     * @return The local file or {@code null} if not set.
+     * @since 2.0.0
+     */
+    public Path getPath() {
+        return path;
     }
 
     /**
@@ -76,9 +91,22 @@ public abstract class MetadataTransfer extends Transfer {
      *
      * @param file The local file, may be {@code null}.
      * @return This transfer for chaining, never {@code null}.
+     * @deprecated Use {@link #setPath(Path)} instead.
      */
+    @Deprecated
     public MetadataTransfer setFile(File file) {
-        this.file = file;
+        return setPath(file != null ? file.toPath() : null);
+    }
+
+    /**
+     * Sets the local file the metadata is downloaded to or uploaded from.
+     *
+     * @param path The local file, may be {@code null}.
+     * @return This transfer for chaining, never {@code null}.
+     * @since 2.0.0
+     */
+    public MetadataTransfer setPath(Path path) {
+        this.path = path;
         return this;
     }
 
diff --git a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/MetadataUpload.java b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/MetadataUpload.java
index d549f7ac..ebaaaaca 100644
--- a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/MetadataUpload.java
+++ b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/connector/MetadataUpload.java
@@ -19,6 +19,7 @@
 package org.eclipse.aether.spi.connector;
 
 import java.io.File;
+import java.nio.file.Path;
 
 import org.eclipse.aether.RequestTrace;
 import org.eclipse.aether.metadata.Metadata;
@@ -43,24 +44,45 @@ public final class MetadataUpload extends MetadataTransfer {
      *
      * @param metadata The metadata to upload, may be {@code null}.
      * @param file The local file to upload the metadata from, may be {@code null}.
+     * @deprecated Use {@link #MetadataUpload(Metadata, Path)} instead.
      */
+    @Deprecated
     public MetadataUpload(Metadata metadata, File file) {
         setMetadata(metadata);
         setFile(file);
     }
 
+    /**
+     * Creates a new upload with the specified properties.
+     *
+     * @param metadata The metadata to upload, may be {@code null}.
+     * @param path The local file to upload the metadata from, may be {@code null}.
+     * @since 2.0.0
+     */
+    public MetadataUpload(Metadata metadata, Path path) {
+        setMetadata(metadata);
+        setPath(path);
+    }
+
     @Override
     public MetadataUpload setMetadata(Metadata metadata) {
         super.setMetadata(metadata);
         return this;
     }
 
+    @Deprecated
     @Override
     public MetadataUpload setFile(File file) {
         super.setFile(file);
         return this;
     }
 
+    @Override
+    public MetadataUpload setPath(Path path) {
+        super.setPath(path);
+        return this;
+    }
+
     @Override
     public MetadataUpload setException(MetadataTransferException exception) {
         super.setException(exception);
@@ -81,6 +103,6 @@ public final class MetadataUpload extends MetadataTransfer {
 
     @Override
     public String toString() {
-        return getMetadata() + " - " + getFile();
+        return getMetadata() + " - " + getPath();
     }
 }
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 ebcc3131..8e311cd8 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
@@ -25,6 +25,7 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.nio.ByteBuffer;
 import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
@@ -66,7 +67,22 @@ public final class ChecksumAlgorithmHelper {
      */
     public static Map<String, String> calculate(File file, List<ChecksumAlgorithmFactory> factories)
             throws IOException {
-        try (InputStream inputStream = new BufferedInputStream(Files.newInputStream(file.toPath()))) {
+        return calculate(file.toPath(), factories);
+    }
+
+    /**
+     * Calculates checksums for specified file.
+     *
+     * @param path        The file for which to calculate checksums, must not be {@code null}.
+     * @param factories   The checksum algorithm factories to use, must not be {@code null}.
+     * @return The calculated checksums, indexed by algorithm name, or the exception that occurred while trying to
+     * calculate it, never {@code null}.
+     * @throws IOException In case of any problem.
+     * @since 2.0.0
+     */
+    public static Map<String, String> calculate(Path path, List<ChecksumAlgorithmFactory> factories)
+            throws IOException {
+        try (InputStream inputStream = new BufferedInputStream(Files.newInputStream(path))) {
             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 bfc873a2..19b3d798 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
@@ -22,9 +22,11 @@ import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.IOException;
 import java.io.OutputStream;
+import java.io.UncheckedIOException;
 import java.net.URI;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
+import java.nio.file.Path;
 import java.nio.file.StandardOpenOption;
 import java.util.Collections;
 import java.util.HashMap;
@@ -37,7 +39,7 @@ import java.util.Map;
  */
 public final class GetTask extends TransportTask {
 
-    private File dataFile;
+    private Path dataPath;
 
     private boolean resume;
 
@@ -78,16 +80,13 @@ public final class GetTask extends TransportTask {
      * @throws IOException If the stream could not be opened.
      */
     public OutputStream newOutputStream(boolean resume) throws IOException {
-        if (dataFile != null) {
+        if (dataPath != null) {
             if (this.resume && resume) {
                 return Files.newOutputStream(
-                        dataFile.toPath(),
-                        StandardOpenOption.CREATE,
-                        StandardOpenOption.WRITE,
-                        StandardOpenOption.APPEND);
+                        dataPath, StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.APPEND);
             } else {
                 return Files.newOutputStream(
-                        dataFile.toPath(),
+                        dataPath,
                         StandardOpenOption.CREATE,
                         StandardOpenOption.WRITE,
                         StandardOpenOption.TRUNCATE_EXISTING);
@@ -106,9 +105,22 @@ public final class GetTask extends TransportTask {
      * be overwritten.
      *
      * @return The data file or {@code null} if the data will be buffered in memory.
+     * @deprecated Use {@link #getDataPath()} instead.
      */
+    @Deprecated
     public File getDataFile() {
-        return dataFile;
+        return dataPath != null ? dataPath.toFile() : null;
+    }
+
+    /**
+     * Gets the file (if any) where the downloaded data should be stored. If the specified file already exists, it will
+     * be overwritten.
+     *
+     * @return The data file or {@code null} if the data will be buffered in memory.
+     * @since 2.0.0
+     */
+    public Path getDataPath() {
+        return dataPath;
     }
 
     /**
@@ -118,11 +130,26 @@ public final class GetTask extends TransportTask {
      *
      * @param dataFile The file to store the downloaded data, may be {@code null} to store the data in memory.
      * @return This task for chaining, never {@code null}.
+     * @deprecated Use {@link #setDataPath(Path)} instead.
      */
+    @Deprecated
     public GetTask setDataFile(File dataFile) {
         return setDataFile(dataFile, false);
     }
 
+    /**
+     * Sets the file where the downloaded data should be stored. If the specified file already exists, it will be
+     * overwritten. Unless the caller can reasonably expect the resource to be small, use of a data file is strongly
+     * recommended to avoid exhausting heap memory during the download.
+     *
+     * @param dataPath The file to store the downloaded data, may be {@code null} to store the data in memory.
+     * @return This task for chaining, never {@code null}.
+     * @since 2.0.0
+     */
+    public GetTask setDataPath(Path dataPath) {
+        return setDataPath(dataPath, false);
+    }
+
     /**
      * Sets the file where the downloaded data should be stored. If the specified file already exists, it will be
      * overwritten or appended to, depending on the {@code resume} argument and the capabilities of the transporter.
@@ -133,9 +160,27 @@ public final class GetTask extends TransportTask {
      * @param resume {@code true} to request resuming a previous download attempt, starting from the current length of
      *            the data file, {@code false} to download the resource from its beginning.
      * @return This task for chaining, never {@code null}.
+     * @deprecated Use {@link #setDataPath(Path, boolean)} instead.
      */
+    @Deprecated
     public GetTask setDataFile(File dataFile, boolean resume) {
-        this.dataFile = dataFile;
+        return setDataPath(dataFile != null ? dataFile.toPath() : null, resume);
+    }
+
+    /**
+     * Sets the file where the downloaded data should be stored. If the specified file already exists, it will be
+     * overwritten or appended to, depending on the {@code resume} argument and the capabilities of the transporter.
+     * Unless the caller can reasonably expect the resource to be small, use of a data file is strongly recommended to
+     * avoid exhausting heap memory during the download.
+     *
+     * @param dataPath The file to store the downloaded data, may be {@code null} to store the data in memory.
+     * @param resume {@code true} to request resuming a previous download attempt, starting from the current length of
+     *            the data file, {@code false} to download the resource from its beginning.
+     * @return This task for chaining, never {@code null}.
+     * @since 2.0.0
+     */
+    public GetTask setDataPath(Path dataPath, boolean resume) {
+        this.dataPath = dataPath;
         this.resume = resume;
         return this;
     }
@@ -148,8 +193,12 @@ public final class GetTask extends TransportTask {
      */
     public long getResumeOffset() {
         if (resume) {
-            if (dataFile != null) {
-                return dataFile.length();
+            if (dataPath != null) {
+                try {
+                    return Files.size(dataPath);
+                } catch (IOException e) {
+                    throw new UncheckedIOException(e);
+                }
             }
             if (dataBytes != null) {
                 return dataBytes.size();
@@ -165,7 +214,7 @@ public final class GetTask extends TransportTask {
      * @return The possibly empty data bytes, never {@code null}.
      */
     public byte[] getDataBytes() {
-        if (dataFile != null || dataBytes == null) {
+        if (dataPath != null || dataBytes == null) {
             return EMPTY;
         }
         return dataBytes.toByteArray();
@@ -179,7 +228,7 @@ public final class GetTask extends TransportTask {
      * @return The possibly empty data string, never {@code null}.
      */
     public String getDataString() {
-        if (dataFile != null || dataBytes == null) {
+        if (dataPath != null || dataBytes == null) {
             return "";
         }
         return new String(dataBytes.toByteArray(), StandardCharsets.UTF_8);
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 273ab113..5860e044 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
@@ -18,13 +18,11 @@
  */
 package org.eclipse.aether.spi.connector.transport;
 
-import java.io.ByteArrayInputStream;
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
+import java.io.*;
 import java.net.URI;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
+import java.nio.file.Path;
 
 /**
  * A task to upload a resource to the remote repository.
@@ -33,7 +31,7 @@ import java.nio.file.Files;
  */
 public final class PutTask extends TransportTask {
 
-    private File dataFile;
+    private Path dataPath;
 
     private byte[] dataBytes = EMPTY;
 
@@ -54,8 +52,8 @@ public final class PutTask extends TransportTask {
      * @throws IOException If the stream could not be opened.
      */
     public InputStream newInputStream() throws IOException {
-        if (dataFile != null) {
-            return Files.newInputStream(dataFile.toPath());
+        if (dataPath != null) {
+            return Files.newInputStream(dataPath);
         }
         return new ByteArrayInputStream(dataBytes);
     }
@@ -66,8 +64,12 @@ public final class PutTask extends TransportTask {
      * @return The total number of bytes to be uploaded.
      */
     public long getDataLength() {
-        if (dataFile != null) {
-            return dataFile.length();
+        if (dataPath != null) {
+            try {
+                return Files.size(dataPath);
+            } catch (IOException e) {
+                throw new UncheckedIOException(e);
+            }
         }
         return dataBytes.length;
     }
@@ -76,9 +78,21 @@ public final class PutTask extends TransportTask {
      * Gets the file (if any) with the data to be uploaded.
      *
      * @return The data file or {@code null} if the data resides in memory.
+     * @deprecated Use {@link #getDataPath()} instead.
      */
+    @Deprecated
     public File getDataFile() {
-        return dataFile;
+        return dataPath != null ? dataPath.toFile() : null;
+    }
+
+    /**
+     * Gets the file (if any) with the data to be uploaded.
+     *
+     * @return The data file or {@code null} if the data resides in memory.
+     * @since 2.0.0
+     */
+    public Path getDataPath() {
+        return dataPath;
     }
 
     /**
@@ -87,9 +101,23 @@ public final class PutTask extends TransportTask {
      *
      * @param dataFile The data file, may be {@code null} if the resource data is provided directly from memory.
      * @return This task for chaining, never {@code null}.
+     * @deprecated Use {@link #setDataPath(Path)} instead.
      */
+    @Deprecated
     public PutTask setDataFile(File dataFile) {
-        this.dataFile = dataFile;
+        return setDataPath(dataFile.toPath());
+    }
+
+    /**
+     * Sets the file with the data to be uploaded. To upload some data residing already in memory, use
+     * {@link #setDataString(String)} or {@link #setDataBytes(byte[])}.
+     *
+     * @param dataPath The data file, may be {@code null} if the resource data is provided directly from memory.
+     * @return This task for chaining, never {@code null}.
+     * @since 2.0.0
+     */
+    public PutTask setDataPath(Path dataPath) {
+        this.dataPath = dataPath;
         dataBytes = EMPTY;
         return this;
     }
@@ -102,7 +130,7 @@ public final class PutTask extends TransportTask {
      */
     public PutTask setDataBytes(byte[] bytes) {
         this.dataBytes = (bytes != null) ? bytes : EMPTY;
-        dataFile = null;
+        dataPath = null;
         return this;
     }
 
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/TrackingFileManager.java b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/io/ChecksumProcessor.java
similarity index 58%
copy from maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/TrackingFileManager.java
copy to maven-resolver-spi/src/main/java/org/eclipse/aether/spi/io/ChecksumProcessor.java
index 5e922bf5..e9dcb3e9 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/TrackingFileManager.java
+++ b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/io/ChecksumProcessor.java
@@ -16,24 +16,30 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.eclipse.aether.internal.impl;
+package org.eclipse.aether.spi.io;
 
-import java.io.File;
-import java.util.Map;
-import java.util.Properties;
+import java.io.IOException;
+import java.nio.file.Path;
 
 /**
- * Manages access to a properties file.
+ * A utility component to perform checksum related operations.
+ *
+ * @since 2.0.0
  */
-public interface TrackingFileManager {
+public interface ChecksumProcessor {
     /**
-     * Reads up the specified properties file into {@link Properties}, if exists, otherwise {@code null} is returned.
+     * Reads checksum from specified file.
+     *
+     * @throws IOException in case of any IO error.
+     * @since 1.8.0
      */
-    Properties read(File file);
+    String readChecksum(Path checksumFile) throws IOException;
 
     /**
-     * Applies updates to specified properties file and returns resulting {@link Properties} with contents same
-     * as in updated file, never {@code null}.
+     * Writes checksum to specified file.
+     *
+     * @throws IOException in case of any IO error.
+     * @since 1.8.0
      */
-    Properties update(File file, Map<String, String> updates);
+    void writeChecksum(Path checksumFile, String checksum) throws IOException;
 }
diff --git a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/io/FileProcessor.java b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/io/FileProcessor.java
index 98a60658..6ad9512d 100644
--- a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/io/FileProcessor.java
+++ b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/io/FileProcessor.java
@@ -25,7 +25,10 @@ import java.nio.ByteBuffer;
 
 /**
  * A utility component to perform file-based operations.
+ *
+ * @deprecated Use {@link PathProcessor} or {@link ChecksumProcessor} instead.
  */
+@Deprecated
 public interface FileProcessor {
 
     /**
@@ -97,7 +100,7 @@ public interface FileProcessor {
      *
      * @see FileProcessor#copy(File, File, ProgressListener)
      */
-    interface ProgressListener {
+    interface ProgressListener extends PathProcessor.ProgressListener {
 
         void progressed(ByteBuffer buffer) throws IOException;
     }
diff --git a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/io/FileProcessor.java b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/io/PathProcessor.java
similarity index 71%
copy from maven-resolver-spi/src/main/java/org/eclipse/aether/spi/io/FileProcessor.java
copy to maven-resolver-spi/src/main/java/org/eclipse/aether/spi/io/PathProcessor.java
index 98a60658..9b28ec2d 100644
--- a/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/io/FileProcessor.java
+++ b/maven-resolver-spi/src/main/java/org/eclipse/aether/spi/io/PathProcessor.java
@@ -18,26 +18,49 @@
  */
 package org.eclipse.aether.spi.io;
 
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
+import java.io.*;
 import java.nio.ByteBuffer;
+import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.Path;
 
 /**
  * A utility component to perform file-based operations.
+ *
+ * @since 2.0.0
  */
-public interface FileProcessor {
+public interface PathProcessor {
+    /**
+     * Returns last modified of path in milliseconds, if exists.
+     *
+     * @param path The path, may be {@code null}.
+     * @throws UncheckedIOException If an I/O error occurs.
+     */
+    default long lastModified(Path path, long defValue) {
+        try {
+            return Files.getLastModifiedTime(path).toMillis();
+        } catch (NoSuchFileException e) {
+            return defValue;
+        } catch (IOException e) {
+            throw new UncheckedIOException(e);
+        }
+    }
 
     /**
-     * Creates the directory named by the given abstract pathname, including any necessary but nonexistent parent
-     * directories. Note that if this operation fails it may have succeeded in creating some of the necessary parent
-     * directories.
+     * Returns size of file, if exists.
      *
-     * @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
+     * @param path The path, may be {@code null}.
+     * @throws UncheckedIOException If an I/O error occurs.
      */
-    boolean mkdirs(File directory);
+    default long size(Path path, long defValue) {
+        try {
+            return Files.size(path);
+        } catch (NoSuchFileException e) {
+            return defValue;
+        } catch (IOException e) {
+            throw new UncheckedIOException(e);
+        }
+    }
 
     /**
      * Writes the given data to a file. UTF-8 is assumed as encoding for the data. Creates the necessary directories for
@@ -47,7 +70,7 @@ public interface FileProcessor {
      * @param data The data to write, may be {@code null}.
      * @throws IOException If an I/O error occurs.
      */
-    void write(File target, String data) throws IOException;
+    void write(Path target, String data) throws IOException;
 
     /**
      * Writes the given stream to a file. Creates the necessary directories for the target file. In case of an error,
@@ -57,7 +80,7 @@ public interface FileProcessor {
      * @param source The stream to write to the file, must not be {@code null}.
      * @throws IOException If an I/O error occurs.
      */
-    void write(File target, InputStream source) throws IOException;
+    void write(Path target, InputStream source) throws IOException;
 
     /**
      * Moves the specified source file to the given target file. If the target file already exists, it is overwritten.
@@ -68,7 +91,7 @@ public interface FileProcessor {
      * @param target The file to move to, must not be {@code null}.
      * @throws IOException If an I/O error occurs.
      */
-    void move(File source, File target) throws IOException;
+    void move(Path source, Path target) throws IOException;
 
     /**
      * Copies the specified source file to the given target file. Creates the necessary directories for the target file.
@@ -78,7 +101,7 @@ public interface FileProcessor {
      * @param target The file to copy to, must not be {@code null}.
      * @throws IOException If an I/O error occurs.
      */
-    void copy(File source, File target) throws IOException;
+    void copy(Path source, Path target) throws IOException;
 
     /**
      * Copies the specified source file to the given target file. Creates the necessary directories for the target file.
@@ -90,31 +113,15 @@ public interface FileProcessor {
      * @return The number of copied bytes.
      * @throws IOException If an I/O error occurs.
      */
-    long copy(File source, File target, ProgressListener listener) throws IOException;
+    long copy(Path source, Path target, ProgressListener listener) throws IOException;
 
     /**
      * A listener object that is notified for every progress made while copying files.
      *
-     * @see FileProcessor#copy(File, File, ProgressListener)
+     * @see PathProcessor#copy(Path, Path, ProgressListener)
      */
     interface ProgressListener {
 
         void progressed(ByteBuffer buffer) throws IOException;
     }
-
-    /**
-     * Reads checksum from specified file.
-     *
-     * @throws IOException in case of any IO error.
-     * @since 1.8.0
-     */
-    String readChecksum(File checksumFile) throws IOException;
-
-    /**
-     * Writes checksum to specified file.
-     *
-     * @throws IOException in case of any IO error.
-     * @since 1.8.0
-     */
-    void writeChecksum(File checksumFile, String checksum) throws IOException;
 }
diff --git a/maven-resolver-supplier/src/main/java/org/eclipse/aether/supplier/RepositorySystemSupplier.java b/maven-resolver-supplier/src/main/java/org/eclipse/aether/supplier/RepositorySystemSupplier.java
index 7e180790..561535d4 100644
--- a/maven-resolver-supplier/src/main/java/org/eclipse/aether/supplier/RepositorySystemSupplier.java
+++ b/maven-resolver-supplier/src/main/java/org/eclipse/aether/supplier/RepositorySystemSupplier.java
@@ -93,7 +93,8 @@ import org.eclipse.aether.spi.connector.transport.TransporterFactory;
 import org.eclipse.aether.spi.connector.transport.TransporterProvider;
 import org.eclipse.aether.spi.connector.transport.http.ChecksumExtractor;
 import org.eclipse.aether.spi.connector.transport.http.ChecksumExtractorStrategy;
-import org.eclipse.aether.spi.io.FileProcessor;
+import org.eclipse.aether.spi.io.ChecksumProcessor;
+import org.eclipse.aether.spi.io.PathProcessor;
 import org.eclipse.aether.spi.localrepo.LocalRepositoryManagerFactory;
 import org.eclipse.aether.spi.resolution.ArtifactResolverPostProcessor;
 import org.eclipse.aether.spi.synccontext.SyncContextFactory;
@@ -136,18 +137,32 @@ public class RepositorySystemSupplier implements Supplier<RepositorySystem> {
         }
     }
 
-    private FileProcessor fileProcessor;
+    private PathProcessor pathProcessor;
 
-    public final FileProcessor getFileProcessor() {
+    public final PathProcessor getPathProcessor() {
         checkClosed();
-        if (fileProcessor == null) {
-            fileProcessor = createFileProcessor();
+        if (pathProcessor == null) {
+            pathProcessor = createPathProcessor();
         }
-        return fileProcessor;
+        return pathProcessor;
     }
 
-    protected FileProcessor createFileProcessor() {
-        return new DefaultFileProcessor();
+    protected PathProcessor createPathProcessor() {
+        return new DefaultPathProcessor();
+    }
+
+    private ChecksumProcessor checksumProcessor;
+
+    public final ChecksumProcessor getChecksumProcessor() {
+        checkClosed();
+        if (checksumProcessor == null) {
+            checksumProcessor = createChecksumProcessor();
+        }
+        return checksumProcessor;
+    }
+
+    protected ChecksumProcessor createChecksumProcessor() {
+        return new DefaultChecksumProcessor(getPathProcessor());
     }
 
     private TrackingFileManager trackingFileManager;
@@ -260,7 +275,7 @@ public class RepositorySystemSupplier implements Supplier<RepositorySystem> {
     }
 
     protected UpdateCheckManager createUpdateCheckManager() {
-        return new DefaultUpdateCheckManager(getTrackingFileManager(), getUpdatePolicyAnalyzer());
+        return new DefaultUpdateCheckManager(getTrackingFileManager(), getUpdatePolicyAnalyzer(), getPathProcessor());
     }
 
     private Map<String, NamedLockFactory> namedLockFactories;
@@ -509,7 +524,7 @@ public class RepositorySystemSupplier implements Supplier<RepositorySystem> {
         HashMap<String, TrustedChecksumsSource> result = new HashMap<>();
         result.put(
                 SparseDirectoryTrustedChecksumsSource.NAME,
-                new SparseDirectoryTrustedChecksumsSource(getFileProcessor(), getLocalPathComposer()));
+                new SparseDirectoryTrustedChecksumsSource(getChecksumProcessor(), getLocalPathComposer()));
         result.put(
                 SummaryFileTrustedChecksumsSource.NAME,
                 new SummaryFileTrustedChecksumsSource(getLocalPathComposer(), getRepositorySystemLifecycle()));
@@ -611,7 +626,7 @@ public class RepositorySystemSupplier implements Supplier<RepositorySystem> {
                 getTransporterProvider(),
                 getRepositoryLayoutProvider(),
                 getChecksumPolicyProvider(),
-                getFileProcessor(),
+                getChecksumProcessor(),
                 getProvidedChecksumsSources());
     }
 
@@ -658,7 +673,7 @@ public class RepositorySystemSupplier implements Supplier<RepositorySystem> {
 
     protected Installer createInstaller() {
         return new DefaultInstaller(
-                getFileProcessor(),
+                getPathProcessor(),
                 getRepositoryEventDispatcher(),
                 getMetadataGeneratorFactories(),
                 getSyncContextFactory());
@@ -676,7 +691,7 @@ public class RepositorySystemSupplier implements Supplier<RepositorySystem> {
 
     protected Deployer createDeployer() {
         return new DefaultDeployer(
-                getFileProcessor(),
+                getPathProcessor(),
                 getRepositoryEventDispatcher(),
                 getRepositoryConnectorProvider(),
                 getRemoteRepositoryManager(),
@@ -755,7 +770,7 @@ public class RepositorySystemSupplier implements Supplier<RepositorySystem> {
 
     protected ArtifactResolver createArtifactResolver() {
         return new DefaultArtifactResolver(
-                getFileProcessor(),
+                getPathProcessor(),
                 getRepositoryEventDispatcher(),
                 getVersionResolver(),
                 getUpdateCheckManager(),
@@ -785,7 +800,8 @@ public class RepositorySystemSupplier implements Supplier<RepositorySystem> {
                 getRemoteRepositoryManager(),
                 getSyncContextFactory(),
                 getOfflineController(),
-                getRemoteRepositoryFilterManager());
+                getRemoteRepositoryFilterManager(),
+                getPathProcessor());
     }
 
     private VersionScheme versionScheme;
diff --git a/maven-resolver-supplier/src/test/java/org/eclipse/aether/supplier/RepositorySystemSupplierTest.java b/maven-resolver-supplier/src/test/java/org/eclipse/aether/supplier/RepositorySystemSupplierTest.java
index 77e793ca..b3ac64c6 100644
--- a/maven-resolver-supplier/src/test/java/org/eclipse/aether/supplier/RepositorySystemSupplierTest.java
+++ b/maven-resolver-supplier/src/test/java/org/eclipse/aether/supplier/RepositorySystemSupplierTest.java
@@ -30,7 +30,7 @@ import org.eclipse.aether.impl.Deployer;
 import org.eclipse.aether.repository.RemoteRepository;
 import org.eclipse.aether.resolution.VersionRangeRequest;
 import org.eclipse.aether.resolution.VersionRangeResult;
-import org.eclipse.aether.spi.io.FileProcessor;
+import org.eclipse.aether.spi.io.PathProcessor;
 import org.eclipse.aether.version.Version;
 import org.junit.jupiter.api.Test;
 
@@ -42,7 +42,7 @@ public class RepositorySystemSupplierTest {
         try (RepositorySystem system = new RepositorySystemSupplier().get();
                 CloseableSession session = new SessionBuilderSupplier(system)
                         .get()
-                        .withLocalRepositoryBaseDirectories(new File("target/local-repo"))
+                        .withLocalRepositoryBaseDirectories(new File("target/local-repo").toPath())
                         .build()) {
             Artifact artifact = new DefaultArtifact("org.apache.maven.resolver:maven-resolver-util:[0,)");
             VersionRangeRequest rangeRequest = new VersionRangeRequest();
@@ -65,22 +65,22 @@ public class RepositorySystemSupplierTest {
         systemSupplier.get().close(); // get an instance and immediately shut it down, this closes supplier as well
         assertThrows(IllegalStateException.class, systemSupplier::get);
         assertThrows(IllegalStateException.class, systemSupplier::getRepositorySystem);
-        assertThrows(IllegalStateException.class, systemSupplier::getFileProcessor);
+        assertThrows(IllegalStateException.class, systemSupplier::getChecksumProcessor);
     }
 
     @Test
     void memorizing() {
         RepositorySystemSupplier systemSupplier = new RepositorySystemSupplier();
-        FileProcessor fileProcessor = systemSupplier.getFileProcessor();
+        PathProcessor pathProcessor = systemSupplier.getPathProcessor();
         Deployer deployer = systemSupplier.getDeployer();
         try (RepositorySystem repositorySystem = systemSupplier.get()) {
             assertSame(systemSupplier.get(), repositorySystem);
-            assertSame(systemSupplier.getFileProcessor(), fileProcessor);
+            assertSame(systemSupplier.getPathProcessor(), pathProcessor);
             assertSame(systemSupplier.getDeployer(), deployer);
         }
         assertThrows(IllegalStateException.class, systemSupplier::get);
         assertThrows(IllegalStateException.class, systemSupplier::getRepositorySystem);
         assertThrows(IllegalStateException.class, systemSupplier::getDeployer);
-        assertThrows(IllegalStateException.class, systemSupplier::getFileProcessor);
+        assertThrows(IllegalStateException.class, systemSupplier::getPathProcessor);
     }
 }
diff --git a/maven-resolver-test-http/src/main/java/org/eclipse/aether/internal/test/util/http/HttpTransporterTest.java b/maven-resolver-test-http/src/main/java/org/eclipse/aether/internal/test/util/http/HttpTransporterTest.java
index 0b0a74af..c7ab6985 100644
--- a/maven-resolver-test-http/src/main/java/org/eclipse/aether/internal/test/util/http/HttpTransporterTest.java
+++ b/maven-resolver-test-http/src/main/java/org/eclipse/aether/internal/test/util/http/HttpTransporterTest.java
@@ -344,8 +344,9 @@ public class HttpTransporterTest {
     protected void testGet_ToFile() throws Exception {
         File file = TestFileUtils.createTempFile("failure");
         RecordingTransportListener listener = new RecordingTransportListener();
-        GetTask task =
-                new GetTask(URI.create("repo/file.txt")).setDataFile(file).setListener(listener);
+        GetTask task = new GetTask(URI.create("repo/file.txt"))
+                .setDataPath(file.toPath())
+                .setListener(listener);
         transporter.get(task);
         assertEquals("test", TestFileUtils.readString(file));
         assertEquals(0L, listener.getDataOffset());
@@ -360,7 +361,7 @@ public class HttpTransporterTest {
         File file = TestFileUtils.createTempFile("failure");
         RecordingTransportListener listener = new RecordingTransportListener();
         GetTask task = new GetTask(URI.create("repo/dir/oldFile.txt"))
-                .setDataFile(file)
+                .setDataPath(file.toPath())
                 .setListener(listener);
         transporter.get(task);
         assertEquals("oldTest", TestFileUtils.readString(file));
@@ -376,8 +377,9 @@ public class HttpTransporterTest {
     protected void testGet_EmptyResource() throws Exception {
         File file = TestFileUtils.createTempFile("failure");
         RecordingTransportListener listener = new RecordingTransportListener();
-        GetTask task =
-                new GetTask(URI.create("repo/empty.txt")).setDataFile(file).setListener(listener);
+        GetTask task = new GetTask(URI.create("repo/empty.txt"))
+                .setDataPath(file.toPath())
+                .setListener(listener);
         transporter.get(task);
         assertEquals("", TestFileUtils.readString(file));
         assertEquals(0L, listener.getDataOffset());
@@ -546,7 +548,7 @@ public class HttpTransporterTest {
         File file = TestFileUtils.createTempFile("re");
         RecordingTransportListener listener = new RecordingTransportListener();
         GetTask task = new GetTask(URI.create("repo/resume.txt"))
-                .setDataFile(file, true)
+                .setDataPath(file.toPath(), true)
                 .setListener(listener);
         transporter.get(task);
         assertEquals("resumable", TestFileUtils.readString(file));
@@ -563,7 +565,7 @@ public class HttpTransporterTest {
         file.setLastModified(System.currentTimeMillis() - 5 * 60 * 1000);
         RecordingTransportListener listener = new RecordingTransportListener();
         GetTask task = new GetTask(URI.create("repo/resume.txt"))
-                .setDataFile(file, true)
+                .setDataPath(file.toPath(), true)
                 .setListener(listener);
         transporter.get(task);
         assertEquals("resumable", TestFileUtils.readString(file));
@@ -580,7 +582,7 @@ public class HttpTransporterTest {
         File file = TestFileUtils.createTempFile("re");
         RecordingTransportListener listener = new RecordingTransportListener();
         GetTask task = new GetTask(URI.create("repo/resume.txt"))
-                .setDataFile(file, true)
+                .setDataPath(file.toPath(), true)
                 .setListener(listener);
         transporter.get(task);
         assertEquals("resumable", TestFileUtils.readString(file));
@@ -615,7 +617,7 @@ public class HttpTransporterTest {
     protected void testGet_FileHandleLeak() throws Exception {
         for (int i = 0; i < 100; i++) {
             File file = TestFileUtils.createTempFile("failure");
-            transporter.get(new GetTask(URI.create("repo/file.txt")).setDataFile(file));
+            transporter.get(new GetTask(URI.create("repo/file.txt")).setDataPath(file.toPath()));
             assertTrue(file.delete(), i + ", " + file.getAbsolutePath());
         }
     }
@@ -694,7 +696,7 @@ public class HttpTransporterTest {
         File file = TestFileUtils.createTempFile("upload");
         RecordingTransportListener listener = new RecordingTransportListener();
         PutTask task =
-                new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataFile(file);
+                new PutTask(URI.create("repo/file.txt")).setListener(listener).setDataPath(file.toPath());
         transporter.put(task);
         assertEquals(0L, listener.getDataOffset());
         assertEquals(6L, listener.getDataLength());
@@ -915,7 +917,7 @@ public class HttpTransporterTest {
         for (int i = 0; i < 100; i++) {
             File src = TestFileUtils.createTempFile("upload");
             File dst = new File(repoDir, "file.txt");
-            transporter.put(new PutTask(URI.create("repo/file.txt")).setDataFile(src));
+            transporter.put(new PutTask(URI.create("repo/file.txt")).setDataPath(src.toPath()));
             assertTrue(src.delete(), i + ", " + src.getAbsolutePath());
             assertTrue(dst.delete(), i + ", " + dst.getAbsolutePath());
         }
diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/TrackingFileManager.java b/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/TestChecksumProcessor.java
similarity index 52%
copy from maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/TrackingFileManager.java
copy to maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/TestChecksumProcessor.java
index 5e922bf5..34899260 100644
--- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/TrackingFileManager.java
+++ b/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/TestChecksumProcessor.java
@@ -16,24 +16,26 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.eclipse.aether.internal.impl;
+package org.eclipse.aether.internal.test.util;
 
-import java.io.File;
-import java.util.Map;
-import java.util.Properties;
+import java.io.IOException;
+import java.nio.file.Path;
+
+import org.eclipse.aether.spi.io.ChecksumProcessor;
 
 /**
- * Manages access to a properties file.
+ * A simple file processor implementation to help satisfy component requirements during tests.
  */
-public interface TrackingFileManager {
-    /**
-     * Reads up the specified properties file into {@link Properties}, if exists, otherwise {@code null} is returned.
-     */
-    Properties read(File file);
+public class TestChecksumProcessor implements ChecksumProcessor {
+    private final TestFileProcessor testFileProcessor = new TestFileProcessor();
+
+    @Override
+    public String readChecksum(Path checksumFile) throws IOException {
+        return testFileProcessor.readChecksum(checksumFile.toFile());
+    }
 
-    /**
-     * 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);
+    @Override
+    public void writeChecksum(Path checksumFile, String checksum) throws IOException {
+        testFileProcessor.writeChecksum(checksumFile.toFile(), checksum);
+    }
 }
diff --git a/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/TestPathProcessor.java b/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/TestPathProcessor.java
new file mode 100644
index 00000000..0a263c88
--- /dev/null
+++ b/maven-resolver-test-util/src/main/java/org/eclipse/aether/internal/test/util/TestPathProcessor.java
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.eclipse.aether.internal.test.util;
+
+import java.io.*;
+import java.nio.file.Path;
+
+import org.eclipse.aether.spi.io.PathProcessor;
+
+/**
+ * A simple file processor implementation to help satisfy component requirements during tests.
+ */
+public class TestPathProcessor implements PathProcessor {
+
+    private final TestFileProcessor testFileProcessor = new TestFileProcessor();
+
+    public void mkdirs(Path directory) {
+        if (directory == null) {
+            return;
+        }
+        testFileProcessor.mkdirs(directory.toFile());
+    }
+
+    public void write(Path file, String data) throws IOException {
+        testFileProcessor.write(file.toFile(), data);
+    }
+
+    public void write(Path target, InputStream source) throws IOException {
+        testFileProcessor.write(target.toFile(), source);
+    }
+
+    public void copy(Path source, Path target) throws IOException {
+        copy(source, target, null);
+    }
+
+    public long copy(Path source, Path target, ProgressListener listener) throws IOException {
+        return testFileProcessor.copy(source.toFile(), target.toFile(), null);
+    }
+
+    public void move(Path source, Path target) throws IOException {
+        testFileProcessor.move(source.toFile(), target.toFile());
+    }
+}
diff --git a/maven-resolver-transport-apache/src/main/java/org/eclipse/aether/transport/apache/ApacheTransporter.java b/maven-resolver-transport-apache/src/main/java/org/eclipse/aether/transport/apache/ApacheTransporter.java
index fea00f06..4ca9707f 100644
--- a/maven-resolver-transport-apache/src/main/java/org/eclipse/aether/transport/apache/ApacheTransporter.java
+++ b/maven-resolver-transport-apache/src/main/java/org/eclipse/aether/transport/apache/ApacheTransporter.java
@@ -18,7 +18,6 @@
  */
 package org.eclipse.aether.transport.apache;
 
-import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InterruptedIOException;
@@ -30,6 +29,7 @@ import java.net.URISyntaxException;
 import java.net.UnknownHostException;
 import java.nio.charset.Charset;
 import java.nio.file.Files;
+import java.nio.file.Path;
 import java.nio.file.StandardCopyOption;
 import java.nio.file.attribute.FileTime;
 import java.util.Collections;
@@ -582,13 +582,13 @@ final class ApacheTransporter extends AbstractTransporter implements HttpTranspo
     }
 
     @SuppressWarnings("checkstyle:magicnumber")
-    private <T extends HttpUriRequest> void resume(T request, GetTask task) {
+    private <T extends HttpUriRequest> void resume(T request, GetTask task) throws IOException {
         long resumeOffset = task.getResumeOffset();
-        if (resumeOffset > 0L && task.getDataFile() != null) {
+        if (resumeOffset > 0L && task.getDataPath() != null) {
+            long lastModified = Files.getLastModifiedTime(task.getDataPath()).toMillis();
             request.setHeader(HttpHeaders.RANGE, "bytes=" + resumeOffset + '-');
             request.setHeader(
-                    HttpHeaders.IF_UNMODIFIED_SINCE,
-                    DateUtils.formatDate(new Date(task.getDataFile().lastModified() - 60L * 1000L)));
+                    HttpHeaders.IF_UNMODIFIED_SINCE, DateUtils.formatDate(new Date(lastModified - 60L * 1000L)));
             request.setHeader(HttpHeaders.ACCEPT_ENCODING, "identity");
         }
     }
@@ -644,17 +644,17 @@ final class ApacheTransporter extends AbstractTransporter implements HttpTranspo
             }
 
             final boolean resume = offset > 0L;
-            final File dataFile = task.getDataFile();
+            final Path dataFile = task.getDataPath();
             if (dataFile == null) {
                 try (InputStream is = entity.getContent()) {
                     utilGet(task, is, true, length, resume);
                     extractChecksums(response);
                 }
             } else {
-                try (FileUtils.CollocatedTempFile tempFile = FileUtils.newTempFile(dataFile.toPath())) {
-                    task.setDataFile(tempFile.getPath().toFile(), resume);
-                    if (resume && Files.isRegularFile(dataFile.toPath())) {
-                        try (InputStream inputStream = Files.newInputStream(dataFile.toPath())) {
+                try (FileUtils.CollocatedTempFile tempFile = FileUtils.newTempFile(dataFile)) {
+                    task.setDataPath(tempFile.getPath(), resume);
+                    if (resume && Files.isRegularFile(dataFile)) {
+                        try (InputStream inputStream = Files.newInputStream(dataFile)) {
                             Files.copy(inputStream, tempFile.getPath(), StandardCopyOption.REPLACE_EXISTING);
                         }
                     }
@@ -663,17 +663,16 @@ final class ApacheTransporter extends AbstractTransporter implements HttpTranspo
                     }
                     tempFile.move();
                 } finally {
-                    task.setDataFile(dataFile);
+                    task.setDataPath(dataFile);
                 }
             }
-            if (task.getDataFile() != null) {
+            if (task.getDataPath() != null) {
                 Header lastModifiedHeader =
                         response.getFirstHeader(HttpHeaders.LAST_MODIFIED); // note: Wagon also does first not last
                 if (lastModifiedHeader != null) {
                     Date lastModified = DateUtils.parseDate(lastModifiedHeader.getValue());
                     if (lastModified != null) {
-                        Files.setLastModifiedTime(
-                                task.getDataFile().toPath(), FileTime.fromMillis(lastModified.getTime()));
+                        Files.setLastModifiedTime(task.getDataPath(), FileTime.fromMillis(lastModified.getTime()));
                     }
                 }
             }
diff --git a/maven-resolver-transport-file/pom.xml b/maven-resolver-transport-file/pom.xml
index 1f19bdf5..8813871b 100644
--- a/maven-resolver-transport-file/pom.xml
+++ b/maven-resolver-transport-file/pom.xml
@@ -61,6 +61,11 @@
       <artifactId>junit-jupiter-api</artifactId>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>org.junit.jupiter</groupId>
+      <artifactId>junit-jupiter-params</artifactId>
+      <scope>test</scope>
+    </dependency>
     <dependency>
       <groupId>org.apache.maven.resolver</groupId>
       <artifactId>maven-resolver-test-util</artifactId>
@@ -71,6 +76,11 @@
       <artifactId>slf4j-simple</artifactId>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>com.google.jimfs</groupId>
+      <artifactId>jimfs</artifactId>
+      <scope>test</scope>
+    </dependency>
   </dependencies>
 
   <build>
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 e2cde760..eb378e11 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
@@ -18,39 +18,37 @@
  */
 package org.eclipse.aether.transport.file;
 
-import java.io.File;
-import java.nio.file.Files;
+import java.nio.file.*;
 
 import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.repository.RepositoryUriUtils;
 import org.eclipse.aether.spi.connector.transport.AbstractTransporter;
 import org.eclipse.aether.spi.connector.transport.GetTask;
 import org.eclipse.aether.spi.connector.transport.PeekTask;
 import org.eclipse.aether.spi.connector.transport.PutTask;
 import org.eclipse.aether.spi.connector.transport.TransportTask;
 import org.eclipse.aether.transfer.NoTransporterException;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * A transporter using {@link java.io.File}.
  */
 final class FileTransporter extends AbstractTransporter {
 
-    private static final Logger LOGGER = LoggerFactory.getLogger(FileTransporter.class);
-
-    private final File basedir;
+    private final Path basePath;
 
     FileTransporter(RemoteRepository repository) throws NoTransporterException {
-        if (!"file".equalsIgnoreCase(repository.getProtocol())) {
-            throw new NoTransporterException(repository);
+        try {
+            basePath = Paths.get(RepositoryUriUtils.toUri(repository.getUrl())).toAbsolutePath();
+        } catch (FileSystemNotFoundException | IllegalArgumentException e) {
+            throw new NoTransporterException(repository, e);
         }
-        basedir = new File(PathUtils.basedir(repository.getUrl())).getAbsoluteFile();
     }
 
-    File getBasedir() {
-        return basedir;
+    Path getBasePath() {
+        return basePath;
     }
 
+    @Override
     public int classify(Throwable error) {
         if (error instanceof ResourceNotFoundException) {
             return ERROR_NOT_FOUND;
@@ -60,36 +58,34 @@ final class FileTransporter extends AbstractTransporter {
 
     @Override
     protected void implPeek(PeekTask task) throws Exception {
-        getFile(task, true);
+        getPath(task, true);
     }
 
     @Override
     protected void implGet(GetTask task) throws Exception {
-        File file = getFile(task, true);
-        utilGet(task, Files.newInputStream(file.toPath()), true, file.length(), false);
+        Path path = getPath(task, true);
+        utilGet(task, Files.newInputStream(path), true, Files.size(path), false);
     }
 
     @Override
     protected void implPut(PutTask task) throws Exception {
-        File file = getFile(task, false);
-        file.getParentFile().mkdirs();
+        Path path = getPath(task, false);
+        Files.createDirectories(path.getParent());
         try {
-            utilPut(task, Files.newOutputStream(file.toPath()), true);
+            utilPut(task, Files.newOutputStream(path), true);
         } catch (Exception e) {
-            if (!file.delete() && file.exists()) {
-                LOGGER.debug("Could not delete partial file {}", file);
-            }
+            Files.deleteIfExists(path);
             throw e;
         }
     }
 
-    private File getFile(TransportTask task, boolean required) throws Exception {
+    private Path getPath(TransportTask task, boolean required) throws Exception {
         String path = task.getLocation().getPath();
         if (path.contains("../")) {
             throw new IllegalArgumentException("illegal resource path: " + path);
         }
-        File file = new File(basedir, path);
-        if (required && !file.exists()) {
+        Path file = basePath.resolve(path);
+        if (required && !Files.isRegularFile(file)) {
             throw new ResourceNotFoundException("Could not locate " + file);
         }
         return file;
diff --git a/maven-resolver-transport-file/src/test/java/org/eclipse/aether/transport/file/FileTransporterTest.java b/maven-resolver-transport-file/src/test/java/org/eclipse/aether/transport/file/FileTransporterTest.java
index 87c58028..a6455fe2 100644
--- a/maven-resolver-transport-file/src/test/java/org/eclipse/aether/transport/file/FileTransporterTest.java
+++ b/maven-resolver-transport-file/src/test/java/org/eclipse/aether/transport/file/FileTransporterTest.java
@@ -18,11 +18,16 @@
  */
 package org.eclipse.aether.transport.file;
 
-import java.io.File;
 import java.io.FileNotFoundException;
 import java.net.URI;
 import java.nio.charset.StandardCharsets;
+import java.nio.file.FileSystem;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 
+import com.google.common.jimfs.Configuration;
+import com.google.common.jimfs.Jimfs;
 import org.eclipse.aether.DefaultRepositorySystemSession;
 import org.eclipse.aether.internal.test.util.TestFileUtils;
 import org.eclipse.aether.internal.test.util.TestUtils;
@@ -35,8 +40,10 @@ import org.eclipse.aether.spi.connector.transport.TransporterFactory;
 import org.eclipse.aether.transfer.NoTransporterException;
 import org.eclipse.aether.transfer.TransferCancelledException;
 import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.EnumSource;
 
 import static org.junit.jupiter.api.Assertions.*;
 
@@ -50,7 +57,14 @@ public class FileTransporterTest {
 
     private Transporter transporter;
 
-    private File repoDir;
+    private Path repoDir;
+
+    private FileSystem fileSystem;
+
+    enum FS {
+        DEFAULT,
+        JIMFS
+    }
 
     private RemoteRepository newRepo(String url) {
         return new RemoteRepository.Builder("test", "default", url).build();
@@ -61,43 +75,60 @@ public class FileTransporterTest {
             transporter.close();
             transporter = null;
         }
+        if (factory == null) {
+            factory = new FileTransporterFactory();
+        }
+        if (session == null) {
+            session = TestUtils.newSession();
+        }
         transporter = factory.newInstance(session, newRepo(url));
     }
 
-    @BeforeEach
-    void setUp() throws Exception {
-        session = TestUtils.newSession();
-        factory = new FileTransporterFactory();
-        repoDir = TestFileUtils.createTempDir();
-        TestFileUtils.writeString(new File(repoDir, "file.txt"), "test");
-        TestFileUtils.writeString(new File(repoDir, "empty.txt"), "");
-        TestFileUtils.writeString(new File(repoDir, "some space.txt"), "space");
-        newTransporter(repoDir.toURI().toString());
+    void setUp(FS fs) {
+        try {
+            fileSystem = fs == FS.JIMFS ? Jimfs.newFileSystem() : null;
+            repoDir = fileSystem == null ? TestFileUtils.createTempDir().toPath() : fileSystem.getPath(".");
+            Files.write(repoDir.resolve("file.txt"), "test".getBytes(StandardCharsets.UTF_8));
+            Files.write(repoDir.resolve("empty.txt"), "".getBytes(StandardCharsets.UTF_8));
+            Files.write(repoDir.resolve("some space.txt"), "space".getBytes(StandardCharsets.UTF_8));
+            newTransporter(repoDir.toUri().toASCIIString());
+        } catch (Exception e) {
+            Assertions.fail(e);
+        }
     }
 
     @AfterEach
-    void tearDown() {
+    void tearDown() throws Exception {
         if (transporter != null) {
             transporter.close();
             transporter = null;
         }
+        if (fileSystem != null) {
+            fileSystem.close();
+        }
         factory = null;
         session = null;
     }
 
-    @Test
-    void testClassify() {
+    @ParameterizedTest
+    @EnumSource(FS.class)
+    void testClassify(FS fs) {
+        setUp(fs);
         assertEquals(Transporter.ERROR_OTHER, transporter.classify(new FileNotFoundException()));
         assertEquals(Transporter.ERROR_NOT_FOUND, transporter.classify(new ResourceNotFoundException("test")));
     }
 
-    @Test
-    void testPeek() throws Exception {
+    @ParameterizedTest
+    @EnumSource(FS.class)
+    void testPeek(FS fs) throws Exception {
+        setUp(fs);
         transporter.peek(new PeekTask(URI.create("file.txt")));
     }
 
-    @Test
-    void testPeek_NotFound() throws Exception {
+    @ParameterizedTest
+    @EnumSource(FS.class)
+    void testPeek_NotFound(FS fs) throws Exception {
+        setUp(fs);
         try {
             transporter.peek(new PeekTask(URI.create("missing.txt")));
             fail("Expected error");
@@ -106,8 +137,10 @@ public class FileTransporterTest {
         }
     }
 
-    @Test
-    void testPeek_Closed() throws Exception {
+    @ParameterizedTest
+    @EnumSource(FS.class)
+    void testPeek_Closed(FS fs) throws Exception {
+        setUp(fs);
         transporter.close();
         try {
             transporter.peek(new PeekTask(URI.create("missing.txt")));
@@ -117,8 +150,10 @@ public class FileTransporterTest {
         }
     }
 
-    @Test
-    void testGet_ToMemory() throws Exception {
+    @ParameterizedTest
+    @EnumSource(FS.class)
+    void testGet_ToMemory(FS fs) throws Exception {
+        setUp(fs);
         RecordingTransportListener listener = new RecordingTransportListener();
         GetTask task = new GetTask(URI.create("file.txt")).setListener(listener);
         transporter.get(task);
@@ -130,13 +165,15 @@ public class FileTransporterTest {
         assertEquals(task.getDataString(), new String(listener.baos.toByteArray(), StandardCharsets.UTF_8));
     }
 
-    @Test
-    void testGet_ToFile() throws Exception {
-        File file = TestFileUtils.createTempFile("failure");
+    @ParameterizedTest
+    @EnumSource(FS.class)
+    void testGet_ToFile(FS fs) throws Exception {
+        setUp(fs);
+        Path file = TestFileUtils.createTempFile("failure").toPath();
         RecordingTransportListener listener = new RecordingTransportListener();
-        GetTask task = new GetTask(URI.create("file.txt")).setDataFile(file).setListener(listener);
+        GetTask task = new GetTask(URI.create("file.txt")).setDataPath(file).setListener(listener);
         transporter.get(task);
-        assertEquals("test", TestFileUtils.readString(file));
+        assertEquals("test", TestFileUtils.readString(file.toFile()));
         assertEquals(0L, listener.dataOffset);
         assertEquals(4L, listener.dataLength);
         assertEquals(1, listener.startedCount);
@@ -144,13 +181,15 @@ public class FileTransporterTest {
         assertEquals("test", new String(listener.baos.toByteArray(), StandardCharsets.UTF_8));
     }
 
-    @Test
-    void testGet_EmptyResource() throws Exception {
-        File file = TestFileUtils.createTempFile("failure");
+    @ParameterizedTest
+    @EnumSource(FS.class)
+    void testGet_EmptyResource(FS fs) throws Exception {
+        setUp(fs);
+        Path file = TestFileUtils.createTempFile("failure").toPath();
         RecordingTransportListener listener = new RecordingTransportListener();
-        GetTask task = new GetTask(URI.create("empty.txt")).setDataFile(file).setListener(listener);
+        GetTask task = new GetTask(URI.create("empty.txt")).setDataPath(file).setListener(listener);
         transporter.get(task);
-        assertEquals("", TestFileUtils.readString(file));
+        assertEquals("", TestFileUtils.readString(file.toFile()));
         assertEquals(0L, listener.dataOffset);
         assertEquals(0L, listener.dataLength);
         assertEquals(1, listener.startedCount);
@@ -158,38 +197,48 @@ public class FileTransporterTest {
         assertEquals("", new String(listener.baos.toByteArray(), StandardCharsets.UTF_8));
     }
 
-    @Test
-    void testGet_EncodedResourcePath() throws Exception {
+    @ParameterizedTest
+    @EnumSource(FS.class)
+    void testGet_EncodedResourcePath(FS fs) throws Exception {
+        setUp(fs);
         GetTask task = new GetTask(URI.create("some%20space.txt"));
         transporter.get(task);
         assertEquals("space", task.getDataString());
     }
 
-    @Test
-    void testGet_Fragment() throws Exception {
+    @ParameterizedTest
+    @EnumSource(FS.class)
+    void testGet_Fragment(FS fs) throws Exception {
+        setUp(fs);
         GetTask task = new GetTask(URI.create("file.txt#ignored"));
         transporter.get(task);
         assertEquals("test", task.getDataString());
     }
 
-    @Test
-    void testGet_Query() throws Exception {
+    @ParameterizedTest
+    @EnumSource(FS.class)
+    void testGet_Query(FS fs) throws Exception {
+        setUp(fs);
         GetTask task = new GetTask(URI.create("file.txt?ignored"));
         transporter.get(task);
         assertEquals("test", task.getDataString());
     }
 
-    @Test
-    void testGet_FileHandleLeak() throws Exception {
+    @ParameterizedTest
+    @EnumSource(FS.class)
+    void testGet_FileHandleLeak(FS fs) throws Exception {
+        setUp(fs);
         for (int i = 0; i < 100; i++) {
-            File file = TestFileUtils.createTempFile("failure");
-            transporter.get(new GetTask(URI.create("file.txt")).setDataFile(file));
-            assertTrue(file.delete(), i + ", " + file.getAbsolutePath());
+            Path file = TestFileUtils.createTempFile("failure").toPath();
+            transporter.get(new GetTask(URI.create("file.txt")).setDataPath(file));
+            assertTrue(file.toFile().delete(), i + ", " + file.toFile().getAbsolutePath());
         }
     }
 
-    @Test
-    void testGet_NotFound() throws Exception {
+    @ParameterizedTest
+    @EnumSource(FS.class)
+    void testGet_NotFound(FS fs) throws Exception {
+        setUp(fs);
         try {
             transporter.get(new GetTask(URI.create("missing.txt")));
             fail("Expected error");
@@ -198,8 +247,10 @@ public class FileTransporterTest {
         }
     }
 
-    @Test
-    void testGet_Closed() throws Exception {
+    @ParameterizedTest
+    @EnumSource(FS.class)
+    void testGet_Closed(FS fs) throws Exception {
+        setUp(fs);
         transporter.close();
         try {
             transporter.get(new GetTask(URI.create("file.txt")));
@@ -209,8 +260,10 @@ public class FileTransporterTest {
         }
     }
 
-    @Test
-    void testGet_StartCancelled() throws Exception {
+    @ParameterizedTest
+    @EnumSource(FS.class)
+    void testGet_StartCancelled(FS fs) throws Exception {
+        setUp(fs);
         RecordingTransportListener listener = new RecordingTransportListener();
         listener.cancelStart = true;
         GetTask task = new GetTask(URI.create("file.txt")).setListener(listener);
@@ -226,8 +279,10 @@ public class FileTransporterTest {
         assertEquals(0, listener.progressedCount);
     }
 
-    @Test
-    void testGet_ProgressCancelled() throws Exception {
+    @ParameterizedTest
+    @EnumSource(FS.class)
+    void testGet_ProgressCancelled(FS fs) throws Exception {
+        setUp(fs);
         RecordingTransportListener listener = new RecordingTransportListener();
         listener.cancelProgress = true;
         GetTask task = new GetTask(URI.create("file.txt")).setListener(listener);
@@ -243,8 +298,10 @@ public class FileTransporterTest {
         assertEquals(1, listener.progressedCount);
     }
 
-    @Test
-    void testPut_FromMemory() throws Exception {
+    @ParameterizedTest
+    @EnumSource(FS.class)
+    void testPut_FromMemory(FS fs) throws Exception {
+        setUp(fs);
         RecordingTransportListener listener = new RecordingTransportListener();
         PutTask task = new PutTask(URI.create("file.txt")).setListener(listener).setDataString("upload");
         transporter.put(task);
@@ -252,24 +309,28 @@ public class FileTransporterTest {
         assertEquals(6L, listener.dataLength);
         assertEquals(1, listener.startedCount);
         assertTrue(listener.progressedCount > 0, "Count: " + listener.progressedCount);
-        assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt")));
+        assertEquals("upload", new String(Files.readAllBytes(repoDir.resolve("file.txt")), StandardCharsets.UTF_8));
     }
 
-    @Test
-    void testPut_FromFile() throws Exception {
-        File file = TestFileUtils.createTempFile("upload");
+    @ParameterizedTest
+    @EnumSource(FS.class)
+    void testPut_FromFile(FS fs) throws Exception {
+        setUp(fs);
+        Path file = TestFileUtils.createTempFile("upload").toPath();
         RecordingTransportListener listener = new RecordingTransportListener();
-        PutTask task = new PutTask(URI.create("file.txt")).setListener(listener).setDataFile(file);
+        PutTask task = new PutTask(URI.create("file.txt")).setListener(listener).setDataPath(file);
         transporter.put(task);
         assertEquals(0L, listener.dataOffset);
         assertEquals(6L, listener.dataLength);
         assertEquals(1, listener.startedCount);
         assertTrue(listener.progressedCount > 0, "Count: " + listener.progressedCount);
-        assertEquals("upload", TestFileUtils.readString(new File(repoDir, "file.txt")));
+        assertEquals("upload", new String(Files.readAllBytes(repoDir.resolve("file.txt")), StandardCharsets.UTF_8));
     }
 
-    @Test
-    void testPut_EmptyResource() throws Exception {
+    @ParameterizedTest
+    @EnumSource(FS.class)
+    void testPut_EmptyResource(FS fs) throws Exception {
+        setUp(fs);
         RecordingTransportListener listener = new RecordingTransportListener();
         PutTask task = new PutTask(URI.create("file.txt")).setListener(listener);
         transporter.put(task);
@@ -277,11 +338,13 @@ public class FileTransporterTest {
         assertEquals(0L, listener.dataLength);
         assertEquals(1, listener.startedCount);
         assertEquals(0, listener.progressedCount);
-        assertEquals("", TestFileUtils.readString(new File(repoDir, "file.txt")));
+        assertEquals("", new String(Files.readAllBytes(repoDir.resolve("file.txt")), StandardCharsets.UTF_8));
     }
 
-    @Test
-    void testPut_NonExistentParentDir() throws Exception {
+    @ParameterizedTest
+    @EnumSource(FS.class)
+    void testPut_NonExistentParentDir(FS fs) throws Exception {
+        setUp(fs);
         RecordingTransportListener listener = new RecordingTransportListener();
         PutTask task = new PutTask(URI.create("dir/sub/dir/file.txt"))
                 .setListener(listener)
@@ -291,11 +354,15 @@ public class FileTransporterTest {
         assertEquals(6L, listener.dataLength);
         assertEquals(1, listener.startedCount);
         assertTrue(listener.progressedCount > 0, "Count: " + listener.progressedCount);
-        assertEquals("upload", TestFileUtils.readString(new File(repoDir, "dir/sub/dir/file.txt")));
+        assertEquals(
+                "upload",
+                new String(Files.readAllBytes(repoDir.resolve("dir/sub/dir/file.txt")), StandardCharsets.UTF_8));
     }
 
-    @Test
-    void testPut_EncodedResourcePath() throws Exception {
+    @ParameterizedTest
+    @EnumSource(FS.class)
+    void testPut_EncodedResourcePath(FS fs) throws Exception {
+        setUp(fs);
         RecordingTransportListener listener = new RecordingTransportListener();
         PutTask task = new PutTask(URI.create("some%20space.txt"))
                 .setListener(listener)
@@ -305,22 +372,26 @@ public class FileTransporterTest {
         assertEquals(2L, listener.dataLength);
         assertEquals(1, listener.startedCount);
         assertTrue(listener.progressedCount > 0, "Count: " + listener.progressedCount);
-        assertEquals("OK", TestFileUtils.readString(new File(repoDir, "some space.txt")));
+        assertEquals("OK", new String(Files.readAllBytes(repoDir.resolve("some space.txt")), StandardCharsets.UTF_8));
     }
 
-    @Test
-    void testPut_FileHandleLeak() throws Exception {
+    @ParameterizedTest
+    @EnumSource(FS.class)
+    void testPut_FileHandleLeak(FS fs) throws Exception {
+        setUp(fs);
         for (int i = 0; i < 100; i++) {
-            File src = TestFileUtils.createTempFile("upload");
-            File dst = new File(repoDir, "file.txt");
-            transporter.put(new PutTask(URI.create("file.txt")).setDataFile(src));
-            assertTrue(src.delete(), i + ", " + src.getAbsolutePath());
-            assertTrue(dst.delete(), i + ", " + dst.getAbsolutePath());
+            Path src = TestFileUtils.createTempFile("upload").toPath();
+            Path dst = repoDir.resolve("file.txt");
+            transporter.put(new PutTask(URI.create("file.txt")).setDataPath(src));
+            assertTrue(src.toFile().delete(), i + ", " + src.toFile().getAbsolutePath());
+            assertTrue(Files.deleteIfExists(dst), i + ", " + dst.toAbsolutePath());
         }
     }
 
-    @Test
-    void testPut_Closed() throws Exception {
+    @ParameterizedTest
+    @EnumSource(FS.class)
+    void testPut_Closed(FS fs) throws Exception {
+        setUp(fs);
         transporter.close();
         try {
             transporter.put(new PutTask(URI.create("missing.txt")));
@@ -330,8 +401,10 @@ public class FileTransporterTest {
         }
     }
 
-    @Test
-    void testPut_StartCancelled() throws Exception {
+    @ParameterizedTest
+    @EnumSource(FS.class)
+    void testPut_StartCancelled(FS fs) throws Exception {
+        setUp(fs);
         RecordingTransportListener listener = new RecordingTransportListener();
         listener.cancelStart = true;
         PutTask task = new PutTask(URI.create("file.txt")).setListener(listener).setDataString("upload");
@@ -345,11 +418,13 @@ public class FileTransporterTest {
         assertEquals(6L, listener.dataLength);
         assertEquals(1, listener.startedCount);
         assertEquals(0, listener.progressedCount);
-        assertFalse(new File(repoDir, "file.txt").exists());
+        assertFalse(Files.exists(repoDir.resolve("file.txt")));
     }
 
-    @Test
-    void testPut_ProgressCancelled() throws Exception {
+    @ParameterizedTest
+    @EnumSource(FS.class)
+    void testPut_ProgressCancelled(FS fs) throws Exception {
+        setUp(fs);
         RecordingTransportListener listener = new RecordingTransportListener();
         listener.cancelProgress = true;
         PutTask task = new PutTask(URI.create("file.txt")).setListener(listener).setDataString("upload");
@@ -363,7 +438,7 @@ public class FileTransporterTest {
         assertEquals(6L, listener.dataLength);
         assertEquals(1, listener.startedCount);
         assertEquals(1, listener.progressedCount);
-        assertFalse(new File(repoDir, "file.txt").exists());
+        assertFalse(Files.exists(repoDir.resolve("file.txt")));
     }
 
     @Test
@@ -433,9 +508,33 @@ public class FileTransporterTest {
         testInit("file://host/dir", "/dir");
     }
 
+    @Test
+    void testInit_NonDefaultFileSystemRelative() throws Exception {
+        try (FileSystem fs = Jimfs.newFileSystem(Configuration.unix())) {
+            Path path = fs.getPath("dir");
+            testInit(path.toUri().toASCIIString(), "/work/dir");
+        }
+    }
+
+    @Test
+    void testInit_NonDefaultFileSystemAbsolute() throws Exception {
+        try (FileSystem fs = Jimfs.newFileSystem(Configuration.unix())) {
+            Path path = fs.getPath("/dir");
+            testInit(path.toUri().toASCIIString(), "/dir");
+        }
+    }
+
     private void testInit(String base, String expected) throws Exception {
         newTransporter(base);
-        File exp = new File(expected).getAbsoluteFile();
-        assertEquals(exp, ((FileTransporter) transporter).getBasedir());
+        String exp = expected;
+        if (base.startsWith("file:")) {
+            // on def FileSystem we do extra dance that we do NOT do in case of non-default File Systems:
+            // like accepting weird URLs/URIs and resolving/abs against CWD, that may not be defined in case
+            // of non-default FileSystems (OTOH, they MAY do it, like JIMFS has $cwd="/work")
+            exp = Paths.get(expected).toAbsolutePath().toString();
+        }
+        // compare path string representation only, as otherwise (Object equality) it would fail
+        // if we end up with non default FS for example
+        assertEquals(exp, ((FileTransporter) transporter).getBasePath().toString());
     }
 }
diff --git a/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-11/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporter.java b/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-11/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporter.java
index 29b4e24a..77a2312d 100644
--- a/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-11/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporter.java
+++ b/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-11/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporter.java
@@ -20,7 +20,7 @@ package org.eclipse.aether.transport.jdk;
 
 import javax.net.ssl.*;
 
-import java.io.File;
+import java.io.BufferedInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.lang.reflect.InvocationTargetException;
@@ -30,6 +30,7 @@ import java.net.http.HttpClient;
 import java.net.http.HttpRequest;
 import java.net.http.HttpResponse;
 import java.nio.file.Files;
+import java.nio.file.Path;
 import java.nio.file.StandardCopyOption;
 import java.nio.file.attribute.FileTime;
 import java.security.cert.X509Certificate;
@@ -60,6 +61,7 @@ import org.eclipse.aether.spi.connector.transport.TransportTask;
 import org.eclipse.aether.spi.connector.transport.http.ChecksumExtractor;
 import org.eclipse.aether.spi.connector.transport.http.HttpTransporter;
 import org.eclipse.aether.spi.connector.transport.http.HttpTransporterException;
+import org.eclipse.aether.spi.io.PathProcessor;
 import org.eclipse.aether.transfer.NoTransporterException;
 import org.eclipse.aether.util.ConfigUtils;
 import org.eclipse.aether.util.FileUtils;
@@ -86,6 +88,8 @@ final class JdkTransporter extends AbstractTransporter implements HttpTransporte
 
     private final ChecksumExtractor checksumExtractor;
 
+    private final PathProcessor pathProcessor;
+
     private final URI baseUri;
 
     private final HttpClient client;
@@ -102,9 +106,11 @@ final class JdkTransporter extends AbstractTransporter implements HttpTransporte
             RepositorySystemSession session,
             RemoteRepository repository,
             int javaVersion,
-            ChecksumExtractor checksumExtractor)
+            ChecksumExtractor checksumExtractor,
+            PathProcessor pathProcessor)
             throws NoTransporterException {
         this.checksumExtractor = checksumExtractor;
+        this.pathProcessor = pathProcessor;
         try {
             URI uri = new URI(repository.getUrl()).parseServerAuthority();
             if (uri.isOpaque()) {
@@ -214,7 +220,7 @@ final class JdkTransporter extends AbstractTransporter implements HttpTransporte
 
     @Override
     protected void implGet(GetTask task) throws Exception {
-        boolean resume = task.getResumeOffset() > 0L && task.getDataFile() != null;
+        boolean resume = task.getResumeOffset() > 0L && task.getDataPath() != null;
         HttpResponse<InputStream> response = null;
 
         try {
@@ -227,11 +233,11 @@ final class JdkTransporter extends AbstractTransporter implements HttpTransporte
 
                 if (resume) {
                     long resumeOffset = task.getResumeOffset();
+                    long lastModified = pathProcessor.lastModified(task.getDataPath(), 0L);
                     request.header(RANGE, "bytes=" + resumeOffset + '-');
                     request.header(
                             IF_UNMODIFIED_SINCE,
-                            RFC7231.format(
-                                    Instant.ofEpochMilli(task.getDataFile().lastModified() - MODIFICATION_THRESHOLD)));
+                            RFC7231.format(Instant.ofEpochMilli(lastModified - MODIFICATION_THRESHOLD)));
                     request.header(ACCEPT_ENCODING, "identity");
                 }
 
@@ -271,16 +277,16 @@ final class JdkTransporter extends AbstractTransporter implements HttpTransporte
             }
 
             final boolean downloadResumed = offset > 0L;
-            final File dataFile = task.getDataFile();
+            final Path dataFile = task.getDataPath();
             if (dataFile == null) {
                 try (InputStream is = response.body()) {
                     utilGet(task, is, true, length, downloadResumed);
                 }
             } else {
-                try (FileUtils.CollocatedTempFile tempFile = FileUtils.newTempFile(dataFile.toPath())) {
-                    task.setDataFile(tempFile.getPath().toFile(), downloadResumed);
-                    if (downloadResumed && Files.isRegularFile(dataFile.toPath())) {
-                        try (InputStream inputStream = Files.newInputStream(dataFile.toPath())) {
+                try (FileUtils.CollocatedTempFile tempFile = FileUtils.newTempFile(dataFile)) {
+                    task.setDataPath(tempFile.getPath(), downloadResumed);
+                    if (downloadResumed && Files.isRegularFile(dataFile)) {
+                        try (InputStream inputStream = new BufferedInputStream(Files.newInputStream(dataFile))) {
                             Files.copy(inputStream, tempFile.getPath(), StandardCopyOption.REPLACE_EXISTING);
                         }
                     }
@@ -289,17 +295,17 @@ final class JdkTransporter extends AbstractTransporter implements HttpTransporte
                     }
                     tempFile.move();
                 } finally {
-                    task.setDataFile(dataFile);
+                    task.setDataPath(dataFile);
                 }
             }
-            if (task.getDataFile() != null) {
+            if (task.getDataPath() != null) {
                 String lastModifiedHeader = response.headers()
                         .firstValue(LAST_MODIFIED)
                         .orElse(null); // note: Wagon also does first not last
                 if (lastModifiedHeader != null) {
                     try {
                         Files.setLastModifiedTime(
-                                task.getDataFile().toPath(),
+                                task.getDataPath(),
                                 FileTime.fromMillis(ZonedDateTime.parse(lastModifiedHeader, RFC7231)
                                         .toInstant()
                                         .toEpochMilli()));
diff --git a/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-11/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporterFactory.java b/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-11/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporterFactory.java
index 2d048a79..a91044ec 100644
--- a/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-11/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporterFactory.java
+++ b/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-11/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporterFactory.java
@@ -26,6 +26,7 @@ import org.eclipse.aether.repository.RemoteRepository;
 import org.eclipse.aether.spi.connector.transport.http.ChecksumExtractor;
 import org.eclipse.aether.spi.connector.transport.http.HttpTransporter;
 import org.eclipse.aether.spi.connector.transport.http.HttpTransporterFactory;
+import org.eclipse.aether.spi.io.PathProcessor;
 import org.eclipse.aether.transfer.NoTransporterException;
 
 import static java.util.Objects.requireNonNull;
@@ -43,9 +44,12 @@ public final class JdkTransporterFactory implements HttpTransporterFactory {
 
     private final ChecksumExtractor checksumExtractor;
 
+    private final PathProcessor pathProcessor;
+
     @Inject
-    public JdkTransporterFactory(ChecksumExtractor checksumExtractor) {
+    public JdkTransporterFactory(ChecksumExtractor checksumExtractor, PathProcessor pathProcessor) {
         this.checksumExtractor = requireNonNull(checksumExtractor, "checksumExtractor");
+        this.pathProcessor = requireNonNull(pathProcessor, "pathProcessor");
     }
 
     @Override
@@ -68,7 +72,7 @@ public final class JdkTransporterFactory implements HttpTransporterFactory {
             throw new NoTransporterException(repository, "Only HTTP/HTTPS is supported");
         }
 
-        return new JdkTransporter(session, repository, javaVersion(), checksumExtractor);
+        return new JdkTransporter(session, repository, javaVersion(), checksumExtractor, pathProcessor);
     }
 
     private static int javaVersion() {
diff --git a/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-11/src/test/java/org/eclipse/aether/transport/jdk/JdkTransporterTest.java b/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-11/src/test/java/org/eclipse/aether/transport/jdk/JdkTransporterTest.java
index 3dc2eaed..1a15aaed 100644
--- a/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-11/src/test/java/org/eclipse/aether/transport/jdk/JdkTransporterTest.java
+++ b/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-11/src/test/java/org/eclipse/aether/transport/jdk/JdkTransporterTest.java
@@ -21,6 +21,7 @@ package org.eclipse.aether.transport.jdk;
 import java.net.ConnectException;
 import java.net.URI;
 
+import org.eclipse.aether.internal.impl.DefaultPathProcessor;
 import org.eclipse.aether.internal.test.util.TestUtils;
 import org.eclipse.aether.internal.test.util.http.HttpTransporterTest;
 import org.eclipse.aether.repository.RemoteRepository;
@@ -84,14 +85,14 @@ class JdkTransporterTest extends HttpTransporterTest {
     protected void testPut_Authenticated_ExpectContinueRejected_ExplicitlyConfiguredHeader() {}
 
     public JdkTransporterTest() {
-        super(() -> new JdkTransporterFactory(standardChecksumExtractor()));
+        super(() -> new JdkTransporterFactory(standardChecksumExtractor(), new DefaultPathProcessor()));
     }
 
     @Test
     void enhanceConnectExceptionMessages() {
         String uri = "https://localhost:12345/";
         RemoteRepository remoteRepository = new RemoteRepository.Builder("central", "default", uri).build();
-        JdkTransporterFactory factory = new JdkTransporterFactory(s -> null);
+        JdkTransporterFactory factory = new JdkTransporterFactory(s -> null, new DefaultPathProcessor());
 
         try (Transporter transporter = factory.newInstance(TestUtils.newSession(), remoteRepository)) {
             transporter.peek(new PeekTask(URI.create("repo/file.txt")));
diff --git a/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-8/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporterFactory.java b/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-8/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporterFactory.java
index 2096ac31..dcea7ce8 100644
--- a/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-8/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporterFactory.java
+++ b/maven-resolver-transport-jdk-parent/maven-resolver-transport-jdk-8/src/main/java/org/eclipse/aether/transport/jdk/JdkTransporterFactory.java
@@ -26,6 +26,7 @@ import org.eclipse.aether.repository.RemoteRepository;
 import org.eclipse.aether.spi.connector.transport.http.ChecksumExtractor;
 import org.eclipse.aether.spi.connector.transport.http.HttpTransporter;
 import org.eclipse.aether.spi.connector.transport.http.HttpTransporterFactory;
+import org.eclipse.aether.spi.io.PathProcessor;
 import org.eclipse.aether.transfer.NoTransporterException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -46,7 +47,7 @@ public final class JdkTransporterFactory implements HttpTransporterFactory {
     private float priority = 10.0f;
 
     @Inject
-    public JdkTransporterFactory(ChecksumExtractor checksumExtractor) {
+    public JdkTransporterFactory(ChecksumExtractor checksumExtractor, PathProcessor pathProcessor) {
         // this is to equalize all Java version constructors to be same, so Supplier could work across all versions
     }
 
diff --git a/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporter.java b/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporter.java
index 6050d884..107dda51 100644
--- a/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporter.java
+++ b/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/JettyTransporter.java
@@ -20,12 +20,12 @@ package org.eclipse.aether.transport.jetty;
 
 import javax.net.ssl.*;
 
-import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.nio.file.Files;
+import java.nio.file.Path;
 import java.nio.file.StandardCopyOption;
 import java.nio.file.attribute.FileTime;
 import java.security.cert.X509Certificate;
@@ -205,7 +205,7 @@ final class JettyTransporter extends AbstractTransporter implements HttpTranspor
 
     @Override
     protected void implGet(GetTask task) throws Exception {
-        boolean resume = task.getResumeOffset() > 0L && task.getDataFile() != null;
+        boolean resume = task.getResumeOffset() > 0L && task.getDataPath() != null;
         Response response;
         InputStreamResponseListener listener;
 
@@ -225,9 +225,11 @@ final class JettyTransporter extends AbstractTransporter implements HttpTranspor
 
             if (resume) {
                 long resumeOffset = task.getResumeOffset();
+                long lastModified =
+                        Files.getLastModifiedTime(task.getDataPath()).toMillis();
                 request.headers(h -> {
                     h.add(RANGE, "bytes=" + resumeOffset + '-');
-                    h.addDateField(IF_UNMODIFIED_SINCE, task.getDataFile().lastModified() - MODIFICATION_THRESHOLD);
+                    h.addDateField(IF_UNMODIFIED_SINCE, lastModified - MODIFICATION_THRESHOLD);
                     h.remove(HttpHeader.ACCEPT_ENCODING);
                     h.add(ACCEPT_ENCODING, "identity");
                 });
@@ -273,16 +275,16 @@ final class JettyTransporter extends AbstractTransporter implements HttpTranspor
         }
 
         final boolean downloadResumed = offset > 0L;
-        final File dataFile = task.getDataFile();
+        final Path dataFile = task.getDataPath();
         if (dataFile == null) {
             try (InputStream is = listener.getInputStream()) {
                 utilGet(task, is, true, length, downloadResumed);
             }
         } else {
-            try (FileUtils.CollocatedTempFile tempFile = FileUtils.newTempFile(dataFile.toPath())) {
-                task.setDataFile(tempFile.getPath().toFile(), downloadResumed);
-                if (downloadResumed && Files.isRegularFile(dataFile.toPath())) {
-                    try (InputStream inputStream = Files.newInputStream(dataFile.toPath())) {
+            try (FileUtils.CollocatedTempFile tempFile = FileUtils.newTempFile(dataFile)) {
+                task.setDataPath(tempFile.getPath(), downloadResumed);
+                if (downloadResumed && Files.isRegularFile(dataFile)) {
+                    try (InputStream inputStream = Files.newInputStream(dataFile)) {
                         Files.copy(inputStream, tempFile.getPath(), StandardCopyOption.REPLACE_EXISTING);
                     }
                 }
@@ -291,15 +293,15 @@ final class JettyTransporter extends AbstractTransporter implements HttpTranspor
                 }
                 tempFile.move();
             } finally {
-                task.setDataFile(dataFile);
+                task.setDataPath(dataFile);
             }
         }
-        if (task.getDataFile() != null && response.getHeaders().getDateField(LAST_MODIFIED) != -1) {
+        if (task.getDataPath() != null && response.getHeaders().getDateField(LAST_MODIFIED) != -1) {
             long lastModified =
                     response.getHeaders().getDateField(LAST_MODIFIED); // note: Wagon also does first not last
             if (lastModified != -1) {
                 try {
-                    Files.setLastModifiedTime(task.getDataFile().toPath(), FileTime.fromMillis(lastModified));
+                    Files.setLastModifiedTime(task.getDataPath(), FileTime.fromMillis(lastModified));
                 } catch (DateTimeParseException e) {
                     // fall through
                 }
diff --git a/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/PutTaskRequestContent.java b/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/PutTaskRequestContent.java
index 889187ff..05af716b 100644
--- a/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/PutTaskRequestContent.java
+++ b/maven-resolver-transport-jetty/src/main/java/org/eclipse/aether/transport/jetty/PutTaskRequestContent.java
@@ -94,8 +94,8 @@ class PutTaskRequestContent extends AbstractRequestContent {
             ByteBuffer buffer;
             boolean last;
             if (channel == null) {
-                if (putTask.getDataFile() != null) {
-                    channel = Files.newByteChannel(putTask.getDataFile().toPath(), StandardOpenOption.READ);
+                if (putTask.getDataPath() != null) {
+                    channel = Files.newByteChannel(putTask.getDataPath(), StandardOpenOption.READ);
                 } else {
                     channel = Channels.newChannel(putTask.newInputStream());
                 }
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/DirectoryUtils.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/DirectoryUtils.java
index 244c7bd3..8a13e742 100644
--- a/maven-resolver-util/src/main/java/org/eclipse/aether/util/DirectoryUtils.java
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/DirectoryUtils.java
@@ -24,6 +24,7 @@ import java.nio.file.Path;
 import java.nio.file.Paths;
 
 import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.repository.LocalRepository;
 
 import static java.util.Objects.requireNonNull;
 
@@ -67,7 +68,7 @@ public final class DirectoryUtils {
         if (namePath.isAbsolute()) {
             result = namePath.normalize();
         } else {
-            result = base.resolve(namePath).normalize();
+            result = base.resolve(name).normalize();
         }
 
         if (!Files.exists(result)) {
@@ -84,7 +85,7 @@ public final class DirectoryUtils {
      * Creates {@link Path} instance out of session configuration, and (if relative) resolve it against local
      * repository basedir. Pre-populates values and invokes {@link #resolveDirectory(String, Path, boolean)}.
      * <p>
-     * For this method to work, {@link org.eclipse.aether.repository.LocalRepository#getBasedir()} must return
+     * For this method to work, {@link LocalRepository#getBasePath()} must return
      * non-{@code null} value, otherwise {@link NullPointerException} is thrown.
      *
      * @param session     The session, may not be {@code null}.
@@ -100,10 +101,10 @@ public final class DirectoryUtils {
         requireNonNull(session, "session is null");
         requireNonNull(defaultName, "defaultName is null");
         requireNonNull(nameKey, "nameKey is null");
-        requireNonNull(session.getLocalRepository().getBasedir(), "session.localRepository.basedir is null");
+        requireNonNull(session.getLocalRepository().getBasePath(), "session.localRepository.basePath is null");
         return resolveDirectory(
                 ConfigUtils.getString(session, defaultName, nameKey),
-                session.getLocalRepository().getBasedir().toPath(),
+                session.getLocalRepository().getBasePath(),
                 mayCreate);
     }
 }
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 0a430f89..fe0768ef 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
@@ -132,7 +132,7 @@ public final class FileUtils {
                     if (IS_WINDOWS) {
                         copy(tempFile, file);
                     } else {
-                        Files.move(tempFile, file, StandardCopyOption.ATOMIC_MOVE);
+                        Files.move(tempFile, file, StandardCopyOption.REPLACE_EXISTING);
                     }
                 }
                 Files.deleteIfExists(tempFile);
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/artifact/DelegatingArtifact.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/artifact/DelegatingArtifact.java
index ef48e8b5..256c640d 100644
--- a/maven-resolver-util/src/main/java/org/eclipse/aether/util/artifact/DelegatingArtifact.java
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/artifact/DelegatingArtifact.java
@@ -19,6 +19,7 @@
 package org.eclipse.aether.util.artifact;
 
 import java.io.File;
+import java.nio.file.Path;
 import java.util.Map;
 
 import org.eclipse.aether.artifact.AbstractArtifact;
@@ -52,18 +53,22 @@ public abstract class DelegatingArtifact extends AbstractArtifact {
      */
     protected abstract DelegatingArtifact newInstance(Artifact delegate);
 
+    @Override
     public String getGroupId() {
         return delegate.getGroupId();
     }
 
+    @Override
     public String getArtifactId() {
         return delegate.getArtifactId();
     }
 
+    @Override
     public String getVersion() {
         return delegate.getVersion();
     }
 
+    @Override
     public Artifact setVersion(String version) {
         Artifact artifact = delegate.setVersion(version);
         if (artifact != delegate) {
@@ -72,26 +77,39 @@ public abstract class DelegatingArtifact extends AbstractArtifact {
         return this;
     }
 
+    @Override
     public String getBaseVersion() {
         return delegate.getBaseVersion();
     }
 
+    @Override
     public boolean isSnapshot() {
         return delegate.isSnapshot();
     }
 
+    @Override
     public String getClassifier() {
         return delegate.getClassifier();
     }
 
+    @Override
     public String getExtension() {
         return delegate.getExtension();
     }
 
+    @Deprecated
+    @Override
     public File getFile() {
         return delegate.getFile();
     }
 
+    @Override
+    public Path getPath() {
+        return delegate.getPath();
+    }
+
+    @Deprecated
+    @Override
     public Artifact setFile(File file) {
         Artifact artifact = delegate.setFile(file);
         if (artifact != delegate) {
@@ -100,6 +118,15 @@ public abstract class DelegatingArtifact extends AbstractArtifact {
         return this;
     }
 
+    @Override
+    public Artifact setPath(Path path) {
+        Artifact artifact = delegate.setPath(path);
+        if (artifact != delegate) {
+            return newInstance(artifact);
+        }
+        return this;
+    }
+
     public String getProperty(String key, String defaultValue) {
         return delegate.getProperty(key, defaultValue);
     }
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/artifact/SubArtifact.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/artifact/SubArtifact.java
index 1e3282ad..5e8c677e 100644
--- a/maven-resolver-util/src/main/java/org/eclipse/aether/util/artifact/SubArtifact.java
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/artifact/SubArtifact.java
@@ -19,6 +19,7 @@
 package org.eclipse.aether.util.artifact;
 
 import java.io.File;
+import java.nio.file.Path;
 import java.util.Map;
 import java.util.Objects;
 
@@ -39,7 +40,7 @@ public final class SubArtifact extends AbstractArtifact {
 
     private final String extension;
 
-    private final File file;
+    private final Path path;
 
     private final Map<String, String> properties;
 
@@ -72,6 +73,22 @@ public final class SubArtifact extends AbstractArtifact {
         this(mainArtifact, classifier, extension, null, file);
     }
 
+    /**
+     * Creates a new sub artifact. The classifier and extension specified for this artifact may use the asterisk
+     * character "*" to refer to the corresponding property of the main artifact. For instance, the classifier
+     * "*-sources" can be used to refer to the source attachment of an artifact. Likewise, the extension "*.asc" can be
+     * used to refer to the GPG signature of an artifact.
+     *
+     * @param mainArtifact The artifact from which to derive the identity, must not be {@code null}.
+     * @param classifier The classifier for this artifact, may be {@code null} if none.
+     * @param extension The extension for this artifact, may be {@code null} if none.
+     * @param path The file for this artifact, may be {@code null} if unresolved.
+     * @since 2.0.0
+     */
+    public SubArtifact(Artifact mainArtifact, String classifier, String extension, Path path) {
+        this(mainArtifact, classifier, extension, null, path);
+    }
+
     /**
      * Creates a new sub artifact. The classifier and extension specified for this artifact may use the asterisk
      * character "*" to refer to the corresponding property of the main artifact. For instance, the classifier
@@ -84,7 +101,7 @@ public final class SubArtifact extends AbstractArtifact {
      * @param properties The properties of the artifact, may be {@code null}.
      */
     public SubArtifact(Artifact mainArtifact, String classifier, String extension, Map<String, String> properties) {
-        this(mainArtifact, classifier, extension, properties, null);
+        this(mainArtifact, classifier, extension, properties, (Path) null);
     }
 
     /**
@@ -101,71 +118,112 @@ public final class SubArtifact extends AbstractArtifact {
      */
     public SubArtifact(
             Artifact mainArtifact, String classifier, String extension, Map<String, String> properties, File file) {
+        this(mainArtifact, classifier, extension, properties, file != null ? file.toPath() : null);
+    }
+
+    /**
+     * Creates a new sub artifact. The classifier and extension specified for this artifact may use the asterisk
+     * character "*" to refer to the corresponding property of the main artifact. For instance, the classifier
+     * "*-sources" can be used to refer to the source attachment of an artifact. Likewise, the extension "*.asc" can be
+     * used to refer to the GPG signature of an artifact.
+     *
+     * @param mainArtifact The artifact from which to derive the identity, must not be {@code null}.
+     * @param classifier The classifier for this artifact, may be {@code null} if none.
+     * @param extension The extension for this artifact, may be {@code null} if none.
+     * @param properties The properties of the artifact, may be {@code null}.
+     * @param path The file for this artifact, may be {@code null} if unresolved.
+     * @since 2.0.0
+     */
+    public SubArtifact(
+            Artifact mainArtifact, String classifier, String extension, Map<String, String> properties, Path path) {
         this.mainArtifact = requireNonNull(mainArtifact, "main artifact cannot be null");
         this.classifier = classifier;
         this.extension = extension;
-        this.file = file;
+        this.path = path;
         this.properties = copyProperties(properties);
     }
 
     private SubArtifact(
-            Artifact mainArtifact, String classifier, String extension, File file, Map<String, String> properties) {
+            Artifact mainArtifact, String classifier, String extension, Path path, Map<String, String> properties) {
         // NOTE: This constructor assumes immutability of the provided properties, for internal use only
         this.mainArtifact = mainArtifact;
         this.classifier = classifier;
         this.extension = extension;
-        this.file = file;
+        this.path = path;
         this.properties = properties;
     }
 
+    @Override
     public String getGroupId() {
         return mainArtifact.getGroupId();
     }
 
+    @Override
     public String getArtifactId() {
         return mainArtifact.getArtifactId();
     }
 
+    @Override
     public String getVersion() {
         return mainArtifact.getVersion();
     }
 
+    @Override
     public String getBaseVersion() {
         return mainArtifact.getBaseVersion();
     }
 
+    @Override
     public boolean isSnapshot() {
         return mainArtifact.isSnapshot();
     }
 
+    @Override
     public String getClassifier() {
         return expand(classifier, mainArtifact.getClassifier());
     }
 
+    @Override
     public String getExtension() {
         return expand(extension, mainArtifact.getExtension());
     }
 
+    @Deprecated
+    @Override
     public File getFile() {
-        return file;
+        return path != null ? path.toFile() : null;
     }
 
+    @Override
+    public Path getPath() {
+        return path;
+    }
+
+    @Deprecated
+    @Override
     public Artifact setFile(File file) {
-        if (Objects.equals(this.file, file)) {
+        return setPath(file != null ? file.toPath() : null);
+    }
+
+    @Override
+    public Artifact setPath(Path path) {
+        if (Objects.equals(this.path, path)) {
             return this;
         }
-        return new SubArtifact(mainArtifact, classifier, extension, file, properties);
+        return new SubArtifact(mainArtifact, classifier, extension, path, properties);
     }
 
+    @Override
     public Map<String, String> getProperties() {
         return properties;
     }
 
+    @Override
     public Artifact setProperties(Map<String, String> properties) {
         if (this.properties.equals(properties) || (properties == null && this.properties.isEmpty())) {
             return this;
         }
-        return new SubArtifact(mainArtifact, classifier, extension, properties, file);
+        return new SubArtifact(mainArtifact, classifier, extension, properties, path);
     }
 
     private static String expand(String pattern, String replacement) {
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/NodeListGenerator.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/NodeListGenerator.java
index 44d45831..68b61d73 100644
--- a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/NodeListGenerator.java
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/visitor/NodeListGenerator.java
@@ -19,6 +19,7 @@
 package org.eclipse.aether.util.graph.visitor;
 
 import java.io.File;
+import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
@@ -97,11 +98,23 @@ public final class NodeListGenerator implements Consumer<DependencyNode> {
      * Gets the files of resolved artifacts seen during the graph traversal.
      *
      * @return The list of artifact files, never {@code null}.
+     * @deprecated Use {@link #getPaths()} instead.
      */
+    @Deprecated
     public List<File> getFiles() {
         return getFiles(getNodes());
     }
 
+    /**
+     * Gets the files of resolved artifacts seen during the graph traversal.
+     *
+     * @return The list of artifact files, never {@code null}.
+     * @since 2.0.0
+     */
+    public List<Path> getPaths() {
+        return getPaths(getNodes());
+    }
+
     /**
      * Gets a class path by concatenating the artifact files of the visited dependency nodes. Nodes with unresolved
      * artifacts are automatically skipped.
@@ -119,17 +132,18 @@ public final class NodeListGenerator implements Consumer<DependencyNode> {
     static List<Dependency> getDependencies(List<DependencyNode> nodes, boolean includeUnresolved) {
         return getNodesWithDependencies(nodes).stream()
                 .map(DependencyNode::getDependency)
-                .filter(d -> includeUnresolved || d.getArtifact().getFile() != null)
+                .filter(d -> includeUnresolved || d.getArtifact().getPath() != null)
                 .collect(toList());
     }
 
     static List<Artifact> getArtifacts(List<DependencyNode> nodes, boolean includeUnresolved) {
         return getNodesWithDependencies(nodes).stream()
                 .map(d -> d.getDependency().getArtifact())
-                .filter(artifact -> includeUnresolved || artifact.getFile() != null)
+                .filter(artifact -> includeUnresolved || artifact.getPath() != null)
                 .collect(toList());
     }
 
+    @Deprecated
     static List<File> getFiles(List<DependencyNode> nodes) {
         return getNodesWithDependencies(nodes).stream()
                 .map(d -> d.getDependency().getArtifact().getFile())
@@ -137,7 +151,17 @@ public final class NodeListGenerator implements Consumer<DependencyNode> {
                 .collect(toList());
     }
 
+    static List<Path> getPaths(List<DependencyNode> nodes) {
+        return getNodesWithDependencies(nodes).stream()
+                .map(d -> d.getDependency().getArtifact().getPath())
+                .filter(Objects::nonNull)
+                .collect(toList());
+    }
+
     static String getClassPath(List<DependencyNode> nodes) {
-        return getFiles(nodes).stream().map(File::getAbsolutePath).collect(joining(File.pathSeparator));
+        return getPaths(nodes).stream()
+                .map(Path::toAbsolutePath)
+                .map(Path::toString)
+                .collect(joining(File.pathSeparator));
     }
 }
diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/repository/ChainedLocalRepositoryManager.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/repository/ChainedLocalRepositoryManager.java
index 90239559..6b32ab2f 100644
--- a/maven-resolver-util/src/main/java/org/eclipse/aether/util/repository/ChainedLocalRepositoryManager.java
+++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/repository/ChainedLocalRepositoryManager.java
@@ -18,7 +18,6 @@
  */
 package org.eclipse.aether.util.repository;
 
-import java.io.File;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.List;
@@ -120,7 +119,7 @@ public final class ChainedLocalRepositoryManager implements LocalRepositoryManag
 
         for (LocalRepositoryManager lrm : tail) {
             result = lrm.find(session, request);
-            if (result.getFile() != null) {
+            if (result.getPath() != null) {
                 if (ignoreTailAvailability) {
                     result.setAvailable(true);
                     return result;
@@ -141,7 +140,7 @@ public final class ChainedLocalRepositoryManager implements LocalRepositoryManag
             artifactPath = getPathForLocalArtifact(request.getArtifact());
         }
 
-        Path file = new File(head.getRepository().getBasedir(), artifactPath).toPath();
+        Path file = head.getRepository().getBasePath().resolve(artifactPath);
         if (Files.isRegularFile(file)) {
             head.add(session, request);
         }
@@ -150,13 +149,13 @@ public final class ChainedLocalRepositoryManager implements LocalRepositoryManag
     @Override
     public LocalMetadataResult find(RepositorySystemSession session, LocalMetadataRequest request) {
         LocalMetadataResult result = head.find(session, request);
-        if (result.getFile() != null) {
+        if (result.getPath() != null) {
             return result;
         }
 
         for (LocalRepositoryManager lrm : tail) {
             result = lrm.find(session, request);
-            if (result.getFile() != null) {
+            if (result.getPath() != null) {
                 return result;
             }
         }
@@ -172,7 +171,7 @@ public final class ChainedLocalRepositoryManager implements LocalRepositoryManag
             metadataPath = getPathForLocalMetadata(request.getMetadata());
         }
 
-        Path file = new File(head.getRepository().getBasedir(), metadataPath).toPath();
+        Path file = head.getRepository().getBasePath().resolve(metadataPath);
         if (Files.isRegularFile(file)) {
             head.add(session, request);
         }
diff --git a/maven-resolver-util/src/test/java/org/eclipse/aether/util/artifact/SubArtifactTest.java b/maven-resolver-util/src/test/java/org/eclipse/aether/util/artifact/SubArtifactTest.java
index f3bbe3dd..c7fd3335 100644
--- a/maven-resolver-util/src/test/java/org/eclipse/aether/util/artifact/SubArtifactTest.java
+++ b/maven-resolver-util/src/test/java/org/eclipse/aether/util/artifact/SubArtifactTest.java
@@ -128,7 +128,7 @@ public class SubArtifactTest {
         Map<String, String> props = new HashMap<>();
         props.put("key", "value1");
 
-        Artifact a = new SubArtifact(newMainArtifact("gid:aid:ver"), "", "pom", props, null);
+        Artifact a = new SubArtifact(newMainArtifact("gid:aid:ver"), "", "pom", props, (File) null);
         assertEquals("value1", a.getProperty("key", null));
         props.clear();
         assertEquals("value1", a.getProperty("key", null));
diff --git a/pom.xml b/pom.xml
index 51ac0164..a9a9e37a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -102,7 +102,7 @@
     <!-- Used by Jetty Transport (client) and Test HTTP (server) -->
     <jettyVersion>10.0.20</jettyVersion>
     <!-- used by supplier and demo only -->
-    <mavenVersion>4.0.0-alpha-12</mavenVersion>
+    <mavenVersion>4.0.0-alpha-13-SNAPSHOT</mavenVersion>
     <minimalMavenBuildVersion>[3.8.8,)</minimalMavenBuildVersion>
     <!-- MRESOLVER-422: keep this in sync with Javadoc plugin configuration (but cannot directly, as this below is range) -->
     <minimalJavaBuildVersion>[21,)</minimalJavaBuildVersion>
@@ -265,6 +265,12 @@
         <version>${slf4jVersion}</version>
         <scope>test</scope>
       </dependency>
+
+      <dependency>
+        <groupId>com.google.jimfs</groupId>
+        <artifactId>jimfs</artifactId>
+        <version>1.3.0</version>
+      </dependency>
     </dependencies>
   </dependencyManagement>