mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-05-21 12:11:28 +00:00
unified "check for update" and "do update" button
This commit is contained in:
@@ -52,12 +52,13 @@ open module org.cryptomator.desktop {
|
||||
requires io.github.coffeelibs.tinyoauth2client;
|
||||
requires org.slf4j;
|
||||
requires org.apache.commons.lang3;
|
||||
requires com.github.benmanes.caffeine;
|
||||
requires com.fasterxml.jackson.annotation;
|
||||
|
||||
/* dagger bs */
|
||||
requires jakarta.inject;
|
||||
requires static javax.inject;
|
||||
requires java.compiler;
|
||||
requires com.github.benmanes.caffeine;
|
||||
|
||||
uses org.cryptomator.common.locationpresets.LocationPresetsProvider;
|
||||
uses SSLContextProvider;
|
||||
|
||||
@@ -74,13 +74,6 @@ public abstract class CommonsModule {
|
||||
return new MasterkeyFileAccess(Constants.PEPPER, csprng);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@Named("SemVer")
|
||||
static Comparator<String> providesSemVerComparator() {
|
||||
return new SemVerComparator();
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
static Optional<RevealPathService> provideRevealPathService() {
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2016, 2017 Sebastian Stenzel and others.
|
||||
* All rights reserved.
|
||||
* This program and the accompanying materials are made available under the terms of the accompanying LICENSE file.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.common;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.Comparator;
|
||||
|
||||
/**
|
||||
* Compares version strings according to <a href="http://semver.org/spec/v2.0.0.html">SemVer 2.0.0</a>.
|
||||
*/
|
||||
public class SemVerComparator implements Comparator<String> {
|
||||
|
||||
private static final char VERSION_SEP = '.'; // http://semver.org/spec/v2.0.0.html#spec-item-2
|
||||
private static final String PRE_RELEASE_SEP = "-"; // http://semver.org/spec/v2.0.0.html#spec-item-9
|
||||
private static final String BUILD_SEP = "+"; // http://semver.org/spec/v2.0.0.html#spec-item-10
|
||||
|
||||
@Override
|
||||
public int compare(String version1, String version2) {
|
||||
// "Build metadata SHOULD be ignored when determining version precedence.
|
||||
// Thus two versions that differ only in the build metadata, have the same precedence."
|
||||
String v1WithoutBuildMetadata = StringUtils.substringBefore(version1, BUILD_SEP);
|
||||
String v2WithoutBuildMetadata = StringUtils.substringBefore(version2, BUILD_SEP);
|
||||
|
||||
if (v1WithoutBuildMetadata.equals(v2WithoutBuildMetadata)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
String v1MajorMinorPatch = StringUtils.substringBefore(v1WithoutBuildMetadata, PRE_RELEASE_SEP);
|
||||
String v2MajorMinorPatch = StringUtils.substringBefore(v2WithoutBuildMetadata, PRE_RELEASE_SEP);
|
||||
String v1PreReleaseVersion = StringUtils.substringAfter(v1WithoutBuildMetadata, PRE_RELEASE_SEP);
|
||||
String v2PreReleaseVersion = StringUtils.substringAfter(v2WithoutBuildMetadata, PRE_RELEASE_SEP);
|
||||
return compare(v1MajorMinorPatch, v1PreReleaseVersion, v2MajorMinorPatch, v2PreReleaseVersion);
|
||||
}
|
||||
|
||||
private int compare(String v1MajorMinorPatch, String v1PreReleaseVersion, String v2MajorMinorPatch, String v2PreReleaseVersion) {
|
||||
int comparisonResult = compareNumericallyThenLexicographically(v1MajorMinorPatch, v2MajorMinorPatch);
|
||||
if (comparisonResult == 0) {
|
||||
if (v1PreReleaseVersion.isEmpty()) {
|
||||
return 1; // 1.0.0 > 1.0.0-BETA
|
||||
} else if (v2PreReleaseVersion.isEmpty()) {
|
||||
return -1; // 1.0.0-BETA < 1.0.0
|
||||
} else {
|
||||
return compareNumericallyThenLexicographically(v1PreReleaseVersion, v2PreReleaseVersion);
|
||||
}
|
||||
} else {
|
||||
return comparisonResult;
|
||||
}
|
||||
}
|
||||
|
||||
private int compareNumericallyThenLexicographically(String version1, String version2) {
|
||||
final String[] vComps1 = StringUtils.split(version1, VERSION_SEP);
|
||||
final String[] vComps2 = StringUtils.split(version2, VERSION_SEP);
|
||||
final int commonCompCount = Math.min(vComps1.length, vComps2.length);
|
||||
|
||||
for (int i = 0; i < commonCompCount; i++) {
|
||||
int subversionComparisonResult = 0;
|
||||
try {
|
||||
final int v1 = Integer.parseInt(vComps1[i]);
|
||||
final int v2 = Integer.parseInt(vComps2[i]);
|
||||
subversionComparisonResult = v1 - v2;
|
||||
} catch (NumberFormatException ex) {
|
||||
// ok, lets compare this fragment lexicographically
|
||||
subversionComparisonResult = vComps1[i].compareTo(vComps2[i]);
|
||||
}
|
||||
if (subversionComparisonResult != 0) {
|
||||
return subversionComparisonResult;
|
||||
}
|
||||
}
|
||||
|
||||
// all in common so far? longest version string is considered the higher version:
|
||||
return vComps1.length - vComps2.length;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package org.cryptomator.ui.fxapp;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLParameters;
|
||||
import java.io.IOException;
|
||||
import java.net.Authenticator;
|
||||
import java.net.CookieHandler;
|
||||
import java.net.ProxySelector;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.time.Duration;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
abstract class DelegatingHttpClient extends HttpClient {
|
||||
|
||||
private final HttpClient delegate;
|
||||
|
||||
public DelegatingHttpClient(HttpClient delegate) {
|
||||
this.delegate = Objects.requireNonNull(delegate, "delegate must not be null");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<CookieHandler> cookieHandler() {
|
||||
return delegate.cookieHandler();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Duration> connectTimeout() {
|
||||
return delegate.connectTimeout();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Redirect followRedirects() {
|
||||
return delegate.followRedirects();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<ProxySelector> proxy() {
|
||||
return delegate.proxy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public SSLContext sslContext() {
|
||||
return delegate.sslContext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public SSLParameters sslParameters() {
|
||||
return delegate.sslParameters();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Authenticator> authenticator() {
|
||||
return delegate.authenticator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Version version() {
|
||||
return delegate.version();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Executor> executor() {
|
||||
return delegate.executor();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> HttpResponse<T> send(HttpRequest request, HttpResponse.BodyHandler<T> responseBodyHandler) throws IOException, InterruptedException {
|
||||
return delegate.send(request, responseBodyHandler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> CompletableFuture<HttpResponse<T>> sendAsync(HttpRequest request, HttpResponse.BodyHandler<T> responseBodyHandler) {
|
||||
return delegate.sendAsync(request, responseBodyHandler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> CompletableFuture<HttpResponse<T>> sendAsync(HttpRequest request, HttpResponse.BodyHandler<T> responseBodyHandler, HttpResponse.PushPromiseHandler<T> pushPromiseHandler) {
|
||||
return delegate.sendAsync(request, responseBodyHandler, pushPromiseHandler);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -26,7 +26,7 @@ import javafx.scene.image.Image;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
@Module(includes = {UpdateCheckerModule.class}, subcomponents = {TrayMenuComponent.class, //
|
||||
@Module(subcomponents = {TrayMenuComponent.class, //
|
||||
DecryptNameComponent.class, //
|
||||
MainWindowComponent.class, //
|
||||
PreferencesComponent.class, //
|
||||
|
||||
@@ -1,53 +1,68 @@
|
||||
package org.cryptomator.ui.fxapp;
|
||||
|
||||
import org.cryptomator.common.Environment;
|
||||
import org.cryptomator.common.SemVerComparator;
|
||||
import org.cryptomator.common.settings.Settings;
|
||||
import org.cryptomator.integrations.update.UpdateFailedException;
|
||||
import org.cryptomator.integrations.update.UpdateInfo;
|
||||
import org.cryptomator.integrations.update.UpdateMechanism;
|
||||
import org.cryptomator.updater.FallbackUpdateMechanism;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
import javafx.beans.binding.StringExpression;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyStringProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.concurrent.ScheduledService;
|
||||
import javafx.concurrent.Task;
|
||||
import javafx.concurrent.Worker;
|
||||
import javafx.concurrent.WorkerStateEvent;
|
||||
import javafx.util.Duration;
|
||||
import java.net.http.HttpClient;
|
||||
import java.time.Instant;
|
||||
import java.util.Comparator;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
@Deprecated(forRemoval = true) // to be replaced by fallback org.cryptomator.integrations.update.UpdateMechanism
|
||||
@FxApplicationScoped
|
||||
public class UpdateChecker {
|
||||
public class UpdateChecker extends ScheduledService<UpdateInfo> {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(UpdateChecker.class);
|
||||
private static final Duration AUTO_CHECK_DELAY = Duration.seconds(5);
|
||||
private static final Duration UPDATE_CHECK_INTERVAL = Duration.hours(3);
|
||||
private static final Duration DISABLED_UPDATE_CHECK_INTERVAL = Duration.hours(100000); // Duration.INDEFINITE leads to overflows...
|
||||
|
||||
public enum UpdateCheckState {
|
||||
NOT_CHECKED,
|
||||
IS_CHECKING,
|
||||
CHECK_SUCCESSFUL,
|
||||
CHECK_FAILED
|
||||
}
|
||||
|
||||
private final Environment env;
|
||||
private final Settings settings;
|
||||
private final StringProperty latestVersion = new SimpleStringProperty();
|
||||
private final ScheduledService<String> updateCheckerService;
|
||||
private final ObjectProperty<UpdateCheckState> state = new SimpleObjectProperty<>(UpdateCheckState.NOT_CHECKED);
|
||||
private final ObjectProperty<Instant> lastSuccessfulUpdateCheck;
|
||||
private final Comparator<String> versionComparator = new SemVerComparator();
|
||||
private final BooleanBinding updateAvailable;
|
||||
private final BooleanBinding checkFailed;
|
||||
private final StringExpression latestVersion = StringExpression.stringExpression(lastValueProperty().map(UpdateInfo::version));
|
||||
private final BooleanBinding updateAvailable = lastValueProperty().isNotNull();
|
||||
private final ObjectBinding<UpdateCheckState> updateState = Bindings.createObjectBinding(this::getUpdateCheckState, stateProperty());
|
||||
private final BooleanBinding checkFailed = Bindings.equal(UpdateCheckState.CHECK_FAILED, updateState);
|
||||
private final HttpClient httpClient;
|
||||
private final UpdateMechanism primaryUpdateMechanism;
|
||||
private final UpdateMechanism fallbackUpdateMechanism;
|
||||
|
||||
@Inject
|
||||
UpdateChecker(Settings settings, //
|
||||
Environment env, //
|
||||
ScheduledService<String> updateCheckerService) {
|
||||
Environment env,
|
||||
FallbackUpdateMechanism fallbackUpdateMechanism,
|
||||
UpdateCheckerHttpClient httpClient) {
|
||||
this.env = env;
|
||||
this.settings = settings;
|
||||
this.updateCheckerService = updateCheckerService;
|
||||
this.lastSuccessfulUpdateCheck = settings.lastSuccessfulUpdateCheck;
|
||||
this.updateAvailable = Bindings.createBooleanBinding(this::isUpdateAvailable, latestVersion);
|
||||
this.checkFailed = Bindings.equal(UpdateCheckState.CHECK_FAILED, state);
|
||||
this.httpClient = httpClient;
|
||||
this.primaryUpdateMechanism = UpdateMechanism.get().orElse(fallbackUpdateMechanism);
|
||||
this.fallbackUpdateMechanism = fallbackUpdateMechanism;
|
||||
|
||||
setExecutor(Executors.newVirtualThreadPerTaskExecutor());
|
||||
periodProperty().bind(Bindings.when(settings.checkForUpdates).then(UPDATE_CHECK_INTERVAL).otherwise(DISABLED_UPDATE_CHECK_INTERVAL));
|
||||
}
|
||||
|
||||
public void automaticallyCheckForUpdatesIfEnabled() {
|
||||
@@ -61,46 +76,42 @@ public class UpdateChecker {
|
||||
}
|
||||
|
||||
private void startCheckingForUpdates(Duration initialDelay) {
|
||||
updateCheckerService.cancel();
|
||||
updateCheckerService.reset();
|
||||
updateCheckerService.setDelay(initialDelay);
|
||||
updateCheckerService.setOnRunning(this::checkStarted);
|
||||
updateCheckerService.setOnSucceeded(this::checkSucceeded);
|
||||
updateCheckerService.setOnFailed(this::checkFailed);
|
||||
updateCheckerService.start();
|
||||
cancel();
|
||||
reset();
|
||||
setDelay(initialDelay);
|
||||
start();
|
||||
}
|
||||
|
||||
private void checkStarted(WorkerStateEvent event) {
|
||||
LOG.debug("Checking for updates...");
|
||||
state.set(UpdateCheckState.IS_CHECKING);
|
||||
@Override
|
||||
protected void succeeded() {
|
||||
var updateInfo = getValue();
|
||||
super.succeeded(); // this will nil the value property!
|
||||
if (updateInfo != null) {
|
||||
LOG.info("Current version: {}, latest version: {}", getCurrentVersion(), updateInfo.version());
|
||||
lastSuccessfulUpdateCheck.set(Instant.now());
|
||||
}
|
||||
}
|
||||
|
||||
private void checkSucceeded(WorkerStateEvent event) {
|
||||
var latestVersionString = updateCheckerService.getValue();
|
||||
LOG.info("Current version: {}, latest version: {}", getCurrentVersion(), latestVersionString);
|
||||
lastSuccessfulUpdateCheck.set(Instant.now());
|
||||
latestVersion.set(latestVersionString);
|
||||
state.set(UpdateCheckState.CHECK_SUCCESSFUL);
|
||||
@Override
|
||||
protected Task<UpdateInfo> createTask() {
|
||||
return new UpdateCheckTask();
|
||||
}
|
||||
|
||||
private void checkFailed(WorkerStateEvent event) {
|
||||
state.set(UpdateCheckState.CHECK_FAILED);
|
||||
@Override
|
||||
protected void failed() {
|
||||
super.failed();
|
||||
LOG.error("Update check failed.", getException());
|
||||
}
|
||||
|
||||
public enum UpdateCheckState {
|
||||
NOT_CHECKED,
|
||||
IS_CHECKING,
|
||||
CHECK_SUCCESSFUL,
|
||||
CHECK_FAILED
|
||||
}
|
||||
|
||||
/* Observable Properties */
|
||||
public BooleanBinding checkingForUpdatesProperty() {
|
||||
return updateCheckerService.stateProperty().isEqualTo(Worker.State.RUNNING);
|
||||
|
||||
public StringExpression latestVersionProperty() {
|
||||
return latestVersion;
|
||||
}
|
||||
|
||||
public ReadOnlyStringProperty latestVersionProperty() {
|
||||
return latestVersion;
|
||||
public boolean isUpdateAvailable() {
|
||||
return updateAvailable.get();
|
||||
}
|
||||
|
||||
public BooleanBinding updateAvailableProperty() {
|
||||
@@ -111,26 +122,38 @@ public class UpdateChecker {
|
||||
return checkFailed;
|
||||
}
|
||||
|
||||
public boolean isUpdateAvailable() {
|
||||
String currentVersion = getCurrentVersion();
|
||||
String latestVersionString = latestVersion.get();
|
||||
|
||||
if (currentVersion == null || latestVersionString == null) {
|
||||
return false;
|
||||
} else {
|
||||
return versionComparator.compare(currentVersion, latestVersionString) < 0;
|
||||
}
|
||||
}
|
||||
|
||||
public ObjectProperty<Instant> lastSuccessfulUpdateCheckProperty() {
|
||||
return lastSuccessfulUpdateCheck;
|
||||
}
|
||||
|
||||
public ObjectProperty<UpdateCheckState> updateCheckStateProperty() {
|
||||
return state;
|
||||
public ObjectBinding<UpdateCheckState> updateCheckStateProperty() {
|
||||
return updateState;
|
||||
}
|
||||
|
||||
private UpdateCheckState getUpdateCheckState() {
|
||||
return switch (getState()) {
|
||||
case READY -> UpdateCheckState.NOT_CHECKED;
|
||||
case SCHEDULED, RUNNING -> UpdateCheckState.IS_CHECKING;
|
||||
case SUCCEEDED -> UpdateCheckState.CHECK_SUCCESSFUL;
|
||||
case FAILED, CANCELLED -> UpdateCheckState.CHECK_FAILED;
|
||||
};
|
||||
}
|
||||
|
||||
public String getCurrentVersion() {
|
||||
return env.getAppVersion();
|
||||
}
|
||||
|
||||
private class UpdateCheckTask extends Task<UpdateInfo> {
|
||||
|
||||
@Override
|
||||
protected UpdateInfo call() throws UpdateFailedException, InterruptedException {
|
||||
var result = primaryUpdateMechanism.checkForUpdate(env.getAppVersion(), httpClient);
|
||||
if (result == null && primaryUpdateMechanism != fallbackUpdateMechanism) {
|
||||
LOG.debug("Primary update mechanism did not find an update. Try fallback update mechanism...");
|
||||
result = fallbackUpdateMechanism.checkForUpdate(env.getAppVersion(), httpClient);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package org.cryptomator.ui.fxapp;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.common.Environment;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.ProxySelector;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
@FxApplicationScoped
|
||||
public class UpdateCheckerHttpClient extends DelegatingHttpClient {
|
||||
|
||||
private final String userAgent;
|
||||
|
||||
@Inject
|
||||
public UpdateCheckerHttpClient(Environment env) {
|
||||
var delegate = HttpClient.newBuilder() //
|
||||
.followRedirects(HttpClient.Redirect.NORMAL) // from version 1.6.11 onwards, Cryptomator can follow redirects, in case this URL ever changes
|
||||
.proxy(ProxySelector.getDefault()).build();
|
||||
super(delegate);
|
||||
this.userAgent = String.format("Cryptomator VersionChecker/%s %s %s (%s)", env.getAppVersion(), SystemUtils.OS_NAME, SystemUtils.OS_VERSION, SystemUtils.OS_ARCH);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> HttpResponse<T> send(HttpRequest request, HttpResponse.BodyHandler<T> responseBodyHandler) throws IOException, InterruptedException {
|
||||
return super.send(decorateRequest(request), responseBodyHandler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> CompletableFuture<HttpResponse<T>> sendAsync(HttpRequest request, HttpResponse.BodyHandler<T> responseBodyHandler) {
|
||||
return super.sendAsync(decorateRequest(request), responseBodyHandler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> CompletableFuture<HttpResponse<T>> sendAsync(HttpRequest request, HttpResponse.BodyHandler<T> responseBodyHandler, HttpResponse.PushPromiseHandler<T> pushPromiseHandler) {
|
||||
return super.sendAsync(decorateRequest(request), responseBodyHandler, pushPromiseHandler);
|
||||
}
|
||||
|
||||
private HttpRequest decorateRequest(HttpRequest request) {
|
||||
return HttpRequest.newBuilder(request, (_, _) -> true) //
|
||||
.header("User-Agent", this.userAgent) //
|
||||
.timeout(Duration.ofSeconds(10)) //
|
||||
.build();
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
package org.cryptomator.ui.fxapp;
|
||||
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.common.Environment;
|
||||
import org.cryptomator.common.settings.Settings;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Named;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
import javafx.concurrent.ScheduledService;
|
||||
import javafx.concurrent.Task;
|
||||
import javafx.util.Duration;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
|
||||
@Module
|
||||
public abstract class UpdateCheckerModule {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(UpdateCheckerModule.class);
|
||||
|
||||
private static final URI LATEST_VERSION_URI = URI.create("https://api.cryptomator.org/desktop/latest-version.json");
|
||||
private static final Duration UPDATE_CHECK_INTERVAL = Duration.hours(3);
|
||||
private static final Duration DISABLED_UPDATE_CHECK_INTERVAL = Duration.hours(100000); // Duration.INDEFINITE leads to overflows...
|
||||
|
||||
@Provides
|
||||
@FxApplicationScoped
|
||||
static Optional<HttpClient> provideHttpClient() {
|
||||
try {
|
||||
return Optional.of(HttpClient.newBuilder() //
|
||||
.followRedirects(HttpClient.Redirect.NORMAL) // from version 1.6.11 onwards, Cryptomator can follow redirects, in case this URL ever changes
|
||||
.build());
|
||||
} catch (UncheckedIOException e) {
|
||||
LOG.error("HttpClient for update check cannot be created.", e);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FxApplicationScoped
|
||||
static HttpRequest provideCheckForUpdatesRequest(Environment env) {
|
||||
String userAgent = String.format("Cryptomator VersionChecker/%s %s %s (%s)", //
|
||||
env.getAppVersion(), //
|
||||
SystemUtils.OS_NAME, //
|
||||
SystemUtils.OS_VERSION, //
|
||||
SystemUtils.OS_ARCH); //
|
||||
return HttpRequest.newBuilder() //
|
||||
.uri(LATEST_VERSION_URI) //
|
||||
.header("User-Agent", userAgent) //
|
||||
.timeout(java.time.Duration.ofSeconds(10))
|
||||
.build();
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Named("checkForUpdatesInterval")
|
||||
@FxApplicationScoped
|
||||
static ObjectBinding<Duration> provideCheckForUpdateInterval(Settings settings) {
|
||||
return Bindings.when(settings.checkForUpdates).then(UPDATE_CHECK_INTERVAL).otherwise(DISABLED_UPDATE_CHECK_INTERVAL);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FxApplicationScoped
|
||||
static ScheduledService<String> provideCheckForUpdatesService(ExecutorService executor, Optional<HttpClient> httpClient, HttpRequest checkForUpdatesRequest, @Named("checkForUpdatesInterval") ObjectBinding<Duration> period) {
|
||||
ScheduledService<String> service = new ScheduledService<>() {
|
||||
@Override
|
||||
protected Task<String> createTask() {
|
||||
if (httpClient.isPresent()) {
|
||||
return new UpdateCheckerTask(httpClient.get(), checkForUpdatesRequest);
|
||||
} else {
|
||||
return new Task<>() {
|
||||
@Override
|
||||
protected String call() {
|
||||
throw new NullPointerException("No HttpClient present.");
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
service.setOnFailed(event -> LOG.error("Failed to execute update service", service.getException()));
|
||||
service.setExecutor(executor);
|
||||
service.periodProperty().bind(period);
|
||||
return service;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
package org.cryptomator.ui.fxapp;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.google.common.io.ByteStreams;
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javafx.concurrent.Task;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
|
||||
public class UpdateCheckerTask extends Task<String> {
|
||||
|
||||
private static final ObjectMapper JSON = new ObjectMapper();
|
||||
private static final Logger LOG = LoggerFactory.getLogger(UpdateCheckerTask.class);
|
||||
|
||||
private static final long MAX_RESPONSE_SIZE = 10L * 1024; // 10kb should be sufficient. protect against flooding
|
||||
|
||||
private final HttpClient httpClient;
|
||||
private final HttpRequest checkForUpdatesRequest;
|
||||
|
||||
UpdateCheckerTask(HttpClient httpClient, HttpRequest checkForUpdatesRequest) {
|
||||
this.httpClient = httpClient;
|
||||
this.checkForUpdatesRequest = checkForUpdatesRequest;
|
||||
|
||||
setOnFailed(event -> LOG.error("Failed to check for updates", getException()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String call() throws IOException, InterruptedException {
|
||||
HttpResponse<InputStream> response = httpClient.send(checkForUpdatesRequest, HttpResponse.BodyHandlers.ofInputStream());
|
||||
if (response.statusCode() == 200) {
|
||||
return processBody(response);
|
||||
} else {
|
||||
throw new IOException("Unexpected HTTP response code " + response.statusCode());
|
||||
}
|
||||
}
|
||||
|
||||
private String processBody(HttpResponse<InputStream> response) throws IOException {
|
||||
try (InputStream in = response.body(); //
|
||||
InputStream limitedIn = ByteStreams.limit(in, MAX_RESPONSE_SIZE)) {
|
||||
var json = JSON.reader().readTree(limitedIn);
|
||||
if (SystemUtils.IS_OS_MAC_OSX) {
|
||||
return json.get("mac").asText();
|
||||
} else if (SystemUtils.IS_OS_WINDOWS) {
|
||||
return json.get("win").asText();
|
||||
} else if (SystemUtils.IS_OS_LINUX) {
|
||||
return json.get("linux").asText();
|
||||
} else {
|
||||
throw new IllegalStateException("Unsupported operating system");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ package org.cryptomator.ui.preferences;
|
||||
|
||||
import org.cryptomator.common.Environment;
|
||||
import org.cryptomator.common.settings.Settings;
|
||||
import org.cryptomator.integrations.update.UpdateMechanism;
|
||||
import org.cryptomator.integrations.update.UpdateInfo;
|
||||
import org.cryptomator.integrations.update.UpdateStep;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.fxapp.UpdateChecker;
|
||||
@@ -17,21 +17,18 @@ import javafx.application.Application;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.binding.BooleanExpression;
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
import javafx.beans.binding.StringBinding;
|
||||
import javafx.beans.binding.StringExpression;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ReadOnlyStringProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.concurrent.Service;
|
||||
import javafx.concurrent.Worker;
|
||||
import javafx.concurrent.WorkerStateEvent;
|
||||
import javafx.event.EventType;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.CheckBox;
|
||||
import javafx.scene.control.ContentDisplay;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
@@ -46,60 +43,56 @@ import java.util.ResourceBundle;
|
||||
public class UpdatesPreferencesController implements FxController {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(UpdatesPreferencesController.class);
|
||||
private static final String DOWNLOADS_URI_TEMPLATE = "https://cryptomator.org/downloads/" //
|
||||
+ "?utm_source=cryptomator-desktop" //
|
||||
+ "&utm_medium=update-notification&" //
|
||||
+ "utm_campaign=app-update-%s";
|
||||
|
||||
private final Application application;
|
||||
private final Environment environment;
|
||||
private final ResourceBundle resourceBundle;
|
||||
private final Settings settings;
|
||||
private final Environment env;
|
||||
private final UpdateChecker updateChecker;
|
||||
private final ObjectBinding<ContentDisplay> checkForUpdatesButtonState;
|
||||
private final ReadOnlyStringProperty latestVersion;
|
||||
private final ObjectBinding<ContentDisplay> updateButtonState;
|
||||
private final StringExpression latestVersion;
|
||||
private final ObservableValue<Instant> lastSuccessfulUpdateCheck;
|
||||
private final StringBinding lastUpdateCheckMessage;
|
||||
private final ObservableValue<String> timeDifferenceMessage;
|
||||
private final String currentVersion;
|
||||
private final BooleanBinding updateAvailable;
|
||||
private final BooleanBinding checkFailed;
|
||||
private final BooleanProperty upToDateLabelVisible = new SimpleBooleanProperty(false);
|
||||
private final DateTimeFormatter formatter;
|
||||
private final BooleanBinding upToDate;
|
||||
private final String downloadsUri;
|
||||
private final UpdateMechanism updateMechanism;
|
||||
private final Service<UpdateStep> updateService;
|
||||
private final UpdateService updateService;
|
||||
private final StringBinding updateButtonTitle;
|
||||
private final BooleanBinding updateReady;
|
||||
|
||||
private final ObjectBinding<Worker<?>> worker;
|
||||
private final BooleanExpression running;
|
||||
|
||||
/* FXML */
|
||||
public CheckBox checkForUpdatesCheckbox;
|
||||
|
||||
@Inject
|
||||
UpdatesPreferencesController(Application application, Environment environment, ResourceBundle resourceBundle, Settings settings, UpdateChecker updateChecker, Environment env, FallbackUpdateMechanism fallbackUpdateMechanism) {
|
||||
UpdatesPreferencesController(Application application, Environment environment, ResourceBundle resourceBundle, Settings settings, UpdateChecker updateChecker, FallbackUpdateMechanism fallbackUpdateMechanism) {
|
||||
this.application = application;
|
||||
this.environment = environment;
|
||||
this.resourceBundle = resourceBundle;
|
||||
this.settings = settings;
|
||||
this.env = env;
|
||||
this.updateChecker = updateChecker;
|
||||
this.checkForUpdatesButtonState = Bindings.when(updateChecker.checkingForUpdatesProperty()).then(ContentDisplay.LEFT).otherwise(ContentDisplay.TEXT_ONLY);
|
||||
|
||||
this.latestVersion = updateChecker.latestVersionProperty();
|
||||
this.lastSuccessfulUpdateCheck = updateChecker.lastSuccessfulUpdateCheckProperty();
|
||||
this.timeDifferenceMessage = Bindings.createStringBinding(this::getTimeDifferenceMessage, lastSuccessfulUpdateCheck);
|
||||
this.currentVersion = environment.getAppVersion();
|
||||
this.updateAvailable = updateChecker.updateAvailableProperty();
|
||||
this.lastUpdateCheckMessage = Bindings.createStringBinding(this::getLastUpdateCheckMessage, lastSuccessfulUpdateCheck);
|
||||
|
||||
this.currentVersion = updateChecker.getCurrentVersion();
|
||||
this.formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).withLocale(Locale.getDefault());
|
||||
this.upToDate = updateChecker.updateCheckStateProperty().isEqualTo(UpdateChecker.UpdateCheckState.CHECK_SUCCESSFUL).and(latestVersion.isEqualTo(currentVersion));
|
||||
this.checkFailed = updateChecker.checkFailedProperty();
|
||||
this.lastUpdateCheckMessage = Bindings.createStringBinding(this::getLastUpdateCheckMessage, lastSuccessfulUpdateCheck);
|
||||
this.downloadsUri = DOWNLOADS_URI_TEMPLATE.formatted(URLEncoder.encode(currentVersion, StandardCharsets.US_ASCII));
|
||||
this.updateMechanism = UpdateMechanism.get().orElse(fallbackUpdateMechanism);
|
||||
this.updateService = new UpdateService(updateMechanism);
|
||||
this.updateButtonTitle = Bindings.createStringBinding(this::getUpdateButtonTitle, updateService.stateProperty(), updateService.messageProperty());
|
||||
this.updateReady = updateService.stateProperty().isEqualTo(Worker.State.READY);
|
||||
|
||||
this.updateService = new UpdateService(updateChecker.lastValueProperty().map(UpdateInfo::updateMechanism));
|
||||
this.worker = Bindings.when(updateChecker.updateAvailableProperty()).<Worker<?>>then(updateService).otherwise(updateChecker);
|
||||
this.running = Bindings.createBooleanBinding(this::isRunning, updateService.stateProperty(), updateChecker.stateProperty());
|
||||
this.updateButtonTitle = Bindings.createStringBinding(this::getUpdateButtonTitle, worker, updateService.stateProperty(), updateService.messageProperty());
|
||||
this.updateButtonState = Bindings.createObjectBinding(this::getUpdateButtonState, updateChecker.stateProperty(), updateService.stateProperty());
|
||||
|
||||
updateChecker.updateAvailableProperty().addListener((_, _, newVal) -> LOG.info("Update available: {}", newVal));
|
||||
|
||||
updateService.setOnSucceeded(this::updateSucceeded);
|
||||
updateService.setOnFailed(this::updateFailed);
|
||||
@@ -117,24 +110,18 @@ public class UpdatesPreferencesController implements FxController {
|
||||
});
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void checkNow() {
|
||||
updateChecker.checkForUpdatesNow();
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void visitDownloadsPage() {
|
||||
application.getHostServices().showDocument(downloadsUri);
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void showLogfileDirectory() {
|
||||
environment.getLogDir().ifPresent(logDirPath -> application.getHostServices().showDocument(logDirPath.toUri().toString()));
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void doUpdate() {
|
||||
updateService.start();
|
||||
public void startWork() {
|
||||
if (worker.get().equals(updateChecker)) {
|
||||
updateChecker.checkForUpdatesNow();
|
||||
} else if (worker.get().equals(updateService)) {
|
||||
updateService.start();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateSucceeded(WorkerStateEvent workerStateEvent) {
|
||||
@@ -157,15 +144,21 @@ public class UpdatesPreferencesController implements FxController {
|
||||
|
||||
/* Observable Properties */
|
||||
|
||||
public ObjectBinding<ContentDisplay> checkForUpdatesButtonStateProperty() {
|
||||
return checkForUpdatesButtonState;
|
||||
public ObjectBinding<ContentDisplay> updateButtonStateProperty() {
|
||||
return updateButtonState;
|
||||
}
|
||||
|
||||
public ContentDisplay getCheckForUpdatesButtonState() {
|
||||
return checkForUpdatesButtonState.get();
|
||||
public ContentDisplay getUpdateButtonState() {
|
||||
if (updateService.isRunning()) { // isRunning() covers RUNNING and SCHEDULED states
|
||||
return ContentDisplay.BOTTOM;
|
||||
} else if (updateChecker.getState() == Worker.State.RUNNING) {
|
||||
return ContentDisplay.LEFT;
|
||||
} else {
|
||||
return ContentDisplay.TEXT_ONLY;
|
||||
}
|
||||
}
|
||||
|
||||
public ReadOnlyStringProperty latestVersionProperty() {
|
||||
public StringExpression latestVersionProperty() {
|
||||
return latestVersion;
|
||||
}
|
||||
|
||||
@@ -217,14 +210,6 @@ public class UpdatesPreferencesController implements FxController {
|
||||
return upToDateLabelVisible.get();
|
||||
}
|
||||
|
||||
public BooleanBinding updateAvailableProperty() {
|
||||
return updateAvailable;
|
||||
}
|
||||
|
||||
public boolean isUpdateAvailable() {
|
||||
return updateAvailable.get();
|
||||
}
|
||||
|
||||
public BooleanBinding checkFailedProperty() {
|
||||
return checkFailed;
|
||||
}
|
||||
@@ -233,8 +218,20 @@ public class UpdatesPreferencesController implements FxController {
|
||||
return checkFailed.getValue();
|
||||
}
|
||||
|
||||
public Service<UpdateStep> getUpdateService() {
|
||||
return updateService;
|
||||
public ObjectBinding<Worker<?>> workerProperty() {
|
||||
return worker;
|
||||
}
|
||||
|
||||
public Worker<?> getWorker() {
|
||||
return worker.get();
|
||||
}
|
||||
|
||||
public BooleanExpression runningProperty() {
|
||||
return running;
|
||||
}
|
||||
|
||||
public boolean isRunning() {
|
||||
return updateChecker.getState() == Worker.State.RUNNING || updateService.getState() == Worker.State.RUNNING;
|
||||
}
|
||||
|
||||
public StringBinding updateButtonTitleProperty() {
|
||||
@@ -242,20 +239,20 @@ public class UpdatesPreferencesController implements FxController {
|
||||
}
|
||||
|
||||
public String getUpdateButtonTitle() {
|
||||
return switch (updateService.getState()) {
|
||||
case READY -> updateMechanism.getName();
|
||||
case SCHEDULED, RUNNING -> updateService.getMessage(); // "Preparing Update..."; // TODO: resourceBundle.getString("preferences.updates.preparingUpdate")...
|
||||
case SUCCEEDED -> "Restart to Update"; // TODO: resourceBundle.getString("preferences.updates.readyToRestart")...
|
||||
case FAILED, CANCELLED -> "failed";
|
||||
};
|
||||
if (worker.get() == updateChecker) {
|
||||
return resourceBundle.getString("preferences.updates.checkNowBtn");
|
||||
} else {
|
||||
return switch (updateService.getState()) {
|
||||
case READY -> updateChecker.getLastValue().updateMechanism().getName();
|
||||
case SCHEDULED, RUNNING -> updateService.getMessage(); // "Preparing Update..."; // TODO: resourceBundle.getString("preferences.updates.preparingUpdate")...
|
||||
case SUCCEEDED -> "Restart to Update"; // TODO: resourceBundle.getString("preferences.updates.readyToRestart")...
|
||||
case FAILED, CANCELLED -> "failed";
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isUpdateReady() {
|
||||
return updateReady.get();
|
||||
}
|
||||
|
||||
public BooleanBinding updateReadyProperty() {
|
||||
return updateReady;
|
||||
public UpdateChecker getUpdateChecker() {
|
||||
return updateChecker;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,7 +3,12 @@ package org.cryptomator.updater;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.cryptomator.integrations.update.UpdateInfo;
|
||||
import org.cryptomator.integrations.update.UpdateMechanism;
|
||||
import org.jetbrains.annotations.Blocking;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
@@ -15,45 +20,53 @@ import java.util.List;
|
||||
|
||||
public abstract class DownloadUpdateMechanism implements UpdateMechanism {
|
||||
|
||||
private static final Logger LOG = LoggerFactory .getLogger(DownloadUpdateMechanism.class);
|
||||
private static final String LATEST_VERSION_API_URL = "https://api.cryptomator.org/connect/apps/desktop/latest-version?format=1";
|
||||
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||
|
||||
private final String assetSuffix;
|
||||
|
||||
protected DownloadUpdateMechanism(String assetSuffix) {
|
||||
this.assetSuffix = assetSuffix;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUpdateAvailable(String currentVersion) {
|
||||
try (var client = HttpClient.newHttpClient()) {
|
||||
// TODO: check different source
|
||||
HttpRequest request = HttpRequest.newBuilder().uri(URI.create("https://api.github.com/repos/cryptomator/cryptomator/releases/latest")).header("Accept", "application/vnd.github+json").build();
|
||||
|
||||
HttpResponse<InputStream> response = client.send(request, HttpResponse.BodyHandlers.ofInputStream());
|
||||
|
||||
public UpdateInfo checkForUpdate(String currentVersion, HttpClient httpClient) {
|
||||
try {
|
||||
HttpRequest request = HttpRequest.newBuilder().uri(URI.create(LATEST_VERSION_API_URL)).build();
|
||||
HttpResponse<InputStream> response = httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream());
|
||||
if (response.statusCode() != 200) {
|
||||
throw new RuntimeException("Failed to fetch release: " + response.statusCode());
|
||||
}
|
||||
|
||||
var release = MAPPER.readValue(response.body(), GitHubRelease.class);
|
||||
|
||||
return release.assets.stream().anyMatch(a -> a.name.endsWith(assetSuffix))
|
||||
&& UpdateMechanism.isUpdateAvailable(release.tagName, currentVersion);
|
||||
} catch (IOException | InterruptedException e) {
|
||||
return false;
|
||||
var release = MAPPER.readValue(response.body(), LatestVersionResponse.class);
|
||||
return checkForUpdate(currentVersion, release);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
LOG.debug("Update check interrupted.");
|
||||
return null;
|
||||
} catch (IOException e) {
|
||||
LOG.warn("Update check failed", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Blocking
|
||||
abstract UpdateInfo checkForUpdate(String currentVersion, LatestVersionResponse response);
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public record GitHubRelease(
|
||||
@JsonProperty("tag_name") String tagName,
|
||||
List<Asset> assets
|
||||
public record LatestVersionResponse(
|
||||
@JsonProperty("latestVersion") LatestVersion latestVersion,
|
||||
@JsonProperty("assets") List<Asset> assets
|
||||
) {}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public record LatestVersion(
|
||||
@JsonProperty("mac") String macVersion,
|
||||
@JsonProperty("win") String winVersion,
|
||||
@JsonProperty("linux") String linuxVersion
|
||||
) {}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public record Asset(
|
||||
String name,
|
||||
@JsonProperty("browser_download_url") String downloadUrl
|
||||
@JsonProperty("name") String name,
|
||||
@JsonProperty("digest") String digest,
|
||||
@JsonProperty("size") long size,
|
||||
@JsonProperty("downloadUrl") String downloadUrl
|
||||
) {}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,18 +1,31 @@
|
||||
package org.cryptomator.updater;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.common.Environment;
|
||||
import org.cryptomator.integrations.common.DisplayName;
|
||||
import org.cryptomator.integrations.common.Priority;
|
||||
import org.cryptomator.integrations.update.UpdateInfo;
|
||||
import org.cryptomator.integrations.update.UpdateMechanism;
|
||||
import org.cryptomator.integrations.update.UpdateStep;
|
||||
import org.cryptomator.integrations.update.UpdateStepAdapter;
|
||||
import org.cryptomator.ui.fxapp.FxApplicationScoped;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.application.Application;
|
||||
import javafx.application.Platform;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.net.URLEncoder;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@@ -21,6 +34,9 @@ import java.util.concurrent.TimeUnit;
|
||||
@DisplayName("Show Download Page") // TODO localize
|
||||
public class FallbackUpdateMechanism implements UpdateMechanism {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(FallbackUpdateMechanism.class);
|
||||
private static final String LATEST_VERSION_API_URL = "https://api.cryptomator.org/connect/apps/desktop/latest-version";
|
||||
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||
private static final String DOWNLOADS_URI_TEMPLATE = "https://cryptomator.org/downloads/" //
|
||||
+ "?utm_source=cryptomator-desktop" //
|
||||
+ "&utm_medium=update-notification&" //
|
||||
@@ -36,9 +52,24 @@ public class FallbackUpdateMechanism implements UpdateMechanism {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUpdateAvailable(String currentVersion) {
|
||||
// FIXME: what source shall we use? self-hosted JSON?
|
||||
return true;
|
||||
public UpdateInfo checkForUpdate(String currentVersion, HttpClient httpClient) {
|
||||
try {
|
||||
HttpRequest request = HttpRequest.newBuilder().uri(URI.create(LATEST_VERSION_API_URL)).build();
|
||||
HttpResponse<InputStream> response = httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream());
|
||||
if (response.statusCode() != 200) {
|
||||
throw new RuntimeException("Failed to fetch release: " + response.statusCode());
|
||||
}
|
||||
var release = MAPPER.readValue(response.body(), LatestVersion.class);
|
||||
var updateVersion = release.versionForCurrentOS();
|
||||
if (UpdateMechanism.isUpdateAvailable(currentVersion, updateVersion)) {
|
||||
return new UpdateInfo(updateVersion, this);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (IOException | InterruptedException e) {
|
||||
LOG.warn("Update check failed", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -54,4 +85,23 @@ public class FallbackUpdateMechanism implements UpdateMechanism {
|
||||
return UpdateStep.RETRY; // allow running this "update mechanism" as many times as the user wants
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public record LatestVersion(
|
||||
@JsonProperty("mac") String macVersion,
|
||||
@JsonProperty("win") String winVersion,
|
||||
@JsonProperty("linux") String linuxVersion
|
||||
) {
|
||||
public String versionForCurrentOS() {
|
||||
if (SystemUtils.IS_OS_MAC_OSX) {
|
||||
return macVersion;
|
||||
} else if (SystemUtils.IS_OS_WINDOWS) {
|
||||
return winVersion;
|
||||
} else if (SystemUtils.IS_OS_LINUX) {
|
||||
return linuxVersion;
|
||||
} else {
|
||||
throw new IllegalStateException("Unsupported operating system");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import org.cryptomator.integrations.common.OperatingSystem;
|
||||
import org.cryptomator.integrations.common.Priority;
|
||||
import org.cryptomator.integrations.update.DownloadUpdateStep;
|
||||
import org.cryptomator.integrations.update.UpdateFailedException;
|
||||
import org.cryptomator.integrations.update.UpdateInfo;
|
||||
import org.cryptomator.integrations.update.UpdateMechanism;
|
||||
import org.cryptomator.integrations.update.UpdateStep;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
@@ -27,12 +29,18 @@ public class MacOsDmgUpdateMechanism extends DownloadUpdateMechanism {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(MacOsDmgUpdateMechanism.class);
|
||||
|
||||
public MacOsDmgUpdateMechanism() {
|
||||
@Override
|
||||
UpdateInfo checkForUpdate(String currentVersion, LatestVersionResponse response) {
|
||||
String suffix = switch (System.getProperty("os.arch")) {
|
||||
case "aarch64", "arm64" -> "arm64.dmg";
|
||||
default -> "x64.dmg";
|
||||
};
|
||||
super(suffix);
|
||||
if (UpdateMechanism.isUpdateAvailable(response.latestVersion().macVersion(), currentVersion)
|
||||
&& response.assets().stream().map(Asset::name).anyMatch(s -> s.endsWith(suffix))) {
|
||||
return new UpdateInfo(response.latestVersion().macVersion(), this);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -3,10 +3,12 @@ package org.cryptomator.updater;
|
||||
import org.cryptomator.integrations.update.UpdateMechanism;
|
||||
import org.cryptomator.integrations.update.UpdateStep;
|
||||
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.concurrent.Service;
|
||||
import javafx.concurrent.Task;
|
||||
import java.io.IOException;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@@ -15,18 +17,25 @@ import java.util.concurrent.TimeUnit;
|
||||
*/
|
||||
public class UpdateService extends Service<UpdateStep> {
|
||||
|
||||
private final UpdateMechanism updateMechanism;
|
||||
private ObservableValue<UpdateMechanism> updateMechanism;
|
||||
|
||||
public UpdateService(UpdateMechanism updateMechanism) {
|
||||
public UpdateService(ObservableValue<UpdateMechanism> updateMechanism) {
|
||||
setExecutor(Executors.newVirtualThreadPerTaskExecutor());
|
||||
this.updateMechanism = updateMechanism;
|
||||
setExecutor(Executors.newVirtualThreadPerTaskExecutor()); }
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Task<UpdateStep> createTask() {
|
||||
return new RunAllStepsTask();
|
||||
return new RunAllStepsTask(updateMechanism.getValue());
|
||||
}
|
||||
|
||||
private class RunAllStepsTask extends Task<UpdateStep> {
|
||||
private static class RunAllStepsTask extends Task<UpdateStep> {
|
||||
|
||||
private final UpdateMechanism updateMechanism;
|
||||
|
||||
public RunAllStepsTask(UpdateMechanism updateMechanism) {
|
||||
this.updateMechanism = Objects.requireNonNull(updateMechanism);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected UpdateStep call() throws IOException {
|
||||
@@ -52,8 +61,6 @@ public class UpdateService extends Service<UpdateStep> {
|
||||
updateMessage(step.description());
|
||||
} while (!step.await(100, TimeUnit.MILLISECONDS));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -18,9 +18,6 @@
|
||||
xmlns="http://javafx.com/javafx"
|
||||
fx:controller="org.cryptomator.ui.preferences.UpdatesPreferencesController"
|
||||
spacing="12">
|
||||
<fx:define>
|
||||
<FormattedString fx:id="linkLabel" format="%preferences.updates.updateAvailable" arg1="${controller.latestVersion}"/>
|
||||
</fx:define>
|
||||
<padding>
|
||||
<Insets topRightBottomLeft="24"/>
|
||||
</padding>
|
||||
@@ -29,9 +26,20 @@
|
||||
<CheckBox fx:id="checkForUpdatesCheckbox" text="%preferences.updates.autoUpdateCheck"/>
|
||||
|
||||
<VBox alignment="CENTER" spacing="12">
|
||||
<Button text="%preferences.updates.checkNowBtn" defaultButton="true" onAction="#checkNow" contentDisplay="${controller.checkForUpdatesButtonState}">
|
||||
<FormattedLabel format="%preferences.updates.updateAvailable" arg1="${controller.latestVersion}" textAlignment="CENTER" wrapText="true" visible="${controller.updateChecker.updateAvailable}"/>
|
||||
|
||||
<Button text="${controller.updateButtonTitle}" defaultButton="true" onAction="#startWork" disable="${controller.running}" contentDisplay="${controller.updateButtonState}">
|
||||
<graphic>
|
||||
<FontAwesome5Spinner glyphSize="12"/>
|
||||
<VBox spacing="5" alignment="CENTER">
|
||||
<ProgressBar maxWidth="200"
|
||||
maxHeight="12"
|
||||
visible="${controller.running && controller.worker.progress != -1.0}"
|
||||
managed="${controller.running && controller.worker.progress != -1.0}"
|
||||
progress="${controller.worker.progress}"/>
|
||||
<FontAwesome5Spinner glyphSize="12"
|
||||
visible="${controller.running && controller.worker.progress == -1.0}"
|
||||
managed="${controller.running && controller.worker.progress == -1.0}"/>
|
||||
</VBox>
|
||||
</graphic>
|
||||
</Button>
|
||||
|
||||
@@ -52,19 +60,5 @@
|
||||
<FontAwesome5IconView glyphSize="12" styleClass="glyph-icon-primary" glyph="CHECK"/>
|
||||
</graphic>
|
||||
</Label>
|
||||
<Hyperlink text="${linkLabel.value}" onAction="#visitDownloadsPage" textAlignment="CENTER" wrapText="true" styleClass="hyperlink-underline" visible="${controller.updateAvailable}" managed="${controller.updateAvailable}"/>
|
||||
<Button onAction="#doUpdate" disable="${!controller.updateReady}"> <!-- TODO: visible="${controller.updateAvailable}" -->
|
||||
<graphic>
|
||||
<VBox spacing="5" alignment="CENTER">
|
||||
<Label text="${controller.updateButtonTitle}"/>
|
||||
<ProgressBar maxWidth="200"
|
||||
maxHeight="12"
|
||||
visible="${controller.updateService.running}"
|
||||
managed="${controller.updateService.running}"
|
||||
progress="${controller.updateService.progress}"/>
|
||||
</VBox>
|
||||
</graphic>
|
||||
</Button>
|
||||
|
||||
</VBox>
|
||||
</VBox>
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2016, 2017 Sebastian Stenzel and others.
|
||||
* All rights reserved.
|
||||
* This program and the accompanying materials are made available under the terms of the accompanying LICENSE file.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.common;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Comparator;
|
||||
|
||||
public class SemVerComparatorTest {
|
||||
|
||||
private final Comparator<String> semVerComparator = new SemVerComparator();
|
||||
|
||||
// equal versions
|
||||
|
||||
@Test
|
||||
public void compareEqualVersions() {
|
||||
Assertions.assertEquals(0, Integer.signum(semVerComparator.compare("1.23.4", "1.23.4")));
|
||||
Assertions.assertEquals(0, Integer.signum(semVerComparator.compare("1.23.4-alpha", "1.23.4-alpha")));
|
||||
Assertions.assertEquals(0, Integer.signum(semVerComparator.compare("1.23.4+20170101", "1.23.4+20171231")));
|
||||
Assertions.assertEquals(0, Integer.signum(semVerComparator.compare("1.23.4-alpha+20170101", "1.23.4-alpha+20171231")));
|
||||
}
|
||||
|
||||
// newer versions in first argument
|
||||
|
||||
@Test
|
||||
public void compareHigherToLowerVersions() {
|
||||
Assertions.assertEquals(1, Integer.signum(semVerComparator.compare("1.23.5", "1.23.4")));
|
||||
Assertions.assertEquals(1, Integer.signum(semVerComparator.compare("1.24.4", "1.23.4")));
|
||||
Assertions.assertEquals(1, Integer.signum(semVerComparator.compare("1.23.4", "1.23")));
|
||||
Assertions.assertEquals(1, Integer.signum(semVerComparator.compare("1.23.4", "1.23.4-SNAPSHOT")));
|
||||
Assertions.assertEquals(1, Integer.signum(semVerComparator.compare("1.23.4", "1.23.4-56.78")));
|
||||
Assertions.assertEquals(1, Integer.signum(semVerComparator.compare("1.23.4-beta", "1.23.4-alpha")));
|
||||
Assertions.assertEquals(1, Integer.signum(semVerComparator.compare("1.23.4-alpha.1", "1.23.4-alpha")));
|
||||
Assertions.assertEquals(1, Integer.signum(semVerComparator.compare("1.23.4-56.79", "1.23.4-56.78")));
|
||||
}
|
||||
|
||||
// newer versions in second argument
|
||||
|
||||
@Test
|
||||
public void compareLowerToHigherVersions() {
|
||||
Assertions.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23.4", "1.23.5")));
|
||||
Assertions.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23.4", "1.24.4")));
|
||||
Assertions.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23", "1.23.4")));
|
||||
Assertions.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23.4-SNAPSHOT", "1.23.4")));
|
||||
Assertions.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23.4-56.78", "1.23.4")));
|
||||
Assertions.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23.4-alpha", "1.23.4-beta")));
|
||||
Assertions.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23.4-alpha", "1.23.4-alpha.1")));
|
||||
Assertions.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23.4-56.78", "1.23.4-56.79")));
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user