diff options
-rw-r--r-- | README.md | 12 | ||||
-rw-r--r-- | build.gradle.kts | 15 | ||||
-rw-r--r-- | src/main/java/moe/nea/libautoupdate/CurrentVersion.java | 66 | ||||
-rw-r--r-- | src/main/java/moe/nea/libautoupdate/ExitHookInvoker.java | 12 | ||||
-rw-r--r-- | src/main/java/moe/nea/libautoupdate/GistSource.java | 3 | ||||
-rw-r--r-- | src/main/java/moe/nea/libautoupdate/GithubReleaseUpdateSource.java | 136 | ||||
-rw-r--r-- | src/main/java/moe/nea/libautoupdate/JsonUpdateSource.java | 23 | ||||
-rw-r--r-- | src/main/java/moe/nea/libautoupdate/Main.java | 9 | ||||
-rw-r--r-- | src/main/java/moe/nea/libautoupdate/PotentialUpdate.java | 9 | ||||
-rw-r--r-- | src/main/java/moe/nea/libautoupdate/UpdateData.java | 3 | ||||
-rw-r--r-- | src/main/java/moe/nea/libautoupdate/UpdateSource.java | 7 | ||||
-rw-r--r-- | src/main/java/moe/nea/libautoupdate/UpdateUtils.java | 26 | ||||
-rw-r--r-- | updater/src/main/java/moe/nea/libautoupdate/postexit/PostExitMain.java | 4 |
13 files changed, 280 insertions, 45 deletions
@@ -32,7 +32,7 @@ UpdateContext updateContext=new UpdateContext( ); ``` -You will have to specify an update source (currently either a `gistSource` or your own implementation), +You will have to specify an update source, an update target (the file to replace), the version that is currently being ran, and a string id to prevent files from being overwritten. @@ -42,7 +42,7 @@ application. ### Sources - - GistSource + - Gist Source Uses a gist with multiple (or just one) files called `<upstream>.json` in the format ```json5 @@ -53,6 +53,14 @@ Uses a gist with multiple (or just one) files called `<upstream>.json` in the fo } ``` + - GitHub Releases Source + +Uses a GitHub release to either source a pre-release or full-release JAR. This release source does not support hashes or +newer-than-latest local versions. The current version also has to be the tag name, unlike the gist source which can have +any version type. The GitHub release needs to have a tag that is also present in the jar itself, and there should only +be one JAR in each GitHub release. Subclasses of this source may provide custom logic for choosing the jar / version. + + ### Targets - DeleteAndSaveInSameFolder diff --git a/build.gradle.kts b/build.gradle.kts index d323f7a..7f078c6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,11 +1,13 @@ + plugins { `maven-publish` signing java + id("io.freefair.lombok") version "6.5.1" } group = "moe.nea" -version = "0.1.0" +version = "1.0.0" allprojects { apply(plugin = "java") @@ -32,11 +34,18 @@ project(":updater") { } dependencies { - compileOnly("org.projectlombok:lombok:1.18.24") - annotationProcessor("org.projectlombok:lombok:1.18.24") + @Suppress("VulnerableLibrariesLocal") + // We use this version of gson, because this is intended to be used for minecraft 1.8.9 (which bundles that gson version) implementation("com.google.code.gson:gson:2.2.4") } +java { + withJavadocJar() + withSourcesJar() +} +tasks.javadoc { + isFailOnError = false +} tasks.processResources { val updateJar = tasks.getByPath(":updater:jar") diff --git a/src/main/java/moe/nea/libautoupdate/CurrentVersion.java b/src/main/java/moe/nea/libautoupdate/CurrentVersion.java index 8c0837e..e6e4ff8 100644 --- a/src/main/java/moe/nea/libautoupdate/CurrentVersion.java +++ b/src/main/java/moe/nea/libautoupdate/CurrentVersion.java @@ -1,21 +1,73 @@ package moe.nea.libautoupdate; +import com.google.gson.JsonElement; +import com.google.gson.JsonPrimitive; + /** * Provider interface for getting the current version of this jar. */ public interface CurrentVersion { + /** - * @return the version number + * Create a {@link CurrentVersion} that compares only numbers. (Other version types will be assumed to be newer.) + * + * @param number the current version number */ - int getCurrentVersionNumber(); + static CurrentVersion of(int number) { + return new CurrentVersion() { + @Override + public String display() { + return String.valueOf(number); + } + + @Override + public boolean isOlderThan(JsonElement element) { + if (!element.isJsonPrimitive()) return true; + JsonPrimitive prim = element.getAsJsonPrimitive(); + if (!prim.isNumber()) return true; + return prim.getAsInt() > number; + } + + @Override + public String toString() { + return "VersionNumber (" + number + ")"; + } + }; + } /** - * Create a constant {@link CurrentVersion} + * Create a {@link CurrentVersion} that uses git tag names. Any difference in tag name will be treated as newer. * - * @param number the constant version number - * @return + * @param tagName the current tag name */ - static CurrentVersion of(int number) { - return () -> number; + static CurrentVersion ofTag(String tagName) { + return new CurrentVersion() { + @Override + public String display() { + return tagName; + } + + @Override + public boolean isOlderThan(JsonElement element) { + return !new JsonPrimitive(tagName).equals(element); + } + + @Override + public String toString() { + return "VersionTag (" + tagName + ")"; + } + }; } + + /** + * @return a user-friendly representation of this version + */ + String display(); + + /** + * Compare to another version, represented as JSON. + * + * @return true, if this version is older than the other version and an update to that version should occur. + */ + boolean isOlderThan(JsonElement element); } diff --git a/src/main/java/moe/nea/libautoupdate/ExitHookInvoker.java b/src/main/java/moe/nea/libautoupdate/ExitHookInvoker.java index 0b1eaee..971a5de 100644 --- a/src/main/java/moe/nea/libautoupdate/ExitHookInvoker.java +++ b/src/main/java/moe/nea/libautoupdate/ExitHookInvoker.java @@ -3,6 +3,7 @@ package moe.nea.libautoupdate; import java.io.File; import java.util.ArrayList; import java.util.List; +import java.util.UUID; /** * A Utility class for setting up the exit hook, which then launches the next stage (postexit) using the same java runtime. @@ -13,6 +14,8 @@ public class ExitHookInvoker { private static List<UpdateAction> actions; private static File updaterJar; private static boolean cancelled = false; + private static String identifer; + private static UUID uuid; /** * Set up the exit hook to run post exit actions. @@ -20,15 +23,19 @@ public class ExitHookInvoker { * <p><b>N.B.:</b> Calling this multiple times will only invoke the last set of actions. * In case of multiple updates the update actions should be joined in the same list.</p> * + * @param identifier the identifier for this updater for logging purposes + * @param uuid the uuid of this update for logging purposes * @param updaterJar the extracted updater jar * @param actions the actions to execute */ - public static synchronized void setExitHook(File updaterJar, List<UpdateAction> actions) { + public static synchronized void setExitHook(String identifier, UUID uuid, File updaterJar, List<UpdateAction> actions) { if (!isExitHookRegistered) { Runtime.getRuntime().addShutdownHook(new Thread(ExitHookInvoker::runExitHook)); isExitHookRegistered = true; } + ExitHookInvoker.identifer = identifier; + ExitHookInvoker.uuid = uuid; ExitHookInvoker.cancelled = false; ExitHookInvoker.actions = actions; ExitHookInvoker.updaterJar = updaterJar; @@ -51,6 +58,9 @@ public class ExitHookInvoker { arguments.add("-jar"); arguments.add(updaterJar.getAbsolutePath()); + arguments.add(identifer); + arguments.add(String.valueOf(uuid)); + for (UpdateAction action : actions) { action.encode(arguments); } diff --git a/src/main/java/moe/nea/libautoupdate/GistSource.java b/src/main/java/moe/nea/libautoupdate/GistSource.java index a8ae5df..c2a776b 100644 --- a/src/main/java/moe/nea/libautoupdate/GistSource.java +++ b/src/main/java/moe/nea/libautoupdate/GistSource.java @@ -1,6 +1,7 @@ package moe.nea.libautoupdate; import lombok.EqualsAndHashCode; +import lombok.NonNull; import lombok.Value; import java.util.concurrent.CompletableFuture; @@ -8,7 +9,9 @@ import java.util.concurrent.CompletableFuture; @Value @EqualsAndHashCode(callSuper = false) public class GistSource extends JsonUpdateSource { + @NonNull String owner; + @NonNull String gistId; private static final String GIST_RAW_URL = "https://gist.githubusercontent.com/%s/%s/raw/%s.json"; diff --git a/src/main/java/moe/nea/libautoupdate/GithubReleaseUpdateSource.java b/src/main/java/moe/nea/libautoupdate/GithubReleaseUpdateSource.java new file mode 100644 index 0000000..067ea4b --- /dev/null +++ b/src/main/java/moe/nea/libautoupdate/GithubReleaseUpdateSource.java @@ -0,0 +1,136 @@ +package moe.nea.libautoupdate; + +import com.google.gson.JsonPrimitive; +import com.google.gson.annotations.SerializedName; +import com.google.gson.reflect.TypeToken; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NonNull; + +import java.util.Comparator; +import java.util.Date; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +/** + * Update source pulling from a GitHub repositories releases. + * <p> + * The stream {@code pre} is dedicated to pre-releases (or full releases, if the newest full release is newer than + * the newest pre releases). + * The stream {@code full} is dedicated to only full releases. + * Override {@link #selectUpdate(String, List)} to change this behaviour. + * </p> + * <p> + * By default the first JAR that is in that release will be selected. + * Override {@link #findAsset(GithubRelease)} to change this behaviour + * </p> + * <p>This {@link UpdateSource} does not support newer than latest releases, since it uses the git tag as update version, and does not support hash checking.</p> + */ +@Data +@EqualsAndHashCode(callSuper = false) +public class GithubReleaseUpdateSource extends JsonUpdateSource { + @NonNull + final String owner; + @NonNull + final String repository; + + /** + * Find an update out of the list of releases from a GitHub repository. + * + * @param updateStream the update stream to find the update for + * @param releases the list of releases for this GitHub repository + * @return the latest update that matches this update stream + */ + protected UpdateData selectUpdate(String updateStream, List<GithubRelease> releases) { + if (Objects.equals("pre", updateStream)) { + return findLatestRelease(releases.stream().filter(it -> !it.isDraft()).collect(Collectors.toList())); + } + if (Objects.equals("full", updateStream)) { + return findLatestRelease(releases.stream().filter(it -> !it.isDraft() && !it.isPrerelease()).collect(Collectors.toList())); + } + return null; + } + + /** + * Find the matching Jar from a GitHub release. + * + * @param release a release containing assets + * @return an update data referencing one of the assets of that release + */ + protected UpdateData findAsset(GithubRelease release) { + if (release.getAssets() == null) return null; + return release.getAssets().stream() + .filter(it -> Objects.equals(it.getContentType(), "application/x-java-archive") && it.getBrowserDownloadUrl() != null) + .map(it -> new UpdateData( + release.getName() == null ? release.getTagName() : release.getName(), + new JsonPrimitive(release.getTagName()), + null, + it.getBrowserDownloadUrl() + )) + .findFirst().orElse(null); + } + + /** + * Find the latest release out of a list of releases that are valid for an updateStream. + * Uses {@link #findAsset(GithubRelease)} to find which jar file to use. + * + * @param validReleases the list of valid releases + * @return the latest release (or null) + */ + protected UpdateData findLatestRelease(Iterable<GithubRelease> validReleases) { + return StreamSupport.stream(validReleases.spliterator(), false) + .max(Comparator.comparing(GithubRelease::getPublishedAt)) + .map(this::findAsset) + .orElse(null); + } + + + private String getReleaseApiUrl() { + return String.format("https://api.github.com/repos/%s/%s/releases", owner, repository); + } + + @Override + public CompletableFuture<UpdateData> checkUpdate(String updateStream) { + CompletableFuture<List<GithubRelease>> releases = getJsonFromURL(getReleaseApiUrl(), new TypeToken<List<GithubRelease>>() { + }.getType()); + return releases.thenApply(it -> it == null ? null : selectUpdate(updateStream, it)); + } + + /** + * A data class representing the GitHub API response to + * api.github.com/repos/a/b/releases + */ + @Data + public static class GithubRelease { + @SerializedName("tag_name") + String tagName; + @SerializedName("target_commitish") + String targetCommitish; + String name; + boolean draft = false; + boolean prerelease = false; + @SerializedName("created_at") + Date created_at; + @SerializedName("published_at") + Date publishedAt; + int id = 0; + List<Download> assets; + String body; + @SerializedName("html_url") + String htmlUrl; + + @Data + public static class Download { + int id = 0; + String name; + @SerializedName("content_type") + String contentType; + String label; + @SerializedName("browser_download_url") + String browserDownloadUrl; + } + } +} diff --git a/src/main/java/moe/nea/libautoupdate/JsonUpdateSource.java b/src/main/java/moe/nea/libautoupdate/JsonUpdateSource.java index ffbfa9a..95c0b9f 100644 --- a/src/main/java/moe/nea/libautoupdate/JsonUpdateSource.java +++ b/src/main/java/moe/nea/libautoupdate/JsonUpdateSource.java @@ -1,31 +1,18 @@ package moe.nea.libautoupdate; import com.google.gson.Gson; -import lombok.val; -import java.io.IOException; -import java.io.InputStreamReader; -import java.net.URL; -import java.nio.charset.StandardCharsets; +import java.lang.reflect.Type; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionException; + public abstract class JsonUpdateSource implements UpdateSource { - private static final Gson gson = new Gson(); protected Gson getGson() { - return gson; + return UpdateUtils.gson; } - protected <T> CompletableFuture<T> getJsonFromURL(String url, Class<T> clazz) { - return CompletableFuture.supplyAsync(() -> { - try { - try (val is = new URL(url).openStream()) { - return getGson().fromJson(new InputStreamReader(is, StandardCharsets.UTF_8), clazz); - } - } catch (IOException e) { - throw new CompletionException(e); - } - }); + protected <T> CompletableFuture<T> getJsonFromURL(String url, Type clazz) { + return UpdateUtils.httpGet(url, getGson(), clazz); } } diff --git a/src/main/java/moe/nea/libautoupdate/Main.java b/src/main/java/moe/nea/libautoupdate/Main.java index 5d98df1..3043348 100644 --- a/src/main/java/moe/nea/libautoupdate/Main.java +++ b/src/main/java/moe/nea/libautoupdate/Main.java @@ -4,16 +4,17 @@ public class Main { public static void main(String[] args) { UpdateContext updater = new UpdateContext( - UpdateSource.gistSource("romangraef", "9b62fe32bc41c09d2d7e2d3153f14ee8"), + UpdateSource.githubUpdateSource("NotEnoughUpdates", "NotEnoughUpdates"), UpdateTarget.deleteAndSaveInTheSameFolder(Main.class), - CurrentVersion.of(10000), + CurrentVersion.ofTag("v2.1.1-pre1"), "test" ); updater.cleanup(); System.out.println("Update cleaned"); System.out.println("Created update context: " + updater); - updater.checkUpdate("stable").thenCompose(it -> { - System.out.println("Checked for update on stable: " + it); + String stream = "pre"; + updater.checkUpdate(stream).thenCompose(it -> { + System.out.println("Checked for update on " + stream + ": " + it); System.out.println("Can update: " + it.isUpdateAvailable()); System.out.println("Executing update."); return it.launchUpdate(); diff --git a/src/main/java/moe/nea/libautoupdate/PotentialUpdate.java b/src/main/java/moe/nea/libautoupdate/PotentialUpdate.java index febaa80..33d4026 100644 --- a/src/main/java/moe/nea/libautoupdate/PotentialUpdate.java +++ b/src/main/java/moe/nea/libautoupdate/PotentialUpdate.java @@ -39,7 +39,7 @@ public class PotentialUpdate { */ public boolean isUpdateAvailable() { if (update == null) return false; - return update.getVersionNumber() > context.getCurrentVersion().getCurrentVersionNumber(); + return context.getCurrentVersion().isOlderThan(update.getVersionNumber()); } private File getFile(String name) { @@ -84,7 +84,7 @@ public class PotentialUpdate { } try (val check = new FileInputStream(getUpdateJarStorage())) { val updateSha = UpdateUtils.sha256sum(check); - if (!update.getSha256().equalsIgnoreCase(updateSha)) { + if (update.getSha256() != null && !update.getSha256().equalsIgnoreCase(updateSha)) { throw new UpdateException( "Hash of downloaded file " + getUpdateJarStorage() + " (" + updateSha + ") does not match expected hash of " + @@ -107,7 +107,10 @@ public class PotentialUpdate { */ public void executeUpdate() throws IOException { prepareUpdate(); - ExitHookInvoker.setExitHook(getFile("updater.jar"), + ExitHookInvoker.setExitHook( + getContext().getIdentifier(), + getUpdateUUID(), + getFile("updater.jar"), context.getTarget().generateUpdateActions(this)); } diff --git a/src/main/java/moe/nea/libautoupdate/UpdateData.java b/src/main/java/moe/nea/libautoupdate/UpdateData.java index 276ae5f..2ef476a 100644 --- a/src/main/java/moe/nea/libautoupdate/UpdateData.java +++ b/src/main/java/moe/nea/libautoupdate/UpdateData.java @@ -1,5 +1,6 @@ package moe.nea.libautoupdate; +import com.google.gson.JsonElement; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -12,7 +13,7 @@ import java.net.URL; @AllArgsConstructor public class UpdateData { String versionName; - int versionNumber; + JsonElement versionNumber; String sha256; String download; diff --git a/src/main/java/moe/nea/libautoupdate/UpdateSource.java b/src/main/java/moe/nea/libautoupdate/UpdateSource.java index 41765a5..9a0e7ae 100644 --- a/src/main/java/moe/nea/libautoupdate/UpdateSource.java +++ b/src/main/java/moe/nea/libautoupdate/UpdateSource.java @@ -11,6 +11,13 @@ public interface UpdateSource { } /** + * Create a {@link GithubReleaseUpdateSource}. + */ + static UpdateSource githubUpdateSource(String owner, String repository) { + return new GithubReleaseUpdateSource(owner, repository); + } + + /** * Check for updates in the given update stream. * * @param updateStream the update stream to check for updates. diff --git a/src/main/java/moe/nea/libautoupdate/UpdateUtils.java b/src/main/java/moe/nea/libautoupdate/UpdateUtils.java index ce09c0b..f7319ca 100644 --- a/src/main/java/moe/nea/libautoupdate/UpdateUtils.java +++ b/src/main/java/moe/nea/libautoupdate/UpdateUtils.java @@ -1,15 +1,16 @@ package moe.nea.libautoupdate; +import com.google.gson.Gson; import lombok.val; import lombok.var; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; +import java.io.*; +import java.lang.reflect.Type; import java.math.BigInteger; import java.net.URI; import java.net.URISyntaxException; +import java.net.URL; +import java.nio.charset.StandardCharsets; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; @@ -18,11 +19,15 @@ import java.nio.file.attribute.BasicFileAttributes; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Locale; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; public class UpdateUtils { private UpdateUtils() { } + static final Gson gson = new Gson(); + public static File getJarFileContainingClass(Class<?> clazz) { val location = clazz.getProtectionDomain().getCodeSource().getLocation(); if (location == null) @@ -82,5 +87,18 @@ public class UpdateUtils { } }); } + + public static <T> CompletableFuture<T> httpGet(String url, Gson gson, Type clazz) { + return CompletableFuture.supplyAsync(() -> { + try { + try (val is = new URL(url).openStream()) { + return gson.fromJson(new InputStreamReader(is, StandardCharsets.UTF_8), clazz); + } + } catch (IOException e) { + throw new CompletionException(e); + } + }); + } + } diff --git a/updater/src/main/java/moe/nea/libautoupdate/postexit/PostExitMain.java b/updater/src/main/java/moe/nea/libautoupdate/postexit/PostExitMain.java index cf9a820..33db32e 100644 --- a/updater/src/main/java/moe/nea/libautoupdate/postexit/PostExitMain.java +++ b/updater/src/main/java/moe/nea/libautoupdate/postexit/PostExitMain.java @@ -14,8 +14,8 @@ public class PostExitMain { PrintStream printStream = new PrintStream(new FileOutputStream(outputFile, true)); System.setErr(printStream); System.setOut(printStream); - - for (int i = 0; i < args.length; i++) { + System.out.println("Starting update (with identifier " + args[0] + " and uuid " + args[1] + ")"); + for (int i = 2; i < args.length; i++) { switch (args[i].intern()) { case "delete": File file = unlockedFile(args[++i]); |