diff options
Diffstat (limited to 'src/main')
10 files changed, 256 insertions, 38 deletions
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); + } + }); + } + } |