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
2 changes: 2 additions & 0 deletions src/main/java/me/itzg/helpers/McImageHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import me.itzg.helpers.mvn.MavenDownloadCommand;
import me.itzg.helpers.paper.InstallPaperCommand;
import me.itzg.helpers.patch.PatchCommand;
import me.itzg.helpers.purpur.InstallPurpurCommand;
import me.itzg.helpers.quilt.InstallQuiltCommand;
import me.itzg.helpers.singles.Asciify;
import me.itzg.helpers.singles.HashCommand;
Expand Down Expand Up @@ -58,6 +59,7 @@
InstallForgeCommand.class,
InstallModrinthModpackCommand.class,
InstallPaperCommand.class,
InstallPurpurCommand.class,
InstallQuiltCommand.class,
InterpolateCommand.class,
JavaReleaseCommand.class,
Expand Down
17 changes: 17 additions & 0 deletions src/main/java/me/itzg/helpers/http/Fetch.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.net.URI;
import me.itzg.helpers.http.SharedFetch.Options;
import org.slf4j.Logger;

public class Fetch {

Expand All @@ -22,4 +23,20 @@ public static SharedFetch sharedFetch(String forCommand, Options options) {

private Fetch() {
}

public static FileDownloadStatusHandler loggingDownloadStatusHandler(Logger log) {
return (status, uri, file) -> {
switch (status) {
case DOWNLOADING:
log.debug("Downloading {}", file);
break;
case DOWNLOADED:
log.info("Downloaded {}", file);
break;
case SKIP_FILE_UP_TO_DATE:
log.info("The file {} is already up to date", file);
break;
}
};
}
}
21 changes: 3 additions & 18 deletions src/main/java/me/itzg/helpers/paper/InstallPaperCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
import me.itzg.helpers.files.Manifests;
import me.itzg.helpers.files.ResultsFileWriter;
import me.itzg.helpers.http.Fetch;
import me.itzg.helpers.http.FileDownloadStatus;
import me.itzg.helpers.http.SharedFetch;
import me.itzg.helpers.http.SharedFetchArgs;
import me.itzg.helpers.sync.MultiCopyManifest;
Expand All @@ -35,20 +34,6 @@ public class InstallPaperCommand implements Callable<Integer> {
@ArgGroup
Inputs inputs = new Inputs();

private static void logDownloadingStatus(FileDownloadStatus status, URI uri, Path file) {
switch (status) {
case DOWNLOADING:
log.debug("Downloading {}", file);
break;
case DOWNLOADED:
log.info("Downloaded {}", file);
break;
case SKIP_FILE_UP_TO_DATE:
log.info("The file {} is already up to date", file);
break;
}
}

static class Inputs {

@Option(names = "--url", description = "Use a custom URL location")
Expand All @@ -65,7 +50,7 @@ static class Coordinates {
@Option(names = "--project", defaultValue = "paper")
String project;

private static final Pattern ALLOWED_VERSIONS = Pattern.compile("latest|\\d+\\.\\d+\\.\\d+",
private static final Pattern ALLOWED_VERSIONS = Pattern.compile("latest|\\d+\\.\\d+(\\.\\d+)?",
Pattern.CASE_INSENSITIVE
);

Expand Down Expand Up @@ -139,7 +124,7 @@ private Result useCoordinates(PaperDownloadsClient client, String project, Strin
.flatMap(b -> {
log.info("Resolved {} to version {} build {}", project, v, b);

return client.download(project, v, b, outputDirectory, InstallPaperCommand::logDownloadingStatus)
return client.download(project, v, b, outputDirectory, Fetch.loggingDownloadStatusHandler(log))
.map(serverJar ->
Result.builder()
.newManifest(
Expand All @@ -165,7 +150,7 @@ private Result downloadCustom(URI downloadUrl) {
.fetch(downloadUrl)
.toDirectory(outputDirectory)
.skipUpToDate(true)
.handleStatus(InstallPaperCommand::logDownloadingStatus)
.handleStatus(Fetch.loggingDownloadStatusHandler(log))
.assemble()
.map(serverJar ->
Result.builder()
Expand Down
214 changes: 214 additions & 0 deletions src/main/java/me/itzg/helpers/purpur/InstallPurpurCommand.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
package me.itzg.helpers.purpur;

import com.fasterxml.jackson.databind.exc.InvalidTypeIdException;
import java.net.URI;
import java.nio.file.Path;
import java.util.Collections;
import java.util.concurrent.Callable;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import lombok.Builder;
import lombok.extern.slf4j.Slf4j;
import me.itzg.helpers.errors.GenericException;
import me.itzg.helpers.errors.InvalidParameterException;
import me.itzg.helpers.files.ManifestException;
import me.itzg.helpers.files.Manifests;
import me.itzg.helpers.files.ResultsFileWriter;
import me.itzg.helpers.http.Fetch;
import me.itzg.helpers.http.SharedFetch;
import me.itzg.helpers.http.SharedFetchArgs;
import me.itzg.helpers.sync.MultiCopyManifest;
import picocli.CommandLine;
import picocli.CommandLine.ArgGroup;
import picocli.CommandLine.Command;
import picocli.CommandLine.ExitCode;
import picocli.CommandLine.Option;
import picocli.CommandLine.ParameterException;
import picocli.CommandLine.Spec;
import reactor.core.publisher.Mono;

@Command(name = "install-purpur", description = "Downloads latest or selected version of Purpur")
@Slf4j
public class InstallPurpurCommand implements Callable<Integer> {

@ArgGroup
Inputs inputs = new Inputs();

static class Inputs {

@Option(names = "--url", description = "Use a custom URL location")
URI downloadUrl;

@ArgGroup(exclusive = false)
Coordinates coordinates = new Coordinates();

static class Coordinates {

@Spec
CommandLine.Model.CommandSpec spec;

private static final Pattern ALLOWED_VERSIONS = Pattern.compile("latest|\\d+\\.\\d+(\\.\\d+)?",
Pattern.CASE_INSENSITIVE
);

@Option(names = "--version", defaultValue = "latest", description = "May be 'latest' or specific version")
public void setVersion(String version) {
final Matcher m = ALLOWED_VERSIONS.matcher(version);
if (!m.matches()) {
throw new ParameterException(spec.commandLine(), "Invalid value for minecraft version: " + version);
}
this.version = version.toLowerCase();

}

String version;

@Option(names = "--build")
public void setBuild(String build) {
if (build != null && build.equalsIgnoreCase("latest")) {
this.build = null;
}
else {
this.build = build;
}
}
String build;
}
}

@Option(names = {"--output-directory", "-o"}, defaultValue = ".")
Path outputDirectory;

@Option(names = "--base-url", defaultValue = "https://api.purpurmc.org")
String baseUrl;

@Option(names = "--results-file", description = ResultsFileWriter.OPTION_DESCRIPTION, paramLabel = "FILE")
Path resultsFile;

@ArgGroup
SharedFetchArgs sharedFetchArgs = new SharedFetchArgs();

@Builder
private static class Result {

final PurpurManifest newManifest;
final Path serverJar;
}

@Override
public Integer call() throws Exception {
final PurpurManifest oldManifest = loadOldManifest();

final Result result;
try (PurpurDownloadsClient client = new PurpurDownloadsClient(baseUrl, sharedFetchArgs.options())) {
if (inputs.downloadUrl != null) {
result = downloadCustom(inputs.downloadUrl);
}
else {
result = useCoordinates(client,
inputs.coordinates.version, inputs.coordinates.build
);
}
}

if (resultsFile != null) {
try (ResultsFileWriter results = new ResultsFileWriter(resultsFile)) {
results.writeServer(result.serverJar);
}
}

Manifests.cleanup(outputDirectory, oldManifest, result.newManifest, log);
Manifests.save(outputDirectory, PurpurManifest.ID, result.newManifest);

return ExitCode.OK;
}

private Result useCoordinates(PurpurDownloadsClient client, String version, String build) {
return resolveVersion(client, version)
.flatMap(v -> resolveBuild(client, v, build)
.flatMap(b -> {
log.info("Resolved Purpur version {} build {}", v, b);

return client.download(v, b, outputDirectory, Fetch.loggingDownloadStatusHandler(log))
.map(serverJar ->
Result.builder()
.newManifest(
PurpurManifest.builder()
.minecraftVersion(v)
.build(b)
.files(Collections.singleton(Manifests.relativize(outputDirectory, serverJar)))
.build()
)
.serverJar(serverJar)
.build()
);
}
)
)
.block();
}

private Result downloadCustom(URI downloadUrl) {
try (SharedFetch sharedFetch = Fetch.sharedFetch("install-paper", sharedFetchArgs.options())) {
return sharedFetch
.fetch(downloadUrl)
.toDirectory(outputDirectory)
.skipUpToDate(true)
.handleStatus(Fetch.loggingDownloadStatusHandler(log))
.assemble()
.map(serverJar ->
Result.builder()
.serverJar(serverJar)
.newManifest(
PurpurManifest.builder()
.customDownloadUrl(downloadUrl)
.files(Collections.singleton(Manifests.relativize(outputDirectory, serverJar)))
.build()
)
.build()
)
.block();
}
}

private PurpurManifest loadOldManifest() {
try {
return Manifests.load(outputDirectory, PurpurManifest.ID, PurpurManifest.class);
} catch (ManifestException e) {
if (e.getCause() instanceof InvalidTypeIdException) {
final MultiCopyManifest mcopyManifest = Manifests.load(outputDirectory, PurpurManifest.ID,
MultiCopyManifest.class
);
if (mcopyManifest == null) {
throw new GenericException("Failed to load manifest as MultiCopyManifest");
}
return PurpurManifest.builder()
.files(mcopyManifest.getFiles())
.build();
}
throw new GenericException("Failed to load manifest", e);
}
}

private Mono<String> resolveVersion(PurpurDownloadsClient client, String version) {
if (version.equals("latest")) {
return client.getLatestVersion();
}
return client.hasVersion(version)
.flatMap(exists -> exists ? Mono.just(version) : Mono.error(() -> new InvalidParameterException(
String.format("Version %s does not exist", version)
)));
}

private Mono<String > resolveBuild(PurpurDownloadsClient client, String version, String build) {
if (build == null) {
return client.getLatestBuild(version);
}
else {
return client.hasBuild(version, build)
.flatMap(exists -> exists ? Mono.just(build) : Mono.error(() ->
new InvalidParameterException(String.format("Build %s does not exist for version %s", build, version))
));
}
}
}
78 changes: 78 additions & 0 deletions src/main/java/me/itzg/helpers/purpur/PurpurDownloadsClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package me.itzg.helpers.purpur;

import java.nio.file.Path;
import me.itzg.helpers.http.Fetch;
import me.itzg.helpers.http.FileDownloadStatusHandler;
import me.itzg.helpers.http.SharedFetch;
import me.itzg.helpers.http.UriBuilder;
import me.itzg.helpers.purpur.model.ProjectInfo;
import me.itzg.helpers.purpur.model.VersionInfo;
import reactor.core.publisher.Mono;

public class PurpurDownloadsClient implements AutoCloseable{

private final UriBuilder uriBuilder;
private final SharedFetch sharedFetch;

public PurpurDownloadsClient(String baseUrl, SharedFetch.Options options) {
uriBuilder = UriBuilder.withBaseUrl(baseUrl);
sharedFetch = Fetch.sharedFetch("install-paper", options);
}

public Mono<String> getLatestVersion() {
return sharedFetch.fetch(
uriBuilder.resolve("/v2/purpur")
)
.toObject(ProjectInfo.class)
.assemble()
.map(projectInfo -> projectInfo.getVersions().get(projectInfo.getVersions().size() - 1));
}

public Mono<Boolean> hasVersion(String version) {
return sharedFetch.fetch(
uriBuilder.resolve("/v2/purpur")
)
.toObject(ProjectInfo.class)
.assemble()
.map(projectInfo -> projectInfo.getVersions().contains(version));
}

public Mono<String> getLatestBuild(String version) {
return sharedFetch.fetch(
uriBuilder.resolve("/v2/purpur/{version}", version)
)
.toObject(VersionInfo.class)
.assemble()
.map(
versionInfo -> versionInfo.getBuilds().getLatest()
);
}

public Mono<Boolean> hasBuild(String version, String build) {
return sharedFetch.fetch(
uriBuilder.resolve("/v2/purpur/{version}",
version
)
)
.toObject(VersionInfo.class)
.assemble()
.map(versionInfo -> versionInfo.getBuilds().getAll().contains(build));
}

public Mono<Path> download(String version, String build, Path outputDirectory,
FileDownloadStatusHandler downloadStatusHandler
) {
return sharedFetch.fetch(
uriBuilder.resolve("/v2/purpur/{version}/{build}/download", version, build)
)
.toDirectory(outputDirectory)
.handleStatus(downloadStatusHandler)
.skipUpToDate(true)
.assemble();
}

@Override
public void close() {
sharedFetch.close();
}
}
Loading