Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion dev/paper.http
Original file line number Diff line number Diff line change
@@ -1,2 +1,9 @@
@project =paper
@version =1.21.9
@build =49

###
GET https://api.papermc.io/v2/projects/paper
GET https://fill.papermc.io/v3/projects/paper/versions

###
GET https://fill.papermc.io/v3/projects/{{project}}/versions/{{version}}/builds/{{build}}
21 changes: 13 additions & 8 deletions src/main/java/me/itzg/helpers/paper/InstallPaperCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -120,12 +120,14 @@ public Integer call() throws Exception {
else {
if (requestCheckUpdates) {
return checkForUpdates(client, oldManifest,
inputs.coordinates.project, inputs.coordinates.version, inputs.coordinates.build
inputs.coordinates.project, inputs.coordinates.version, inputs.coordinates.build,
inputs.coordinates.channel
);
}

result = downloadUsingCoordinates(client, inputs.coordinates.project,
inputs.coordinates.version, inputs.coordinates.build
inputs.coordinates.version, inputs.coordinates.build,
inputs.coordinates.channel
)
.block();
}
Expand All @@ -151,7 +153,8 @@ public Integer call() throws Exception {
}

private Integer checkForUpdates(PaperDownloadsClient client, PaperManifest oldManifest,
String project, String version, Integer build
String project, String version, Integer build,
RequestedChannel channel
) {
if (oldManifest != null && oldManifest.getCustomDownloadUrl() != null) {
log.info("Using custom download URL before");
Expand Down Expand Up @@ -184,7 +187,7 @@ private Integer checkForUpdates(PaperDownloadsClient client, PaperManifest oldMa
}
}
else {
return client.getLatestVersionBuild(project)
return client.getLatestVersionBuild(project, channel)
.map(versionBuild -> {
if (oldManifest == null) {
return logVersion(project, versionBuild.getVersion(), versionBuild.getBuild());
Expand Down Expand Up @@ -223,10 +226,11 @@ private static boolean mismatchingVersions(PaperManifest oldManifest, String pro
}

private Mono<Result> downloadUsingCoordinates(PaperDownloadsClient client, String project,
String version, Integer build
String version, Integer build,
RequestedChannel channel
) {
return
assembleDownload(client, project, version, build)
assembleDownload(client, project, version, build, channel)
.map(result ->
Result.builder()
.newManifest(
Expand All @@ -244,7 +248,8 @@ private Mono<Result> downloadUsingCoordinates(PaperDownloadsClient client, Strin
}

private Mono<VersionBuildFile> assembleDownload(PaperDownloadsClient client, String project, String version,
Integer build
Integer build,
RequestedChannel channel
) {
final FileDownloadStatusHandler downloadStatusHandler = Fetch.loggingDownloadStatusHandler(log);

Expand All @@ -257,7 +262,7 @@ private Mono<VersionBuildFile> assembleDownload(PaperDownloadsClient client, Str
}
}
else {
return client.downloadLatest(project, outputDirectory, downloadStatusHandler);
return client.downloadLatest(project, channel, outputDirectory, downloadStatusHandler);
}
}

Expand Down
62 changes: 40 additions & 22 deletions src/main/java/me/itzg/helpers/paper/PaperDownloadsClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.net.URI;
import java.nio.file.Path;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import me.itzg.helpers.errors.GenericException;
import me.itzg.helpers.errors.InvalidParameterException;
Expand All @@ -12,10 +13,11 @@
import me.itzg.helpers.http.SharedFetch;
import me.itzg.helpers.http.UriBuilder;
import me.itzg.helpers.paper.model.BuildResponse;
import me.itzg.helpers.paper.model.Channel;
import me.itzg.helpers.paper.model.Download;
import me.itzg.helpers.paper.model.ProjectResponse;
import me.itzg.helpers.paper.model.Version;
import me.itzg.helpers.paper.model.VersionResponse;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

/**
Expand Down Expand Up @@ -45,10 +47,10 @@ public static class VersionBuildFile {
final Path file;
}

public Mono<VersionBuild> getLatestVersionBuild(String project) {
public Mono<VersionBuild> getLatestVersionBuild(String project, RequestedChannel requestedChannel) {
return getProjectVersions(project)
.flatMap(projectResponse ->
extractVersionBuild(project, projectResponse)
extractLatestVersionBuild(project, requestedChannel, projectResponse)
);
}

Expand All @@ -68,12 +70,12 @@ public Mono<Integer> getLatestBuild(String project, String version) {
.map(BuildResponse::getId);
}

public Mono<VersionBuildFile> downloadLatest(String project,
public Mono<VersionBuildFile> downloadLatest(String project, RequestedChannel requestedChannel,
Path outputDirectory, FileDownloadStatusHandler downloadStatusHandler
) {
return getProjectVersions(project)
.flatMap(projectResponse ->
extractVersionBuild(project, projectResponse)
extractLatestVersionBuild(project, requestedChannel, projectResponse)
.flatMap(versionBuild ->
download(project, outputDirectory, downloadStatusHandler,
versionBuild.getVersion(),
Expand Down Expand Up @@ -111,6 +113,15 @@ public Mono<VersionBuildFile> download(String project,
String version,
int build
) {
return getBuild(project, version, build)
.flatMap(buildResponse ->
downloadWithBuildResponse(outputDirectory, downloadStatusHandler, version, buildResponse)
.map(path -> new VersionBuildFile(version, build, path))
);
}

private Mono<BuildResponse> getBuild(String project, String version, int build) {

return sharedFetch.fetch(
uriBuilder.resolve("/v3/projects/{project}/versions/{version}/builds/{build}",
project, version, build
Expand All @@ -122,10 +133,6 @@ public Mono<VersionBuildFile> download(String project,
FailedRequestException::isNotFound,
throwable -> new InvalidParameterException(
String.format("Requested version %s, build %d is not available", version, build))
)
.flatMap(buildResponse ->
downloadWithBuildResponse(outputDirectory, downloadStatusHandler, version, buildResponse)
.map(path -> new VersionBuildFile(version, build, path))
);
}

Expand All @@ -142,26 +149,37 @@ private Mono<ProjectResponse> getProjectVersions(String project) {
);
}

private Mono<VersionBuild> extractVersionBuild(String project, ProjectResponse projectResponse) {
@RequiredArgsConstructor
private static class VersionAndBuildResponse {
final VersionResponse versionResponse;
final BuildResponse buildResponse;
}

private Mono<VersionBuild> extractLatestVersionBuild(String project, RequestedChannel requestedChannel, ProjectResponse projectResponse) {
if (projectResponse.getVersions() == null ||
projectResponse.getVersions().isEmpty()) {
log.warn("No versions found for project={}", project);
return Mono.error(() -> new InvalidParameterException("No versions found for project"));
}

final VersionResponse versionResponse = projectResponse.getVersions().get(0);
final Version version = versionResponse.getVersion();
if (versionResponse.getBuilds() == null ||
versionResponse.getBuilds().isEmpty()) {
log.warn("No builds found for project={} version={}", project, version.getId());
return Mono.error(() -> new InvalidParameterException(
String.format("No builds found for project version %s", version.getId()))
);
}
return Flux.fromIterable(projectResponse.getVersions())
.filter(versionResponse -> versionResponse.getBuilds() != null && !versionResponse.getBuilds().isEmpty())
.concatMap(versionResponse ->
getBuild(project, versionResponse.getVersion().getId(), versionResponse.getBuilds().get(0))
.map(buildResponse -> new VersionAndBuildResponse(versionResponse, buildResponse))
)
.takeUntil(vAndB -> acceptableChannel(vAndB.buildResponse.getChannel(), requestedChannel))
.last()
.map(vAndB -> new VersionBuild(vAndB.versionResponse.getVersion().getId(), vAndB.buildResponse.getId()));
}

return Mono.just(
new VersionBuild(version.getId(), versionResponse.getBuilds().get(0))
);
private boolean acceptableChannel(Channel channel, RequestedChannel requestedChannel) {
for (final Channel mapped : requestedChannel.getMappedTo()) {
if (mapped.equals(channel)) {
return true;
}
}
return false;
}

private Mono<Path> downloadWithBuildResponse(Path outputDirectory, FileDownloadStatusHandler downloadStatusHandler,
Expand Down
17 changes: 15 additions & 2 deletions src/main/java/me/itzg/helpers/paper/RequestedChannel.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
package me.itzg.helpers.paper;

import lombok.Getter;
import lombok.ToString;
import me.itzg.helpers.paper.model.Channel;

@ToString
@Getter
public enum RequestedChannel {
DEFAULT,
EXPERIMENTAL
DEFAULT(Channel.STABLE, Channel.RECOMMENDED),
EXPERIMENTAL(Channel.ALPHA, Channel.BETA),
ALPHA(Channel.ALPHA);

private final Channel[] mappedTo;

RequestedChannel(Channel... mappedTo) {
this.mappedTo = mappedTo;
}
}
52 changes: 41 additions & 11 deletions src/test/java/me/itzg/helpers/paper/PaperDownloadsClientTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,36 +7,66 @@
import com.github.tomakehurst.wiremock.junit5.WireMockTest;
import java.net.URI;
import java.nio.file.Path;
import java.util.stream.Stream;
import me.itzg.helpers.http.FileDownloadStatusHandler;
import me.itzg.helpers.http.SharedFetch.Options;
import me.itzg.helpers.paper.PaperDownloadsClient.VersionBuild;
import me.itzg.helpers.paper.PaperDownloadsClient.VersionBuildFile;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.Mockito;

@WireMockTest
class PaperDownloadsClientTest {

@Test
void latestVersionBuild(WireMockRuntimeInfo wmInfo) {
public static Stream<Arguments> latestVersionBuild_args() {
return Stream.of(
Arguments.arguments(RequestedChannel.DEFAULT, "1.21.8", 60),
Arguments.arguments(RequestedChannel.EXPERIMENTAL, "1.21.9", 49)
);
}

@ParameterizedTest
@MethodSource("latestVersionBuild_args")
void latestVersionBuild(RequestedChannel requestedChannel, String expectedVersion, int expectedBuild, WireMockRuntimeInfo wmInfo) {
//TODO use urlPathTemplate with Wiremock 3.x
stubFor(get(urlPathEqualTo("/v3/projects/paper/versions"))
.willReturn(aResponse()
.withHeader("Content-Type", "application/json")
.withBodyFile("paper/v3/response_paper_project.json")
.withBodyFile("paper/v3/projects_paper_versions_with_alphas.json")
)
);
stubFor(get(urlPathEqualTo("/v3/projects/paper/versions/1.21.9/builds/49"))
.willReturn(aResponse()
.withHeader("Content-Type", "application/json")
.withBodyFile("paper/v3/projects_paper_1_21_9_builds_49.json")
)
);
stubFor(get(urlPathEqualTo("/v3/projects/paper/versions/1.21.9-rc1/builds/36"))
.willReturn(aResponse()
.withHeader("Content-Type", "application/json")
.withBodyFile("paper/v3/projects_paper_1_21_9-rc1_builds_36.json")
)
);
stubFor(get(urlPathEqualTo("/v3/projects/paper/versions/1.21.8/builds/60"))
.willReturn(aResponse()
.withHeader("Content-Type", "application/json")
.withBodyFile("paper/v3/projects_paper_1_21_8_builds_60.json")
)
);

try (PaperDownloadsClient client = new PaperDownloadsClient(wmInfo.getHttpBaseUrl(),
Options.builder().build()
)) {
final VersionBuild result = client.getLatestVersionBuild("paper")
final VersionBuild result = client.getLatestVersionBuild("paper", requestedChannel)
.block();

assertThat(result).isNotNull();
assertThat(result.getVersion()).isEqualTo("1.21.6");
assertThat(result.getBuild()).isEqualTo(46);
assertThat(result.getVersion()).isEqualTo(expectedVersion);
assertThat(result.getBuild()).isEqualTo(expectedBuild);
}

}
Expand All @@ -46,7 +76,7 @@ void latestBuild(WireMockRuntimeInfo wmInfo) {
stubFor(get(urlPathEqualTo("/v3/projects/paper/versions/1.21.6/builds/latest"))
.willReturn(aResponse()
.withHeader("Content-Type", "application/json")
.withBodyFile("paper/v3/response_paper_build_response.json")
.withBodyFile("paper/v3/projects_paper_1_21_6_builds_46.json")
)
);

Expand All @@ -66,7 +96,7 @@ void downloadsSpecific(WireMockRuntimeInfo wmInfo, @TempDir Path tempDir) {
stubFor(get(urlPathEqualTo("/v3/projects/paper/versions/1.21.6/builds/46"))
.willReturn(aResponse()
.withHeader("Content-Type", "application/json")
.withBodyFile("paper/v3/response_paper_build_response.json")
.withBodyFile("paper/v3/projects_paper_1_21_6_builds_46.json")
)
);

Expand Down Expand Up @@ -112,7 +142,7 @@ void downloadsLatest(WireMockRuntimeInfo wmInfo, @TempDir Path tempDir) {
stubFor(get(urlPathEqualTo("/v3/projects/paper/versions/1.21.6/builds/46"))
.willReturn(aResponse()
.withHeader("Content-Type", "application/json")
.withBodyFile("paper/v3/response_paper_build_response.json")
.withBodyFile("paper/v3/projects_paper_1_21_6_builds_46.json")
)
);
stubFor(get(urlPathEqualTo("/v1/objects/bfca155b4a6b45644bfc1766f4e02a83c736e45fcc060e8788c71d6e7b3d56f6/paper-1.21.6-46.jar"))
Expand All @@ -129,7 +159,7 @@ void downloadsLatest(WireMockRuntimeInfo wmInfo, @TempDir Path tempDir) {
.filesViaUrl(URI.create(wmInfo.getHttpBaseUrl()))
.build()
)) {
final VersionBuildFile result = client.downloadLatest("paper", tempDir, statusHandler)
final VersionBuildFile result = client.downloadLatest("paper", RequestedChannel.DEFAULT, tempDir, statusHandler)
.block();

assertThat(result).isNotNull();
Expand All @@ -151,7 +181,7 @@ void downloadsLatestBuild(WireMockRuntimeInfo wmInfo, @TempDir Path tempDir) {
stubFor(get(urlPathEqualTo("/v3/projects/paper/versions/1.21.6/builds/latest"))
.willReturn(aResponse()
.withHeader("Content-Type", "application/json")
.withBodyFile("paper/v3/response_paper_build_response.json")
.withBodyFile("paper/v3/projects_paper_1_21_6_builds_46.json")
)
);
stubFor(get(urlPathEqualTo("/v1/objects/bfca155b4a6b45644bfc1766f4e02a83c736e45fcc060e8788c71d6e7b3d56f6/paper-1.21.6-46.jar"))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"id": 60,
"time": "2025-09-06T21:50:11.982Z",
"channel": "STABLE",
"commits": [
{
"sha": "29c8822d90899c89d2689338e81a98f690bcba12",
"time": "2025-09-06T21:38:29Z",
"message": "Remove no longer needed MC-210802 fix (#13059)\n\n"
}
],
"downloads": {
"server:default": {
"name": "paper-1.21.8-60.jar",
"checksums": {
"sha256": "8de7c52c3b02403503d16fac58003f1efef7dd7a0256786843927fa92ee57f1e"
},
"size": 52811717,
"url": "https://fill-data.papermc.io/v1/objects/8de7c52c3b02403503d16fac58003f1efef7dd7a0256786843927fa92ee57f1e/paper-1.21.8-60.jar"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"id": 36,
"time": "2025-09-30T13:38:40.064Z",
"channel": "ALPHA",
"commits": [
{
"sha": "5b6165c48d64b4daa44ac5f498bc11ee928e4637",
"time": "2025-09-30T13:26:15Z",
"message": "Add spawnreason for building copper golems (#13112)\n\n"
}
],
"downloads": {
"server:default": {
"name": "paper-1.21.9-rc1-36.jar",
"checksums": {
"sha256": "f737c4ce0afd8ca897c5330188634859148419c6c2d2e172c65f581c47430ab1"
},
"size": 52515036,
"url": "https://fill-data.papermc.io/v1/objects/f737c4ce0afd8ca897c5330188634859148419c6c2d2e172c65f581c47430ab1/paper-1.21.9-rc1-36.jar"
}
}
}
Loading