diff --git a/src/main/java/eu/m724/jarupdater/download/Downloader.java b/src/main/java/eu/m724/jarupdater/download/Downloader.java index d07437d..abdfa67 100644 --- a/src/main/java/eu/m724/jarupdater/download/Downloader.java +++ b/src/main/java/eu/m724/jarupdater/download/Downloader.java @@ -1,22 +1,22 @@ package eu.m724.jarupdater.download; -import java.io.File; +import java.nio.file.Path; import java.util.concurrent.CompletableFuture; public interface Downloader { /** - * downloads a file and verifies it - * @param url the file url to download from - * @param sha256hex the sha256 hash to verify - * @return a future which can throw ioexception or signatureexception + * Downloads a file and verifies its checksum + * @param url The file URL + * @param sha256hex The expected SHA-256 hash (in hex) + * @return A future which returns the saved file. It can throw {@link java.io.IOException} and {@link java.security.SignatureException} */ - CompletableFuture downloadAndVerify(String url, String sha256hex); + CompletableFuture downloadAndVerify(String url, String sha256hex); /** - * moves source into destination - * @param source source file - * @param destination destination file (path) - * @return a future which can throw ioexception + * Moves source into destination + * @param source The source file + * @param destination The destination file (not folder) + * @return A future which can throw {@link java.io.IOException} */ - CompletableFuture install(File source, File destination); + CompletableFuture install(Path source, Path destination); } diff --git a/src/main/java/eu/m724/jarupdater/download/SimpleDownloader.java b/src/main/java/eu/m724/jarupdater/download/SimpleDownloader.java index 52a29a1..933157a 100644 --- a/src/main/java/eu/m724/jarupdater/download/SimpleDownloader.java +++ b/src/main/java/eu/m724/jarupdater/download/SimpleDownloader.java @@ -1,9 +1,8 @@ package eu.m724.jarupdater.download; -import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.net.ProxySelector; import java.net.URI; import java.net.http.HttpClient; @@ -12,6 +11,7 @@ import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandlers; import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; @@ -21,14 +21,14 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; public class SimpleDownloader implements Downloader { - private String branding; + private final String branding; public SimpleDownloader(String branding) { this.branding = branding; } @Override - public CompletableFuture downloadAndVerify(String url, String sha256hex) { // TODO progress? + public CompletableFuture downloadAndVerify(String url, String sha256hex) { // TODO progress? HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(url)) @@ -40,44 +40,41 @@ public class SimpleDownloader implements Downloader { .followRedirects(Redirect.NORMAL) .proxy(ProxySelector.getDefault()).build(). sendAsync(request, BodyHandlers.ofInputStream()); - - CompletableFuture fileFuture = - responseFuture.thenApply(response -> { - - try (InputStream bodyStream = response.body()) { - - File downloadedFile = Files.createTempFile(branding, null).toFile(); - MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); - - try (FileOutputStream fileStream = new FileOutputStream(downloadedFile)) { - while (bodyStream.available() > 0) { - byte[] bytes = bodyStream.readAllBytes(); - - messageDigest.update(bytes); - fileStream.write(bytes); - } - } - - if (!Arrays.equals(messageDigest.digest(), hexStringToByteArray(sha256hex))) - throw new SignatureException(); // This is not outside try because impact is too small to justify more code - - return downloadedFile; - - } catch (IOException | NoSuchAlgorithmException | SignatureException e) { - throw new CompletionException(e); - } - }); - - return fileFuture; + + return responseFuture.thenApply(response -> { + + try (InputStream bodyStream = response.body()) { + + Path tempFile = Files.createTempFile(branding, null); + MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); + + try (OutputStream tempFileOutputStream = Files.newOutputStream(tempFile)) { + while (bodyStream.available() > 0) { + byte[] bytes = bodyStream.readAllBytes(); + + messageDigest.update(bytes); + tempFileOutputStream.write(bytes); + } + } + + if (!Arrays.equals(messageDigest.digest(), hexStringToByteArray(sha256hex))) + throw new SignatureException(); // This is not outside try because impact is too small to justify more code + + return tempFile; + + } catch (IOException | NoSuchAlgorithmException | SignatureException e) { + throw new CompletionException(e); + } + + }); } @Override - public CompletableFuture install(File source, File destination) { + public CompletableFuture install(Path source, Path destination) { return CompletableFuture.runAsync(() -> { - // TODO what if we changed File to Path and every ref in this file try { - Files.move(source.toPath(), destination.toPath(), StandardCopyOption.REPLACE_EXISTING); + Files.move(source, destination, StandardCopyOption.REPLACE_EXISTING); } catch (IOException e) { throw new CompletionException(e); } diff --git a/src/main/java/eu/m724/jarupdater/environment/Environment.java b/src/main/java/eu/m724/jarupdater/environment/Environment.java index 04b057a..a10a9f6 100644 --- a/src/main/java/eu/m724/jarupdater/environment/Environment.java +++ b/src/main/java/eu/m724/jarupdater/environment/Environment.java @@ -3,7 +3,21 @@ package eu.m724.jarupdater.environment; import java.nio.file.Path; public interface Environment { + /** + * Get the version of running software. + * @return The running software version + */ String getRunningVersion(); + + /** + * Get the configured update channel. + * @return The configured update channel + */ String getChannel(); + + /** + * Get the path to the running JAR file. + * @return The path to the running JAR file. + */ Path getRunningJarFilePath(); } diff --git a/src/main/java/eu/m724/jarupdater/live/HttpMetadataDAO.java b/src/main/java/eu/m724/jarupdater/live/HttpMetadataDAO.java index aa01506..bcaee7d 100644 --- a/src/main/java/eu/m724/jarupdater/live/HttpMetadataDAO.java +++ b/src/main/java/eu/m724/jarupdater/live/HttpMetadataDAO.java @@ -41,7 +41,7 @@ public class HttpMetadataDAO implements MetadataDAO { return makeRequest(url).thenApply(response -> { if (response.statusCode() == 404) { - throw new CompletionException(new NoSuchVersionException(versionLabel)); + throw new CompletionException(new NoSuchVersionException(channel, versionLabel)); } if (response.statusCode() != 200) { diff --git a/src/main/java/eu/m724/jarupdater/object/NoSuchVersionException.java b/src/main/java/eu/m724/jarupdater/object/NoSuchVersionException.java index ae432db..25e269a 100644 --- a/src/main/java/eu/m724/jarupdater/object/NoSuchVersionException.java +++ b/src/main/java/eu/m724/jarupdater/object/NoSuchVersionException.java @@ -2,9 +2,12 @@ package eu.m724.jarupdater.object; import java.io.IOException; +/** + * Thrown when trying to retrieve metadata of a non-existent version. + */ public class NoSuchVersionException extends IOException { - public NoSuchVersionException(String version) { - super(version); + public NoSuchVersionException(String channel, String version) { + super(String.format("Version %s not found in channel %s", version, channel)); } } diff --git a/src/main/java/eu/m724/jarupdater/object/Version.java b/src/main/java/eu/m724/jarupdater/object/Version.java index 587d093..953064a 100644 --- a/src/main/java/eu/m724/jarupdater/object/Version.java +++ b/src/main/java/eu/m724/jarupdater/object/Version.java @@ -34,36 +34,35 @@ public class Version { } /** - * release time of a version + * The release time of this version in Unix seconds */ public long getTimestamp() { return timestamp; } /** - * label aka version string
- * example: 1.0.0 + * The label / name of this version, like 1.0.0 */ public String getLabel() { return label; } /** - * url of the downloadable jar file + * URL of this version's downloadable JAR file */ public String getFileUrl() { return fileUrl; } /** - * url of changelog file + * The URL to this version's changelog */ public String getChangelogUrl() { return changelogUrl; } /** - * sha256 hash of that file + * SHA-256 hash of this version's JAR file */ public String getSha256() { return sha256; diff --git a/src/main/java/eu/m724/jarupdater/updater/Updater.java b/src/main/java/eu/m724/jarupdater/updater/Updater.java index c7d7372..18bfa0b 100644 --- a/src/main/java/eu/m724/jarupdater/updater/Updater.java +++ b/src/main/java/eu/m724/jarupdater/updater/Updater.java @@ -7,20 +7,20 @@ import eu.m724.jarupdater.object.Version; import eu.m724.jarupdater.verify.VerificationException; import eu.m724.jarupdater.verify.Verifier; -import java.io.File; import java.io.IOException; import java.nio.file.NoSuchFileException; +import java.nio.file.Path; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; public class Updater { - protected Environment environment; - protected MetadataFacade metadataProvider; - protected Downloader downloader; - protected Verifier verifier; + protected final Environment environment; + protected final MetadataFacade metadataProvider; + protected final Downloader downloader; + protected final Verifier verifier; - protected CompletableFuture downloaded; + protected CompletableFuture downloadFuture; public Updater(Environment environment, MetadataFacade metadataProvider, Downloader downloader, Verifier verifier) { this.environment = environment; @@ -28,10 +28,6 @@ public class Updater { this.downloader = downloader; this.verifier = verifier; } - - public Environment getEnvironment() { - return environment; - } /** * Get all channels @@ -71,18 +67,17 @@ public class Updater { * Download the latest available version * @return a future which returns the downloaded file */ - public CompletableFuture downloadLatestVersion() { + public CompletableFuture downloadLatestVersion() { CompletableFuture latestVersionFuture = metadataProvider.getLatestVersionMetadata(); - downloaded = latestVersionFuture.thenApply(latestVersion -> { + downloadFuture = latestVersionFuture.thenComposeAsync(latestVersion -> { String url = latestVersion.getFileUrl(); String hash = latestVersion.getSha256(); - // TODO better way of catching exception? - return downloader.downloadAndVerify(url, hash).join(); + return downloader.downloadAndVerify(url, hash); }); - return downloaded; + return downloadFuture; } /** @@ -92,16 +87,16 @@ public class Updater { * @throws NoSuchFileException if you didn't download it first */ public CompletableFuture installLatestVersion() throws NoSuchFileException { - if (downloaded == null) + if (downloadFuture == null) throw new NoSuchFileException("Download it first"); - return downloaded.thenCompose(file -> { + return downloadFuture.thenCompose(file -> { try { - verifier.verify(file.getAbsolutePath()); + verifier.verify(file.toString()); } catch (VerificationException e) { throw new CompletionException(e); } - return downloader.install(file, environment.getRunningJarFilePath().toFile()); + return downloader.install(file, environment.getRunningJarFilePath()); }); } diff --git a/src/test/java/jarupdater/DownloaderTest.java b/src/test/java/jarupdater/DownloaderTest.java index 5b2020c..8f42528 100644 --- a/src/test/java/jarupdater/DownloaderTest.java +++ b/src/test/java/jarupdater/DownloaderTest.java @@ -1,23 +1,22 @@ package jarupdater; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.fail; +import eu.m724.jarupdater.download.Downloader; +import org.junit.Test; -import java.io.File; import java.io.IOException; +import java.nio.file.Path; import java.security.SignatureException; import java.util.concurrent.CompletionException; -import org.junit.Test; - -import eu.m724.jarupdater.download.Downloader; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; public class DownloaderTest { @Test public void testDownloader() { Downloader downloader = new MockDownloader(); - File file = downloader.downloadAndVerify("good", "a9194ba3e955ba7482c6552894d4ca41b9bbafd86f4d90f3564c02fcb9e917c2").join(); + Path file = downloader.downloadAndVerify("good", "a9194ba3e955ba7482c6552894d4ca41b9bbafd86f4d90f3564c02fcb9e917c2").join(); assertNotNull(file); try { @@ -36,7 +35,7 @@ public class DownloaderTest { downloader.install(file, file).join(); try { - downloader.install(new File("ialsoexistfortesting"), file).join(); + downloader.install(Path.of("ialsoexistfortesting"), file).join(); fail("this should have thrown"); } catch (CompletionException e) { assert e.getCause() instanceof IOException; diff --git a/src/test/java/jarupdater/MetadataTest.java b/src/test/java/jarupdater/MetadataTest.java index 1f2cb75..897e568 100644 --- a/src/test/java/jarupdater/MetadataTest.java +++ b/src/test/java/jarupdater/MetadataTest.java @@ -1,18 +1,17 @@ package jarupdater; -import static org.junit.Assert.*; - -import java.io.IOException; -import java.nio.file.Path; -import java.util.concurrent.CompletionException; - -import org.junit.Test; - import eu.m724.jarupdater.environment.ConstantEnvironment; import eu.m724.jarupdater.environment.Environment; import eu.m724.jarupdater.live.MetadataDAO; import eu.m724.jarupdater.live.MetadataFacade; import eu.m724.jarupdater.object.Version; +import org.junit.Test; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.concurrent.CompletionException; + +import static org.junit.Assert.*; public class MetadataTest { @Test @@ -29,10 +28,10 @@ public class MetadataTest { assert cur != null; assert lat != null; - - assertFalse(cur.equals(lat)); - assertTrue(cur.getLabel().equals(environment.getRunningVersion())); - assertTrue(lat.getLabel().equals("1.1")); + + assertNotEquals(cur, lat); + assertEquals(cur.getLabel(), environment.getRunningVersion()); + assertEquals("1.1", lat.getLabel()); try { facade.getVersionMetadata("invalidversion").join(); diff --git a/src/test/java/jarupdater/MockDownloader.java b/src/test/java/jarupdater/MockDownloader.java index df28d94..a316f3c 100644 --- a/src/test/java/jarupdater/MockDownloader.java +++ b/src/test/java/jarupdater/MockDownloader.java @@ -1,22 +1,22 @@ package jarupdater; -import java.io.File; +import eu.m724.jarupdater.download.Downloader; + import java.io.IOException; +import java.nio.file.Path; import java.security.SignatureException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; -import eu.m724.jarupdater.download.Downloader; - public class MockDownloader implements Downloader { @Override - public CompletableFuture downloadAndVerify(String url, String sha256hex) { - CompletableFuture future = null; + public CompletableFuture downloadAndVerify(String url, String sha256hex) { + CompletableFuture future = null; switch (url) { case "good": - future = CompletableFuture.completedFuture(new File("ionlyexistfortesting")); + future = CompletableFuture.completedFuture(Path.of("ionlyexistfortesting")); break; case "invalid": future = CompletableFuture.failedFuture(new CompletionException(new IOException())); @@ -30,10 +30,10 @@ public class MockDownloader implements Downloader { } @Override - public CompletableFuture install(File source, File destination) { + public CompletableFuture install(Path source, Path destination) { CompletableFuture future = null; - if (source.getName().equals("ionlyexistfortesting")) + if (source.toString().equals("ionlyexistfortesting")) future = CompletableFuture.completedFuture(null); else future = CompletableFuture.failedFuture(new CompletionException(new IOException())); diff --git a/src/test/java/jarupdater/MockMetadataDAO.java b/src/test/java/jarupdater/MockMetadataDAO.java index 3e01760..2003cd1 100644 --- a/src/test/java/jarupdater/MockMetadataDAO.java +++ b/src/test/java/jarupdater/MockMetadataDAO.java @@ -1,9 +1,9 @@ package jarupdater; import eu.m724.jarupdater.live.MetadataDAO; +import eu.m724.jarupdater.object.NoSuchVersionException; import eu.m724.jarupdater.object.Version; -import java.io.IOException; import java.util.Arrays; import java.util.List; import java.util.concurrent.CompletableFuture; @@ -20,20 +20,18 @@ public class MockMetadataDAO implements MetadataDAO { @Override public CompletableFuture getMetadata(String channel, String versionLabel) { Version version = null; - String fileUrl = "http://127.0.0.1:17357/%s/%s.jar".formatted(channel, versionLabel); + String fileUrl = "http://127.0.0.1:17357/" + channel + "/" + versionLabel + ".jar"; switch (versionLabel) { - case "1.0": - version = new Version(1, 100, "1.0", fileUrl, null, "dd3822ed965b2820aa0800f04b86f26d0b656fca3b97ddd264046076f81e3a24"); - break; - case "1.1": - version = new Version(2, 200, "1.1", fileUrl, null,"4d59994f607b89987d5bff430a4229edbbb045b3da9eaf1c63fa8e5fb208d824"); - break; - case "latest": - version = new Version(2, 200, "1.1", fileUrl, null, "4d59994f607b89987d5bff430a4229edbbb045b3da9eaf1c63fa8e5fb208d824"); - break; - default: - return CompletableFuture.failedFuture(new CompletionException(new IOException("no such version"))); + case "1.0": + version = new Version(1, 100, "1.0", fileUrl, null, "dd3822ed965b2820aa0800f04b86f26d0b656fca3b97ddd264046076f81e3a24"); + break; + case "1.1": + case "latest": + version = new Version(2, 200, "1.1", fileUrl, null,"4d59994f607b89987d5bff430a4229edbbb045b3da9eaf1c63fa8e5fb208d824"); + break; + default: + return CompletableFuture.failedFuture(new CompletionException(new NoSuchVersionException(channel, versionLabel))); } return CompletableFuture.completedFuture(version);