From adf27b95b1fb1d4a1327d36075ac85d10a1a5bdd Mon Sep 17 00:00:00 2001 From: Geoff Bourne Date: Tue, 13 Jun 2023 20:59:32 -0500 Subject: [PATCH 1/3] Added install-purpur subcommand --- .../java/me/itzg/helpers/McImageHelper.java | 2 + src/main/java/me/itzg/helpers/http/Fetch.java | 17 ++ .../helpers/paper/InstallPaperCommand.java | 21 +- .../helpers/purpur/InstallPurpurCommand.java | 206 ++++++++++++++++++ .../helpers/purpur/PurpurDownloadsClient.java | 78 +++++++ .../itzg/helpers/purpur/PurpurManifest.java | 21 ++ .../helpers/purpur/model/ProjectInfo.java | 12 + .../helpers/purpur/model/VersionInfo.java | 18 ++ 8 files changed, 357 insertions(+), 18 deletions(-) create mode 100644 src/main/java/me/itzg/helpers/purpur/InstallPurpurCommand.java create mode 100644 src/main/java/me/itzg/helpers/purpur/PurpurDownloadsClient.java create mode 100644 src/main/java/me/itzg/helpers/purpur/PurpurManifest.java create mode 100644 src/main/java/me/itzg/helpers/purpur/model/ProjectInfo.java create mode 100644 src/main/java/me/itzg/helpers/purpur/model/VersionInfo.java diff --git a/src/main/java/me/itzg/helpers/McImageHelper.java b/src/main/java/me/itzg/helpers/McImageHelper.java index 9e845827..f6ad1d09 100644 --- a/src/main/java/me/itzg/helpers/McImageHelper.java +++ b/src/main/java/me/itzg/helpers/McImageHelper.java @@ -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; @@ -58,6 +59,7 @@ InstallForgeCommand.class, InstallModrinthModpackCommand.class, InstallPaperCommand.class, + InstallPurpurCommand.class, InstallQuiltCommand.class, InterpolateCommand.class, JavaReleaseCommand.class, diff --git a/src/main/java/me/itzg/helpers/http/Fetch.java b/src/main/java/me/itzg/helpers/http/Fetch.java index c11e1fe3..aa4daf26 100644 --- a/src/main/java/me/itzg/helpers/http/Fetch.java +++ b/src/main/java/me/itzg/helpers/http/Fetch.java @@ -2,6 +2,7 @@ import java.net.URI; import me.itzg.helpers.http.SharedFetch.Options; +import org.slf4j.Logger; public class Fetch { @@ -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; + } + }; + } } diff --git a/src/main/java/me/itzg/helpers/paper/InstallPaperCommand.java b/src/main/java/me/itzg/helpers/paper/InstallPaperCommand.java index f4d26bd4..a9c93da9 100644 --- a/src/main/java/me/itzg/helpers/paper/InstallPaperCommand.java +++ b/src/main/java/me/itzg/helpers/paper/InstallPaperCommand.java @@ -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; @@ -35,20 +34,6 @@ public class InstallPaperCommand implements Callable { @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") @@ -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 ); @@ -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( @@ -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() diff --git a/src/main/java/me/itzg/helpers/purpur/InstallPurpurCommand.java b/src/main/java/me/itzg/helpers/purpur/InstallPurpurCommand.java new file mode 100644 index 00000000..ddf96e9c --- /dev/null +++ b/src/main/java/me/itzg/helpers/purpur/InstallPurpurCommand.java @@ -0,0 +1,206 @@ +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 { + + @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") + 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 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 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 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)) + )); + } + } +} diff --git a/src/main/java/me/itzg/helpers/purpur/PurpurDownloadsClient.java b/src/main/java/me/itzg/helpers/purpur/PurpurDownloadsClient.java new file mode 100644 index 00000000..1fd2082f --- /dev/null +++ b/src/main/java/me/itzg/helpers/purpur/PurpurDownloadsClient.java @@ -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 getLatestVersion() { + return sharedFetch.fetch( + uriBuilder.resolve("/v2/purpur") + ) + .toObject(ProjectInfo.class) + .assemble() + .map(projectInfo -> projectInfo.getVersions().get(projectInfo.getVersions().size() - 1)); + } + + public Mono hasVersion(String version) { + return sharedFetch.fetch( + uriBuilder.resolve("/v2/purpur") + ) + .toObject(ProjectInfo.class) + .assemble() + .map(projectInfo -> projectInfo.getVersions().contains(version)); + } + + public Mono getLatestBuild(String version) { + return sharedFetch.fetch( + uriBuilder.resolve("/v2/purpur/{version}", version) + ) + .toObject(VersionInfo.class) + .assemble() + .map( + versionInfo -> versionInfo.getBuilds().getLatest() + ); + } + + public Mono 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 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(); + } +} diff --git a/src/main/java/me/itzg/helpers/purpur/PurpurManifest.java b/src/main/java/me/itzg/helpers/purpur/PurpurManifest.java new file mode 100644 index 00000000..994b9205 --- /dev/null +++ b/src/main/java/me/itzg/helpers/purpur/PurpurManifest.java @@ -0,0 +1,21 @@ +package me.itzg.helpers.purpur; + +import java.net.URI; +import lombok.Getter; +import lombok.experimental.SuperBuilder; +import lombok.extern.jackson.Jacksonized; +import me.itzg.helpers.files.BaseManifest; + +@Getter +@SuperBuilder +@Jacksonized +public class PurpurManifest extends BaseManifest { + + public static final String ID = "purpur"; + + String minecraftVersion; + + String build; + + URI customDownloadUrl; +} diff --git a/src/main/java/me/itzg/helpers/purpur/model/ProjectInfo.java b/src/main/java/me/itzg/helpers/purpur/model/ProjectInfo.java new file mode 100644 index 00000000..ca5cc00d --- /dev/null +++ b/src/main/java/me/itzg/helpers/purpur/model/ProjectInfo.java @@ -0,0 +1,12 @@ +package me.itzg.helpers.purpur.model; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import java.util.List; +import lombok.Data; + +@Data +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public class ProjectInfo { + List versions; +} diff --git a/src/main/java/me/itzg/helpers/purpur/model/VersionInfo.java b/src/main/java/me/itzg/helpers/purpur/model/VersionInfo.java new file mode 100644 index 00000000..36ab05ab --- /dev/null +++ b/src/main/java/me/itzg/helpers/purpur/model/VersionInfo.java @@ -0,0 +1,18 @@ +package me.itzg.helpers.purpur.model; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import java.util.List; +import lombok.Data; + +@Data +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public class VersionInfo { + private Builds builds; + + @Data + public static class Builds { + private String latest; + private List all; + } +} From 4184a40fd510773b2b30aa0f55d0c871d8c53a6c Mon Sep 17 00:00:00 2001 From: Geoff Bourne Date: Tue, 13 Jun 2023 21:11:28 -0500 Subject: [PATCH 2/3] Support "latest" for build value --- .../java/me/itzg/helpers/purpur/InstallPurpurCommand.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/java/me/itzg/helpers/purpur/InstallPurpurCommand.java b/src/main/java/me/itzg/helpers/purpur/InstallPurpurCommand.java index ddf96e9c..dec36a11 100644 --- a/src/main/java/me/itzg/helpers/purpur/InstallPurpurCommand.java +++ b/src/main/java/me/itzg/helpers/purpur/InstallPurpurCommand.java @@ -64,6 +64,14 @@ public void setVersion(String version) { 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; } } From 733a567cdee6c8e0b1dd6aaa32e34349b257edbf Mon Sep 17 00:00:00 2001 From: Geoff Bourne Date: Tue, 13 Jun 2023 21:14:33 -0500 Subject: [PATCH 3/3] Log tweak --- src/main/java/me/itzg/helpers/purpur/InstallPurpurCommand.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/me/itzg/helpers/purpur/InstallPurpurCommand.java b/src/main/java/me/itzg/helpers/purpur/InstallPurpurCommand.java index dec36a11..430a4695 100644 --- a/src/main/java/me/itzg/helpers/purpur/InstallPurpurCommand.java +++ b/src/main/java/me/itzg/helpers/purpur/InstallPurpurCommand.java @@ -127,7 +127,7 @@ private Result useCoordinates(PurpurDownloadsClient client, String version, Stri return resolveVersion(client, version) .flatMap(v -> resolveBuild(client, v, build) .flatMap(b -> { - log.info("Resolved version {} build {}", v, b); + log.info("Resolved Purpur version {} build {}", v, b); return client.download(v, b, outputDirectory, Fetch.loggingDownloadStatusHandler(log)) .map(serverJar ->