mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-05-22 04:31:27 +00:00
Merge branch 'feature/self-update-poc' into feature/app-update
# Conflicts: # src/main/java/org/cryptomator/ui/preferences/UpdatesPreferencesController.java # src/main/resources/fxml/preferences_updates.fxml
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import ch.qos.logback.classic.spi.Configurator;
|
||||
import org.cryptomator.integrations.update.UpdateMechanism;
|
||||
import org.cryptomator.networking.SSLContextWithPKCS12TrustStore;
|
||||
import org.cryptomator.common.locationpresets.DropboxLinuxLocationPresetsProvider;
|
||||
import org.cryptomator.common.locationpresets.DropboxMacLocationPresetsProvider;
|
||||
@@ -20,6 +21,7 @@ import org.cryptomator.networking.SSLContextWithWindowsCertStore;
|
||||
import org.cryptomator.integrations.tray.TrayMenuController;
|
||||
import org.cryptomator.logging.LogbackConfiguratorFactory;
|
||||
import org.cryptomator.ui.traymenu.AwtTrayMenuController;
|
||||
import org.cryptomator.updater.MacOsDmgUpdateMechanism;
|
||||
|
||||
open module org.cryptomator.desktop {
|
||||
requires static org.jetbrains.annotations;
|
||||
@@ -50,7 +52,6 @@ open module org.cryptomator.desktop {
|
||||
requires io.github.coffeelibs.tinyoauth2client;
|
||||
requires org.slf4j;
|
||||
requires org.apache.commons.lang3;
|
||||
requires org.purejava.portal;
|
||||
|
||||
/* dagger bs */
|
||||
requires jakarta.inject;
|
||||
@@ -62,6 +63,9 @@ open module org.cryptomator.desktop {
|
||||
uses SSLContextProvider;
|
||||
uses org.cryptomator.event.NotificationHandler;
|
||||
|
||||
// opens org.cryptomator.updater to org.cryptomator.integrations.api;
|
||||
provides UpdateMechanism with MacOsDmgUpdateMechanism;
|
||||
|
||||
provides TrayMenuController with AwtTrayMenuController;
|
||||
provides Configurator with LogbackConfiguratorFactory;
|
||||
provides SSLContextProvider with SSLContextWithWindowsCertStore, SSLContextWithMacKeychain, SSLContextWithPKCS12TrustStore;
|
||||
|
||||
@@ -11,7 +11,6 @@ import org.cryptomator.common.keychain.KeychainModule;
|
||||
import org.cryptomator.common.mount.MountModule;
|
||||
import org.cryptomator.common.settings.Settings;
|
||||
import org.cryptomator.common.settings.SettingsProvider;
|
||||
import org.cryptomator.common.updates.UpdatesModule;
|
||||
import org.cryptomator.common.vaults.VaultComponent;
|
||||
import org.cryptomator.common.vaults.VaultListModule;
|
||||
import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
|
||||
@@ -31,7 +30,7 @@ import java.util.concurrent.SynchronousQueue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
@Module(subcomponents = {VaultComponent.class}, includes = {VaultListModule.class, KeychainModule.class, MountModule.class, UpdatesModule.class})
|
||||
@Module(subcomponents = {VaultComponent.class}, includes = {VaultListModule.class, KeychainModule.class, MountModule.class})
|
||||
public abstract class CommonsModule {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(CommonsModule.class);
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
package org.cryptomator.common.updates;
|
||||
|
||||
import org.cryptomator.integrations.common.DisplayName;
|
||||
import org.cryptomator.integrations.update.UpdateService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public class AppUpdateChecker {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AppUpdateChecker.class);
|
||||
private static final String DISPLAY_NAME_FLATPAK = "Update via Flatpak update";
|
||||
private final List<UpdateService> updateServices;
|
||||
|
||||
@Inject
|
||||
public AppUpdateChecker(List<UpdateService> updateServices) {
|
||||
this.updateServices = updateServices;
|
||||
}
|
||||
|
||||
public boolean isUpdateServiceAvailable(Optional<String> buildNumber) {
|
||||
if (buildNumber.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
switch (buildNumber.get()) {
|
||||
case "flatpak-1" -> {
|
||||
return !updateServices.isEmpty() && doServicesContainChannel(updateServices, DISPLAY_NAME_FLATPAK);
|
||||
}
|
||||
|
||||
default -> {
|
||||
LOG.error("Unexpected value 'buildNumber': {}", buildNumber.get());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Object getUpdater(Optional<String> buildNumber) {
|
||||
if (updateServices.isEmpty()) {
|
||||
LOG.error("No UpdateService found");
|
||||
return null;
|
||||
}
|
||||
switch (buildNumber.get()) {
|
||||
case "flatpak-1" -> {
|
||||
var flatpakService = getServiceForChannel(updateServices, DISPLAY_NAME_FLATPAK);
|
||||
if(null == flatpakService) {
|
||||
LOG.error("Required service for channel LINUX_FLATPAK not available");
|
||||
return null;
|
||||
} else {
|
||||
return flatpakService.getLatestReleaseChecker();
|
||||
}
|
||||
}
|
||||
default -> throw new IllegalStateException("Unexpected value 'buildNumber': " + buildNumber.get());
|
||||
}
|
||||
}
|
||||
|
||||
private boolean doServicesContainChannel(List<UpdateService> services, String displayName) {
|
||||
return services.stream().anyMatch(service -> {
|
||||
DisplayName annotation = service.getClass().getAnnotation(DisplayName.class);
|
||||
return annotation != null && annotation.value().equals(displayName);
|
||||
});
|
||||
}
|
||||
|
||||
private UpdateService getServiceForChannel(List<UpdateService> services, String displayName) {
|
||||
return services.stream().filter(service -> {
|
||||
DisplayName annotation = service.getClass().getAnnotation(DisplayName.class);
|
||||
return annotation != null && annotation.value().equals(displayName);
|
||||
}).findFirst().orElse(null);
|
||||
}
|
||||
|
||||
public UpdateService getServiceForChannel(String displayName) {
|
||||
return updateServices.stream().filter(service -> {
|
||||
DisplayName annotation = service.getClass().getAnnotation(DisplayName.class);
|
||||
return annotation != null && annotation.value().equals(displayName);
|
||||
}).findFirst().orElse(null);
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package org.cryptomator.common.updates;
|
||||
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import org.cryptomator.integrations.update.UpdateService;
|
||||
|
||||
import javax.inject.Singleton;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Module
|
||||
public class UpdatesModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
static List<UpdateService> provideSupportedUpdateServices() {
|
||||
return UpdateService.get().toList();
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
static Optional<UpdateService> provideUpdateService(List<UpdateService> updateServices) {
|
||||
return updateServices.stream().findFirst();
|
||||
}
|
||||
}
|
||||
@@ -3,17 +3,10 @@ package org.cryptomator.ui.fxapp;
|
||||
import org.cryptomator.common.Environment;
|
||||
import org.cryptomator.common.SemVerComparator;
|
||||
import org.cryptomator.common.settings.Settings;
|
||||
import org.cryptomator.common.updates.AppUpdateChecker;
|
||||
import org.cryptomator.integrations.update.Progress;
|
||||
import org.cryptomator.integrations.update.ProgressListener;
|
||||
import org.cryptomator.integrations.update.UpdateFailedException;
|
||||
import org.cryptomator.ui.preferences.UpdatesPreferencesController;
|
||||
import org.purejava.portal.rest.UpdateCheckerTask;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
@@ -26,105 +19,45 @@ import javafx.concurrent.Worker;
|
||||
import javafx.concurrent.WorkerStateEvent;
|
||||
import javafx.util.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Comparator;
|
||||
|
||||
@Deprecated(forRemoval = true) // to be replaced by fallback org.cryptomator.integrations.update.UpdateMechanism
|
||||
@FxApplicationScoped
|
||||
public class UpdateChecker {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(UpdateChecker.class);
|
||||
private static final Duration AUTO_CHECK_DELAY = Duration.seconds(5);
|
||||
private static final String DISPLAY_NAME_FLATPAK = "Update via Flatpak update";
|
||||
|
||||
private final Environment env;
|
||||
private final Settings settings;
|
||||
private final StringProperty latestVersion = new SimpleStringProperty();
|
||||
private final StringProperty latestAppUpdaterVersion = 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 appUpdateAvailable;
|
||||
private final BooleanBinding checkFailed;
|
||||
private final AppUpdateChecker updateChecker;
|
||||
private final FxApplicationTerminator appTerminator;
|
||||
|
||||
@Inject
|
||||
UpdateChecker(Settings settings, //
|
||||
Environment env, //
|
||||
ScheduledService<String> updateCheckerService, //
|
||||
AppUpdateChecker updateChecker, //
|
||||
FxApplicationTerminator appTerminator) {
|
||||
ScheduledService<String> updateCheckerService) {
|
||||
this.env = env;
|
||||
this.settings = settings;
|
||||
this.updateCheckerService = updateCheckerService;
|
||||
this.lastSuccessfulUpdateCheck = settings.lastSuccessfulUpdateCheck;
|
||||
this.updateAvailable = Bindings.createBooleanBinding(this::isUpdateAvailable, latestVersion);
|
||||
this.appUpdateAvailable = Bindings.createBooleanBinding(this::isAppUpdateAvailable, latestAppUpdaterVersion);
|
||||
this.checkFailed = Bindings.equal(UpdateCheckState.CHECK_FAILED, state);
|
||||
this.updateChecker = updateChecker;
|
||||
this.appTerminator = appTerminator;
|
||||
}
|
||||
|
||||
public void automaticallyCheckForUpdatesIfEnabled() {
|
||||
if (!env.disableUpdateCheck() && settings.checkForUpdates.get()) {
|
||||
decideOnUpdateChecker();
|
||||
startCheckingForUpdates(AUTO_CHECK_DELAY);
|
||||
}
|
||||
}
|
||||
|
||||
public void checkForUpdatesNow() {
|
||||
decideOnUpdateChecker();
|
||||
}
|
||||
|
||||
private void decideOnUpdateChecker() {
|
||||
if (updateChecker.isUpdateServiceAvailable(env.getBuildNumber())) { // prefer AppUpdateChecker
|
||||
switch (env.getBuildNumber().get()) {
|
||||
case "flatpak-1" -> startCheckingWithFlatpakUpdater((UpdateCheckerTask) updateChecker.getUpdater(env.getBuildNumber()), Duration.ZERO);
|
||||
default -> LOG.error("Unexpected value 'buildNumber': {}", env.getBuildNumber().get());
|
||||
}
|
||||
} else { // fallback is the "redirect user to website" approach
|
||||
startCheckingForUpdates(Duration.ZERO);
|
||||
}
|
||||
}
|
||||
|
||||
public void updateAppNow() throws UpdateFailedException {
|
||||
var service = updateChecker.getServiceForChannel(DISPLAY_NAME_FLATPAK);
|
||||
service.triggerUpdate();
|
||||
}
|
||||
|
||||
public void terminateFlatpakOnUpdateCompleted(Runnable onComplete, UpdatesPreferencesController controller) {
|
||||
var service = updateChecker.getServiceForChannel(DISPLAY_NAME_FLATPAK);
|
||||
service.addProgressListener(new ProgressListener() {
|
||||
@Override
|
||||
public void onProgress(Progress progress) {
|
||||
|
||||
if (progress.getStatus() == 1) {
|
||||
LOG.info("No update to install");
|
||||
return;
|
||||
}
|
||||
|
||||
if (progress.getStatus() == 3) {
|
||||
LOG.info("Update failed");
|
||||
return;
|
||||
}
|
||||
|
||||
if (progress.getStatus() == 0 || progress.getStatus() == 2) {
|
||||
LOG.debug("Update progess is at percentage: {} and has status: {}", progress.getProgress(), progress.getStatus());
|
||||
Platform.runLater(() -> controller.flatpakProgressProperty().set(progress.getProgress() / 100.0));
|
||||
}
|
||||
|
||||
if (progress.getStatus() == 2 && progress.getProgress() == 100) {
|
||||
LOG.debug("Update successfully finished, restarting App now");
|
||||
service.removeProgressListener(this);
|
||||
if (onComplete != null) {
|
||||
Platform.runLater(onComplete);
|
||||
}
|
||||
service.spawnApp();
|
||||
appTerminator.terminate();
|
||||
}
|
||||
}
|
||||
});
|
||||
startCheckingForUpdates(Duration.ZERO);
|
||||
}
|
||||
|
||||
private void startCheckingForUpdates(Duration initialDelay) {
|
||||
@@ -137,31 +70,11 @@ public class UpdateChecker {
|
||||
updateCheckerService.start();
|
||||
}
|
||||
|
||||
private void startCheckingWithFlatpakUpdater(UpdateCheckerTask service, Duration initialDelay) {
|
||||
service.cancel();
|
||||
service.reset();
|
||||
service.setDelay(convertFxToJavaTime(initialDelay));
|
||||
service.setOnRunning(this::checkStarted);
|
||||
service.setOnSucceeded(this::checkSucceeded);
|
||||
service.setOnFailed(this::checkFailed);
|
||||
service.start();
|
||||
}
|
||||
|
||||
private java.time.Duration convertFxToJavaTime(javafx.util.Duration fxDuration) {
|
||||
double millis = fxDuration.toMillis();
|
||||
return java.time.Duration.of((long) millis, ChronoUnit.MILLIS);
|
||||
}
|
||||
|
||||
private void checkStarted(WorkerStateEvent event) {
|
||||
LOG.debug("Checking for updates...");
|
||||
state.set(UpdateCheckState.IS_CHECKING);
|
||||
}
|
||||
|
||||
private void checkStarted() {
|
||||
LOG.debug("Checking for updates...");
|
||||
state.set(UpdateCheckState.IS_CHECKING);
|
||||
}
|
||||
|
||||
private void checkSucceeded(WorkerStateEvent event) {
|
||||
var latestVersionString = updateCheckerService.getValue();
|
||||
LOG.info("Current version: {}, latest version: {}", getCurrentVersion(), latestVersionString);
|
||||
@@ -170,21 +83,10 @@ public class UpdateChecker {
|
||||
state.set(UpdateCheckState.CHECK_SUCCESSFUL);
|
||||
}
|
||||
|
||||
private void checkSucceeded(String version) {
|
||||
LOG.info("Current version: {}, latest version: {}", getCurrentVersion(), version);
|
||||
lastSuccessfulUpdateCheck.set(Instant.now());
|
||||
latestAppUpdaterVersion.set(version);
|
||||
state.set(UpdateCheckState.CHECK_SUCCESSFUL);
|
||||
}
|
||||
|
||||
private void checkFailed(WorkerStateEvent event) {
|
||||
state.set(UpdateCheckState.CHECK_FAILED);
|
||||
}
|
||||
|
||||
private void checkFailed(Throwable throwable) {
|
||||
state.set(UpdateCheckState.CHECK_FAILED);
|
||||
}
|
||||
|
||||
public enum UpdateCheckState {
|
||||
NOT_CHECKED,
|
||||
IS_CHECKING,
|
||||
@@ -201,38 +103,23 @@ public class UpdateChecker {
|
||||
return latestVersion;
|
||||
}
|
||||
|
||||
public ReadOnlyStringProperty latestAppUpdaterVersionProperty() {
|
||||
return latestAppUpdaterVersion;
|
||||
}
|
||||
|
||||
public BooleanBinding updateAvailableProperty() {
|
||||
return updateAvailable;
|
||||
}
|
||||
|
||||
public BooleanBinding appUpdateAvailableProperty() {
|
||||
return appUpdateAvailable;
|
||||
}
|
||||
|
||||
public BooleanBinding checkFailedProperty() {
|
||||
return checkFailed;
|
||||
}
|
||||
|
||||
public boolean isUpdateAvailable(StringProperty versionProperty) {
|
||||
public boolean isUpdateAvailable() {
|
||||
String currentVersion = getCurrentVersion();
|
||||
String latestVersionString = versionProperty.get();
|
||||
String latestVersionString = latestVersion.get();
|
||||
|
||||
if (currentVersion == null || latestVersionString == null) {
|
||||
return false;
|
||||
} else {
|
||||
return versionComparator.compare(currentVersion, latestVersionString) < 0;
|
||||
}
|
||||
return versionComparator.compare(currentVersion, latestVersionString) < 0;
|
||||
}
|
||||
|
||||
public boolean isUpdateAvailable() {
|
||||
return isUpdateAvailable(latestVersion);
|
||||
}
|
||||
|
||||
public boolean isAppUpdateAvailable() {
|
||||
return isUpdateAvailable(latestAppUpdaterVersion);
|
||||
}
|
||||
|
||||
public ObjectProperty<Instant> lastSuccessfulUpdateCheckProperty() {
|
||||
|
||||
@@ -51,7 +51,7 @@ public class MainWindowController implements FxController {
|
||||
this.selectedVault = selectedVault;
|
||||
this.settings = settings;
|
||||
this.appWindows = appWindows;
|
||||
this.updateAvailable = updateChecker.updateAvailableProperty().or(updateChecker.appUpdateAvailableProperty());
|
||||
this.updateAvailable = updateChecker.updateAvailableProperty();
|
||||
this.licenseHolder = licenseHolder;
|
||||
updateChecker.automaticallyCheckForUpdatesIfEnabled();
|
||||
|
||||
|
||||
@@ -2,9 +2,8 @@ package org.cryptomator.ui.preferences;
|
||||
|
||||
import org.cryptomator.common.Environment;
|
||||
import org.cryptomator.common.settings.Settings;
|
||||
import org.cryptomator.common.updates.AppUpdateChecker;
|
||||
import org.cryptomator.integrations.common.DisplayName;
|
||||
import org.cryptomator.integrations.update.UpdateFailedException;
|
||||
import org.cryptomator.integrations.update.UpdateMechanism;
|
||||
import org.cryptomator.integrations.update.UpdateProcess;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.fxapp.UpdateChecker;
|
||||
import org.slf4j.Logger;
|
||||
@@ -13,21 +12,20 @@ import org.slf4j.LoggerFactory;
|
||||
import javax.inject.Inject;
|
||||
import javafx.animation.PauseTransition;
|
||||
import javafx.application.Application;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
import javafx.beans.binding.StringBinding;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.DoubleProperty;
|
||||
import javafx.beans.property.ReadOnlyStringProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleDoubleProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.concurrent.Task;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.CheckBox;
|
||||
import javafx.scene.control.ContentDisplay;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ProgressBar;
|
||||
import java.io.IOException;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
@@ -38,6 +36,8 @@ import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.FormatStyle;
|
||||
import java.util.Locale;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
|
||||
@PreferencesScoped
|
||||
@@ -48,7 +48,6 @@ public class UpdatesPreferencesController implements FxController {
|
||||
+ "?utm_source=cryptomator-desktop" //
|
||||
+ "&utm_medium=update-notification&" //
|
||||
+ "utm_campaign=app-update-%s";
|
||||
private static final String DISPLAY_NAME_FLATPAK = "Update via Flatpak update";
|
||||
|
||||
private final Application application;
|
||||
private final Environment environment;
|
||||
@@ -56,7 +55,6 @@ public class UpdatesPreferencesController implements FxController {
|
||||
private final Settings settings;
|
||||
private final Environment env;
|
||||
private final UpdateChecker updateChecker;
|
||||
private final AppUpdateChecker appUpdateChecker;
|
||||
private final ObjectBinding<ContentDisplay> checkForUpdatesButtonState;
|
||||
private final ReadOnlyStringProperty latestVersion;
|
||||
private final ObservableValue<Instant> lastSuccessfulUpdateCheck;
|
||||
@@ -64,49 +62,53 @@ public class UpdatesPreferencesController implements FxController {
|
||||
private final ObservableValue<String> timeDifferenceMessage;
|
||||
private final String currentVersion;
|
||||
private final BooleanBinding updateAvailable;
|
||||
private final BooleanBinding appUpdateAvailable;
|
||||
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 BooleanProperty updatingFlatpak = new SimpleBooleanProperty(false);
|
||||
private final DoubleProperty flatpakProgress = new SimpleDoubleProperty(ProgressBar.INDETERMINATE_PROGRESS);
|
||||
private final UpdateMechanism updateMechanism;
|
||||
public final Task<UpdateProcess> updatePreparationTask;
|
||||
private final StringBinding updateButtonTitle;
|
||||
|
||||
/* FXML */
|
||||
public CheckBox checkForUpdatesCheckbox;
|
||||
public Label flatpakButtonLabel;
|
||||
|
||||
@Inject
|
||||
UpdatesPreferencesController(Application application, Environment environment, ResourceBundle resourceBundle, Settings settings, UpdateChecker updateChecker, AppUpdateChecker appUpdateChecker, Environment env) {
|
||||
UpdatesPreferencesController(Application application, Environment environment, ResourceBundle resourceBundle, Settings settings, UpdateChecker updateChecker, Environment env) {
|
||||
this.application = application;
|
||||
this.environment = environment;
|
||||
this.resourceBundle = resourceBundle;
|
||||
this.settings = settings;
|
||||
this.env = env;
|
||||
this.updateChecker = updateChecker;
|
||||
this.appUpdateChecker = appUpdateChecker;
|
||||
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.appUpdateAvailable = updateChecker.appUpdateAvailableProperty();
|
||||
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();
|
||||
this.updatePreparationTask = new Task<>() { // TODO custom class?
|
||||
@Override
|
||||
protected UpdateProcess call() throws IOException, InterruptedException {
|
||||
var updateProcess = updateMechanism.prepareUpdate();
|
||||
do {
|
||||
updateProgress(updateProcess.preparationProgress(), 1.0);
|
||||
} while (!updateProcess.await(100, TimeUnit.MILLISECONDS));
|
||||
return updateProcess;
|
||||
}
|
||||
};
|
||||
this.updateButtonTitle = Bindings.createStringBinding(this::getUpdateButtonTitle, updatePreparationTask.stateProperty());
|
||||
}
|
||||
|
||||
public void initialize() {
|
||||
checkForUpdatesCheckbox.selectedProperty().bindBidirectional(settings.checkForUpdates);
|
||||
switch (env.getBuildNumber().get()) {
|
||||
case "flatpak-1" -> flatpakButtonLabel.setText(appUpdateChecker.getServiceForChannel(DISPLAY_NAME_FLATPAK).getClass().getAnnotation(DisplayName.class).value());
|
||||
default -> LOG.error("Unexpected value 'buildNumber': {}", env.getBuildNumber().get());
|
||||
}
|
||||
|
||||
upToDate.addListener((_, _, newVal) -> {
|
||||
if (newVal) {
|
||||
upToDateLabelVisible.set(true);
|
||||
@@ -122,20 +124,6 @@ public class UpdatesPreferencesController implements FxController {
|
||||
updateChecker.checkForUpdatesNow();
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void updateFlatpakNow() {
|
||||
updatingFlatpak.set(true);
|
||||
updateChecker.terminateFlatpakOnUpdateCompleted(
|
||||
() -> updatingFlatpak.set(false), this
|
||||
);
|
||||
|
||||
try {
|
||||
updateChecker.updateAppNow();
|
||||
} catch (UpdateFailedException e) {
|
||||
updatingFlatpak.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void visitDownloadsPage() {
|
||||
application.getHostServices().showDocument(downloadsUri);
|
||||
@@ -146,6 +134,30 @@ public class UpdatesPreferencesController implements FxController {
|
||||
environment.getLogDir().ifPresent(logDirPath -> application.getHostServices().showDocument(logDirPath.toUri().toString()));
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void doUpdate() {
|
||||
if (updatePreparationTask.isDone()) {
|
||||
try {
|
||||
// TODO: check if all vaults closed?
|
||||
var restartProcess = updatePreparationTask.get().applyUpdate();
|
||||
if (restartProcess.isAlive()) {
|
||||
Platform.exit();
|
||||
} else {
|
||||
LOG.error("Update process terminated prematurely: {}", restartProcess.info().commandLine());
|
||||
}
|
||||
Platform.exit(); // TODO: prompt?
|
||||
} catch (IOException | InterruptedException | ExecutionException e) {
|
||||
LOG.error("Oh no", e); // TODO: Show error dialog
|
||||
}
|
||||
} else if (updatePreparationTask.isRunning()) {
|
||||
throw new IllegalStateException("Update already in progress");
|
||||
} else if (updatePreparationTask.isCancelled()) {
|
||||
throw new IllegalStateException("Update preparation task was cancelled");
|
||||
} else {
|
||||
Thread.startVirtualThread(updatePreparationTask);
|
||||
}
|
||||
}
|
||||
|
||||
/* Observable Properties */
|
||||
|
||||
public ObjectBinding<ContentDisplay> checkForUpdatesButtonStateProperty() {
|
||||
@@ -212,18 +224,10 @@ public class UpdatesPreferencesController implements FxController {
|
||||
return updateAvailable;
|
||||
}
|
||||
|
||||
public BooleanBinding appUdateAvailableProperty() {
|
||||
return appUpdateAvailable;
|
||||
}
|
||||
|
||||
public boolean isUpdateAvailable() {
|
||||
return updateAvailable.get();
|
||||
}
|
||||
|
||||
public boolean isAppUpdateAvailable() {
|
||||
return appUpdateAvailable.get();
|
||||
}
|
||||
|
||||
public BooleanBinding checkFailedProperty() {
|
||||
return checkFailed;
|
||||
}
|
||||
@@ -232,19 +236,20 @@ public class UpdatesPreferencesController implements FxController {
|
||||
return checkFailed.getValue();
|
||||
}
|
||||
|
||||
public BooleanProperty updatingFlatpakProperty() {
|
||||
return updatingFlatpak;
|
||||
public Task<UpdateProcess> getUpdatePreparationTask() {
|
||||
return updatePreparationTask;
|
||||
}
|
||||
|
||||
public boolean isUpdatingFlatpak() {
|
||||
return updatingFlatpak.get();
|
||||
public StringBinding updateButtonTitleProperty() {
|
||||
return updateButtonTitle;
|
||||
}
|
||||
|
||||
public DoubleProperty flatpakProgressProperty() {
|
||||
return flatpakProgress;
|
||||
}
|
||||
|
||||
public double getFlatpakProgress() {
|
||||
return flatpakProgress.get();
|
||||
public String getUpdateButtonTitle() {
|
||||
return switch (updatePreparationTask.getState()) {
|
||||
case READY -> updateMechanism.getName();
|
||||
case SCHEDULED, RUNNING -> "Preparing Update..."; // TODO: resourceBundle.getString("preferences.updates.preparingUpdate")...
|
||||
case SUCCEEDED -> "Restart to Update"; // TODO: resourceBundle.getString("preferences.updates.readyToRestart")...
|
||||
case FAILED, CANCELLED -> "failed";
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
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.UpdateMechanism;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.util.List;
|
||||
|
||||
public abstract class DownloadUpdateMechanism implements UpdateMechanism {
|
||||
|
||||
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||
|
||||
@Override
|
||||
public boolean isUpdateAvailable() {
|
||||
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());
|
||||
|
||||
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("arm64.dmg"));
|
||||
} catch (IOException | InterruptedException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public record GitHubRelease(
|
||||
@JsonProperty("tag_name") String tagName,
|
||||
List<Asset> assets
|
||||
) {}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public record Asset(
|
||||
String name,
|
||||
@JsonProperty("browser_download_url") String downloadUrl
|
||||
) {}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package org.cryptomator.updater;
|
||||
|
||||
import org.cryptomator.integrations.common.DisplayName;
|
||||
import org.cryptomator.integrations.common.OperatingSystem;
|
||||
import org.cryptomator.integrations.common.Priority;
|
||||
import org.cryptomator.integrations.update.DownloadUpdateProcess;
|
||||
import org.cryptomator.integrations.update.UpdateFailedException;
|
||||
import org.cryptomator.integrations.update.UpdateProcess;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.HexFormat;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
|
||||
@Priority(1000)
|
||||
@OperatingSystem(OperatingSystem.Value.MAC)
|
||||
@DisplayName("download .dmg file") // TODO: localize
|
||||
public class MacOsDmgUpdateMechanism extends DownloadUpdateMechanism {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(MacOsDmgUpdateMechanism.class);
|
||||
|
||||
@Override
|
||||
public UpdateProcess prepareUpdate() throws UpdateFailedException {
|
||||
try {
|
||||
Path workDir = Files.createTempDirectory("cryptomator-update");
|
||||
return new UpdateProcessImpl(workDir);
|
||||
} catch (IOException e) {
|
||||
throw new UpdateFailedException("Failed to create temporary directory for update", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static class UpdateProcessImpl extends DownloadUpdateProcess {
|
||||
|
||||
// FIXME: use URI and CHECKSUM from update API
|
||||
private static final URI UPDATE_URI = URI.create("https://github.com/cryptomator/cryptomator/releases/download/1.17.0/Cryptomator-1.17.0-arm64.dmg");
|
||||
private static final byte[] CHECKSUM = HexFormat.of().withLowerCase().parseHex("03f45e203204e93b39925cbb04e19c9316da4f77debaba4fb5071f0ec8e727e8");
|
||||
|
||||
public UpdateProcessImpl(Path downloadPath) {
|
||||
super(downloadPath, "update.dmg", UPDATE_URI, CHECKSUM,60_000_000L); // initially assume 60 MB for the update size
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void postDownload(Path downloadPath) throws IOException {
|
||||
// Extract Cryptomator.app from the .dmg file
|
||||
String script = """
|
||||
hdiutil attach 'update.dmg' -mountpoint "/Volumes/Cryptomator_${MOUNT_ID}" -nobrowse -quiet &&
|
||||
cp -R "/Volumes/Cryptomator_${MOUNT_ID}/Cryptomator.app" 'Cryptomator.app' &&
|
||||
hdiutil detach "/Volumes/Cryptomator_${MOUNT_ID}" -quiet
|
||||
""";
|
||||
var command = List.of("bash", "-c", script);
|
||||
var processBuilder = new ProcessBuilder(command);
|
||||
processBuilder.directory(workDir.toFile());
|
||||
processBuilder.environment().put("MOUNT_ID", UUID.randomUUID().toString());
|
||||
Process p = processBuilder.start();
|
||||
try {
|
||||
if (p.waitFor() != 0) {
|
||||
LOG.error("Failed to extract DMG, exit code: {}, output: {}", p.exitValue(), new String(p.getErrorStream().readAllBytes()));
|
||||
throw new IOException("Failed to extract DMG, exit code: " + p.exitValue());
|
||||
}
|
||||
LOG.debug("Update ready: {}", workDir.resolve("Cryptomator.app"));
|
||||
} catch (InterruptedException e) {
|
||||
throw new InterruptedIOException("Failed to extract DMG, interrupted");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProcessHandle applyUpdate() throws IllegalStateException, IOException {
|
||||
if (!isDone()) {
|
||||
throw new IllegalStateException("Update not yet downloaded");
|
||||
} else if (downloadException != null) {
|
||||
throw new IllegalStateException("Downloading update failed", downloadException);
|
||||
}
|
||||
|
||||
String selfPath = ProcessHandle.current().info().command().orElse("");
|
||||
String installPath;
|
||||
if (selfPath.startsWith("/Applications/Cryptomator.app")) {
|
||||
installPath = "/Applications/Cryptomator.app";
|
||||
} else if (selfPath.contains("/Cryptomator.app/")) {
|
||||
installPath = selfPath.substring(0, selfPath.indexOf("/Cryptomator.app/")) + "/Cryptomator.app";
|
||||
} else {
|
||||
throw new UpdateFailedException("Cannot determine destination path for Cryptomator.app, current path: " + selfPath);
|
||||
}
|
||||
String script = """
|
||||
while kill -0 ${CRYPTOMATOR_PID} 2> /dev/null; do sleep 0.5; done;
|
||||
cp -R 'Cryptomator.app' "${CRYPTOMATOR_INSTALL_PATH}";
|
||||
open -a "${CRYPTOMATOR_INSTALL_PATH}"
|
||||
""";
|
||||
var command = List.of("bash", "-c", "nohup bash -c \"" + script + "\" >/Users/sebastian/Downloads/nohup.out 2>&1 &");
|
||||
var processBuilder = new ProcessBuilder(command);
|
||||
processBuilder.directory(workDir.toFile());
|
||||
processBuilder.environment().put("CRYPTOMATOR_PID", String.valueOf(ProcessHandle.current().pid()));
|
||||
processBuilder.environment().put("CRYPTOMATOR_INSTALL_PATH", installPath);
|
||||
return processBuilder.start().toHandle();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user