Skip to content

Commit 156b036

Browse files
authored
[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
1 parent 093866a commit 156b036

File tree

5 files changed

+181
-50
lines changed

5 files changed

+181
-50
lines changed
+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<!---
2+
Licensed to the Apache Software Foundation (ASF) under one or more
3+
contributor license agreements. See the NOTICE file distributed with
4+
this work for additional information regarding copyright ownership.
5+
The ASF licenses this file to You under the Apache License, Version 2.0
6+
(the "License"); you may not use this file except in compliance with
7+
the License. You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
-->
17+
18+
# File transport
19+
20+
This transport uses Java NIO2 `java.nio.file.FileSystem` to implement "remote storage". It is usable in variety of
21+
use cases, from plain local directory to much more.
22+
23+
Valid file URLs:
24+
* as before (unchanged)
25+
26+
Valid bundle URLs:
27+
* `bundle:file.zip` - should point to an existing ZIP file

maven-resolver-transport-file/src/main/java/org/eclipse/aether/transport/file/FileTransporter.java

+55-15
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,11 @@
1818
*/
1919
package org.eclipse.aether.transport.file;
2020

21+
import java.io.IOException;
22+
import java.io.UncheckedIOException;
2123
import java.nio.ByteBuffer;
2224
import java.nio.channels.FileChannel;
25+
import java.nio.file.FileSystem;
2326
import java.nio.file.Files;
2427
import java.nio.file.Path;
2528

@@ -28,29 +31,55 @@
2831
import org.eclipse.aether.spi.connector.transport.PeekTask;
2932
import org.eclipse.aether.spi.connector.transport.PutTask;
3033
import org.eclipse.aether.spi.connector.transport.TransportTask;
31-
import org.eclipse.aether.transfer.NoTransporterException;
34+
35+
import static java.util.Objects.requireNonNull;
3236

3337
/**
34-
* A transporter using {@link java.io.File}.
38+
* A transporter using {@link java.nio.file.Path} that is reading and writing from specified base directory
39+
* of given {@link java.nio.file.FileSystem}. It supports multiple {@link WriteOp} and obeys read-only property.
3540
*/
3641
final class FileTransporter extends AbstractTransporter {
3742
/**
38-
* The file op transport can use.
43+
* The write operation transport can use to write contents to the target (usually in local repository) of the
44+
* file in remote repository reached by this transporter. Historically, and in some special cases (ZIP file system),
45+
* it is only {@link #COPY} that can be used.
46+
* <p>
47+
* In case when contents of remote repository reached by this transport and target are on same volume,
48+
* then {@link #SYMLINK} and {@link #HARDLINK} can be used as well, to reduce storage redundancy. Still, Resolver
49+
* cannot do much smartness here, it is user who should evaluate this possibility, and if all conditions are met,
50+
* apply it. Resolver does not try play smart here, it will obey configuration and most probably fail (ie cross
51+
* volume hardlink).
3952
*
4053
* @since 2.0.2
4154
*/
42-
enum FileOp {
55+
enum WriteOp {
4356
COPY,
4457
SYMLINK,
4558
HARDLINK;
4659
}
4760

61+
private final FileSystem fileSystem;
62+
private final boolean closeFileSystem;
63+
private final boolean writableFileSystem;
4864
private final Path basePath;
49-
private final FileOp fileOp;
65+
private final WriteOp writeOp;
66+
67+
FileTransporter(
68+
FileSystem fileSystem,
69+
boolean closeFileSystem,
70+
boolean writableFileSystem,
71+
Path basePath,
72+
WriteOp writeOp) {
73+
this.fileSystem = requireNonNull(fileSystem);
74+
this.closeFileSystem = closeFileSystem;
75+
this.writableFileSystem = writableFileSystem;
76+
this.basePath = requireNonNull(basePath);
77+
this.writeOp = requireNonNull(writeOp);
5078

51-
FileTransporter(Path basePath, FileOp fileOp) throws NoTransporterException {
52-
this.basePath = basePath;
53-
this.fileOp = fileOp;
79+
// sanity check
80+
if (basePath.getFileSystem() != fileSystem) {
81+
throw new IllegalArgumentException("basePath must originate from the fileSystem");
82+
}
5483
}
5584

5685
Path getBasePath() {
@@ -65,12 +94,12 @@ public int classify(Throwable error) {
6594
return ERROR_OTHER;
6695
}
6796

68-
private FileOp effectiveFileOp(FileOp wanted, GetTask task) {
97+
private WriteOp effectiveFileOp(WriteOp wanted, GetTask task) {
6998
if (task.getDataPath() != null) {
7099
return wanted;
71100
}
72-
// task carries no path, caller wants in-memory read, so COPY must be used
73-
return FileOp.COPY;
101+
// not default FS or task carries no path (caller wants in-memory read) = COPY must be used
102+
return WriteOp.COPY;
74103
}
75104

76105
@Override
@@ -82,7 +111,7 @@ protected void implPeek(PeekTask task) throws Exception {
82111
protected void implGet(GetTask task) throws Exception {
83112
Path path = getPath(task, true);
84113
long size = Files.size(path);
85-
FileOp effective = effectiveFileOp(fileOp, task);
114+
WriteOp effective = effectiveFileOp(writeOp, task);
86115
switch (effective) {
87116
case COPY:
88117
utilGet(task, Files.newInputStream(path), true, size, false);
@@ -91,7 +120,7 @@ protected void implGet(GetTask task) throws Exception {
91120
case HARDLINK:
92121
Files.deleteIfExists(task.getDataPath());
93122
task.getListener().transportStarted(0L, size);
94-
if (effective == FileOp.HARDLINK) {
123+
if (effective == WriteOp.HARDLINK) {
95124
Files.createLink(task.getDataPath(), path);
96125
} else {
97126
Files.createSymbolicLink(task.getDataPath(), path);
@@ -113,12 +142,15 @@ protected void implGet(GetTask task) throws Exception {
113142
}
114143
break;
115144
default:
116-
throw new IllegalStateException("Unknown fileOp" + fileOp);
145+
throw new IllegalStateException("Unknown fileOp " + writeOp);
117146
}
118147
}
119148

120149
@Override
121150
protected void implPut(PutTask task) throws Exception {
151+
if (!writableFileSystem) {
152+
throw new UnsupportedOperationException("Read only FileSystem");
153+
}
122154
Path path = getPath(task, false);
123155
Files.createDirectories(path.getParent());
124156
try {
@@ -142,5 +174,13 @@ private Path getPath(TransportTask task, boolean required) throws Exception {
142174
}
143175

144176
@Override
145-
protected void implClose() {}
177+
protected void implClose() {
178+
if (closeFileSystem) {
179+
try {
180+
fileSystem.close();
181+
} catch (IOException e) {
182+
throw new UncheckedIOException(e);
183+
}
184+
}
185+
}
146186
}

maven-resolver-transport-file/src/main/java/org/eclipse/aether/transport/file/FileTransporterFactory.java

+57-25
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,16 @@
2020

2121
import javax.inject.Named;
2222

23+
import java.io.IOException;
24+
import java.io.UncheckedIOException;
25+
import java.net.URI;
26+
import java.nio.file.FileSystem;
2327
import java.nio.file.FileSystemNotFoundException;
28+
import java.nio.file.FileSystems;
29+
import java.nio.file.Path;
2430
import java.nio.file.Paths;
31+
import java.util.HashMap;
32+
import java.util.Map;
2533

2634
import org.eclipse.aether.RepositorySystemSession;
2735
import org.eclipse.aether.repository.RemoteRepository;
@@ -33,23 +41,14 @@
3341
import static java.util.Objects.requireNonNull;
3442

3543
/**
36-
* A transporter factory for repositories using the {@code file:} protocol.
44+
* A transporter factory for repositories using the {@code file:} or {@code bundle:} protocol.
3745
*/
3846
@Named(FileTransporterFactory.NAME)
3947
public final class FileTransporterFactory implements TransporterFactory {
4048
public static final String NAME = "file";
4149

4250
private float priority;
4351

44-
/**
45-
* Creates an (uninitialized) instance of this transporter factory. <em>Note:</em> In case of manual instantiation
46-
* by clients, the new factory needs to be configured via its various mutators before first use or runtime errors
47-
* will occur.
48-
*/
49-
public FileTransporterFactory() {
50-
// enables default constructor
51-
}
52-
5352
@Override
5453
public float getPriority() {
5554
return priority;
@@ -66,28 +65,61 @@ public FileTransporterFactory setPriority(float priority) {
6665
return this;
6766
}
6867

68+
/**
69+
* Creates new instance of {@link FileTransporter}.
70+
*
71+
* @param session The session.
72+
* @param repository The remote repository.
73+
*/
6974
@Override
7075
public Transporter newInstance(RepositorySystemSession session, RemoteRepository repository)
7176
throws NoTransporterException {
7277
requireNonNull(session, "session cannot be null");
7378
requireNonNull(repository, "repository cannot be null");
7479

75-
// special case in file transport: to support custom FS providers (like JIMFS), we cannot
76-
// cover "all possible protocols" to throw NoTransporterEx, but we rely on FS rejecting the URI
77-
FileTransporter.FileOp fileOp = FileTransporter.FileOp.COPY;
7880
String repositoryUrl = repository.getUrl();
79-
if (repositoryUrl.startsWith("symlink+")) {
80-
fileOp = FileTransporter.FileOp.SYMLINK;
81-
repositoryUrl = repositoryUrl.substring("symlink+".length());
82-
} else if (repositoryUrl.startsWith("hardlink+")) {
83-
fileOp = FileTransporter.FileOp.HARDLINK;
84-
repositoryUrl = repositoryUrl.substring("hardlink+".length());
85-
}
86-
try {
87-
return new FileTransporter(
88-
Paths.get(RepositoryUriUtils.toUri(repositoryUrl)).toAbsolutePath(), fileOp);
89-
} catch (FileSystemNotFoundException | IllegalArgumentException e) {
90-
throw new NoTransporterException(repository, e);
81+
if (repositoryUrl.startsWith("bundle:")) {
82+
try {
83+
repositoryUrl = repositoryUrl.substring("bundle:".length());
84+
URI bundlePath = URI.create("jar:"
85+
+ Paths.get(RepositoryUriUtils.toUri(repositoryUrl))
86+
.toAbsolutePath()
87+
.toUri()
88+
.toASCIIString());
89+
Map<String, String> env = new HashMap<>();
90+
FileSystem fileSystem = FileSystems.newFileSystem(bundlePath, env);
91+
return new FileTransporter(
92+
fileSystem,
93+
true,
94+
false,
95+
fileSystem.getPath(fileSystem.getSeparator()),
96+
FileTransporter.WriteOp.COPY);
97+
} catch (IOException e) {
98+
throw new UncheckedIOException(e); // hard failure; most probably user error (ie wrong path or perm)
99+
}
100+
} else {
101+
// special case in file: transport: to support custom FS providers (like JIMFS), we cannot
102+
// cover all possible protocols (to throw NoTransporterEx), hence we rely on FS rejecting the URI
103+
FileTransporter.WriteOp writeOp = FileTransporter.WriteOp.COPY;
104+
if (repositoryUrl.startsWith("symlink+")) {
105+
writeOp = FileTransporter.WriteOp.SYMLINK;
106+
repositoryUrl = repositoryUrl.substring("symlink+".length());
107+
} else if (repositoryUrl.startsWith("hardlink+")) {
108+
writeOp = FileTransporter.WriteOp.HARDLINK;
109+
repositoryUrl = repositoryUrl.substring("hardlink+".length());
110+
}
111+
try {
112+
Path basePath =
113+
Paths.get(RepositoryUriUtils.toUri(repositoryUrl)).toAbsolutePath();
114+
return new FileTransporter(
115+
basePath.getFileSystem(),
116+
false,
117+
!basePath.getFileSystem().isReadOnly(),
118+
basePath,
119+
writeOp);
120+
} catch (FileSystemNotFoundException | IllegalArgumentException e) {
121+
throw new NoTransporterException(repository, e);
122+
}
91123
}
92124
}
93125
}

maven-resolver-transport-file/src/main/java/org/eclipse/aether/transport/file/package-info.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,6 @@
1818
* under the License.
1919
*/
2020
/**
21-
* Support for downloads/uploads using the local filesystem as "remote" storage.
21+
* Support for downloads/uploads using the Java NIO2 filesystem as "remote" storage.
2222
*/
2323
package org.eclipse.aether.transport.file;

0 commit comments

Comments
 (0)