File -> Path and refactoring

This commit is contained in:
Minecon724 2025-05-05 09:47:06 +02:00
commit 7c8fb07b5d
Signed by untrusted user who does not match committer: m724
GPG key ID: A02E6E67AB961189
11 changed files with 120 additions and 116 deletions

View file

@ -1,22 +1,22 @@
package eu.m724.jarupdater.download; package eu.m724.jarupdater.download;
import java.io.File; import java.nio.file.Path;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
public interface Downloader { public interface Downloader {
/** /**
* downloads a file and verifies it * Downloads a file and verifies its checksum
* @param url the file url to download from * @param url The file URL
* @param sha256hex the sha256 hash to verify * @param sha256hex The expected SHA-256 hash (in hex)
* @return a future which can throw ioexception or signatureexception * @return A future which returns the saved file. It can throw {@link java.io.IOException} and {@link java.security.SignatureException}
*/ */
CompletableFuture<File> downloadAndVerify(String url, String sha256hex); CompletableFuture<Path> downloadAndVerify(String url, String sha256hex);
/** /**
* moves source into destination * Moves source into destination
* @param source source file * @param source The source file
* @param destination destination file (path) * @param destination The destination file (not folder)
* @return a future which can throw ioexception * @return A future which can throw {@link java.io.IOException}
*/ */
CompletableFuture<Void> install(File source, File destination); CompletableFuture<Void> install(Path source, Path destination);
} }

View file

@ -1,9 +1,8 @@
package eu.m724.jarupdater.download; package eu.m724.jarupdater.download;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream;
import java.net.ProxySelector; import java.net.ProxySelector;
import java.net.URI; import java.net.URI;
import java.net.http.HttpClient; import java.net.http.HttpClient;
@ -12,6 +11,7 @@ import java.net.http.HttpRequest;
import java.net.http.HttpResponse; import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandlers; import java.net.http.HttpResponse.BodyHandlers;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption; import java.nio.file.StandardCopyOption;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
@ -21,14 +21,14 @@ import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException; import java.util.concurrent.CompletionException;
public class SimpleDownloader implements Downloader { public class SimpleDownloader implements Downloader {
private String branding; private final String branding;
public SimpleDownloader(String branding) { public SimpleDownloader(String branding) {
this.branding = branding; this.branding = branding;
} }
@Override @Override
public CompletableFuture<File> downloadAndVerify(String url, String sha256hex) { // TODO progress? public CompletableFuture<Path> downloadAndVerify(String url, String sha256hex) { // TODO progress?
HttpRequest request = HttpRequest.newBuilder() HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url)) .uri(URI.create(url))
@ -40,44 +40,41 @@ public class SimpleDownloader implements Downloader {
.followRedirects(Redirect.NORMAL) .followRedirects(Redirect.NORMAL)
.proxy(ProxySelector.getDefault()).build(). .proxy(ProxySelector.getDefault()).build().
sendAsync(request, BodyHandlers.ofInputStream()); sendAsync(request, BodyHandlers.ofInputStream());
CompletableFuture<File> 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 responseFuture.thenApply(response -> {
return fileFuture;
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 @Override
public CompletableFuture<Void> install(File source, File destination) { public CompletableFuture<Void> install(Path source, Path destination) {
return CompletableFuture.runAsync(() -> { return CompletableFuture.runAsync(() -> {
// TODO what if we changed File to Path and every ref in this file
try { try {
Files.move(source.toPath(), destination.toPath(), StandardCopyOption.REPLACE_EXISTING); Files.move(source, destination, StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) { } catch (IOException e) {
throw new CompletionException(e); throw new CompletionException(e);
} }

View file

@ -3,7 +3,21 @@ package eu.m724.jarupdater.environment;
import java.nio.file.Path; import java.nio.file.Path;
public interface Environment { public interface Environment {
/**
* Get the version of running software.
* @return The running software version
*/
String getRunningVersion(); String getRunningVersion();
/**
* Get the configured update channel.
* @return The configured update channel
*/
String getChannel(); String getChannel();
/**
* Get the path to the running JAR file.
* @return The path to the running JAR file.
*/
Path getRunningJarFilePath(); Path getRunningJarFilePath();
} }

View file

@ -41,7 +41,7 @@ public class HttpMetadataDAO implements MetadataDAO {
return makeRequest(url).thenApply(response -> { return makeRequest(url).thenApply(response -> {
if (response.statusCode() == 404) { if (response.statusCode() == 404) {
throw new CompletionException(new NoSuchVersionException(versionLabel)); throw new CompletionException(new NoSuchVersionException(channel, versionLabel));
} }
if (response.statusCode() != 200) { if (response.statusCode() != 200) {

View file

@ -2,9 +2,12 @@ package eu.m724.jarupdater.object;
import java.io.IOException; import java.io.IOException;
/**
* Thrown when trying to retrieve metadata of a non-existent version.
*/
public class NoSuchVersionException extends IOException { public class NoSuchVersionException extends IOException {
public NoSuchVersionException(String version) { public NoSuchVersionException(String channel, String version) {
super(version); super(String.format("Version %s not found in channel %s", version, channel));
} }
} }

View file

@ -34,36 +34,35 @@ public class Version {
} }
/** /**
* release time of a version * The release time of this version in Unix seconds
*/ */
public long getTimestamp() { public long getTimestamp() {
return timestamp; return timestamp;
} }
/** /**
* label aka version string<br> * The label / name of this version, like 1.0.0
* example: 1.0.0
*/ */
public String getLabel() { public String getLabel() {
return label; return label;
} }
/** /**
* url of the downloadable jar file * URL of this version's downloadable JAR file
*/ */
public String getFileUrl() { public String getFileUrl() {
return fileUrl; return fileUrl;
} }
/** /**
* url of changelog file * The URL to this version's changelog
*/ */
public String getChangelogUrl() { public String getChangelogUrl() {
return changelogUrl; return changelogUrl;
} }
/** /**
* sha256 hash of that file * SHA-256 hash of this version's JAR file
*/ */
public String getSha256() { public String getSha256() {
return sha256; return sha256;

View file

@ -7,20 +7,20 @@ import eu.m724.jarupdater.object.Version;
import eu.m724.jarupdater.verify.VerificationException; import eu.m724.jarupdater.verify.VerificationException;
import eu.m724.jarupdater.verify.Verifier; import eu.m724.jarupdater.verify.Verifier;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.file.NoSuchFileException; import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.util.List; import java.util.List;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException; import java.util.concurrent.CompletionException;
public class Updater { public class Updater {
protected Environment environment; protected final Environment environment;
protected MetadataFacade metadataProvider; protected final MetadataFacade metadataProvider;
protected Downloader downloader; protected final Downloader downloader;
protected Verifier verifier; protected final Verifier verifier;
protected CompletableFuture<File> downloaded; protected CompletableFuture<Path> downloadFuture;
public Updater(Environment environment, MetadataFacade metadataProvider, Downloader downloader, Verifier verifier) { public Updater(Environment environment, MetadataFacade metadataProvider, Downloader downloader, Verifier verifier) {
this.environment = environment; this.environment = environment;
@ -28,10 +28,6 @@ public class Updater {
this.downloader = downloader; this.downloader = downloader;
this.verifier = verifier; this.verifier = verifier;
} }
public Environment getEnvironment() {
return environment;
}
/** /**
* Get all channels * Get all channels
@ -71,18 +67,17 @@ public class Updater {
* Download the latest available version * Download the latest available version
* @return a future which returns the downloaded file * @return a future which returns the downloaded file
*/ */
public CompletableFuture<File> downloadLatestVersion() { public CompletableFuture<Path> downloadLatestVersion() {
CompletableFuture<Version> latestVersionFuture = metadataProvider.getLatestVersionMetadata(); CompletableFuture<Version> latestVersionFuture = metadataProvider.getLatestVersionMetadata();
downloaded = latestVersionFuture.thenApply(latestVersion -> { downloadFuture = latestVersionFuture.thenComposeAsync(latestVersion -> {
String url = latestVersion.getFileUrl(); String url = latestVersion.getFileUrl();
String hash = latestVersion.getSha256(); String hash = latestVersion.getSha256();
// TODO better way of catching exception? return downloader.downloadAndVerify(url, hash);
return downloader.downloadAndVerify(url, hash).join();
}); });
return downloaded; return downloadFuture;
} }
/** /**
@ -92,16 +87,16 @@ public class Updater {
* @throws NoSuchFileException if you didn't download it first * @throws NoSuchFileException if you didn't download it first
*/ */
public CompletableFuture<Void> installLatestVersion() throws NoSuchFileException { public CompletableFuture<Void> installLatestVersion() throws NoSuchFileException {
if (downloaded == null) if (downloadFuture == null)
throw new NoSuchFileException("Download it first"); throw new NoSuchFileException("Download it first");
return downloaded.thenCompose(file -> { return downloadFuture.thenCompose(file -> {
try { try {
verifier.verify(file.getAbsolutePath()); verifier.verify(file.toString());
} catch (VerificationException e) { } catch (VerificationException e) {
throw new CompletionException(e); throw new CompletionException(e);
} }
return downloader.install(file, environment.getRunningJarFilePath().toFile()); return downloader.install(file, environment.getRunningJarFilePath());
}); });
} }

View file

@ -1,23 +1,22 @@
package jarupdater; package jarupdater;
import static org.junit.Assert.assertNotNull; import eu.m724.jarupdater.download.Downloader;
import static org.junit.Assert.fail; import org.junit.Test;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Path;
import java.security.SignatureException; import java.security.SignatureException;
import java.util.concurrent.CompletionException; import java.util.concurrent.CompletionException;
import org.junit.Test; import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;
import eu.m724.jarupdater.download.Downloader;
public class DownloaderTest { public class DownloaderTest {
@Test @Test
public void testDownloader() { public void testDownloader() {
Downloader downloader = new MockDownloader(); Downloader downloader = new MockDownloader();
File file = downloader.downloadAndVerify("good", "a9194ba3e955ba7482c6552894d4ca41b9bbafd86f4d90f3564c02fcb9e917c2").join(); Path file = downloader.downloadAndVerify("good", "a9194ba3e955ba7482c6552894d4ca41b9bbafd86f4d90f3564c02fcb9e917c2").join();
assertNotNull(file); assertNotNull(file);
try { try {
@ -36,7 +35,7 @@ public class DownloaderTest {
downloader.install(file, file).join(); downloader.install(file, file).join();
try { try {
downloader.install(new File("ialsoexistfortesting"), file).join(); downloader.install(Path.of("ialsoexistfortesting"), file).join();
fail("this should have thrown"); fail("this should have thrown");
} catch (CompletionException e) { } catch (CompletionException e) {
assert e.getCause() instanceof IOException; assert e.getCause() instanceof IOException;

View file

@ -1,18 +1,17 @@
package jarupdater; 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.ConstantEnvironment;
import eu.m724.jarupdater.environment.Environment; import eu.m724.jarupdater.environment.Environment;
import eu.m724.jarupdater.live.MetadataDAO; import eu.m724.jarupdater.live.MetadataDAO;
import eu.m724.jarupdater.live.MetadataFacade; import eu.m724.jarupdater.live.MetadataFacade;
import eu.m724.jarupdater.object.Version; 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 { public class MetadataTest {
@Test @Test
@ -29,10 +28,10 @@ public class MetadataTest {
assert cur != null; assert cur != null;
assert lat != null; assert lat != null;
assertFalse(cur.equals(lat)); assertNotEquals(cur, lat);
assertTrue(cur.getLabel().equals(environment.getRunningVersion())); assertEquals(cur.getLabel(), environment.getRunningVersion());
assertTrue(lat.getLabel().equals("1.1")); assertEquals("1.1", lat.getLabel());
try { try {
facade.getVersionMetadata("invalidversion").join(); facade.getVersionMetadata("invalidversion").join();

View file

@ -1,22 +1,22 @@
package jarupdater; package jarupdater;
import java.io.File; import eu.m724.jarupdater.download.Downloader;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Path;
import java.security.SignatureException; import java.security.SignatureException;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException; import java.util.concurrent.CompletionException;
import eu.m724.jarupdater.download.Downloader;
public class MockDownloader implements Downloader { public class MockDownloader implements Downloader {
@Override @Override
public CompletableFuture<File> downloadAndVerify(String url, String sha256hex) { public CompletableFuture<Path> downloadAndVerify(String url, String sha256hex) {
CompletableFuture<File> future = null; CompletableFuture<Path> future = null;
switch (url) { switch (url) {
case "good": case "good":
future = CompletableFuture.completedFuture(new File("ionlyexistfortesting")); future = CompletableFuture.completedFuture(Path.of("ionlyexistfortesting"));
break; break;
case "invalid": case "invalid":
future = CompletableFuture.failedFuture(new CompletionException(new IOException())); future = CompletableFuture.failedFuture(new CompletionException(new IOException()));
@ -30,10 +30,10 @@ public class MockDownloader implements Downloader {
} }
@Override @Override
public CompletableFuture<Void> install(File source, File destination) { public CompletableFuture<Void> install(Path source, Path destination) {
CompletableFuture<Void> future = null; CompletableFuture<Void> future = null;
if (source.getName().equals("ionlyexistfortesting")) if (source.toString().equals("ionlyexistfortesting"))
future = CompletableFuture.completedFuture(null); future = CompletableFuture.completedFuture(null);
else else
future = CompletableFuture.failedFuture(new CompletionException(new IOException())); future = CompletableFuture.failedFuture(new CompletionException(new IOException()));

View file

@ -1,9 +1,9 @@
package jarupdater; package jarupdater;
import eu.m724.jarupdater.live.MetadataDAO; import eu.m724.jarupdater.live.MetadataDAO;
import eu.m724.jarupdater.object.NoSuchVersionException;
import eu.m724.jarupdater.object.Version; import eu.m724.jarupdater.object.Version;
import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
@ -20,20 +20,18 @@ public class MockMetadataDAO implements MetadataDAO {
@Override @Override
public CompletableFuture<Version> getMetadata(String channel, String versionLabel) { public CompletableFuture<Version> getMetadata(String channel, String versionLabel) {
Version version = null; 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) { switch (versionLabel) {
case "1.0": case "1.0":
version = new Version(1, 100, "1.0", fileUrl, null, "dd3822ed965b2820aa0800f04b86f26d0b656fca3b97ddd264046076f81e3a24"); version = new Version(1, 100, "1.0", fileUrl, null, "dd3822ed965b2820aa0800f04b86f26d0b656fca3b97ddd264046076f81e3a24");
break; break;
case "1.1": case "1.1":
version = new Version(2, 200, "1.1", fileUrl, null,"4d59994f607b89987d5bff430a4229edbbb045b3da9eaf1c63fa8e5fb208d824"); case "latest":
break; version = new Version(2, 200, "1.1", fileUrl, null,"4d59994f607b89987d5bff430a4229edbbb045b3da9eaf1c63fa8e5fb208d824");
case "latest": break;
version = new Version(2, 200, "1.1", fileUrl, null, "4d59994f607b89987d5bff430a4229edbbb045b3da9eaf1c63fa8e5fb208d824"); default:
break; return CompletableFuture.failedFuture(new CompletionException(new NoSuchVersionException(channel, versionLabel)));
default:
return CompletableFuture.failedFuture(new CompletionException(new IOException("no such version")));
} }
return CompletableFuture.completedFuture(version); return CompletableFuture.completedFuture(version);