diff --git a/.idea/compiler.xml b/.idea/compiler.xml
index 1256745d3..29885bf6a 100644
--- a/.idea/compiler.xml
+++ b/.idea/compiler.xml
@@ -14,15 +14,15 @@
-
-
+
+
-
+
-
-
+
+
@@ -38,7 +38,32 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
index 0634be039..31b119b32 100644
--- a/.idea/inspectionProfiles/Project_Default.xml
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -1,10 +1,8 @@
-
-
-
-
-
+
+
+
\ No newline at end of file
diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java
index 3acbc041d..cb8f9b6bc 100644
--- a/src/main/java/module-info.java
+++ b/src/main/java/module-info.java
@@ -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;
diff --git a/src/main/java/org/cryptomator/common/CommonsModule.java b/src/main/java/org/cryptomator/common/CommonsModule.java
index f510d1828..a1e3c0950 100644
--- a/src/main/java/org/cryptomator/common/CommonsModule.java
+++ b/src/main/java/org/cryptomator/common/CommonsModule.java
@@ -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);
diff --git a/src/main/java/org/cryptomator/common/updates/AppUpdateChecker.java b/src/main/java/org/cryptomator/common/updates/AppUpdateChecker.java
deleted file mode 100644
index f35d6d757..000000000
--- a/src/main/java/org/cryptomator/common/updates/AppUpdateChecker.java
+++ /dev/null
@@ -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 updateServices;
-
- @Inject
- public AppUpdateChecker(List updateServices) {
- this.updateServices = updateServices;
- }
-
- public boolean isUpdateServiceAvailable(Optional 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 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 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 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);
- }
-}
diff --git a/src/main/java/org/cryptomator/common/updates/UpdatesModule.java b/src/main/java/org/cryptomator/common/updates/UpdatesModule.java
deleted file mode 100644
index 986e90f59..000000000
--- a/src/main/java/org/cryptomator/common/updates/UpdatesModule.java
+++ /dev/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 provideSupportedUpdateServices() {
- return UpdateService.get().toList();
- }
-
- @Provides
- @Singleton
- static Optional provideUpdateService(List updateServices) {
- return updateServices.stream().findFirst();
- }
-}
diff --git a/src/main/java/org/cryptomator/ui/fxapp/UpdateChecker.java b/src/main/java/org/cryptomator/ui/fxapp/UpdateChecker.java
index 28520d7a3..e835793c6 100644
--- a/src/main/java/org/cryptomator/ui/fxapp/UpdateChecker.java
+++ b/src/main/java/org/cryptomator/ui/fxapp/UpdateChecker.java
@@ -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 updateCheckerService;
private final ObjectProperty state = new SimpleObjectProperty<>(UpdateCheckState.NOT_CHECKED);
private final ObjectProperty lastSuccessfulUpdateCheck;
private final Comparator 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 updateCheckerService, //
- AppUpdateChecker updateChecker, //
- FxApplicationTerminator appTerminator) {
+ ScheduledService 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 lastSuccessfulUpdateCheckProperty() {
diff --git a/src/main/java/org/cryptomator/ui/mainwindow/MainWindowController.java b/src/main/java/org/cryptomator/ui/mainwindow/MainWindowController.java
index ba7fa43be..c6e084518 100644
--- a/src/main/java/org/cryptomator/ui/mainwindow/MainWindowController.java
+++ b/src/main/java/org/cryptomator/ui/mainwindow/MainWindowController.java
@@ -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();
diff --git a/src/main/java/org/cryptomator/ui/preferences/UpdatesPreferencesController.java b/src/main/java/org/cryptomator/ui/preferences/UpdatesPreferencesController.java
index 4382c63ef..f33b474f1 100644
--- a/src/main/java/org/cryptomator/ui/preferences/UpdatesPreferencesController.java
+++ b/src/main/java/org/cryptomator/ui/preferences/UpdatesPreferencesController.java
@@ -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 checkForUpdatesButtonState;
private final ReadOnlyStringProperty latestVersion;
private final ObservableValue lastSuccessfulUpdateCheck;
@@ -64,49 +62,53 @@ public class UpdatesPreferencesController implements FxController {
private final ObservableValue 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 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 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 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";
+ };
}
}
diff --git a/src/main/java/org/cryptomator/updater/DownloadUpdateMechanism.java b/src/main/java/org/cryptomator/updater/DownloadUpdateMechanism.java
new file mode 100644
index 000000000..8434a9964
--- /dev/null
+++ b/src/main/java/org/cryptomator/updater/DownloadUpdateMechanism.java
@@ -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 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 assets
+ ) {}
+
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ public record Asset(
+ String name,
+ @JsonProperty("browser_download_url") String downloadUrl
+ ) {}
+
+}
diff --git a/src/main/java/org/cryptomator/updater/MacOsDmgUpdateMechanism.java b/src/main/java/org/cryptomator/updater/MacOsDmgUpdateMechanism.java
new file mode 100644
index 000000000..6d288bbae
--- /dev/null
+++ b/src/main/java/org/cryptomator/updater/MacOsDmgUpdateMechanism.java
@@ -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();
+ }
+ }
+
+
+}
diff --git a/src/main/resources/fxml/preferences_updates.fxml b/src/main/resources/fxml/preferences_updates.fxml
index 9f334bcfa..9d72e243a 100644
--- a/src/main/resources/fxml/preferences_updates.fxml
+++ b/src/main/resources/fxml/preferences_updates.fxml
@@ -53,16 +53,15 @@
-