From 093f0e8c94e48b8d1ba0fa3463f8f2c5fb182922 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Tue, 4 Nov 2025 11:12:43 +0100 Subject: [PATCH] cleanup updater --- .../cryptomator/ui/fxapp/UpdateChecker.java | 13 +++- .../UpdatesPreferencesController.java | 75 +++++-------------- .../updater/DownloadUpdateMechanism.java | 12 ++- .../updater/FallbackUpdateMechanism.java | 6 +- .../updater/MacOsDmgUpdateMechanism.java | 37 +++++---- .../cryptomator/updater/UpdateService.java | 17 +++-- .../resources/fxml/preferences_updates.fxml | 7 +- 7 files changed, 75 insertions(+), 92 deletions(-) diff --git a/src/main/java/org/cryptomator/ui/fxapp/UpdateChecker.java b/src/main/java/org/cryptomator/ui/fxapp/UpdateChecker.java index 59adc1910..b825b5017 100644 --- a/src/main/java/org/cryptomator/ui/fxapp/UpdateChecker.java +++ b/src/main/java/org/cryptomator/ui/fxapp/UpdateChecker.java @@ -17,7 +17,6 @@ import javafx.beans.binding.StringExpression; import javafx.beans.property.ObjectProperty; import javafx.concurrent.ScheduledService; import javafx.concurrent.Task; -import javafx.concurrent.Worker; import javafx.util.Duration; import java.net.http.HttpClient; import java.time.Instant; @@ -106,6 +105,10 @@ public class UpdateChecker extends ScheduledService { /* Observable Properties */ + public String getLatestVersion() { + return latestVersion.get(); + } + public StringExpression latestVersionProperty() { return latestVersion; } @@ -118,10 +121,18 @@ public class UpdateChecker extends ScheduledService { return updateAvailable; } + public boolean isCheckFailed() { + return checkFailed.get(); + } + public BooleanBinding checkFailedProperty() { return checkFailed; } + public Instant getLastSuccessfulUpdateCheck() { + return lastSuccessfulUpdateCheck.get(); + } + public ObjectProperty lastSuccessfulUpdateCheckProperty() { return lastSuccessfulUpdateCheck; } diff --git a/src/main/java/org/cryptomator/ui/preferences/UpdatesPreferencesController.java b/src/main/java/org/cryptomator/ui/preferences/UpdatesPreferencesController.java index 9b0282112..9fe004d37 100644 --- a/src/main/java/org/cryptomator/ui/preferences/UpdatesPreferencesController.java +++ b/src/main/java/org/cryptomator/ui/preferences/UpdatesPreferencesController.java @@ -2,7 +2,6 @@ package org.cryptomator.ui.preferences; import org.cryptomator.common.Environment; import org.cryptomator.common.settings.Settings; -import org.cryptomator.integrations.update.UpdateInfo; import org.cryptomator.integrations.update.UpdateStep; import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.fxapp.UpdateChecker; @@ -16,11 +15,9 @@ 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.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.SimpleBooleanProperty; import javafx.beans.value.ObservableValue; @@ -43,27 +40,21 @@ import java.util.ResourceBundle; public class UpdatesPreferencesController implements FxController { private static final Logger LOG = LoggerFactory.getLogger(UpdatesPreferencesController.class); + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).withLocale(Locale.getDefault()); private final Application application; private final Environment environment; private final ResourceBundle resourceBundle; private final Settings settings; private final UpdateChecker updateChecker; - private final ObjectBinding updateButtonState; - private final StringExpression latestVersion; - private final ObservableValue lastSuccessfulUpdateCheck; - private final StringBinding lastUpdateCheckMessage; - private final ObservableValue timeDifferenceMessage; - private final String currentVersion; - private final BooleanBinding checkFailed; - private final BooleanProperty upToDateLabelVisible = new SimpleBooleanProperty(false); - private final DateTimeFormatter formatter; - private final BooleanBinding upToDate; private final UpdateService updateService; - private final StringBinding updateButtonTitle; - private final ObjectBinding> worker; private final BooleanExpression running; + private final StringBinding updateButtonTitle; + private final ObjectBinding updateButtonState; + private final ObservableValue timeDifferenceMessage; + private final StringBinding lastUpdateCheckMessage; + private final BooleanProperty upToDateLabelVisible = new SimpleBooleanProperty(false); /* FXML */ public CheckBox checkForUpdatesCheckbox; @@ -75,39 +66,27 @@ public class UpdatesPreferencesController implements FxController { this.resourceBundle = resourceBundle; this.settings = settings; this.updateChecker = updateChecker; - - this.latestVersion = updateChecker.latestVersionProperty(); - this.lastSuccessfulUpdateCheck = updateChecker.lastSuccessfulUpdateCheckProperty(); - this.timeDifferenceMessage = Bindings.createStringBinding(this::getTimeDifferenceMessage, lastSuccessfulUpdateCheck); - 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.updateService = new UpdateService(updateChecker.lastValueProperty().map(UpdateInfo::updateMechanism)); - this.worker = Bindings.when(updateChecker.updateAvailableProperty()).>then(updateService).otherwise(updateChecker); + this.updateService = new UpdateService(updateChecker.lastValueProperty()); + this.worker = Bindings.when(updateChecker.updateAvailableProperty()).>then(this.updateService).otherwise(this.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); + this.timeDifferenceMessage = Bindings.createStringBinding(this::getTimeDifferenceMessage, updateChecker.lastSuccessfulUpdateCheckProperty()); + this.lastUpdateCheckMessage = Bindings.createStringBinding(this::getLastUpdateCheckMessage, updateChecker.lastSuccessfulUpdateCheckProperty()); } public void initialize() { checkForUpdatesCheckbox.selectedProperty().bindBidirectional(settings.checkForUpdates); - upToDate.addListener((_, _, newVal) -> { - if (newVal) { + updateChecker.updateAvailableProperty().addListener((_, _, hasUpdate) -> { + if (!hasUpdate) { upToDateLabelVisible.set(true); PauseTransition delay = new PauseTransition(javafx.util.Duration.seconds(5)); delay.setOnFinished(_ -> upToDateLabelVisible.set(false)); delay.play(); } }); + updateService.setOnSucceeded(this::updateSucceeded); + updateService.setOnFailed(this::updateFailed); } @FXML @@ -158,26 +137,14 @@ public class UpdatesPreferencesController implements FxController { } } - public StringExpression latestVersionProperty() { - return latestVersion; - } - - public String getLatestVersion() { - return latestVersion.get(); - } - - public String getCurrentVersion() { - return currentVersion; - } - public StringBinding lastUpdateCheckMessageProperty() { return lastUpdateCheckMessage; } public String getLastUpdateCheckMessage() { - Instant lastCheck = lastSuccessfulUpdateCheck.getValue(); + Instant lastCheck = updateChecker.getLastSuccessfulUpdateCheck(); if (lastCheck != null && !lastCheck.equals(Settings.DEFAULT_TIMESTAMP)) { - return formatter.format(LocalDateTime.ofInstant(lastCheck, ZoneId.systemDefault())); + return FORMATTER.format(LocalDateTime.ofInstant(lastCheck, ZoneId.systemDefault())); } else { return "-"; } @@ -188,7 +155,7 @@ public class UpdatesPreferencesController implements FxController { } public String getTimeDifferenceMessage() { - var lastSuccessCheck = lastSuccessfulUpdateCheck.getValue(); + var lastSuccessCheck = updateChecker.getLastSuccessfulUpdateCheck(); var duration = Duration.between(lastSuccessCheck, Instant.now()); var hours = duration.toHours(); if (lastSuccessCheck.equals(Settings.DEFAULT_TIMESTAMP)) { @@ -210,14 +177,6 @@ public class UpdatesPreferencesController implements FxController { return upToDateLabelVisible.get(); } - public BooleanBinding checkFailedProperty() { - return checkFailed; - } - - public boolean isCheckFailed() { - return checkFailed.getValue(); - } - public ObjectBinding> workerProperty() { return worker; } diff --git a/src/main/java/org/cryptomator/updater/DownloadUpdateMechanism.java b/src/main/java/org/cryptomator/updater/DownloadUpdateMechanism.java index 8f0246cb4..d1e168fba 100644 --- a/src/main/java/org/cryptomator/updater/DownloadUpdateMechanism.java +++ b/src/main/java/org/cryptomator/updater/DownloadUpdateMechanism.java @@ -18,14 +18,20 @@ import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.util.List; -public abstract class DownloadUpdateMechanism implements UpdateMechanism { +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(); + public record DownloadUpdateInfo( + DownloadUpdateMechanism updateMechanism, + String version, + Asset asset + ) implements UpdateInfo {} + @Override - public UpdateInfo checkForUpdate(String currentVersion, HttpClient httpClient) { + public DownloadUpdateInfo checkForUpdate(String currentVersion, HttpClient httpClient) { try { HttpRequest request = HttpRequest.newBuilder().uri(URI.create(LATEST_VERSION_API_URL)).build(); HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream()); @@ -46,7 +52,7 @@ public abstract class DownloadUpdateMechanism implements UpdateMechanism { @Nullable @Blocking - abstract UpdateInfo checkForUpdate(String currentVersion, LatestVersionResponse response); + abstract DownloadUpdateInfo checkForUpdate(String currentVersion, LatestVersionResponse response); @JsonIgnoreProperties(ignoreUnknown = true) public record LatestVersionResponse( diff --git a/src/main/java/org/cryptomator/updater/FallbackUpdateMechanism.java b/src/main/java/org/cryptomator/updater/FallbackUpdateMechanism.java index c2448cf68..0331a5a28 100644 --- a/src/main/java/org/cryptomator/updater/FallbackUpdateMechanism.java +++ b/src/main/java/org/cryptomator/updater/FallbackUpdateMechanism.java @@ -32,7 +32,7 @@ import java.util.concurrent.TimeUnit; @FxApplicationScoped @Priority(Priority.FALLBACK) @DisplayName("Show Download Page") // TODO localize -public class FallbackUpdateMechanism implements UpdateMechanism { +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"; @@ -62,7 +62,7 @@ public class FallbackUpdateMechanism implements UpdateMechanism { var release = MAPPER.readValue(response.body(), LatestVersion.class); var updateVersion = release.versionForCurrentOS(); if (UpdateMechanism.isUpdateAvailable(currentVersion, updateVersion)) { - return new UpdateInfo(updateVersion, this); + return UpdateInfo.of(updateVersion, this); } else { return null; } @@ -73,7 +73,7 @@ public class FallbackUpdateMechanism implements UpdateMechanism { } @Override - public UpdateStep firstStep() { + public UpdateStep firstStep(UpdateInfo updateInfo) { return UpdateStep.of("Go to download page", this::openDownloadPage); // TODO localize } diff --git a/src/main/java/org/cryptomator/updater/MacOsDmgUpdateMechanism.java b/src/main/java/org/cryptomator/updater/MacOsDmgUpdateMechanism.java index a6f30ff3e..6b6f187cd 100644 --- a/src/main/java/org/cryptomator/updater/MacOsDmgUpdateMechanism.java +++ b/src/main/java/org/cryptomator/updater/MacOsDmgUpdateMechanism.java @@ -5,7 +5,6 @@ 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; @@ -15,8 +14,10 @@ import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.InterruptedIOException; import java.net.URI; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.StandardOpenOption; import java.util.HexFormat; import java.util.List; import java.util.UUID; @@ -30,24 +31,25 @@ public class MacOsDmgUpdateMechanism extends DownloadUpdateMechanism { private static final Logger LOG = LoggerFactory.getLogger(MacOsDmgUpdateMechanism.class); @Override - UpdateInfo checkForUpdate(String currentVersion, LatestVersionResponse response) { + DownloadUpdateInfo checkForUpdate(String currentVersion, LatestVersionResponse response) { String suffix = switch (System.getProperty("os.arch")) { case "aarch64", "arm64" -> "arm64.dmg"; default -> "x64.dmg"; }; - 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); + var updateVersion = response.latestVersion().macVersion(); + var asset = response.assets().stream().filter(a -> a.name().endsWith(suffix)).findAny().orElse(null); + if (UpdateMechanism.isUpdateAvailable(updateVersion, currentVersion) && asset != null) { + return new DownloadUpdateMechanism.DownloadUpdateInfo(this, updateVersion, asset); } else { return null; } } @Override - public UpdateStep firstStep() throws UpdateFailedException { + public UpdateStep firstStep(DownloadUpdateInfo updateInfo) throws UpdateFailedException { try { Path workDir = Files.createTempDirectory("cryptomator-update"); - return new UpdateProcessImpl(workDir); + return new UpdateProcessImpl(workDir, updateInfo); } catch (IOException e) { throw new UpdateFailedException("Failed to create temporary directory for update", e); } @@ -55,14 +57,13 @@ public class MacOsDmgUpdateMechanism extends DownloadUpdateMechanism { private static class UpdateProcessImpl extends DownloadUpdateStep { - // 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"); private final Path workDir; - public UpdateProcessImpl(Path workDir) { + public UpdateProcessImpl(Path workDir, DownloadUpdateInfo updateInfo) { var destination = workDir.resolve("update.dmg"); - super(UPDATE_URI, destination, CHECKSUM, 60_000_000L); // initially assume 60 MB for the update size + var downloadUri = URI.create(updateInfo.asset().downloadUrl()); + var checksum = HexFormat.of().withLowerCase().parseHex(updateInfo.asset().digest().substring(7)); // remove "sha256:" prefix + super(downloadUri, destination, checksum, 60_000_000L); // initially assume 60 MB for the update size this.workDir = workDir; } @@ -111,12 +112,18 @@ public class MacOsDmgUpdateMechanism extends DownloadUpdateMechanism { installPath = "/Applications/Cryptomator.app"; // throw new UpdateFailedException("Cannot determine destination path for Cryptomator.app, current path: " + selfPath); } + LOG.info("Restarting to apply Update in {} now...", workDir); 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}" + if [ -d "${CRYPTOMATOR_INSTALL_PATH}" ]; then + echo "Removing old installation at ${CRYPTOMATOR_INSTALL_PATH}"; + rm -rf "${CRYPTOMATOR_INSTALL_PATH}" + fi + mv '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 &"); + Files.writeString(workDir.resolve("install.sh"), script, StandardCharsets.US_ASCII, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW); + var command = List.of("bash", "-c", "nohup bash install.sh >install.log 2>&1 &"); var processBuilder = new ProcessBuilder(command); processBuilder.directory(workDir.toFile()); processBuilder.environment().put("CRYPTOMATOR_PID", String.valueOf(ProcessHandle.current().pid())); diff --git a/src/main/java/org/cryptomator/updater/UpdateService.java b/src/main/java/org/cryptomator/updater/UpdateService.java index 7cc3ecf97..88d83e70d 100644 --- a/src/main/java/org/cryptomator/updater/UpdateService.java +++ b/src/main/java/org/cryptomator/updater/UpdateService.java @@ -1,5 +1,6 @@ package org.cryptomator.updater; +import org.cryptomator.integrations.update.UpdateInfo; import org.cryptomator.integrations.update.UpdateMechanism; import org.cryptomator.integrations.update.UpdateStep; @@ -17,30 +18,30 @@ import java.util.concurrent.TimeUnit; */ public class UpdateService extends Service { - private ObservableValue updateMechanism; + private ObservableValue updateInfo; - public UpdateService(ObservableValue updateMechanism) { + public UpdateService(ObservableValue updateInfo) { setExecutor(Executors.newVirtualThreadPerTaskExecutor()); - this.updateMechanism = updateMechanism; + this.updateInfo = updateInfo; } @Override protected Task createTask() { - return new RunAllStepsTask(updateMechanism.getValue()); + return new RunAllStepsTask(updateInfo.getValue()); } private static class RunAllStepsTask extends Task { - private final UpdateMechanism updateMechanism; + private final UpdateInfo updateInfo; - public RunAllStepsTask(UpdateMechanism updateMechanism) { - this.updateMechanism = Objects.requireNonNull(updateMechanism); + public RunAllStepsTask(UpdateInfo updateInfo) { + this.updateInfo = Objects.requireNonNull(updateInfo); } @Override protected UpdateStep call() throws IOException { try { - UpdateStep step = updateMechanism.firstStep(); + UpdateStep step = updateInfo.updateMechanism().firstStep(updateInfo); UpdateStep lastStep; do { step.start(); diff --git a/src/main/resources/fxml/preferences_updates.fxml b/src/main/resources/fxml/preferences_updates.fxml index 52e209f8b..8689d0506 100644 --- a/src/main/resources/fxml/preferences_updates.fxml +++ b/src/main/resources/fxml/preferences_updates.fxml @@ -3,7 +3,6 @@ - @@ -21,12 +20,12 @@ - + - + - +