https://github.com/tomitribe/tomitribe-nexus
https://github.com/tomitribe/tomitribe-nexus
Last synced: about 1 month ago
JSON representation
- Host: GitHub
- URL: https://github.com/tomitribe/tomitribe-nexus
- Owner: tomitribe
- License: apache-2.0
- Created: 2026-06-01T02:37:55.000Z (about 2 months ago)
- Default Branch: main
- Last Pushed: 2026-06-02T20:41:49.000Z (about 2 months ago)
- Last Synced: 2026-06-02T21:26:13.221Z (about 2 months ago)
- Language: Roff
- Size: 156 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# tomitribe-nexus
A read-only [`java.nio.file.FileSystem`](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/nio/file/FileSystem.html)
over a remote Maven repository's HTML index — Nexus 2, Nexus 3, Maven Central, or any
`mod_autoindex`-style listing.
You point it at a repository's base URL and get back an ordinary `Path`. From there you use the
standard `java.nio.file.Files` API — `walk`, `list`, `copy`, `readAttributes` — to browse and
download artifacts as if the remote repository were a local directory tree. No new vocabulary to
learn: if you know NIO, you know this.
```java
Path root = Nexus.builder()
.baseUri("https://repo1.maven.org/maven2/")
.build();
Path version = root.resolve("org/apache/tomee/apache-tomee/9.0.1");
try (Stream tree = Files.walk(version)) {
tree.filter(Files::isRegularFile)
.filter(p -> p.getFileName().toString().endsWith(".tar.gz"))
.forEach(p -> copy(p, target.resolve(p.getFileName().toString())));
}
```
## Dependency
```xml
org.tomitribe.nexus
tomitribe-nexus
1.0.3
```
Requires Java 17+.
## Getting a root
`Nexus.builder()` is the only entry point. `build()` returns the `Path` for the repository root.
```java
// Anonymous (e.g. Maven Central)
Path root = Nexus.builder()
.baseUri("https://repo1.maven.org/maven2/")
.build();
// Authenticated (HTTP Basic)
Path root = Nexus.builder()
.baseUri("https://nexus.example/content/repositories/releases/")
.credentials("user", "pass") // optional — omit for anonymous
.build();
```
Credentials, when supplied, are sent only to the configured base host — they are never attached to
a request for any other host.
## Browsing
The root is a `Path` rooted at the base URL. Build sub-paths with `resolve(...)`, exactly as you
would on the default filesystem:
```java
Path artifact = root.resolve("org/apache/tomee/apache-tomee");
// relative paths built with the ordinary NIO API work too — including on Windows,
// where the separator differs
Path same = root.resolve(Path.of("org", "apache", "tomee", "apache-tomee"));
```
**List the immediate children** of a directory (e.g. the available versions):
```java
try (Stream children = Files.list(artifact)) {
children.filter(Files::isDirectory) // version folders; skip maven-metadata.xml etc.
.map(p -> p.getFileName().toString())
.sorted()
.forEach(System.out::println);
}
// or a DirectoryStream
try (DirectoryStream children = Files.newDirectoryStream(artifact)) {
for (Path child : children) System.out.println(child);
}
```
**Walk a whole subtree:**
```java
try (Stream tree = Files.walk(root.resolve("org/apache/tomee/apache-tomee/9.0.1"))) {
tree.forEach(System.out::println);
}
```
## Downloading
Copy a remote file straight to local disk — it's just `Files.copy` across providers:
```java
Path remote = root.resolve("org/apache/tomee/apache-tomee/9.0.1/apache-tomee-9.0.1.tar.gz");
Files.copy(remote, Path.of("/tmp/apache-tomee-9.0.1.tar.gz"), StandardCopyOption.REPLACE_EXISTING);
```
Or stream the bytes yourself:
```java
try (InputStream in = Files.newInputStream(remote)) {
// ...
}
```
## Attributes
Size and last-modified come back through the standard attribute API. When they were present in the
directory listing they cost nothing; otherwise they are filled by a single `HEAD` on demand:
```java
BasicFileAttributes attrs = Files.readAttributes(p, BasicFileAttributes.class);
attrs.size();
attrs.lastModifiedTime();
attrs.isDirectory();
attrs.isRegularFile();
// or the convenience methods
long size = Files.size(p);
FileTime modified = Files.getLastModifiedTime(p);
boolean exists = Files.exists(p); // false for a 404, never throws
boolean dir = Files.isDirectory(p);
```
## How it behaves
- **Read-only.** Every mutating operation (`createDirectory`, `delete`, `move`, writing) throws
`ReadOnlyFileSystemException`. Reading bytes from a directory, or listing a file, throws
`UnsupportedOperationException`. A path that doesn't exist surfaces as `NoSuchFileException`.
- **Rooted at the base — off-base is unrepresentable.** The root `/` *is* the base URL, and `..`
can never climb above it. There is no host or scheme to point elsewhere, so you cannot, even by
accident, aim an authenticated client at another location.
- **Lazy.** Naming a path (`root.resolve(...)`) costs nothing — no network. The first operation
that needs the remote (`Files.isDirectory`, `newInputStream`, `readAttributes`, ...) resolves it
with a single `HEAD`, cached thereafter. A `Files.walk` is request-minimal: one listing per
directory, with each entry's kind, size, and date read from that listing — it never `HEAD`s a
file just to walk past it.
- **Server-aware.** Directory-vs-file is determined from the listing and, for a hand-built path,
from the `HEAD` response — handling Nexus 2 (a directory `HEAD` is `200` with no `Content-Type`),
Nexus 3 / autoindex (`text/html` listings), and Maven Central.
## Limitations
It is a genuine `java.nio.file.Path`, but a *remote, read-only* one, so two NIO operations don't
apply:
- **`Path.toFile()` throws.** A Nexus path is not backed by a local `java.io.File`. Use `Files.copy`
to bring it to disk, then work with the local file.
- **Byte-channel reads aren't implemented.** `Files.newByteChannel` (and therefore
`Files.readAllBytes` / `Files.readString`) is unsupported — read with `Files.newInputStream` or
copy with `Files.copy` instead.
## License
Apache License 2.0.