parent
87aaadbbd4
commit
61362336a9
35 changed files with 920 additions and 537 deletions
1
.idea/compiler.xml
generated
1
.idea/compiler.xml
generated
|
@ -6,6 +6,7 @@
|
|||
<sourceOutputDir name="target/generated-sources/annotations" />
|
||||
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
|
||||
<outputRelativeToContentRoot value="true" />
|
||||
<module name="jarupdater" />
|
||||
</profile>
|
||||
</annotationProcessing>
|
||||
<bytecodeTargetLevel>
|
||||
|
|
2
.idea/misc.xml
generated
2
.idea/misc.xml
generated
|
@ -8,5 +8,5 @@
|
|||
</list>
|
||||
</option>
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="temurin-11" project-jdk-type="JavaSDK" />
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="temurin-17" project-jdk-type="JavaSDK" />
|
||||
</project>
|
5
pom.xml
5
pom.xml
|
@ -5,7 +5,7 @@
|
|||
<version>0.2.1-SNAPSHOT</version>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.release>11</maven.compiler.release>
|
||||
<maven.compiler.release>17</maven.compiler.release>
|
||||
|
||||
<jarsigner.keystore>${project.basedir}/testkeystore.jks</jarsigner.keystore>
|
||||
<jarsigner.alias>testkey</jarsigner.alias>
|
||||
|
@ -45,11 +45,12 @@
|
|||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-release-plugin</artifactId>
|
||||
<version>3.1.1</version>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jarsigner-plugin</artifactId>
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
package eu.m724.jarupdater.download;
|
||||
|
||||
/**
|
||||
* Represents download progress in a point of time
|
||||
*
|
||||
* @param downloadedBytes The number of bytes already downloaded
|
||||
* @param fileSizeBytes The number of bytes the downloaded file has. -1 if unknown
|
||||
*/
|
||||
public record DownloadProgress(
|
||||
long downloadedBytes,
|
||||
long fileSizeBytes
|
||||
) {
|
||||
/**
|
||||
* Creates a new DownloadProgress with the downloaded value incremented by <code>read</code> bytes.
|
||||
* @param read The bytes now read
|
||||
* @return The new DownloadProgress
|
||||
*/
|
||||
public DownloadProgress plus(int read) {
|
||||
return new DownloadProgress(downloadedBytes + read, fileSizeBytes);
|
||||
}
|
||||
}
|
|
@ -1,22 +1,14 @@
|
|||
package eu.m724.jarupdater.download;
|
||||
|
||||
import java.net.URI;
|
||||
import java.nio.file.Path;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public interface Downloader {
|
||||
/**
|
||||
* 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<Path> downloadAndVerify(String url, String sha256hex);
|
||||
|
||||
/**
|
||||
* 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<Void> install(Path source, Path destination);
|
||||
CompletableFuture<Path> downloadAndVerify(URI uri, byte[] expectedHash, Consumer<DownloadProgress> progressConsumer);
|
||||
|
||||
default CompletableFuture<Path> downloadAndVerify(URI uri, byte[] expectedHash) {
|
||||
return downloadAndVerify(uri, expectedHash, progress -> {});
|
||||
}
|
||||
}
|
||||
|
|
19
src/main/java/eu/m724/jarupdater/download/HTTPException.java
Normal file
19
src/main/java/eu/m724/jarupdater/download/HTTPException.java
Normal file
|
@ -0,0 +1,19 @@
|
|||
package eu.m724.jarupdater.download;
|
||||
|
||||
import java.net.http.HttpResponse;
|
||||
|
||||
/**
|
||||
* An exception holding a {@link HttpResponse}, thrown when an HTTP request completed unsuccessfully.
|
||||
*/
|
||||
public class HTTPException extends Exception {
|
||||
private final HttpResponse<?> response;
|
||||
|
||||
public HTTPException(String message, HttpResponse<?> response) {
|
||||
super(message);
|
||||
this.response = response;
|
||||
}
|
||||
|
||||
public HttpResponse<?> getResponse() {
|
||||
return response;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
package eu.m724.jarupdater.download;
|
||||
|
||||
import java.util.HexFormat;
|
||||
|
||||
/**
|
||||
* Thrown when the file couldn't be verified due to unexpected fileHash.
|
||||
*/
|
||||
public class HashMismatchException extends Exception {
|
||||
private final byte[] expectedHash;
|
||||
private final byte[] actualHash;
|
||||
|
||||
public HashMismatchException(byte[] expectedHash, byte[] actualHash) {
|
||||
super("Expected fileHash != Actual fileHash");
|
||||
|
||||
this.expectedHash = expectedHash;
|
||||
this.actualHash = actualHash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the expected SHA-256 fileHash as a byte array.
|
||||
*
|
||||
* @return The expected SHA-256 fileHash as a byte array.
|
||||
*/
|
||||
public byte[] getExpectedHash() {
|
||||
return expectedHash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the actual SHA-256 fileHash as a byte array.
|
||||
*
|
||||
* @return The actual SHA-256 fileHash as a byte array.
|
||||
*/
|
||||
public byte[] getActualHash() {
|
||||
return actualHash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the expected SHA-256 fileHash in a String hex representation.
|
||||
*
|
||||
* @return The expected SHA-256 fileHash in a String hex representation.
|
||||
*/
|
||||
public String getExpectedHashDecoded() {
|
||||
return HexFormat.of().formatHex(expectedHash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the actual SHA-256 fileHash in a String hex representation.
|
||||
*
|
||||
* @return The actual SHA-256 fileHash in a String hex representation.
|
||||
*/
|
||||
public String getActualHashDecoded() {
|
||||
return HexFormat.of().formatHex(actualHash);
|
||||
}
|
||||
}
|
|
@ -1,93 +0,0 @@
|
|||
package eu.m724.jarupdater.download;
|
||||
|
||||
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;
|
||||
import java.net.http.HttpClient.Redirect;
|
||||
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;
|
||||
import java.security.SignatureException;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
|
||||
public class SimpleDownloader implements Downloader {
|
||||
private final String branding;
|
||||
|
||||
public SimpleDownloader(String branding) {
|
||||
this.branding = branding;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Path> downloadAndVerify(String url, String sha256hex) { // TODO progress?
|
||||
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(url))
|
||||
.header("User-Agent", "ju/1") // jar updater v1
|
||||
.build();
|
||||
|
||||
CompletableFuture<HttpResponse<InputStream>> responseFuture =
|
||||
HttpClient.newBuilder()
|
||||
.followRedirects(Redirect.NORMAL)
|
||||
.proxy(ProxySelector.getDefault()).build().
|
||||
sendAsync(request, BodyHandlers.ofInputStream());
|
||||
|
||||
|
||||
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<Void> install(Path source, Path destination) {
|
||||
return CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
Files.move(source, destination, StandardCopyOption.REPLACE_EXISTING);
|
||||
} catch (IOException e) {
|
||||
throw new CompletionException(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static byte[] hexStringToByteArray(String s) {
|
||||
int len = s.length();
|
||||
byte[] data = new byte[len / 2];
|
||||
for (int i = 0; i < len; i += 2) {
|
||||
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
|
||||
+ Character.digit(s.charAt(i + 1), 16));
|
||||
}
|
||||
return data;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,234 @@
|
|||
package eu.m724.jarupdater.download;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
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.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* A {@link Downloader} that downloads by HTTP.
|
||||
*/
|
||||
public class SimpleHTTPDownloader implements Downloader {
|
||||
private final String branding;
|
||||
private final HttpClient httpClient;
|
||||
private final int bufferSizeBytes;
|
||||
private final String hashingAlgorithm;
|
||||
|
||||
private SimpleHTTPDownloader(String branding, HttpClient httpClient, int bufferSizeBytes, String hashingAlgorithm) {
|
||||
this.branding = branding;
|
||||
this.httpClient = httpClient;
|
||||
this.bufferSizeBytes = bufferSizeBytes;
|
||||
this.hashingAlgorithm = hashingAlgorithm;
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads a file from the specified URI and verifies it.<br>
|
||||
* The future can throw:
|
||||
* <ul>
|
||||
* <li>{@link IOException} - multiple possible reasons</li>
|
||||
* <li>{@link HashMismatchException} - when the fileHash of the downloaded file doesn't match the expected fileHash</li>
|
||||
* <li>{@link InterruptedException} - when the download is interrupted</li>
|
||||
* <li>{@link HTTPException} - when the response's status code is not 200</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param uri The file URI
|
||||
* @param expectedHash The expected SHA-256 fileHash
|
||||
* @param progressConsumer A consumer, which consumes download progress
|
||||
* @return A future
|
||||
*/
|
||||
@Override
|
||||
public CompletableFuture<Path> downloadAndVerify(URI uri, byte[] expectedHash, Consumer<DownloadProgress> progressConsumer) {
|
||||
HttpRequest request = HttpRequest.newBuilder(uri)
|
||||
.header("User-Agent", "jarupdater/1.0 (%s)".formatted(branding))
|
||||
.build();
|
||||
|
||||
CompletableFuture<HttpResponse<InputStream>> responseFuture =
|
||||
httpClient.sendAsync(request, BodyHandlers.ofInputStream());
|
||||
|
||||
return responseFuture.thenApply(response -> {
|
||||
Path tempFile = null;
|
||||
|
||||
try {
|
||||
if (response.statusCode() != 200) {
|
||||
throw new HTTPException("Unexpected status code: " + response.statusCode(), response);
|
||||
}
|
||||
|
||||
tempFile = Files.createTempFile(branding, ".jar.download");
|
||||
MessageDigest messageDigest = MessageDigest.getInstance(hashingAlgorithm);
|
||||
|
||||
long fileSizeBytes = response.headers().firstValueAsLong("Content-Length").orElse(-1);
|
||||
DownloadProgress downloadProgress = new DownloadProgress(0, fileSizeBytes);
|
||||
|
||||
byte[] buffer = new byte[bufferSizeBytes];
|
||||
|
||||
try (
|
||||
InputStream bodyStream = response.body();
|
||||
OutputStream tempFileOutputStream = Files.newOutputStream(tempFile);
|
||||
) {
|
||||
int bytesRead;
|
||||
while ((bytesRead = bodyStream.read(buffer)) != -1) {
|
||||
messageDigest.update(buffer, 0, bytesRead);
|
||||
tempFileOutputStream.write(buffer, 0, bytesRead);
|
||||
|
||||
if (progressConsumer != null) {
|
||||
downloadProgress = downloadProgress.plus(bytesRead);
|
||||
progressConsumer.accept(downloadProgress);
|
||||
}
|
||||
|
||||
if (Thread.currentThread().isInterrupted()) {
|
||||
throw new InterruptedException("Interrupted while downloading");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
byte[] fileHash = messageDigest.digest();
|
||||
if (!Arrays.equals(expectedHash, fileHash)) {
|
||||
throw new HashMismatchException(expectedHash, fileHash);
|
||||
}
|
||||
} catch (IOException | NoSuchAlgorithmException | HashMismatchException | InterruptedException | HTTPException e) {
|
||||
if (tempFile != null) {
|
||||
try {
|
||||
Files.deleteIfExists(tempFile);
|
||||
} catch (IOException ex) {
|
||||
e.addSuppressed(ex);
|
||||
}
|
||||
}
|
||||
|
||||
throw new CompletionException(e);
|
||||
}
|
||||
|
||||
return tempFile;
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Downloads a file from the specified URI and verifies it.<br>
|
||||
* The future can throw:
|
||||
* <ul>
|
||||
* <li>{@link IOException} - multiple possible reasons</li>
|
||||
* <li>{@link HashMismatchException} - when the fileHash of the downloaded file doesn't match the expected fileHash</li>
|
||||
* <li>{@link InterruptedException} - when the download is interrupted</li>
|
||||
* <li>{@link HTTPException} - when the response's status code is not 200</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param uri The file URI
|
||||
* @param expectedHash The expected SHA-256 fileHash
|
||||
* @return A future
|
||||
*/
|
||||
@Override
|
||||
public CompletableFuture<Path> downloadAndVerify(URI uri, byte[] expectedHash) {
|
||||
// just for a tiny bit of performance over the default
|
||||
return downloadAndVerify(uri, expectedHash, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder class that builds {@link SimpleHTTPDownloader} instances.
|
||||
*/
|
||||
public static class Builder {
|
||||
private String branding;
|
||||
private HttpClient httpClient;
|
||||
private Integer bufferSize;
|
||||
private String hashingAlgorithm;
|
||||
|
||||
/**
|
||||
* Sets the branding of the {@link SimpleHTTPDownloader}.
|
||||
* Branding is used for the temporary file and the HTTP user agent.<br>
|
||||
* The default is a generic <code>jarupdater</code>.
|
||||
*
|
||||
* @param branding The branding
|
||||
* @return This builder instance
|
||||
*/
|
||||
public Builder withBranding(String branding) {
|
||||
if (branding.length() > 100) {
|
||||
throw new IllegalArgumentException("Branding must be at most 100 characters long");
|
||||
}
|
||||
|
||||
if (branding.isBlank()) {
|
||||
throw new IllegalArgumentException("Branding mustn't be blank");
|
||||
}
|
||||
|
||||
this.branding = branding;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link HttpClient} instance used in the {@link SimpleHTTPDownloader}.
|
||||
*
|
||||
* @param httpClient The {@link HttpClient}
|
||||
* @return This builder instance
|
||||
*/
|
||||
public Builder withHttpClient(HttpClient httpClient) {
|
||||
this.httpClient = httpClient;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the buffer size used while downloading using the {@link SimpleHTTPDownloader}.
|
||||
*
|
||||
* @param bufferSize The buffer size in bytes
|
||||
* @return This builder instance
|
||||
*/
|
||||
public Builder withBufferSize(int bufferSize) {
|
||||
if (bufferSize <= 0) {
|
||||
throw new IllegalArgumentException("Buffer size must be larger than 0");
|
||||
}
|
||||
|
||||
this.bufferSize = bufferSize;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the hashing algorithm used to verify files downloaded with {@link SimpleHTTPDownloader}.<br>
|
||||
* For more information, see {@link MessageDigest#getInstance(String)}
|
||||
*
|
||||
* @param hashingAlgorithm The hashing algorithm
|
||||
* @return This builder instance
|
||||
*/
|
||||
public Builder withHashingAlgorithm(String hashingAlgorithm) {
|
||||
try {
|
||||
MessageDigest.getInstance(hashingAlgorithm);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
|
||||
this.hashingAlgorithm = hashingAlgorithm;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a new {@link SimpleHTTPDownloader} instance using this builder's settings.
|
||||
*
|
||||
* @return The new {@link SimpleHTTPDownloader} instance using this builder's settings.
|
||||
*/
|
||||
public SimpleHTTPDownloader build() {
|
||||
if (branding == null) this.branding = Defaults.DEFAULT_BRANDING;
|
||||
if (bufferSize == null) this.bufferSize = Defaults.DEFAULT_BUFFER_SIZE;
|
||||
if (hashingAlgorithm == null) this.hashingAlgorithm = Defaults.DEFAULT_HASHING_ALGORITHM;
|
||||
|
||||
if (httpClient == null) {
|
||||
this.httpClient = HttpClient.newBuilder()
|
||||
.followRedirects(HttpClient.Redirect.NORMAL)
|
||||
.build();
|
||||
}
|
||||
|
||||
return new SimpleHTTPDownloader(branding, httpClient, bufferSize, hashingAlgorithm);
|
||||
}
|
||||
}
|
||||
|
||||
private static class Defaults {
|
||||
private static final String DEFAULT_BRANDING = "jarupdater";
|
||||
private static final int DEFAULT_BUFFER_SIZE = 8192;
|
||||
private static final String DEFAULT_HASHING_ALGORITHM = "SHA-256";
|
||||
}
|
||||
}
|
|
@ -2,19 +2,10 @@ package eu.m724.jarupdater.environment;
|
|||
|
||||
import java.nio.file.Path;
|
||||
|
||||
public class ConstantEnvironment implements Environment {
|
||||
private final String runningVersion;
|
||||
private final String channel;
|
||||
private final Path runningJarFilePath;
|
||||
|
||||
public ConstantEnvironment(String runningVersion, String channel, Path runningJarFilePath) {
|
||||
this.runningVersion = runningVersion;
|
||||
this.channel = channel;
|
||||
this.runningJarFilePath = runningJarFilePath;
|
||||
}
|
||||
public record ConstantEnvironment(
|
||||
String runningVersion,
|
||||
String updateChannel,
|
||||
Path runningJarFilePath
|
||||
) implements Environment {
|
||||
|
||||
@Override public String getRunningVersion() { return runningVersion; }
|
||||
@Override public String getChannel() { return channel; }
|
||||
@Override public Path getRunningJarFilePath() { return runningJarFilePath; }
|
||||
|
||||
}
|
||||
|
|
|
@ -5,19 +5,22 @@ import java.nio.file.Path;
|
|||
public interface Environment {
|
||||
/**
|
||||
* Get the version of running software.
|
||||
*
|
||||
* @return The running software version
|
||||
*/
|
||||
String getRunningVersion();
|
||||
String runningVersion();
|
||||
|
||||
/**
|
||||
* Get the configured update channel.
|
||||
*
|
||||
* @return The configured update channel
|
||||
*/
|
||||
String getChannel();
|
||||
String updateChannel();
|
||||
|
||||
/**
|
||||
* Get the path to the running JAR file.
|
||||
*
|
||||
* @return The path to the running JAR file.
|
||||
*/
|
||||
Path getRunningJarFilePath();
|
||||
Path runningJarFilePath();
|
||||
}
|
||||
|
|
20
src/main/java/eu/m724/jarupdater/install/FileInstaller.java
Normal file
20
src/main/java/eu/m724/jarupdater/install/FileInstaller.java
Normal file
|
@ -0,0 +1,20 @@
|
|||
package eu.m724.jarupdater.install;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
|
||||
public class FileInstaller implements Installer {
|
||||
public CompletableFuture<Void> install(Path source, Path destination) {
|
||||
return CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
Files.move(source, destination, StandardCopyOption.REPLACE_EXISTING);
|
||||
} catch (IOException e) {
|
||||
throw new CompletionException(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
15
src/main/java/eu/m724/jarupdater/install/Installer.java
Normal file
15
src/main/java/eu/m724/jarupdater/install/Installer.java
Normal file
|
@ -0,0 +1,15 @@
|
|||
package eu.m724.jarupdater.install;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public interface Installer {
|
||||
/**
|
||||
* 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<Void> install(Path source, Path destination);
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
package eu.m724.jarupdater.live;
|
||||
|
||||
public class GiteaMetadataDAO extends HttpMetadataDAO {
|
||||
public GiteaMetadataDAO(String url, String branch) {
|
||||
super(url + "/raw/branch/" + branch + "/data/");
|
||||
}
|
||||
}
|
|
@ -1,73 +1,196 @@
|
|||
package eu.m724.jarupdater.live;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import eu.m724.jarupdater.object.NoSuchVersionException;
|
||||
import eu.m724.jarupdater.object.Version;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
import eu.m724.jarupdater.download.HTTPException;
|
||||
import eu.m724.jarupdater.updater.NoSuchVersionException;
|
||||
import eu.m724.jarupdater.updater.Version;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.ProxySelector;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* A {@link MetadataDAO} which downloads from HTTP.
|
||||
*/
|
||||
public class HttpMetadataDAO implements MetadataDAO {
|
||||
private final String url;
|
||||
private final URI uri;
|
||||
private final String branding;
|
||||
private final HttpClient httpClient;
|
||||
private final Gson gson;
|
||||
|
||||
public HttpMetadataDAO(String url) {
|
||||
this.url = url;
|
||||
private HttpMetadataDAO(URI uri, String branding, HttpClient httpClient, Gson gson) {
|
||||
this.uri = uri;
|
||||
this.branding = branding;
|
||||
this.httpClient = httpClient;
|
||||
this.gson = gson;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the available channels.<br>
|
||||
*
|
||||
* @return A future, which can throw:
|
||||
* <ul>
|
||||
* <li>{@link HTTPException} - if the request was unsuccessful</li>
|
||||
* </ul>
|
||||
*/
|
||||
@Override
|
||||
public CompletableFuture<List<String>> getChannels() {
|
||||
String url = getFileUrl("channels.txt");
|
||||
URI uri = getFileUri("channels.txt");
|
||||
|
||||
return makeRequest(url).thenApply(response -> {
|
||||
if (response.statusCode() != 200)
|
||||
throw new CompletionException(new IOException("Server returned status code " + response.statusCode()));
|
||||
return makeRequest(uri).thenApply(response -> {
|
||||
if (response.statusCode() != 200) {
|
||||
throw new CompletionException(new HTTPException("Unexpected status code: " + response.statusCode(), response));
|
||||
}
|
||||
|
||||
return response.body().lines().collect(Collectors.toList());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the metadata of the specified version.
|
||||
*
|
||||
* @param channel The release channel
|
||||
* @param versionName The version name
|
||||
* @return A future, which can throw:
|
||||
* <ul>
|
||||
* <li>{@link NoSuchVersionException} - if there's no such version in this channel</li>
|
||||
* <li>{@link HTTPException} - if the request was unsuccessful</li>
|
||||
* <li>{@link JsonSyntaxException} - if the response is not valid JSON</li>
|
||||
* </ul>
|
||||
*/
|
||||
@Override
|
||||
public CompletableFuture<Version> getMetadata(String channel, String versionLabel) {
|
||||
String url = getFileUrl(channel, versionLabel, "meta-v1.json");
|
||||
public CompletableFuture<Version> getMetadata(String channel, String versionName) {
|
||||
URI uri = getFileUri(channel, versionName, "meta-v1.json");
|
||||
|
||||
return makeRequest(url).thenApply(response -> {
|
||||
return makeRequest(uri).thenApply(response -> {
|
||||
if (response.statusCode() == 404) {
|
||||
throw new CompletionException(new NoSuchVersionException(channel, versionLabel));
|
||||
throw new CompletionException(new NoSuchVersionException(channel, versionName));
|
||||
}
|
||||
|
||||
if (response.statusCode() != 200) {
|
||||
throw new CompletionException(new IOException("Server returned status code " + response.statusCode()));
|
||||
throw new CompletionException(new HTTPException("Unexpected status code: " + response.statusCode(), response));
|
||||
}
|
||||
|
||||
return new Gson().fromJson(response.body(), Version.class);
|
||||
try {
|
||||
return gson.fromJson(response.body(), Version.class);
|
||||
} catch (JsonSyntaxException e) {
|
||||
throw new CompletionException(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private CompletableFuture<HttpResponse<String>> makeRequest(String url) {
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(url))
|
||||
.header("User-Agent", "ju/1") // jar updater v1
|
||||
private CompletableFuture<HttpResponse<String>> makeRequest(URI uri) {
|
||||
HttpRequest request = HttpRequest.newBuilder(uri)
|
||||
.header("User-Agent", "jarupdater/1.0 (%s)".formatted(branding))
|
||||
.build();
|
||||
|
||||
HttpClient.Builder httpClientBuilder = HttpClient.newBuilder()
|
||||
.followRedirects(HttpClient.Redirect.NORMAL)
|
||||
.proxy(ProxySelector.getDefault());
|
||||
|
||||
HttpClient httpClient = httpClientBuilder.build();
|
||||
return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString());
|
||||
}
|
||||
|
||||
private String getFileUrl(String... paths) {
|
||||
return url + "/" + String.join("/", paths);
|
||||
private URI getFileUri(String... paths) {
|
||||
return uri.resolve(String.join("/", paths));
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder class that builds {@link HttpMetadataDAO} instances.
|
||||
*/
|
||||
public static class Builder {
|
||||
private URI uri;
|
||||
private String branding;
|
||||
private HttpClient httpClient;
|
||||
private Gson gson;
|
||||
|
||||
/**
|
||||
* Sets the base URI.<br>
|
||||
* This cannot be null.
|
||||
*
|
||||
* @param uri The base URI
|
||||
* @return This builder instance
|
||||
* @throws NullPointerException If <code>uri</code> is null
|
||||
*/
|
||||
public Builder withUri(URI uri) {
|
||||
Objects.requireNonNull(uri, "URI cannot be null");
|
||||
|
||||
this.uri = uri;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the branding.
|
||||
* Branding is used for the HTTP user agent.<br>
|
||||
* If null, will use the default.
|
||||
*
|
||||
* @param branding The branding
|
||||
* @return This builder instance
|
||||
*/
|
||||
public Builder withBranding(String branding) {
|
||||
if (branding.length() > 100) {
|
||||
throw new IllegalArgumentException("Branding must be at most 100 characters long");
|
||||
}
|
||||
|
||||
if (branding.isBlank()) {
|
||||
throw new IllegalArgumentException("Branding mustn't be blank");
|
||||
}
|
||||
|
||||
this.branding = branding;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link HttpClient} instance.<br>
|
||||
* If null, will use the default.
|
||||
*
|
||||
* @param httpClient The {@link HttpClient} instance
|
||||
* @return This builder instance
|
||||
*/
|
||||
public Builder withHttpClient(HttpClient httpClient) {
|
||||
this.httpClient = httpClient;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link Gson} instance used to parse JSON responses.<br>
|
||||
* If null, will use the default.
|
||||
*
|
||||
* @param gson The {@link Gson} instance
|
||||
* @return This builder instance
|
||||
*/
|
||||
public Builder withGson(Gson gson) {
|
||||
this.gson = gson;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a new {@link HttpMetadataDAO} instance using this builder's settings.
|
||||
*
|
||||
* @return The new {@link HttpMetadataDAO} instance using this builder's settings.
|
||||
* @throws IllegalArgumentException If no URI set. See {@link Builder#withUri(URI)}
|
||||
*/
|
||||
public HttpMetadataDAO build() {
|
||||
Objects.requireNonNull(uri, "URI cannot be null");
|
||||
|
||||
if (branding == null) {
|
||||
this.branding = "jarupdater";
|
||||
}
|
||||
|
||||
if (httpClient == null) {
|
||||
this.httpClient = HttpClient.newBuilder()
|
||||
.followRedirects(HttpClient.Redirect.NORMAL)
|
||||
.build();
|
||||
}
|
||||
|
||||
if (gson == null) {
|
||||
this.gson = new Gson();
|
||||
}
|
||||
|
||||
return new HttpMetadataDAO(uri, branding, httpClient, gson);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,22 +1,22 @@
|
|||
package eu.m724.jarupdater.live;
|
||||
|
||||
import eu.m724.jarupdater.updater.Version;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import eu.m724.jarupdater.object.Version;
|
||||
|
||||
public interface MetadataDAO {
|
||||
/**
|
||||
* get available channels online
|
||||
* @return a future which can throw ioexception
|
||||
* Gets the available channels.
|
||||
*
|
||||
* @return A future that results in a list of the available channels.
|
||||
*/
|
||||
CompletableFuture<List<String>> getChannels();
|
||||
|
||||
|
||||
/**
|
||||
* get metadata of a version in channel
|
||||
* @param channel the channel
|
||||
* @param versionLabel basically version number
|
||||
* @return a future which can throw ioexcpeitons or nosuchfilexxception
|
||||
* Gets the metadata of the specified version in the specified channel.
|
||||
*
|
||||
* @return A future that results in a list of the available channels.
|
||||
*/
|
||||
CompletableFuture<Version> getMetadata(String channel, String versionLabel);
|
||||
CompletableFuture<Version> getMetadata(String channel, String versionName);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package eu.m724.jarupdater.live;
|
||||
|
||||
import eu.m724.jarupdater.environment.Environment;
|
||||
import eu.m724.jarupdater.object.Version;
|
||||
import eu.m724.jarupdater.updater.Version;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
@ -9,34 +9,43 @@ import java.util.concurrent.CompletableFuture;
|
|||
public class MetadataFacade {
|
||||
private final Environment environment;
|
||||
private final MetadataDAO metadataDao;
|
||||
|
||||
private CompletableFuture<List<String>> channels = null;
|
||||
|
||||
public MetadataFacade(Environment environment, MetadataDAO metadataDao) {
|
||||
this.environment = environment;
|
||||
this.metadataDao = metadataDao;
|
||||
}
|
||||
|
||||
public CompletableFuture<List<String>> listChannels() {
|
||||
if (channels == null)
|
||||
channels = metadataDao.getChannels();
|
||||
|
||||
return channels;
|
||||
public CompletableFuture<List<String>> getChannels() {
|
||||
return metadataDao.getChannels();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the latest version's metadata.<br>
|
||||
* For possible exceptions, consult {@link MetadataDAO#getMetadata(String, String)} in your implementation of {@link MetadataDAO}
|
||||
*
|
||||
* @return A future which returns the latest version's metadata
|
||||
*/
|
||||
public CompletableFuture<Version> getLatestVersionMetadata() {
|
||||
return getVersionMetadata("latest", true);
|
||||
return getVersionMetadata("latest");
|
||||
}
|
||||
|
||||
public CompletableFuture<Version> getCurrentVersionMetadata() {
|
||||
return getVersionMetadata(environment.getRunningVersion());
|
||||
|
||||
/**
|
||||
* Get the running (from the {@link Environment}) version's metadata.<br>
|
||||
* For possible exceptions, consult {@link MetadataDAO#getMetadata(String, String)} in your implementation of {@link MetadataDAO}
|
||||
*
|
||||
* @return A future which returns the running version's metadata
|
||||
*/
|
||||
public CompletableFuture<Version> getRunningVersionMetadata() {
|
||||
return getVersionMetadata(environment.runningVersion());
|
||||
}
|
||||
|
||||
public CompletableFuture<Version> getVersionMetadata(String version, boolean ignoreCache) {
|
||||
return metadataDao.getMetadata(environment.getChannel(), version);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the specified version's metadata.<br>
|
||||
* For possible exceptions, consult {@link MetadataDAO#getMetadata(String, String)} in your implementation of {@link MetadataDAO}
|
||||
*
|
||||
* @return A future which returns the specified version's metadata
|
||||
*/
|
||||
public CompletableFuture<Version> getVersionMetadata(String version) {
|
||||
return getVersionMetadata(version, false);
|
||||
return metadataDao.getMetadata(environment.updateChannel(), version);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,84 +0,0 @@
|
|||
package eu.m724.jarupdater.object;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class Version {
|
||||
/**
|
||||
* metadata file version
|
||||
*/
|
||||
public static final int SPEC = 1;
|
||||
|
||||
public Version(int id, long timestamp, String label, String fileUrl, String changelogUrl, String sha256) {
|
||||
this.id = id;
|
||||
this.timestamp = timestamp;
|
||||
this.label = label;
|
||||
this.fileUrl = fileUrl;
|
||||
this.changelogUrl = changelogUrl;
|
||||
this.sha256 = sha256;
|
||||
}
|
||||
|
||||
private final int id;
|
||||
private final long timestamp;
|
||||
private final String label;
|
||||
@SerializedName("file")
|
||||
private final String fileUrl;
|
||||
@SerializedName("changelog")
|
||||
private final String changelogUrl;
|
||||
private final String sha256;
|
||||
|
||||
/**
|
||||
* version id. increments with each version
|
||||
*/
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* The release time of this version in Unix seconds
|
||||
*/
|
||||
public long getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* The label / name of this version, like 1.0.0
|
||||
*/
|
||||
public String getLabel() {
|
||||
return label;
|
||||
}
|
||||
|
||||
/**
|
||||
* URL of this version's downloadable JAR file
|
||||
*/
|
||||
public String getFileUrl() {
|
||||
return fileUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* The URL to this version's changelog
|
||||
*/
|
||||
public String getChangelogUrl() {
|
||||
return changelogUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* SHA-256 hash of this version's JAR file
|
||||
*/
|
||||
public String getSha256() {
|
||||
return sha256;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (!(o instanceof Version)) return false;
|
||||
Version version = (Version) o;
|
||||
return id == version.id && timestamp == version.timestamp && Objects.equals(label, version.label) && Objects.equals(fileUrl, version.fileUrl) && Objects.equals(changelogUrl, version.changelogUrl) && Objects.equals(sha256, version.sha256);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(id, timestamp, label, fileUrl, changelogUrl, sha256);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package eu.m724.jarupdater.updater;
|
||||
|
||||
/**
|
||||
* Thrown when trying to install an update, but no update was downloaded.
|
||||
*/
|
||||
public class NoDownloadedUpdateException extends Exception {
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package eu.m724.jarupdater.object;
|
||||
package eu.m724.jarupdater.updater;
|
||||
|
||||
import java.io.IOException;
|
||||
|
|
@ -1,111 +1,159 @@
|
|||
package eu.m724.jarupdater.updater;
|
||||
|
||||
import eu.m724.jarupdater.download.DownloadProgress;
|
||||
import eu.m724.jarupdater.download.Downloader;
|
||||
import eu.m724.jarupdater.environment.Environment;
|
||||
import eu.m724.jarupdater.install.Installer;
|
||||
import eu.m724.jarupdater.live.MetadataDAO;
|
||||
import eu.m724.jarupdater.live.MetadataFacade;
|
||||
import eu.m724.jarupdater.object.Version;
|
||||
import eu.m724.jarupdater.verify.VerificationException;
|
||||
import eu.m724.jarupdater.verify.Verifier;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.NoSuchFileException;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Path;
|
||||
import java.util.HexFormat;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Represents an updater.
|
||||
*/
|
||||
public class Updater {
|
||||
protected final Environment environment;
|
||||
protected final MetadataFacade metadataProvider;
|
||||
protected final Downloader downloader;
|
||||
protected final Installer installer;
|
||||
protected final Verifier verifier;
|
||||
|
||||
protected CompletableFuture<Path> downloadFuture;
|
||||
|
||||
public Updater(Environment environment, MetadataFacade metadataProvider, Downloader downloader, Verifier verifier) {
|
||||
public Updater(Environment environment, MetadataFacade metadataProvider, Downloader downloader, Installer installer, Verifier verifier) {
|
||||
this.environment = environment;
|
||||
this.metadataProvider = metadataProvider;
|
||||
this.downloader = downloader;
|
||||
this.verifier = verifier;
|
||||
this.installer = installer;
|
||||
this.verifier = verifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* List all channels
|
||||
* @return A future which returns the list of all channels
|
||||
* List available update channels.<br>
|
||||
* For possible exceptions, consult {@link MetadataDAO#getMetadata(String, String)} in your implementation of {@link MetadataDAO}
|
||||
*
|
||||
* @return A future which returns the list of the available channels
|
||||
*/
|
||||
public CompletableFuture<List<String>> listChannels() {
|
||||
return metadataProvider.listChannels();
|
||||
public CompletableFuture<List<String>> getChannels() {
|
||||
return metadataProvider.getChannels();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the configured update channel
|
||||
* @return the configured update channel
|
||||
* Gets the update channel.
|
||||
*
|
||||
* @return the update channel
|
||||
*/
|
||||
public String getChannel() {
|
||||
return environment.getChannel();
|
||||
public String getUpdateChannel() {
|
||||
return environment.updateChannel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest available version
|
||||
* @return A future which returns latest version metadata
|
||||
* Get the latest available version. It might even be the running version.<br>
|
||||
* For possible exceptions, consult {@link MetadataDAO#getMetadata(String, String)} in your implementation of {@link MetadataDAO}
|
||||
*
|
||||
* @return A future which returns the latest version's metadata
|
||||
*/
|
||||
public CompletableFuture<Version> getLatestVersion() {
|
||||
CompletableFuture<Version> currentVersionFuture = metadataProvider.getCurrentVersionMetadata();
|
||||
return metadataProvider.getLatestVersionMetadata();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get information about this version.<br>
|
||||
* For possible exceptions, consult {@link MetadataDAO#getMetadata(String, String)} in your implementation of {@link MetadataDAO}
|
||||
*
|
||||
* @return A future which returns the running version's metadata
|
||||
*/
|
||||
public CompletableFuture<Version> getRunningVersion() {
|
||||
return metadataProvider.getRunningVersionMetadata();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the running version is up to date.
|
||||
* For possible exceptions, consult {@link MetadataDAO#getMetadata(String, String)} in your implementation of {@link MetadataDAO}
|
||||
*
|
||||
* @return A future which returns whether the running version is up to date.
|
||||
*/
|
||||
public CompletableFuture<Boolean> isUpToDate() {
|
||||
CompletableFuture<Version> runningVersionFuture = metadataProvider.getRunningVersionMetadata();
|
||||
CompletableFuture<Version> latestVersionFuture = metadataProvider.getLatestVersionMetadata();
|
||||
|
||||
return CompletableFuture.allOf(currentVersionFuture, latestVersionFuture).thenApply(v -> {
|
||||
Version currentVersion = currentVersionFuture.join();
|
||||
|
||||
return CompletableFuture.allOf(runningVersionFuture, latestVersionFuture).thenApply(v -> {
|
||||
Version runningVersion = runningVersionFuture.join();
|
||||
Version latestVersion = latestVersionFuture.join();
|
||||
|
||||
if (currentVersion == null || latestVersion == null)
|
||||
throw new CompletionException(new IOException());
|
||||
|
||||
return latestVersion.getId() > currentVersion.getId() ? latestVersion : null;
|
||||
|
||||
return latestVersion.id() == runningVersion.id();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get information about this version
|
||||
* @return A future which returns current version metadata
|
||||
* Download the latest available version. Doesn't do any version checks, just downloads the latest.<br>
|
||||
* For possible exceptions, consult {@link Downloader#downloadAndVerify(URI, byte[], Consumer)} in your implementation of {@link Downloader}
|
||||
*
|
||||
* @param progressConsumer A consumer which receives progress updates.
|
||||
* @return A future which returns the downloaded file. It can throw:
|
||||
* <ul>
|
||||
* <li>{@link IllegalArgumentException} - If the URI or hash in the metadata was malformed or invalid.</li>
|
||||
* <li>Other exceptions, as mentioned above</li>
|
||||
* </ul>
|
||||
*/
|
||||
public CompletableFuture<Version> getCurrentVersion() {
|
||||
return metadataProvider.getCurrentVersionMetadata();
|
||||
}
|
||||
|
||||
/**
|
||||
* Download the latest available version
|
||||
* @return a future which returns the downloaded file
|
||||
*/
|
||||
public CompletableFuture<Path> downloadLatestVersion() {
|
||||
public CompletableFuture<Path> downloadLatestVersion(Consumer<DownloadProgress> progressConsumer) {
|
||||
CompletableFuture<Version> latestVersionFuture = metadataProvider.getLatestVersionMetadata();
|
||||
|
||||
downloadFuture = latestVersionFuture.thenComposeAsync(latestVersion -> {
|
||||
String url = latestVersion.getFileUrl();
|
||||
String hash = latestVersion.getSha256();
|
||||
this.downloadFuture = latestVersionFuture.thenComposeAsync(latestVersion -> {
|
||||
String url = latestVersion.fileUrl();
|
||||
String hash = latestVersion.fileHash();
|
||||
|
||||
return downloader.downloadAndVerify(url, hash);
|
||||
URI uri = URI.create(url);
|
||||
byte[] hashEncoded = HexFormat.of().parseHex(hash);
|
||||
|
||||
return downloader.downloadAndVerify(uri, hashEncoded, progressConsumer);
|
||||
});
|
||||
|
||||
return downloadFuture;
|
||||
}
|
||||
|
||||
/**
|
||||
* Download the latest available version. Doesn't do any version checks, just downloads the latest.<br>
|
||||
* For possible exceptions, consult {@link Downloader#downloadAndVerify(URI, byte[], Consumer)} in your implementation of {@link Downloader}
|
||||
*
|
||||
* @return A future which returns the downloaded file.
|
||||
*/
|
||||
public CompletableFuture<Path> downloadLatestVersion() {
|
||||
return downloadLatestVersion(progress -> {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs the ALREADY DOWNLOADED latest version<br>
|
||||
* You can call it immediately after {@link #downloadLatestVersion()}
|
||||
* @return a {@link CompletableFuture} that ends after move, it can throw some exceptions.
|
||||
* @throws NoSuchFileException if you didn't download it first
|
||||
* Installs the downloaded latest version.<br>
|
||||
* <strong>Make sure to use {@link #downloadLatestVersion()} before</strong><br>
|
||||
* For possible exceptions, consult {@link Installer#install(Path, Path)} in your implementation of {@link Installer}
|
||||
*
|
||||
* @return A future, which can throw:
|
||||
* <ul>
|
||||
* <li>{@link VerificationException} - If verification failed</li>
|
||||
* <li>Other exceptions, as mentioned above</li>
|
||||
* </ul>
|
||||
*/
|
||||
public CompletableFuture<Void> installLatestVersion() throws NoSuchFileException {
|
||||
if (downloadFuture == null)
|
||||
throw new NoSuchFileException("Download it first");
|
||||
public CompletableFuture<Void> installLatestVersion() {
|
||||
if (downloadFuture == null) {
|
||||
return CompletableFuture.failedFuture(new NoDownloadedUpdateException());
|
||||
}
|
||||
|
||||
return downloadFuture.thenCompose(file -> {
|
||||
try {
|
||||
verifier.verify(file.toString());
|
||||
verifier.verify(file);
|
||||
} catch (VerificationException e) {
|
||||
throw new CompletionException(e);
|
||||
}
|
||||
return downloader.install(file, environment.getRunningJarFilePath());
|
||||
return installer.install(file, environment.runningJarFilePath());
|
||||
});
|
||||
}
|
||||
|
||||
|
|
23
src/main/java/eu/m724/jarupdater/updater/Version.java
Normal file
23
src/main/java/eu/m724/jarupdater/updater/Version.java
Normal file
|
@ -0,0 +1,23 @@
|
|||
package eu.m724.jarupdater.updater;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param id The version ID, which increments with each version.
|
||||
* @param timestamp The release time of this version, in Unix seconds
|
||||
* @param label The label (name) of this version, e.g., 1.0.0
|
||||
* @param fileUrl The URL to the downloadable JAR file of this version
|
||||
* @param changelogUrl The URL to the changelog of this version
|
||||
* @param fileHash The SHA-256 hash of the file <code>fileUrl</code> points to
|
||||
*/
|
||||
public record Version(
|
||||
int id,
|
||||
long timestamp,
|
||||
String label,
|
||||
@SerializedName("file") String fileUrl,
|
||||
@SerializedName("changelog") String changelogUrl,
|
||||
@SerializedName("sha256") String fileHash
|
||||
) {
|
||||
|
||||
}
|
|
@ -2,9 +2,20 @@ package eu.m724.jarupdater.verify;
|
|||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Path;
|
||||
import java.security.CodeSigner;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PublicKey;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.interfaces.RSAPublicKey;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.util.*;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarFile;
|
||||
import java.util.jar.Manifest;
|
||||
|
||||
/**
|
||||
* Utility for verifying downloaded JARs with RSA signatures
|
||||
*/
|
||||
|
@ -12,30 +23,129 @@ public class SignatureVerifier implements Verifier {
|
|||
private final Set<RSAPublicKey> publicKeys = new HashSet<>();
|
||||
|
||||
/**
|
||||
* Load an RSA public key
|
||||
* @param inputStream the input stream of the public key file
|
||||
* Reads and trusts an RSA public key
|
||||
* @param inputStream The input stream to load the RSA public key to trust
|
||||
* @throws IOException If something failed while reading
|
||||
*/
|
||||
public void loadPublicKey(InputStream inputStream) throws IOException {
|
||||
RSAPublicKey publicKey = SignatureVerifierImpl.loadPublicKey(inputStream);
|
||||
public void trustPublicKey(InputStream inputStream) throws IOException {
|
||||
RSAPublicKey publicKey = loadPublicKeyFromInputStream(inputStream);
|
||||
trustPublicKey(publicKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trusts an RSA public key
|
||||
* @param publicKey The RSA public key to trust
|
||||
*/
|
||||
public void trustPublicKey(RSAPublicKey publicKey) {
|
||||
publicKeys.add(publicKey);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* How much public keys loaded
|
||||
* @return amount of public keys loaded
|
||||
* Verify a JAR using the loaded RSA public keys
|
||||
*
|
||||
* @param jarPath The path of the JAR file to verify
|
||||
* @throws VerificationException If something went wrong or the JAR isn't signed with the trusted keys
|
||||
*/
|
||||
public int publicKeysCount() {
|
||||
return publicKeys.size();
|
||||
public void verify(Path jarPath) throws VerificationException {
|
||||
verify(jarPath, publicKeys);
|
||||
}
|
||||
|
||||
private void verify(Path jarPath, Collection<RSAPublicKey> publicKeys) throws VerificationException {
|
||||
try (JarFile jarFile = new JarFile(jarPath.toFile(), true)) {
|
||||
byte[] buffer = new byte[8192];
|
||||
Enumeration<JarEntry> entries = jarFile.entries();
|
||||
|
||||
// Get manifest to check signature files
|
||||
Manifest manifest = jarFile.getManifest();
|
||||
if (manifest == null) {
|
||||
throw new VerificationException("JAR has no manifest");
|
||||
}
|
||||
|
||||
while (entries.hasMoreElements()) {
|
||||
JarEntry entry = entries.nextElement();
|
||||
|
||||
if (entry.isDirectory() || entry.getName().startsWith("META-INF/")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int bytesRead = 0;
|
||||
// Read entry to trigger signature verification
|
||||
try (InputStream inputStream = jarFile.getInputStream(entry)) {
|
||||
while ((bytesRead += inputStream.read(buffer)) != -1) {
|
||||
if (bytesRead > 1024 * 1024 * 100) { // unusual for a public key file to have >100 MiB
|
||||
throw new IOException("File too large: " + entry.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get signers for this entry
|
||||
CodeSigner[] signers = entry.getCodeSigners();
|
||||
if (signers == null || signers.length == 0) {
|
||||
throw new VerificationException("Unsigned entry: " + entry.getName());
|
||||
}
|
||||
|
||||
// Check if any signer's public key matches our RSA key
|
||||
boolean keyMatch = false;
|
||||
|
||||
for (CodeSigner signer : signers) {
|
||||
for (Certificate cert : signer.getSignerCertPath().getCertificates()) {
|
||||
PublicKey certPublicKey = cert.getPublicKey();
|
||||
|
||||
if (certPublicKey instanceof RSAPublicKey rsaKey) {
|
||||
for (RSAPublicKey publicKey : publicKeys) {
|
||||
if (rsaKey.getModulus().equals(publicKey.getModulus()) &&
|
||||
rsaKey.getPublicExponent().equals(publicKey.getPublicExponent())) {
|
||||
keyMatch = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (keyMatch) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!keyMatch) {
|
||||
throw new VerificationException("Entry " + entry.getName() + " signed with unknown key");
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new VerificationException("Verification error: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify a JAR with loaded public keys
|
||||
* Loads an RSA public key from a PEM file
|
||||
*
|
||||
* @param jarPath the path of the JAR file
|
||||
* @throws VerificationException if something went wrong or not signed with known keys
|
||||
* @param keyInputStream inputStream of the public key file
|
||||
* @return {@link RSAPublicKey} instance of the public key
|
||||
* @throws IOException if reading the key input stream failed
|
||||
*/
|
||||
public void verify(String jarPath) throws VerificationException {
|
||||
SignatureVerifierImpl.verifyWithRsaKey(jarPath, publicKeys.toArray(RSAPublicKey[]::new));
|
||||
private RSAPublicKey loadPublicKeyFromInputStream(InputStream keyInputStream) throws IOException {
|
||||
// Read the key file
|
||||
String keyContent = new String(keyInputStream.readAllBytes());
|
||||
|
||||
// Remove PEM headers and newlines
|
||||
keyContent = keyContent.replace("-----BEGIN PUBLIC KEY-----", "")
|
||||
.replace("-----END PUBLIC KEY-----", "")
|
||||
.replaceAll("\\s+", "");
|
||||
|
||||
// Decode the key
|
||||
byte[] keyBytes = Base64.getDecoder().decode(keyContent);
|
||||
|
||||
// Create public key specification
|
||||
X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes);
|
||||
|
||||
// Generate public key
|
||||
try {
|
||||
KeyFactory kf = KeyFactory.getInstance("RSA");
|
||||
return (RSAPublicKey) kf.generatePublic(spec);
|
||||
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,129 +0,0 @@
|
|||
package eu.m724.jarupdater.verify;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.CodeSigner;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.PublicKey;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.interfaces.RSAPublicKey;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.Enumeration;
|
||||
import java.util.List;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarFile;
|
||||
import java.util.jar.Manifest;
|
||||
|
||||
public class SignatureVerifierImpl {
|
||||
/**
|
||||
* Loads an RSA public key from a PEM file
|
||||
*
|
||||
* @param keyInputStream inputStream of the public key file
|
||||
* @return {@link RSAPublicKey} instance of the public key
|
||||
* @throws IOException if reading the key input stream failed
|
||||
*/
|
||||
public static RSAPublicKey loadPublicKey(InputStream keyInputStream) throws IOException {
|
||||
// Read the key file
|
||||
String keyContent = new String(keyInputStream.readAllBytes());
|
||||
|
||||
// Remove PEM headers and newlines
|
||||
keyContent = keyContent.replace("-----BEGIN PUBLIC KEY-----", "")
|
||||
.replace("-----END PUBLIC KEY-----", "")
|
||||
.replaceAll("\\s+", "");
|
||||
|
||||
// Decode the key
|
||||
byte[] keyBytes = Base64.getDecoder().decode(keyContent);
|
||||
|
||||
// Create public key specification
|
||||
X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes);
|
||||
|
||||
// Generate public key
|
||||
try {
|
||||
KeyFactory kf = KeyFactory.getInstance("RSA");
|
||||
return (RSAPublicKey) kf.generatePublic(spec);
|
||||
} catch (GeneralSecurityException e) {
|
||||
// because this shouldn't happen
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies if a JAR file's signature matches an RSA public key
|
||||
*
|
||||
* @param jarPath the path of the JAR file
|
||||
* @param publicKeys the {@link RSAPublicKey}s
|
||||
* @throws VerificationException if verification failed
|
||||
*/
|
||||
public static void verifyWithRsaKey(String jarPath, RSAPublicKey... publicKeys) throws VerificationException {
|
||||
try {
|
||||
// Open the JAR file
|
||||
try (JarFile jarFile = new JarFile(jarPath, true)) {
|
||||
byte[] buffer = new byte[8192];
|
||||
Enumeration<JarEntry> entries = jarFile.entries();
|
||||
|
||||
// Get manifest to check signature files
|
||||
Manifest manifest = jarFile.getManifest();
|
||||
if (manifest == null) {
|
||||
throw new VerificationException("JAR has no manifest");
|
||||
}
|
||||
|
||||
while (entries.hasMoreElements()) {
|
||||
JarEntry entry = entries.nextElement();
|
||||
|
||||
if (entry.isDirectory() || entry.getName().startsWith("META-INF/")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int bytesRead = 0;
|
||||
// Read entry to trigger signature verification
|
||||
try (InputStream is = jarFile.getInputStream(entry)) {
|
||||
while ((bytesRead += is.read(buffer)) != -1) {
|
||||
if (bytesRead > 1024 * 1024 * 100) { // unusual for a file to have >100 MiB
|
||||
throw new IOException("File too large: " + entry.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get signers for this entry
|
||||
CodeSigner[] signers = entry.getCodeSigners();
|
||||
if (signers == null || signers.length == 0) {
|
||||
throw new VerificationException("Unsigned entry: " + entry.getName());
|
||||
}
|
||||
|
||||
// Check if any signer's public key matches our RSA key
|
||||
boolean keyMatch = false;
|
||||
List<String> signerPublicKeys = new ArrayList<>();
|
||||
|
||||
for (CodeSigner signer : signers) {
|
||||
for (Certificate cert : signer.getSignerCertPath().getCertificates()) {
|
||||
PublicKey certPublicKey = cert.getPublicKey();
|
||||
if (certPublicKey instanceof RSAPublicKey) {
|
||||
RSAPublicKey rsaKey = (RSAPublicKey) certPublicKey;
|
||||
signerPublicKeys.add(Base64.getEncoder().encodeToString(rsaKey.getEncoded()));
|
||||
|
||||
for (RSAPublicKey publicKey : publicKeys) {
|
||||
if (rsaKey.getModulus().equals(publicKey.getModulus()) &&
|
||||
rsaKey.getPublicExponent().equals(publicKey.getPublicExponent())) {
|
||||
keyMatch = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (keyMatch) break;
|
||||
}
|
||||
|
||||
if (!keyMatch) {
|
||||
throw new VerificationException("Entry " + entry.getName() + " signed with unknown key");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (IOException e) {
|
||||
throw new VerificationException("Verification error: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,8 @@
|
|||
package eu.m724.jarupdater.verify;
|
||||
|
||||
/**
|
||||
* Thrown when there was a problem verifying the downloaded JAR
|
||||
*/
|
||||
public class VerificationException extends Exception {
|
||||
public VerificationException(String message) {
|
||||
super(message);
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package eu.m724.jarupdater.verify;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
public interface Verifier {
|
||||
void verify(String jarPath) throws VerificationException;
|
||||
void verify(Path jarPath) throws VerificationException;
|
||||
}
|
||||
|
|
|
@ -1,44 +1,36 @@
|
|||
package eu.m724.jarupdater.download;
|
||||
|
||||
import eu.m724.jarupdater.verify.VerificationException;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Path;
|
||||
import java.security.SignatureException;
|
||||
import java.util.HexFormat;
|
||||
import java.util.concurrent.CompletionException;
|
||||
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
public class DownloaderTest {
|
||||
@Test
|
||||
public void testDownloader() {
|
||||
Downloader downloader = new MockDownloader();
|
||||
|
||||
Path file = downloader.downloadAndVerify("good", "a9194ba3e955ba7482c6552894d4ca41b9bbafd86f4d90f3564c02fcb9e917c2").join();
|
||||
Path file = downloader.downloadAndVerify(URI.create("good"), HexFormat.of().parseHex("a9194ba3e955ba7482c6552894d4ca41b9bbafd86f4d90f3564c02fcb9e917c2")).join();
|
||||
assertNotNull(file);
|
||||
|
||||
try {
|
||||
downloader.downloadAndVerify("invalid", "678af2539cb4156f4e092d5e80de23aed7d9355774697267979b94e5cceec1b2").join();
|
||||
downloader.downloadAndVerify(URI.create("invalid"), HexFormat.of().parseHex("678af2539cb4156f4e092d5e80de23aed7d9355774697267979b94e5cceec1b2")).join();
|
||||
fail("this should have thrown");
|
||||
} catch (CompletionException e) {
|
||||
assert e.getCause() instanceof IOException;
|
||||
}
|
||||
|
||||
try {
|
||||
downloader.downloadAndVerify("bad", "1dd7d814fa99d923eee9e483c25a02346c47f84dbc160a1a9f87e9b051e77ee1").join();
|
||||
downloader.downloadAndVerify(URI.create("bad"), HexFormat.of().parseHex("1dd7d814fa99d923eee9e483c25a02346c47f84dbc160a1a9f87e9b051e77ee1")).join();
|
||||
fail("this should have thrown");
|
||||
} catch (CompletionException e) {
|
||||
assert e.getCause() instanceof SignatureException;
|
||||
assert e.getCause() instanceof VerificationException;
|
||||
}
|
||||
|
||||
downloader.install(file, file).join();
|
||||
try {
|
||||
downloader.install(Path.of("ialsoexistfortesting"), file).join();
|
||||
fail("this should have thrown");
|
||||
} catch (CompletionException e) {
|
||||
assert e.getCause() instanceof IOException;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,42 +1,22 @@
|
|||
package eu.m724.jarupdater.download;
|
||||
|
||||
import eu.m724.jarupdater.verify.VerificationException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Path;
|
||||
import java.security.SignatureException;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class MockDownloader implements Downloader {
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Path> downloadAndVerify(String url, String sha256hex) {
|
||||
CompletableFuture<Path> future = null;
|
||||
|
||||
switch (url) {
|
||||
case "good":
|
||||
future = CompletableFuture.completedFuture(Path.of("ionlyexistfortesting"));
|
||||
break;
|
||||
case "invalid":
|
||||
future = CompletableFuture.failedFuture(new CompletionException(new IOException()));
|
||||
break;
|
||||
case "bad":
|
||||
future = CompletableFuture.failedFuture(new CompletionException(new SignatureException()));
|
||||
break;
|
||||
}
|
||||
|
||||
return future;
|
||||
public CompletableFuture<Path> downloadAndVerify(URI uri, byte[] expectedHash, Consumer<DownloadProgress> progressConsumer) {
|
||||
return switch (uri.toString()) {
|
||||
case "good" -> CompletableFuture.completedFuture(Path.of("ionlyexistfortesting"));
|
||||
case "invalid" -> CompletableFuture.failedFuture(new CompletionException(new IOException()));
|
||||
case "bad" -> CompletableFuture.failedFuture(new CompletionException(new VerificationException("exception")));
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> install(Path source, Path destination) {
|
||||
CompletableFuture<Void> future = null;
|
||||
|
||||
if (source.toString().equals("ionlyexistfortesting"))
|
||||
future = CompletableFuture.completedFuture(null);
|
||||
else
|
||||
future = CompletableFuture.failedFuture(new CompletionException(new IOException()));
|
||||
|
||||
return future;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -9,8 +9,8 @@ public class EnvironmentTest {
|
|||
public void testConstantEnvironment() {
|
||||
Environment environment = new ConstantEnvironment("1.0", "stable", Path.of("idontexist"));
|
||||
|
||||
assert environment.getRunningVersion().equals("1.0");
|
||||
assert environment.getChannel().equals("stable");
|
||||
assert environment.getRunningJarFilePath().equals(Path.of("idontexist"));
|
||||
assert environment.runningVersion().equals("1.0");
|
||||
assert environment.updateChannel().equals("stable");
|
||||
assert environment.runningJarFilePath().equals(Path.of("idontexist"));
|
||||
}
|
||||
}
|
||||
|
|
27
src/test/java/eu/m724/jarupdater/install/InstallerTest.java
Normal file
27
src/test/java/eu/m724/jarupdater/install/InstallerTest.java
Normal file
|
@ -0,0 +1,27 @@
|
|||
package eu.m724.jarupdater.install;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.concurrent.CompletionException;
|
||||
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
public class InstallerTest {
|
||||
@Test
|
||||
public void testInstaller() {
|
||||
Installer installer = new MockInstaller();
|
||||
|
||||
Path asd = Path.of("asd");
|
||||
|
||||
installer.install(Path.of("iwillnotthrow"), asd).join();
|
||||
|
||||
try {
|
||||
installer.install(Path.of("iwillthrow"), asd).join();
|
||||
fail("this should have thrown");
|
||||
} catch (CompletionException e) {
|
||||
assert e.getCause() instanceof IOException;
|
||||
}
|
||||
}
|
||||
}
|
17
src/test/java/eu/m724/jarupdater/install/MockInstaller.java
Normal file
17
src/test/java/eu/m724/jarupdater/install/MockInstaller.java
Normal file
|
@ -0,0 +1,17 @@
|
|||
package eu.m724.jarupdater.install;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
|
||||
public class MockInstaller implements Installer {
|
||||
@Override
|
||||
public CompletableFuture<Void> install(Path source, Path destination) {
|
||||
if (source.toString().equals("iwillnotthrow")) {
|
||||
return CompletableFuture.completedFuture(null);
|
||||
} else {
|
||||
return CompletableFuture.failedFuture(new CompletionException(new IOException()));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@ package eu.m724.jarupdater.live;
|
|||
|
||||
import eu.m724.jarupdater.environment.ConstantEnvironment;
|
||||
import eu.m724.jarupdater.environment.Environment;
|
||||
import eu.m724.jarupdater.object.Version;
|
||||
import eu.m724.jarupdater.updater.Version;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
|
@ -19,17 +19,17 @@ public class MetadataTest {
|
|||
MetadataDAO dao = new MockMetadataDAO();
|
||||
MetadataFacade facade = new MetadataFacade(environment, dao);
|
||||
|
||||
assert facade.listChannels().join().contains(environment.getChannel());
|
||||
assert facade.getChannels().join().contains(environment.updateChannel());
|
||||
|
||||
Version cur = facade.getCurrentVersionMetadata().join();
|
||||
Version cur = facade.getRunningVersionMetadata().join();
|
||||
Version lat = facade.getLatestVersionMetadata().join();
|
||||
|
||||
assert cur != null;
|
||||
assert lat != null;
|
||||
|
||||
assertNotEquals(cur, lat);
|
||||
assertEquals(cur.getLabel(), environment.getRunningVersion());
|
||||
assertEquals("1.1", lat.getLabel());
|
||||
assertEquals(cur.label(), environment.runningVersion());
|
||||
assertEquals("1.1", lat.label());
|
||||
|
||||
try {
|
||||
facade.getVersionMetadata("invalidversion").join();
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package eu.m724.jarupdater.live;
|
||||
|
||||
import eu.m724.jarupdater.object.NoSuchVersionException;
|
||||
import eu.m724.jarupdater.object.Version;
|
||||
import eu.m724.jarupdater.updater.NoSuchVersionException;
|
||||
import eu.m724.jarupdater.updater.Version;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
@ -17,11 +17,11 @@ public class MockMetadataDAO implements MetadataDAO {
|
|||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Version> getMetadata(String channel, String versionLabel) {
|
||||
public CompletableFuture<Version> getMetadata(String channel, String versionName) {
|
||||
Version version = null;
|
||||
String fileUrlPrefix = "http://127.0.0.1:17357/" + channel + "/";
|
||||
|
||||
switch (versionLabel) {
|
||||
switch (versionName) {
|
||||
case "1.0":
|
||||
version = new Version(1, 100, "1.0", fileUrlPrefix + "1.0.jar", null, "dd3822ed965b2820aa0800f04b86f26d0b656fca3b97ddd264046076f81e3a24");
|
||||
break;
|
||||
|
@ -30,7 +30,7 @@ public class MockMetadataDAO implements MetadataDAO {
|
|||
version = new Version(2, 200, "1.1", fileUrlPrefix + "1.1.jar", null,"4d59994f607b89987d5bff430a4229edbbb045b3da9eaf1c63fa8e5fb208d824");
|
||||
break;
|
||||
default:
|
||||
return CompletableFuture.failedFuture(new CompletionException(new NoSuchVersionException(channel, versionLabel)));
|
||||
return CompletableFuture.failedFuture(new CompletionException(new NoSuchVersionException(channel, versionName)));
|
||||
}
|
||||
|
||||
return CompletableFuture.completedFuture(version);
|
||||
|
|
|
@ -4,10 +4,11 @@ import eu.m724.jarupdater.download.Downloader;
|
|||
import eu.m724.jarupdater.download.MockDownloader;
|
||||
import eu.m724.jarupdater.environment.ConstantEnvironment;
|
||||
import eu.m724.jarupdater.environment.Environment;
|
||||
import eu.m724.jarupdater.install.Installer;
|
||||
import eu.m724.jarupdater.install.MockInstaller;
|
||||
import eu.m724.jarupdater.live.MetadataDAO;
|
||||
import eu.m724.jarupdater.live.MetadataFacade;
|
||||
import eu.m724.jarupdater.live.MockMetadataDAO;
|
||||
import eu.m724.jarupdater.object.Version;
|
||||
import eu.m724.jarupdater.verify.MockVerifier;
|
||||
import eu.m724.jarupdater.verify.Verifier;
|
||||
import org.junit.Before;
|
||||
|
@ -33,21 +34,22 @@ public class UpdaterTest {
|
|||
MetadataFacade metadataFacade = new MetadataFacade(environment, metadataDAO);
|
||||
|
||||
Downloader downloader = new MockDownloader();
|
||||
Installer installer = new MockInstaller();
|
||||
|
||||
Verifier verifier = new MockVerifier();
|
||||
|
||||
this.updater = new Updater(environment, metadataFacade, downloader, verifier);
|
||||
this.updater = new Updater(environment, metadataFacade, downloader, installer, verifier);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChannels() {
|
||||
List<String> channels = updater.listChannels().join();
|
||||
List<String> channels = updater.getChannels().join();
|
||||
assertEquals(channels, Arrays.asList("stable", "beta", "alpha"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCurrentVersion() {
|
||||
Version currentVersion = updater.getCurrentVersion().join();
|
||||
Version currentVersion = updater.getRunningVersion().join();
|
||||
|
||||
assertEquals(expectedCurrentVersion, currentVersion);
|
||||
}
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
package eu.m724.jarupdater.verify;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
public class MockVerifier implements Verifier {
|
||||
@Override
|
||||
public void verify(String jarPath) throws VerificationException {
|
||||
public void verify(Path jarPath) throws VerificationException {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue