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 156b0361 [MRESOLVER-700] Bundle transport: read support (#685) 156b0361 is described below commit 156b03610f1cd293535125e752ecc2667a404f3c Author: Tamas Cservenak <ta...@cservenak.net> AuthorDate: Mon Apr 14 16:05:15 2025 +0200 [MRESOLVER-700] Bundle transport: read support (#685) This PR slightly improves the file transport to fully utilize FileSystem, which now can be ZipFileSystem as well. The "old" part remains unchanged. For factory just added `bundle:` prefix, that now may be in form `bundle:fileUri`, where `fileUri` is basically a path of a ZIP file that must exist. The ZIP file is read only. The point of this PR is that user can now use a "bundle" (see https://central.sonatype.org/publish/publish-portal-upload/) just like we did before with staging repositories. Various tools (njord?) can produce bundles. The ZIP file is "mounted" and artifacts from it becomes reachable to Maven just like from any remote repository. --- https://issues.apache.org/jira/browse/MRESOLVER-700 --- maven-resolver-transport-file/README.md | 27 +++++++ .../aether/transport/file/FileTransporter.java | 70 ++++++++++++++---- .../transport/file/FileTransporterFactory.java | 82 +++++++++++++++------- .../aether/transport/file/package-info.java | 2 +- .../aether/transport/file/FileTransporterTest.java | 50 ++++++++++--- 5 files changed, 181 insertions(+), 50 deletions(-) diff --git a/maven-resolver-transport-file/README.md b/maven-resolver-transport-file/README.md new file mode 100644 index 00000000..65e44bef --- /dev/null +++ b/maven-resolver-transport-file/README.md @@ -0,0 +1,27 @@ +<!--- + 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. +--> + +# File transport + +This transport uses Java NIO2 `java.nio.file.FileSystem` to implement "remote storage". It is usable in variety of +use cases, from plain local directory to much more. + +Valid file URLs: +* as before (unchanged) + +Valid bundle URLs: +* `bundle:file.zip` - should point to an existing ZIP file 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 631630b9..a5db7562 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,8 +18,11 @@ */ package org.eclipse.aether.transport.file; +import java.io.IOException; +import java.io.UncheckedIOException; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; +import java.nio.file.FileSystem; import java.nio.file.Files; import java.nio.file.Path; @@ -28,29 +31,55 @@ 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 static java.util.Objects.requireNonNull; /** - * A transporter using {@link java.io.File}. + * A transporter using {@link java.nio.file.Path} that is reading and writing from specified base directory + * of given {@link java.nio.file.FileSystem}. It supports multiple {@link WriteOp} and obeys read-only property. */ final class FileTransporter extends AbstractTransporter { /** - * The file op transport can use. + * The write operation transport can use to write contents to the target (usually in local repository) of the + * file in remote repository reached by this transporter. Historically, and in some special cases (ZIP file system), + * it is only {@link #COPY} that can be used. + * <p> + * In case when contents of remote repository reached by this transport and target are on same volume, + * then {@link #SYMLINK} and {@link #HARDLINK} can be used as well, to reduce storage redundancy. Still, Resolver + * cannot do much smartness here, it is user who should evaluate this possibility, and if all conditions are met, + * apply it. Resolver does not try play smart here, it will obey configuration and most probably fail (ie cross + * volume hardlink). * * @since 2.0.2 */ - enum FileOp { + enum WriteOp { COPY, SYMLINK, HARDLINK; } + private final FileSystem fileSystem; + private final boolean closeFileSystem; + private final boolean writableFileSystem; private final Path basePath; - private final FileOp fileOp; + private final WriteOp writeOp; + + FileTransporter( + FileSystem fileSystem, + boolean closeFileSystem, + boolean writableFileSystem, + Path basePath, + WriteOp writeOp) { + this.fileSystem = requireNonNull(fileSystem); + this.closeFileSystem = closeFileSystem; + this.writableFileSystem = writableFileSystem; + this.basePath = requireNonNull(basePath); + this.writeOp = requireNonNull(writeOp); - FileTransporter(Path basePath, FileOp fileOp) throws NoTransporterException { - this.basePath = basePath; - this.fileOp = fileOp; + // sanity check + if (basePath.getFileSystem() != fileSystem) { + throw new IllegalArgumentException("basePath must originate from the fileSystem"); + } } Path getBasePath() { @@ -65,12 +94,12 @@ final class FileTransporter extends AbstractTransporter { return ERROR_OTHER; } - private FileOp effectiveFileOp(FileOp wanted, GetTask task) { + private WriteOp effectiveFileOp(WriteOp wanted, GetTask task) { if (task.getDataPath() != null) { return wanted; } - // task carries no path, caller wants in-memory read, so COPY must be used - return FileOp.COPY; + // not default FS or task carries no path (caller wants in-memory read) = COPY must be used + return WriteOp.COPY; } @Override @@ -82,7 +111,7 @@ final class FileTransporter extends AbstractTransporter { protected void implGet(GetTask task) throws Exception { Path path = getPath(task, true); long size = Files.size(path); - FileOp effective = effectiveFileOp(fileOp, task); + WriteOp effective = effectiveFileOp(writeOp, task); switch (effective) { case COPY: utilGet(task, Files.newInputStream(path), true, size, false); @@ -91,7 +120,7 @@ final class FileTransporter extends AbstractTransporter { case HARDLINK: Files.deleteIfExists(task.getDataPath()); task.getListener().transportStarted(0L, size); - if (effective == FileOp.HARDLINK) { + if (effective == WriteOp.HARDLINK) { Files.createLink(task.getDataPath(), path); } else { Files.createSymbolicLink(task.getDataPath(), path); @@ -113,12 +142,15 @@ final class FileTransporter extends AbstractTransporter { } break; default: - throw new IllegalStateException("Unknown fileOp" + fileOp); + throw new IllegalStateException("Unknown fileOp " + writeOp); } } @Override protected void implPut(PutTask task) throws Exception { + if (!writableFileSystem) { + throw new UnsupportedOperationException("Read only FileSystem"); + } Path path = getPath(task, false); Files.createDirectories(path.getParent()); try { @@ -142,5 +174,13 @@ final class FileTransporter extends AbstractTransporter { } @Override - protected void implClose() {} + protected void implClose() { + if (closeFileSystem) { + try { + fileSystem.close(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + } } diff --git a/maven-resolver-transport-file/src/main/java/org/eclipse/aether/transport/file/FileTransporterFactory.java b/maven-resolver-transport-file/src/main/java/org/eclipse/aether/transport/file/FileTransporterFactory.java index a836194b..a4051b9e 100644 --- a/maven-resolver-transport-file/src/main/java/org/eclipse/aether/transport/file/FileTransporterFactory.java +++ b/maven-resolver-transport-file/src/main/java/org/eclipse/aether/transport/file/FileTransporterFactory.java @@ -20,8 +20,16 @@ package org.eclipse.aether.transport.file; import javax.inject.Named; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.URI; +import java.nio.file.FileSystem; import java.nio.file.FileSystemNotFoundException; +import java.nio.file.FileSystems; +import java.nio.file.Path; import java.nio.file.Paths; +import java.util.HashMap; +import java.util.Map; import org.eclipse.aether.RepositorySystemSession; import org.eclipse.aether.repository.RemoteRepository; @@ -33,7 +41,7 @@ import org.eclipse.aether.transfer.NoTransporterException; import static java.util.Objects.requireNonNull; /** - * A transporter factory for repositories using the {@code file:} protocol. + * A transporter factory for repositories using the {@code file:} or {@code bundle:} protocol. */ @Named(FileTransporterFactory.NAME) public final class FileTransporterFactory implements TransporterFactory { @@ -41,15 +49,6 @@ public final class FileTransporterFactory implements TransporterFactory { private float priority; - /** - * Creates an (uninitialized) instance of this transporter factory. <em>Note:</em> In case of manual instantiation - * by clients, the new factory needs to be configured via its various mutators before first use or runtime errors - * will occur. - */ - public FileTransporterFactory() { - // enables default constructor - } - @Override public float getPriority() { return priority; @@ -66,28 +65,61 @@ public final class FileTransporterFactory implements TransporterFactory { return this; } + /** + * Creates new instance of {@link FileTransporter}. + * + * @param session The session. + * @param repository The remote repository. + */ @Override public Transporter newInstance(RepositorySystemSession session, RemoteRepository repository) throws NoTransporterException { requireNonNull(session, "session cannot be null"); requireNonNull(repository, "repository cannot be null"); - // special case in file transport: to support custom FS providers (like JIMFS), we cannot - // cover "all possible protocols" to throw NoTransporterEx, but we rely on FS rejecting the URI - FileTransporter.FileOp fileOp = FileTransporter.FileOp.COPY; String repositoryUrl = repository.getUrl(); - if (repositoryUrl.startsWith("symlink+")) { - fileOp = FileTransporter.FileOp.SYMLINK; - repositoryUrl = repositoryUrl.substring("symlink+".length()); - } else if (repositoryUrl.startsWith("hardlink+")) { - fileOp = FileTransporter.FileOp.HARDLINK; - repositoryUrl = repositoryUrl.substring("hardlink+".length()); - } - try { - return new FileTransporter( - Paths.get(RepositoryUriUtils.toUri(repositoryUrl)).toAbsolutePath(), fileOp); - } catch (FileSystemNotFoundException | IllegalArgumentException e) { - throw new NoTransporterException(repository, e); + if (repositoryUrl.startsWith("bundle:")) { + try { + repositoryUrl = repositoryUrl.substring("bundle:".length()); + URI bundlePath = URI.create("jar:" + + Paths.get(RepositoryUriUtils.toUri(repositoryUrl)) + .toAbsolutePath() + .toUri() + .toASCIIString()); + Map<String, String> env = new HashMap<>(); + FileSystem fileSystem = FileSystems.newFileSystem(bundlePath, env); + return new FileTransporter( + fileSystem, + true, + false, + fileSystem.getPath(fileSystem.getSeparator()), + FileTransporter.WriteOp.COPY); + } catch (IOException e) { + throw new UncheckedIOException(e); // hard failure; most probably user error (ie wrong path or perm) + } + } else { + // special case in file: transport: to support custom FS providers (like JIMFS), we cannot + // cover all possible protocols (to throw NoTransporterEx), hence we rely on FS rejecting the URI + FileTransporter.WriteOp writeOp = FileTransporter.WriteOp.COPY; + if (repositoryUrl.startsWith("symlink+")) { + writeOp = FileTransporter.WriteOp.SYMLINK; + repositoryUrl = repositoryUrl.substring("symlink+".length()); + } else if (repositoryUrl.startsWith("hardlink+")) { + writeOp = FileTransporter.WriteOp.HARDLINK; + repositoryUrl = repositoryUrl.substring("hardlink+".length()); + } + try { + Path basePath = + Paths.get(RepositoryUriUtils.toUri(repositoryUrl)).toAbsolutePath(); + return new FileTransporter( + basePath.getFileSystem(), + false, + !basePath.getFileSystem().isReadOnly(), + basePath, + writeOp); + } catch (FileSystemNotFoundException | IllegalArgumentException e) { + throw new NoTransporterException(repository, e); + } } } } diff --git a/maven-resolver-transport-file/src/main/java/org/eclipse/aether/transport/file/package-info.java b/maven-resolver-transport-file/src/main/java/org/eclipse/aether/transport/file/package-info.java index 4a9c9030..f7209b22 100644 --- a/maven-resolver-transport-file/src/main/java/org/eclipse/aether/transport/file/package-info.java +++ b/maven-resolver-transport-file/src/main/java/org/eclipse/aether/transport/file/package-info.java @@ -18,6 +18,6 @@ * under the License. */ /** - * Support for downloads/uploads using the local filesystem as "remote" storage. + * Support for downloads/uploads using the Java NIO2 filesystem as "remote" storage. */ package org.eclipse.aether.transport.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 9232e013..0a24bce7 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 @@ -22,9 +22,11 @@ import java.io.FileNotFoundException; import java.net.URI; import java.nio.charset.StandardCharsets; import java.nio.file.FileSystem; +import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.HashMap; import com.google.common.jimfs.Configuration; import com.google.common.jimfs.Jimfs; @@ -45,7 +47,12 @@ 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.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assumptions.assumeTrue; /** */ @@ -64,16 +71,19 @@ public class FileTransporterTest { private FileSystem fileSystem; enum FS { - DEFAULT(""), - DEFAULT_SL("symlink+"), - DEFAULT_HL("hardlink+"), - JIMFS(""), - JIMFS_SL("symlink+"), - JIMFS_HL("hardlink+"); - + DEFAULT(true, ""), + DEFAULT_SL(true, "symlink+"), + DEFAULT_HL(true, "hardlink+"), + JIMFS(true, ""), + JIMFS_SL(true, "symlink+"), + JIMFS_HL(true, "hardlink+"), + BUNDLE(false, "bundle:"); + + final boolean writable; final String uriPrefix; - FS(String uriPrefix) { + FS(boolean writable, String uriPrefix) { + this.writable = writable; this.uriPrefix = uriPrefix; } } @@ -106,6 +116,19 @@ public class FileTransporterTest { 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)); + if (fs == FS.BUNDLE) { + URI bundle = URI.create("jar:" + + repoDir.resolve("bundle.zip").toAbsolutePath().toUri().toASCIIString()); + // zip it up and change uri to zip file path + HashMap<String, String> env = new HashMap<>(); + env.put("create", "true"); + try (FileSystem zfs = FileSystems.newFileSystem(bundle, env)) { + Files.copy(repoDir.resolve("file.txt"), zfs.getPath("file.txt")); + Files.copy(repoDir.resolve("empty.txt"), zfs.getPath("empty.txt")); + Files.copy(repoDir.resolve("some space.txt"), zfs.getPath("some space.txt")); + } + repoDir = repoDir.resolve("bundle.zip"); + } newTransporter(fs.uriPrefix + repoDir.toUri().toASCIIString()); } catch (Exception e) { Assertions.fail(e); @@ -331,6 +354,7 @@ public class FileTransporterTest { @ParameterizedTest @EnumSource(FS.class) void testPut_FromMemory(FS fs) throws Exception { + assumeTrue(fs.writable); setUp(fs); RecordingTransportListener listener = new RecordingTransportListener(); PutTask task = new PutTask(URI.create("file.txt")).setListener(listener).setDataString("upload"); @@ -345,6 +369,7 @@ public class FileTransporterTest { @ParameterizedTest @EnumSource(FS.class) void testPut_FromFile(FS fs) throws Exception { + assumeTrue(fs.writable); setUp(fs); Path file = tempDir.resolve("upload"); Files.write(file, "upload".getBytes(StandardCharsets.UTF_8)); @@ -361,6 +386,7 @@ public class FileTransporterTest { @ParameterizedTest @EnumSource(FS.class) void testPut_EmptyResource(FS fs) throws Exception { + assumeTrue(fs.writable); setUp(fs); RecordingTransportListener listener = new RecordingTransportListener(); PutTask task = new PutTask(URI.create("file.txt")).setListener(listener); @@ -375,6 +401,7 @@ public class FileTransporterTest { @ParameterizedTest @EnumSource(FS.class) void testPut_NonExistentParentDir(FS fs) throws Exception { + assumeTrue(fs.writable); setUp(fs); RecordingTransportListener listener = new RecordingTransportListener(); PutTask task = new PutTask(URI.create("dir/sub/dir/file.txt")) @@ -393,6 +420,7 @@ public class FileTransporterTest { @ParameterizedTest @EnumSource(FS.class) void testPut_EncodedResourcePath(FS fs) throws Exception { + assumeTrue(fs.writable); setUp(fs); RecordingTransportListener listener = new RecordingTransportListener(); PutTask task = new PutTask(URI.create("some%20space.txt")) @@ -409,6 +437,7 @@ public class FileTransporterTest { @ParameterizedTest @EnumSource(FS.class) void testPut_FileHandleLeak(FS fs) throws Exception { + assumeTrue(fs.writable); setUp(fs); for (int i = 0; i < 100; i++) { Path src = tempDir.resolve("upload"); @@ -423,6 +452,7 @@ public class FileTransporterTest { @ParameterizedTest @EnumSource(FS.class) void testPut_Closed(FS fs) throws Exception { + assumeTrue(fs.writable); setUp(fs); transporter.close(); try { @@ -436,6 +466,7 @@ public class FileTransporterTest { @ParameterizedTest @EnumSource(FS.class) void testPut_StartCancelled(FS fs) throws Exception { + assumeTrue(fs.writable); setUp(fs); RecordingTransportListener listener = new RecordingTransportListener(); listener.cancelStart = true; @@ -456,6 +487,7 @@ public class FileTransporterTest { @ParameterizedTest @EnumSource(FS.class) void testPut_ProgressCancelled(FS fs) throws Exception { + assumeTrue(fs.writable); setUp(fs); RecordingTransportListener listener = new RecordingTransportListener(); listener.cancelProgress = true;