From 92f8a59536c1c141e59e6a499c44d79ca1d5b9dd Mon Sep 17 00:00:00 2001 From: Geoff Bourne Date: Tue, 13 Jun 2023 08:32:34 -0500 Subject: [PATCH] Added install-paper subcommand --- dev/modrinth.http | 13 +- .../java/me/itzg/helpers/McImageHelper.java | 2 + .../java/me/itzg/helpers/files/Manifests.java | 10 +- .../itzg/helpers/files/ResultsFileWriter.java | 5 + .../helpers/paper/InstallPaperCommand.java | 226 ++++++++++++++++++ .../helpers/paper/PaperDownloadsClient.java | 101 ++++++++ .../me/itzg/helpers/paper/PaperManifest.java | 23 ++ .../itzg/helpers/paper/model/BuildInfo.java | 17 ++ .../itzg/helpers/paper/model/ProjectInfo.java | 12 + .../itzg/helpers/paper/model/VersionInfo.java | 12 + .../me/itzg/helpers/quilt/QuiltInstaller.java | 21 +- 11 files changed, 419 insertions(+), 23 deletions(-) create mode 100644 src/main/java/me/itzg/helpers/paper/InstallPaperCommand.java create mode 100644 src/main/java/me/itzg/helpers/paper/PaperDownloadsClient.java create mode 100644 src/main/java/me/itzg/helpers/paper/PaperManifest.java create mode 100644 src/main/java/me/itzg/helpers/paper/model/BuildInfo.java create mode 100644 src/main/java/me/itzg/helpers/paper/model/ProjectInfo.java create mode 100644 src/main/java/me/itzg/helpers/paper/model/VersionInfo.java diff --git a/dev/modrinth.http b/dev/modrinth.http index 83f3118c..2701ce37 100644 --- a/dev/modrinth.http +++ b/dev/modrinth.http @@ -1,16 +1,15 @@ ### GET https://api.modrinth.com/v2/project/cobblemon-fabric +### +GET https://api.modrinth.com/v2/projects?ids=["terra"] + +### +GET https://api.modrinth.com/v2/projects?ids=["simple-voice-chat"] + ### GET https://api.modrinth.com/v2/project/cobblemon-fabric/version?loaders=fabric&game_versions=1.19.2 ### GET https://api.modrinth.com/v2/project/cobblemon-fabric/version -### -GET https://api.modrinth.com/v2/project/ids=[ - "qVAfruPH", - "3yD7gvyr", - "kQU5UkIm", - "nvrqJg44" - ] \ No newline at end of file diff --git a/src/main/java/me/itzg/helpers/McImageHelper.java b/src/main/java/me/itzg/helpers/McImageHelper.java index 13f6c4de..9e845827 100644 --- a/src/main/java/me/itzg/helpers/McImageHelper.java +++ b/src/main/java/me/itzg/helpers/McImageHelper.java @@ -22,6 +22,7 @@ import me.itzg.helpers.modrinth.InstallModrinthModpackCommand; import me.itzg.helpers.modrinth.ModrinthCommand; import me.itzg.helpers.mvn.MavenDownloadCommand; +import me.itzg.helpers.paper.InstallPaperCommand; import me.itzg.helpers.patch.PatchCommand; import me.itzg.helpers.quilt.InstallQuiltCommand; import me.itzg.helpers.singles.Asciify; @@ -56,6 +57,7 @@ InstallFabricLoaderCommand.class, InstallForgeCommand.class, InstallModrinthModpackCommand.class, + InstallPaperCommand.class, InstallQuiltCommand.class, InterpolateCommand.class, JavaReleaseCommand.class, diff --git a/src/main/java/me/itzg/helpers/files/Manifests.java b/src/main/java/me/itzg/helpers/files/Manifests.java index d83bcd51..77c69cb9 100644 --- a/src/main/java/me/itzg/helpers/files/Manifests.java +++ b/src/main/java/me/itzg/helpers/files/Manifests.java @@ -1,8 +1,5 @@ package me.itzg.helpers.files; -import me.itzg.helpers.json.ObjectMappers; -import org.slf4j.Logger; - import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -11,6 +8,8 @@ import java.util.List; import java.util.function.Consumer; import java.util.stream.Collectors; +import me.itzg.helpers.json.ObjectMappers; +import org.slf4j.Logger; public class Manifests { @@ -34,8 +33,9 @@ public static void cleanup(Path baseDir, Collection oldFiles, Collection filesToRemove.removeAll(currentFiles); } for (final String fileToRemove : filesToRemove) { - removeListener.accept(fileToRemove); - Files.deleteIfExists(baseDir.resolve(fileToRemove)); + if (Files.deleteIfExists(baseDir.resolve(fileToRemove))) { + removeListener.accept(fileToRemove); + } } } diff --git a/src/main/java/me/itzg/helpers/files/ResultsFileWriter.java b/src/main/java/me/itzg/helpers/files/ResultsFileWriter.java index 14c3824f..29615e00 100644 --- a/src/main/java/me/itzg/helpers/files/ResultsFileWriter.java +++ b/src/main/java/me/itzg/helpers/files/ResultsFileWriter.java @@ -43,4 +43,9 @@ public ResultsFileWriter write(String field, String value) throws IOException { public void close() throws IOException { writer.close(); } + + @SuppressWarnings("UnusedReturnValue") + public ResultsFileWriter writeServer(Path serverJar) throws IOException { + return write("SERVER", serverJar.toString()); + } } diff --git a/src/main/java/me/itzg/helpers/paper/InstallPaperCommand.java b/src/main/java/me/itzg/helpers/paper/InstallPaperCommand.java new file mode 100644 index 00000000..8b615209 --- /dev/null +++ b/src/main/java/me/itzg/helpers/paper/InstallPaperCommand.java @@ -0,0 +1,226 @@ +package me.itzg.helpers.paper; + +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.FileDownloadStatus; +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-paper", description = "Installs selected PaperMC") +@Slf4j +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") + URI downloadUrl; + + @ArgGroup(exclusive = false) + Coordinates coordinates = new Coordinates(); + + static class Coordinates { + + @Spec + CommandLine.Model.CommandSpec spec; + + @Option(names = "--project", defaultValue = "paper") + String project; + + 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") + Integer build; + } + } + + @Option(names = {"--output-directory", "-o"}, defaultValue = ".") + Path outputDirectory; + + @Option(names = "--base-url", defaultValue = "https://api.papermc.io") + 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 PaperManifest newManifest; + final Path serverJar; + } + + @Override + public Integer call() throws Exception { + final PaperManifest oldManifest = loadOldManifest(); + + final Result result; + try (PaperDownloadsClient client = new PaperDownloadsClient(baseUrl, sharedFetchArgs.options())) { + if (inputs.downloadUrl != null) { + result = downloadCustom(inputs.downloadUrl); + } + else { + result = useCoordinates(client, inputs.coordinates.project, + 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, PaperManifest.ID, result.newManifest); + + return ExitCode.OK; + } + + private Result useCoordinates(PaperDownloadsClient client, String project, String version, Integer build) { + return resolveVersion(client, project, version) + .flatMap(v -> resolveBuild(client, project, v, build) + .flatMap(b -> + client.download(project, v, b, outputDirectory, InstallPaperCommand::logDownloadingStatus) + .map(serverJar -> + Result.builder() + .newManifest( + PaperManifest.builder() + .project(project) + .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(InstallPaperCommand::logDownloadingStatus) + .assemble() + .map(serverJar -> + Result.builder() + .serverJar(serverJar) + .newManifest( + PaperManifest.builder() + .customDownloadUrl(downloadUrl) + .files(Collections.singleton(Manifests.relativize(outputDirectory, serverJar))) + .build() + ) + .build() + ) + .block(); + } + } + + private PaperManifest loadOldManifest() { + try { + return Manifests.load(outputDirectory, PaperManifest.ID, PaperManifest.class); + } catch (ManifestException e) { + if (e.getCause() instanceof InvalidTypeIdException) { + final MultiCopyManifest mcopyManifest = Manifests.load(outputDirectory, PaperManifest.ID, + MultiCopyManifest.class + ); + if (mcopyManifest == null) { + throw new GenericException("Failed to load manifest as MultiCopyManifest"); + } + return PaperManifest.builder() + .files(mcopyManifest.getFiles()) + .build(); + } + throw new GenericException("Failed to load manifest", e); + } + } + + private Mono resolveVersion(PaperDownloadsClient client, String project, String version) { + if (version.equals("latest")) { + return client.getLatestProjectVersion(project); + } + return client.hasVersion(project, version) + .flatMap(exists -> exists ? Mono.just(version) : Mono.error(() -> new InvalidParameterException( + String.format("Version %s does not exist for the project %s", + version, project + )))); + } + + private Mono resolveBuild(PaperDownloadsClient client, String project, String version, Integer build) { + if (build == null) { + return client.getLatestBuild(project, version); + } + else { + return client.hasBuild(project, version, build) + .flatMap(exists -> exists ? Mono.just(build) : Mono.error(() -> + new GenericException(String.format("Build %d does not exist for project %s version %s", + build, project, version + ) + ) + )); + } + } +} diff --git a/src/main/java/me/itzg/helpers/paper/PaperDownloadsClient.java b/src/main/java/me/itzg/helpers/paper/PaperDownloadsClient.java new file mode 100644 index 00000000..d8d09cf0 --- /dev/null +++ b/src/main/java/me/itzg/helpers/paper/PaperDownloadsClient.java @@ -0,0 +1,101 @@ +package me.itzg.helpers.paper; + +import java.nio.file.Path; +import me.itzg.helpers.errors.GenericException; +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.paper.model.BuildInfo; +import me.itzg.helpers.paper.model.BuildInfo.DownloadInfo; +import me.itzg.helpers.paper.model.ProjectInfo; +import me.itzg.helpers.paper.model.VersionInfo; +import reactor.core.publisher.Mono; + +public class PaperDownloadsClient implements AutoCloseable{ + + private final UriBuilder uriBuilder; + private final SharedFetch sharedFetch; + + public PaperDownloadsClient(String baseUrl, SharedFetch.Options options) { + uriBuilder = UriBuilder.withBaseUrl(baseUrl); + sharedFetch = Fetch.sharedFetch("install-paper", options); + } + + public Mono getLatestProjectVersion(String project) { + return sharedFetch.fetch( + uriBuilder.resolve("/v2/projects/{project}", project) + ) + .toObject(ProjectInfo.class) + .assemble() + .map(projectInfo -> projectInfo.getVersions().get(projectInfo.getVersions().size() - 1)); + } + + public Mono hasVersion(String project, String version) { + return sharedFetch.fetch( + uriBuilder.resolve("/v2/projects/{project}", project) + ) + .toObject(ProjectInfo.class) + .assemble() + .map(projectInfo -> projectInfo.getVersions().contains(version)); + } + + public Mono getLatestBuild(String project, String version) { + return sharedFetch.fetch( + uriBuilder.resolve("/v2/projects/{project}/versions/{version}", + project, version + ) + ) + .toObject(VersionInfo.class) + .assemble() + .map( + versionInfo -> versionInfo.getBuilds().get(versionInfo.getBuilds().size()-1) + ); + } + + public Mono hasBuild(String project, String version, int build) { + return sharedFetch.fetch( + uriBuilder.resolve("/v2/projects/{project}/versions/{version}", + project, version + ) + ) + .toObject(VersionInfo.class) + .assemble() + .map(versionInfo -> versionInfo.getBuilds().contains(build)); + } + + public Mono download(String project, String version, int build, Path outputDirectory, + FileDownloadStatusHandler downloadStatusHandler + ) { + return sharedFetch.fetch( + uriBuilder.resolve("/v2/projects/{project}/versions/{version}/builds/{build}", + project, version, build + ) + ) + .toObject(BuildInfo.class) + .assemble() + .flatMap(buildInfo -> { + final DownloadInfo downloadInfo = buildInfo.getDownloads().get("application"); + if (downloadInfo == null) { + return Mono.error(new GenericException( + String.format("Unable to locate application in download info for project=%s version=%s build=%d", + project, version, build + ))); + } + return sharedFetch.fetch( + uriBuilder.resolve("/v2/projects/{project}/versions/{version}/builds/{build}/downloads/{download}", + project, version, build, downloadInfo.getName() + ) + ) + .toFile(outputDirectory.resolve(downloadInfo.getName())) + .skipUpToDate(true) + .handleStatus(downloadStatusHandler) + .assemble(); + }); + } + + @Override + public void close() { + sharedFetch.close(); + } +} diff --git a/src/main/java/me/itzg/helpers/paper/PaperManifest.java b/src/main/java/me/itzg/helpers/paper/PaperManifest.java new file mode 100644 index 00000000..708959b9 --- /dev/null +++ b/src/main/java/me/itzg/helpers/paper/PaperManifest.java @@ -0,0 +1,23 @@ +package me.itzg.helpers.paper; + +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 PaperManifest extends BaseManifest { + + public static final String ID = "papermc"; + + String minecraftVersion; + + String project; + + int build; + + URI customDownloadUrl; +} diff --git a/src/main/java/me/itzg/helpers/paper/model/BuildInfo.java b/src/main/java/me/itzg/helpers/paper/model/BuildInfo.java new file mode 100644 index 00000000..f90034e0 --- /dev/null +++ b/src/main/java/me/itzg/helpers/paper/model/BuildInfo.java @@ -0,0 +1,17 @@ +package me.itzg.helpers.paper.model; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import java.util.Map; +import lombok.Data; + +@Data +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public class BuildInfo { + Map downloads; + + @Data + public static class DownloadInfo { + String name; + } +} diff --git a/src/main/java/me/itzg/helpers/paper/model/ProjectInfo.java b/src/main/java/me/itzg/helpers/paper/model/ProjectInfo.java new file mode 100644 index 00000000..80f1e2cd --- /dev/null +++ b/src/main/java/me/itzg/helpers/paper/model/ProjectInfo.java @@ -0,0 +1,12 @@ +package me.itzg.helpers.paper.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/paper/model/VersionInfo.java b/src/main/java/me/itzg/helpers/paper/model/VersionInfo.java new file mode 100644 index 00000000..ee818ddc --- /dev/null +++ b/src/main/java/me/itzg/helpers/paper/model/VersionInfo.java @@ -0,0 +1,12 @@ +package me.itzg.helpers.paper.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 { + List builds; +} diff --git a/src/main/java/me/itzg/helpers/quilt/QuiltInstaller.java b/src/main/java/me/itzg/helpers/quilt/QuiltInstaller.java index 9ce70aff..697b78e2 100644 --- a/src/main/java/me/itzg/helpers/quilt/QuiltInstaller.java +++ b/src/main/java/me/itzg/helpers/quilt/QuiltInstaller.java @@ -1,15 +1,5 @@ package me.itzg.helpers.quilt; -import lombok.Setter; -import lombok.extern.slf4j.Slf4j; -import me.itzg.helpers.errors.GenericException; -import me.itzg.helpers.files.Manifests; -import me.itzg.helpers.files.ResultsFileWriter; -import me.itzg.helpers.http.SharedFetch; -import me.itzg.helpers.mvn.MavenRepoApi; -import reactor.core.publisher.Mono; -import reactor.core.scheduler.Schedulers; - import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; @@ -19,6 +9,15 @@ import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.util.Collections; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import me.itzg.helpers.errors.GenericException; +import me.itzg.helpers.files.Manifests; +import me.itzg.helpers.files.ResultsFileWriter; +import me.itzg.helpers.http.SharedFetch; +import me.itzg.helpers.mvn.MavenRepoApi; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; @Slf4j public class QuiltInstaller { @@ -144,7 +143,7 @@ private QuiltManifest runInstaller(Path installerPath, String resolvedLoaderVers if (resultsFile != null) { try (ResultsFileWriter resultsFileWriter = new ResultsFileWriter(resultsFile, false)) { - resultsFileWriter.write("SERVER", resolvedLauncher.toString()); + resultsFileWriter.writeServer(resolvedLauncher); resultsFileWriter.write("FAMILY", "FABRIC"); } catch (IOException e) { throw new GenericException("Failed to write results file", e);