From b0ed133e05c07479dda400454492a210d77c1b7d Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Sun, 6 Jul 2025 14:28:57 +0200 Subject: [PATCH 01/34] PoC --- src/main/java/module-info.java | 5 + .../updater/DownloadUpdateMechanism.java | 51 ++++++ .../updater/DownloadUpdateProcess.java | 165 ++++++++++++++++++ .../updater/MacOsDmgUpdateMechanism.java | 87 +++++++++ .../cryptomator/updater/UpdateMechanism.java | 31 ++++ .../cryptomator/updater/UpdateProcess.java | 51 ++++++ .../updater/MacOsDmgUpdateMechanismTest.java | 25 +++ 7 files changed, 415 insertions(+) create mode 100644 src/main/java/org/cryptomator/updater/DownloadUpdateMechanism.java create mode 100644 src/main/java/org/cryptomator/updater/DownloadUpdateProcess.java create mode 100644 src/main/java/org/cryptomator/updater/MacOsDmgUpdateMechanism.java create mode 100644 src/main/java/org/cryptomator/updater/UpdateMechanism.java create mode 100644 src/main/java/org/cryptomator/updater/UpdateProcess.java create mode 100644 src/test/java/org/cryptomator/updater/MacOsDmgUpdateMechanismTest.java diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 459d3c52d..c0d7d9499 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -20,6 +20,8 @@ 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; +import org.cryptomator.updater.UpdateMechanism; open module org.cryptomator.desktop { requires static org.jetbrains.annotations; @@ -61,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; // TODO: move to integrations-mac + provides TrayMenuController with AwtTrayMenuController; provides Configurator with LogbackConfiguratorFactory; provides SSLContextProvider with SSLContextWithWindowsCertStore, SSLContextWithMacKeychain, SSLContextWithPKCS12TrustStore; 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..10ca3b137 --- /dev/null +++ b/src/main/java/org/cryptomator/updater/DownloadUpdateMechanism.java @@ -0,0 +1,51 @@ +package org.cryptomator.updater; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.ObjectMapper; + +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/DownloadUpdateProcess.java b/src/main/java/org/cryptomator/updater/DownloadUpdateProcess.java new file mode 100644 index 000000000..4d158e8ba --- /dev/null +++ b/src/main/java/org/cryptomator/updater/DownloadUpdateProcess.java @@ -0,0 +1,165 @@ +package org.cryptomator.updater; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InterruptedIOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.channels.Channels; +import java.nio.channels.FileChannel; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.LongAdder; + +public abstract class DownloadUpdateProcess implements UpdateProcess { + + protected final Path workDir; + private final URI uri; + private final byte[] checksum; + private final AtomicLong totalBytes; + private final LongAdder loadedBytes = new LongAdder(); + private final Thread downloadThread; + private final CountDownLatch downloadCompleted = new CountDownLatch(1); + protected volatile IOException downloadException; + protected volatile boolean downloadSuccessful; + + /** + * Creates a new DownloadUpdateProcess instance. + * @param workdir The directory where the update will be downloaded to. Ideally, this should be a temporary directory that is cleaned up after the update process is complete. + * @param uri The URI from which the update will be downloaded. + * @param checksum (optional) The expected SHA-256 checksum of the downloaded file, can be null if not required. + * @param estDownloadSize The estimated size of the download in bytes. + */ + protected DownloadUpdateProcess(Path workdir, URI uri, byte[] checksum, long estDownloadSize) { + this.workDir = workdir; + this.uri = uri; + this.checksum = checksum; + this.totalBytes = new AtomicLong(estDownloadSize); + this.downloadThread = Thread.ofVirtual().start(this::download); + } + + @Override + public double preparationProgress() { + return (double) loadedBytes.sum() / totalBytes.get(); + } + + @Override + public void await() throws InterruptedException { + downloadCompleted.await(); + } + + @Override + public boolean await(long timeout, TimeUnit unit) throws InterruptedException { + return downloadCompleted.await(timeout, unit); + } + + @Override + public void cancel() { + downloadThread.interrupt(); + } + + private void download() { + try { + download("update.dmg"); + downloadSuccessful = true; + } catch (IOException e) { + // TODO: eventually handle this via structured concurrency? + downloadException = e; + } finally { + downloadCompleted.countDown(); + } + } + + /** + * Downloads the update from the given URI and saves it to the specified filename in the working directory. + * @param filename the name of the file to save the update as in the working directory + * @throws IOException indicating I/O errors during the download or file writing process or due to checksum mismatch + */ + protected void download(String filename) throws IOException { + var request = HttpRequest.newBuilder().uri(uri).GET().build(); + var downloadFile = workDir.resolve(filename); + try (HttpClient client = HttpClient.newBuilder().followRedirects(HttpClient.Redirect.ALWAYS).build()) { + // make download request + var response = client.send(request, HttpResponse.BodyHandlers.ofInputStream()); + if (response.statusCode() != 200) { + throw new IOException("Failed to download update, status code: " + response.statusCode()); + } + + // update totalBytes + response.headers().firstValueAsLong("Content-Length").ifPresent(totalBytes::set); + + // prepare checksum calculation + MessageDigest sha256; + try { + sha256 = MessageDigest.getInstance("SHA-256"); // Initialize SHA-256 digest, not used here but can be extended for checksum validation + } catch (NoSuchAlgorithmException e) { + throw new AssertionError("Every implementation of the Java platform is required to support [...] SHA-256", e); + } + + // write bytes to file + try (var in = new DownloadInputStream(response.body(), loadedBytes, sha256); + var src = Channels.newChannel(in); + var dst = FileChannel.open(downloadFile, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW)) { + dst.transferFrom(src, 0, totalBytes.get()); + } + + // verify checksum if provided + byte[] calculatedChecksum = sha256.digest(); + if (!MessageDigest.isEqual(calculatedChecksum, checksum)) { + throw new IOException("Checksum verification failed for downloaded file: " + filename); + } + + // post-download processing + postDownload(downloadFile); + } catch (InterruptedException e) { + throw new InterruptedIOException("Download interrupted"); + } + } + + protected void postDownload(Path downloadedFile) throws IOException { + // Default implementation does nothing, can be overridden by subclasses for specific post-download actions + } + + /** + * An InputStream decorator that counts the number of bytes read and updates a MessageDigest for checksum calculation. + */ + private static class DownloadInputStream extends FilterInputStream { + + private final LongAdder counter; + private final MessageDigest digest; + + protected DownloadInputStream(InputStream in, LongAdder counter, MessageDigest digest) { + super(in); + this.counter = counter; + this.digest = digest; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + int n = super.read(b, off, len); + digest.update(b, off, n); + counter.add(n); + return n; + } + + @Override + public int read() throws IOException { + int b = super.read(); + if (b != -1) { + digest.update((byte) b); + counter.increment(); + } + return b; + } + + } + +} 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..38af5d84f --- /dev/null +++ b/src/main/java/org/cryptomator/updater/MacOsDmgUpdateMechanism.java @@ -0,0 +1,87 @@ +package org.cryptomator.updater; + +import org.cryptomator.integrations.common.DisplayName; +import org.cryptomator.integrations.common.OperatingSystem; +import org.cryptomator.integrations.common.Priority; +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 IOException { + Path workDir = Files.createTempDirectory("cryptomator-update"); + return new UpdateProcessImpl(workDir); + } + + 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 workDir) { + super(workDir, UPDATE_URI, CHECKSUM,60_000_000L); // initially assume 60 MB for the update size + } + + @Override + protected void postDownload(Path downloadedFile) 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()); + } + } catch (InterruptedException e) { + throw new InterruptedIOException("Failed to extract DMG, interrupted"); + } + } + + @Override + public Process applyUpdate() throws IllegalStateException, IOException { + if (downloadException != null) { + throw new IllegalStateException("Downloading update failed", downloadException); + } else if (!downloadSuccessful) { + throw new IllegalStateException("Update not yet downloaded"); + } + // TODO: use /Applications/Cryptomator.app or ~/Applications/Cryptomator.app depending on the path of the current process (ProcessHandle.current().info().command()?) + String script = """ + while kill -0 ${CRYPTOMATOR_PID} 2> /dev/null; do sleep 0.5; done; + cp -R 'Cryptomator.app' '/Applications/Cryptomator.app'; + open -a '/Applications/Cryptomator.app' + """; + 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())); + return processBuilder.start(); + } + } + + +} diff --git a/src/main/java/org/cryptomator/updater/UpdateMechanism.java b/src/main/java/org/cryptomator/updater/UpdateMechanism.java new file mode 100644 index 000000000..7a599e529 --- /dev/null +++ b/src/main/java/org/cryptomator/updater/UpdateMechanism.java @@ -0,0 +1,31 @@ +package org.cryptomator.updater; + +import org.cryptomator.integrations.common.IntegrationsLoader; +import org.cryptomator.integrations.common.NamedServiceProvider; +import org.jetbrains.annotations.Blocking; + +import javafx.concurrent.Task; +import java.io.IOException; +import java.util.stream.Stream; + +public interface UpdateMechanism extends NamedServiceProvider { + + static Stream get() { + return IntegrationsLoader.loadAll(UpdateMechanism.class); + } + + /** + * Checks whether an update is available. + * @return true if an update is available, false otherwise. + */ + @Blocking + boolean isUpdateAvailable(); + + /** + * Performs as much as possible to prepare the update. This may include downloading the update, checking signatures, etc. + * @return a {@link Task} that can be used to monitor the progress of the update preparation. The task will complete when the preparation is done. + * @throws IOException I/O error during preparation, such as network issues or file access problems. + */ + UpdateProcess prepareUpdate() throws IOException; // TODO: exception types? + +} diff --git a/src/main/java/org/cryptomator/updater/UpdateProcess.java b/src/main/java/org/cryptomator/updater/UpdateProcess.java new file mode 100644 index 000000000..58c1b17a7 --- /dev/null +++ b/src/main/java/org/cryptomator/updater/UpdateProcess.java @@ -0,0 +1,51 @@ +package org.cryptomator.updater; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +public interface UpdateProcess { + + /** + * A thread-safe method to check the progress of the update preparation. + * @return a value between 0.0 and 1.0 indicating the progress of the update preparation. + */ + double preparationProgress(); + + /** + * Cancels the update process and cleans up any resources that were used during the preparation. + */ + void cancel(); + + /** + * Blocks the current thread until the update preparation is complete or an error occurs. + *

+ * If the preparation is already complete, this method returns immediately. + * + * @throws InterruptedException if the current thread is interrupted while waiting. + */ + void await() throws InterruptedException; + + /** + * Blocks the current thread until the update preparation is complete or an error occurs, or until the specified timeout expires. + *

+ * If the preparation is already complete, this method returns immediately. + * + * @param timeout the maximum time to wait + * @param unit the time unit of the {@code timeout} argument + * @return true if the update is prepared + */ + boolean await(long timeout, TimeUnit unit) throws InterruptedException; + + /** + * Once the update preparation is complete, this method can be called to launch the external update process. + *

+ * This method shall be called after making sure that the application is ready to be restarted, e.g. after locking all vaults. + * + * @return a {@link Process} that represents the external update process. + * @throws IllegalStateException if the update preparation is not complete or if the update process cannot be launched. + * @throws IOException if starting the update process fails + */ + Process applyUpdate() throws IllegalStateException, IOException; + + +} diff --git a/src/test/java/org/cryptomator/updater/MacOsDmgUpdateMechanismTest.java b/src/test/java/org/cryptomator/updater/MacOsDmgUpdateMechanismTest.java new file mode 100644 index 000000000..3ec3ffc77 --- /dev/null +++ b/src/test/java/org/cryptomator/updater/MacOsDmgUpdateMechanismTest.java @@ -0,0 +1,25 @@ +package org.cryptomator.updater; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +class MacOsDmgUpdateMechanismTest { + + public static void main(String args[]) throws InterruptedException, IOException { + UpdateMechanism updateMechanism = new MacOsDmgUpdateMechanism(); + if (updateMechanism.isUpdateAvailable()) { + System.out.println("Update is available."); + } + var updateProcess = updateMechanism.prepareUpdate(); + do { + double percentage = updateProcess.preparationProgress() * 100.0; + System.out.printf("\rPreparing update: %.2f%%", percentage); + } while (!updateProcess.await(100, TimeUnit.MILLISECONDS)); + System.out.println("\nUpdate ready..."); + Process p = updateProcess.applyUpdate(); + p.isAlive(); + System.out.println("Update running, exiting..."); + // exit. + } + +} \ No newline at end of file From 02186ca17aae1aa6fa41f25ce0705bf20ab58929 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Sun, 6 Jul 2025 20:02:41 +0200 Subject: [PATCH 02/34] hooked up in UI --- .../UpdatesPreferencesController.java | 62 +++++++++++++++++++ .../cryptomator/updater/UpdateMechanism.java | 8 +-- .../resources/fxml/preferences_updates.fxml | 8 +++ 3 files changed, 73 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/cryptomator/ui/preferences/UpdatesPreferencesController.java b/src/main/java/org/cryptomator/ui/preferences/UpdatesPreferencesController.java index f5a72290f..54c1ac4d3 100644 --- a/src/main/java/org/cryptomator/ui/preferences/UpdatesPreferencesController.java +++ b/src/main/java/org/cryptomator/ui/preferences/UpdatesPreferencesController.java @@ -4,10 +4,15 @@ import org.cryptomator.common.Environment; import org.cryptomator.common.settings.Settings; import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.fxapp.UpdateChecker; +import org.cryptomator.updater.UpdateMechanism; +import org.cryptomator.updater.UpdateProcess; +import org.slf4j.Logger; +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; @@ -16,9 +21,11 @@ import javafx.beans.property.BooleanProperty; import javafx.beans.property.ReadOnlyStringProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.value.ObservableValue; +import javafx.concurrent.Task; import javafx.fxml.FXML; import javafx.scene.control.CheckBox; import javafx.scene.control.ContentDisplay; +import java.io.IOException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.time.Duration; @@ -29,11 +36,14 @@ 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 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&" // @@ -56,6 +66,9 @@ public class UpdatesPreferencesController implements FxController { private final DateTimeFormatter formatter; private final BooleanBinding upToDate; private final String downloadsUri; + private final UpdateMechanism updateMechanism; + public final Task updatePreparationTask; + private final StringBinding updateButtonTitle; /* FXML */ public CheckBox checkForUpdatesCheckbox; @@ -78,6 +91,18 @@ public class UpdatesPreferencesController implements FxController { 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() { @@ -108,6 +133,26 @@ public class UpdatesPreferencesController implements FxController { environment.getLogDir().ifPresent(logDirPath -> application.getHostServices().showDocument(logDirPath.toUri().toString())); } + @FXML + public void prepareUpdate() { + if (updatePreparationTask.isDone()) { + try { + // TODO: check if all vaults closed? + var restartProcess = updatePreparationTask.get().applyUpdate(); + assert restartProcess.isAlive(); + 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() { @@ -186,4 +231,21 @@ public class UpdatesPreferencesController implements FxController { return checkFailed.getValue(); } + public Task getUpdatePreparationTask() { + return updatePreparationTask; + } + + public StringBinding updateButtonTitleProperty() { + return updateButtonTitle; + } + + public String getUpdateButtonTitle() { + return switch (updatePreparationTask.getState()) { + case READY -> "Prepare Update"; // TODO: resourceBundle.getString("preferences.updates.preparingUpdate")... + case SCHEDULED, RUNNING -> "Preparing Update..."; + case SUCCEEDED -> "Restart to Update"; + case FAILED, CANCELLED -> "failed"; + }; + } + } diff --git a/src/main/java/org/cryptomator/updater/UpdateMechanism.java b/src/main/java/org/cryptomator/updater/UpdateMechanism.java index 7a599e529..540633fec 100644 --- a/src/main/java/org/cryptomator/updater/UpdateMechanism.java +++ b/src/main/java/org/cryptomator/updater/UpdateMechanism.java @@ -1,17 +1,15 @@ package org.cryptomator.updater; -import org.cryptomator.integrations.common.IntegrationsLoader; import org.cryptomator.integrations.common.NamedServiceProvider; import org.jetbrains.annotations.Blocking; import javafx.concurrent.Task; import java.io.IOException; -import java.util.stream.Stream; public interface UpdateMechanism extends NamedServiceProvider { - static Stream get() { - return IntegrationsLoader.loadAll(UpdateMechanism.class); + static UpdateMechanism get() { + return new MacOsDmgUpdateMechanism(); // TODO: IntegrationsLoader.load(UpdateMechanism.class).orElseThrow(); } /** @@ -23,7 +21,7 @@ public interface UpdateMechanism extends NamedServiceProvider { /** * Performs as much as possible to prepare the update. This may include downloading the update, checking signatures, etc. - * @return a {@link Task} that can be used to monitor the progress of the update preparation. The task will complete when the preparation is done. + * @return a new {@link Task} that can be used to monitor the progress of the update preparation. The task will complete when the preparation is done. * @throws IOException I/O error during preparation, such as network issues or file access problems. */ UpdateProcess prepareUpdate() throws IOException; // TODO: exception types? diff --git a/src/main/resources/fxml/preferences_updates.fxml b/src/main/resources/fxml/preferences_updates.fxml index d0910949b..264ee7597 100644 --- a/src/main/resources/fxml/preferences_updates.fxml +++ b/src/main/resources/fxml/preferences_updates.fxml @@ -14,6 +14,7 @@ + + + + From 1f1e336d57e15153284bc01310cb444d90d231af Mon Sep 17 00:00:00 2001 From: Ralph Plawetzki Date: Fri, 4 Jul 2025 18:42:38 +0200 Subject: [PATCH 03/34] Start FlatpakUpdater --- pom.xml | 4 +-- .../common/updates/AppUpdateChecker.java | 30 +++++++++++++++++++ .../launcher/CryptomatorComponent.java | 3 ++ .../launcher/CryptomatorModule.java | 6 ++++ .../cryptomator/ui/fxapp/UpdateChecker.java | 8 +++-- 5 files changed, 47 insertions(+), 4 deletions(-) create mode 100644 src/main/java/org/cryptomator/common/updates/AppUpdateChecker.java diff --git a/pom.xml b/pom.xml index caba8e342..25c542099 100644 --- a/pom.xml +++ b/pom.xml @@ -34,10 +34,10 @@ 2.9.0 - 1.6.0 + 1.7.0-SNAPSHOT 1.5.0 1.4.0 - 1.6.0 + 1.7.0-SNAPSHOT 5.0.5 2.0.10 diff --git a/src/main/java/org/cryptomator/common/updates/AppUpdateChecker.java b/src/main/java/org/cryptomator/common/updates/AppUpdateChecker.java new file mode 100644 index 000000000..f6a8224b1 --- /dev/null +++ b/src/main/java/org/cryptomator/common/updates/AppUpdateChecker.java @@ -0,0 +1,30 @@ +package org.cryptomator.common.updates; + +import org.cryptomator.integrations.update.UpdateFailedException; +import org.cryptomator.integrations.update.UpdateService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import java.util.Optional; + +public class AppUpdateChecker { + + private static final Logger LOG = LoggerFactory.getLogger(AppUpdateChecker.class); + private final Optional updateService; + + @Inject + public AppUpdateChecker(Optional updateService) { + this.updateService = updateService; + } + + public void checkForUpdates() { + updateService.ifPresent(service -> { + try { + service.triggerUpdate(); + } catch (UpdateFailedException e) { + LOG.error(e.toString(), e.getCause()); + } + }); + } +} diff --git a/src/main/java/org/cryptomator/launcher/CryptomatorComponent.java b/src/main/java/org/cryptomator/launcher/CryptomatorComponent.java index 72b8af80c..cf8a9493e 100644 --- a/src/main/java/org/cryptomator/launcher/CryptomatorComponent.java +++ b/src/main/java/org/cryptomator/launcher/CryptomatorComponent.java @@ -4,6 +4,7 @@ import dagger.BindsInstance; import dagger.Component; import org.cryptomator.common.CommonsModule; import org.cryptomator.ui.fxapp.FxApplicationComponent; +import org.cryptomator.common.updates.AppUpdateChecker; import javax.inject.Named; import javax.inject.Singleton; @@ -14,6 +15,8 @@ public interface CryptomatorComponent { Cryptomator application(); + AppUpdateChecker appUpdateChecker(); + FxApplicationComponent.Builder fxAppComponentBuilder(); @Component.Factory diff --git a/src/main/java/org/cryptomator/launcher/CryptomatorModule.java b/src/main/java/org/cryptomator/launcher/CryptomatorModule.java index 42e908df2..14481d5b4 100644 --- a/src/main/java/org/cryptomator/launcher/CryptomatorModule.java +++ b/src/main/java/org/cryptomator/launcher/CryptomatorModule.java @@ -5,6 +5,7 @@ import dagger.Provides; import org.cryptomator.integrations.autostart.AutoStartProvider; import org.cryptomator.integrations.tray.TrayIntegrationProvider; import org.cryptomator.integrations.uiappearance.UiAppearanceProvider; +import org.cryptomator.integrations.update.UpdateService; import org.cryptomator.ui.fxapp.FxApplicationComponent; import javax.inject.Named; @@ -48,4 +49,9 @@ class CryptomatorModule { return TrayIntegrationProvider.get(); } + @Provides + @Singleton + static Optional provideUpdateService() { + return UpdateService.get(); + } } diff --git a/src/main/java/org/cryptomator/ui/fxapp/UpdateChecker.java b/src/main/java/org/cryptomator/ui/fxapp/UpdateChecker.java index b857adcae..ada0431cf 100644 --- a/src/main/java/org/cryptomator/ui/fxapp/UpdateChecker.java +++ b/src/main/java/org/cryptomator/ui/fxapp/UpdateChecker.java @@ -3,6 +3,7 @@ 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.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -36,17 +37,20 @@ public class UpdateChecker { private final Comparator versionComparator = new SemVerComparator(); private final BooleanBinding updateAvailable; private final BooleanBinding checkFailed; + private final AppUpdateChecker updateChecker; @Inject UpdateChecker(Settings settings, // Environment env, // - ScheduledService updateCheckerService) { + ScheduledService updateCheckerService, + AppUpdateChecker updateChecker) { 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.updateChecker = updateChecker; } public void automaticallyCheckForUpdatesIfEnabled() { @@ -56,7 +60,7 @@ public class UpdateChecker { } public void checkForUpdatesNow() { - startCheckingForUpdates(Duration.ZERO); + updateChecker.checkForUpdates(); } private void startCheckingForUpdates(Duration initialDelay) { From 62b434f54994698b8b43b203159a09fa03ca5ed7 Mon Sep 17 00:00:00 2001 From: Ralph Plawetzki Date: Sat, 12 Jul 2025 18:30:30 +0200 Subject: [PATCH 04/34] Invent UpdatesModule --- .../org/cryptomator/common/CommonsModule.java | 3 ++- .../common/updates/AppUpdateChecker.java | 22 ++++++++++------ .../common/updates/UpdatesModule.java | 25 +++++++++++++++++++ .../launcher/CryptomatorComponent.java | 3 --- .../launcher/CryptomatorModule.java | 6 ----- .../cryptomator/ui/fxapp/UpdateChecker.java | 11 ++++++-- 6 files changed, 50 insertions(+), 20 deletions(-) create mode 100644 src/main/java/org/cryptomator/common/updates/UpdatesModule.java diff --git a/src/main/java/org/cryptomator/common/CommonsModule.java b/src/main/java/org/cryptomator/common/CommonsModule.java index a1e3c0950..f510d1828 100644 --- a/src/main/java/org/cryptomator/common/CommonsModule.java +++ b/src/main/java/org/cryptomator/common/CommonsModule.java @@ -11,6 +11,7 @@ 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; @@ -30,7 +31,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}) +@Module(subcomponents = {VaultComponent.class}, includes = {VaultListModule.class, KeychainModule.class, MountModule.class, UpdatesModule.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 index f6a8224b1..707e07615 100644 --- a/src/main/java/org/cryptomator/common/updates/AppUpdateChecker.java +++ b/src/main/java/org/cryptomator/common/updates/AppUpdateChecker.java @@ -1,6 +1,5 @@ package org.cryptomator.common.updates; -import org.cryptomator.integrations.update.UpdateFailedException; import org.cryptomator.integrations.update.UpdateService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -18,13 +17,20 @@ public class AppUpdateChecker { this.updateService = updateService; } - public void checkForUpdates() { - updateService.ifPresent(service -> { - try { - service.triggerUpdate(); - } catch (UpdateFailedException e) { - LOG.error(e.toString(), e.getCause()); + public boolean isUpdateServiceAvailable() { + return updateService.isPresent(); + } + + public String checkForUpdates(UpdateService.DistributionChannel channel) { + if (!updateService.isPresent()) { + LOG.error("No UpdateService found"); + return null; + } + switch (channel) { + case LINUX_FLATPAK -> { + return updateService.map(service -> service.isUpdateAvailable(UpdateService.DistributionChannel.LINUX_FLATPAK)).orElse(null); } - }); + default -> throw new IllegalStateException("Unexpected value: " + channel); + } } } diff --git a/src/main/java/org/cryptomator/common/updates/UpdatesModule.java b/src/main/java/org/cryptomator/common/updates/UpdatesModule.java new file mode 100644 index 000000000..986e90f59 --- /dev/null +++ b/src/main/java/org/cryptomator/common/updates/UpdatesModule.java @@ -0,0 +1,25 @@ +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/launcher/CryptomatorComponent.java b/src/main/java/org/cryptomator/launcher/CryptomatorComponent.java index cf8a9493e..72b8af80c 100644 --- a/src/main/java/org/cryptomator/launcher/CryptomatorComponent.java +++ b/src/main/java/org/cryptomator/launcher/CryptomatorComponent.java @@ -4,7 +4,6 @@ import dagger.BindsInstance; import dagger.Component; import org.cryptomator.common.CommonsModule; import org.cryptomator.ui.fxapp.FxApplicationComponent; -import org.cryptomator.common.updates.AppUpdateChecker; import javax.inject.Named; import javax.inject.Singleton; @@ -15,8 +14,6 @@ public interface CryptomatorComponent { Cryptomator application(); - AppUpdateChecker appUpdateChecker(); - FxApplicationComponent.Builder fxAppComponentBuilder(); @Component.Factory diff --git a/src/main/java/org/cryptomator/launcher/CryptomatorModule.java b/src/main/java/org/cryptomator/launcher/CryptomatorModule.java index 14481d5b4..42e908df2 100644 --- a/src/main/java/org/cryptomator/launcher/CryptomatorModule.java +++ b/src/main/java/org/cryptomator/launcher/CryptomatorModule.java @@ -5,7 +5,6 @@ import dagger.Provides; import org.cryptomator.integrations.autostart.AutoStartProvider; import org.cryptomator.integrations.tray.TrayIntegrationProvider; import org.cryptomator.integrations.uiappearance.UiAppearanceProvider; -import org.cryptomator.integrations.update.UpdateService; import org.cryptomator.ui.fxapp.FxApplicationComponent; import javax.inject.Named; @@ -49,9 +48,4 @@ class CryptomatorModule { return TrayIntegrationProvider.get(); } - @Provides - @Singleton - static Optional provideUpdateService() { - return UpdateService.get(); - } } diff --git a/src/main/java/org/cryptomator/ui/fxapp/UpdateChecker.java b/src/main/java/org/cryptomator/ui/fxapp/UpdateChecker.java index ada0431cf..0854d9d90 100644 --- a/src/main/java/org/cryptomator/ui/fxapp/UpdateChecker.java +++ b/src/main/java/org/cryptomator/ui/fxapp/UpdateChecker.java @@ -4,6 +4,7 @@ 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.UpdateService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -55,12 +56,18 @@ public class UpdateChecker { public void automaticallyCheckForUpdatesIfEnabled() { if (!env.disableUpdateCheck() && settings.checkForUpdates.get()) { - startCheckingForUpdates(AUTO_CHECK_DELAY); + if (updateChecker.isUpdateServiceAvailable()) { // prefer AppUpdateChecker + var x = updateChecker.checkForUpdates(UpdateService.DistributionChannel.LINUX_FLATPAK); + LOG.info("Retrieved version from Update Service {}", x); + } else { // fallback is the "redirect user to website" approach + LOG.info("Common \"redirect user to website\" approach"); + startCheckingForUpdates(AUTO_CHECK_DELAY); + } } } public void checkForUpdatesNow() { - updateChecker.checkForUpdates(); + startCheckingForUpdates(Duration.ZERO); } private void startCheckingForUpdates(Duration initialDelay) { From 510e134605d4023caeeebf8c0dc610d2fa309cc0 Mon Sep 17 00:00:00 2001 From: Ralph Plawetzki Date: Sun, 13 Jul 2025 17:00:17 +0200 Subject: [PATCH 05/34] Have multiple services --- .../common/updates/AppUpdateChecker.java | 53 +++++++++++++++---- .../cryptomator/ui/fxapp/UpdateChecker.java | 11 ++-- 2 files changed, 52 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/cryptomator/common/updates/AppUpdateChecker.java b/src/main/java/org/cryptomator/common/updates/AppUpdateChecker.java index 707e07615..aaa90e737 100644 --- a/src/main/java/org/cryptomator/common/updates/AppUpdateChecker.java +++ b/src/main/java/org/cryptomator/common/updates/AppUpdateChecker.java @@ -1,36 +1,71 @@ package org.cryptomator.common.updates; +import org.cryptomator.integrations.common.DistributionChannel; 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 final Optional updateService; + private final List updateServices; @Inject - public AppUpdateChecker(Optional updateService) { - this.updateService = updateService; + public AppUpdateChecker(List updateServices) { + this.updateServices = updateServices; } - public boolean isUpdateServiceAvailable() { - return updateService.isPresent(); + public boolean isUpdateServiceAvailable(Optional buildNumber) { + if (buildNumber.isEmpty()) { + return false; + } + switch (buildNumber.get()) { + case "flatpak-1" -> { + return !updateServices.isEmpty() && doServicesContainChannel(updateServices, DistributionChannel.Value.LINUX_FLATPAK); + } + + default -> { + LOG.error("Unexpected value 'buildNumber': {}", buildNumber.get()); + return false; + } + } } - public String checkForUpdates(UpdateService.DistributionChannel channel) { - if (!updateService.isPresent()) { + public String checkForUpdates(DistributionChannel.Value channel) { + if (updateServices.isEmpty()) { LOG.error("No UpdateService found"); return null; } switch (channel) { case LINUX_FLATPAK -> { - return updateService.map(service -> service.isUpdateAvailable(UpdateService.DistributionChannel.LINUX_FLATPAK)).orElse(null); + var flatpakService = getServiceForChannel(updateServices, DistributionChannel.Value.LINUX_FLATPAK); + if(null == flatpakService) { + LOG.error("Required service for channel LINUX_FLATPAK not available"); + return null; + } else { + return flatpakService.isUpdateAvailable(DistributionChannel.Value.LINUX_FLATPAK); + } } - default -> throw new IllegalStateException("Unexpected value: " + channel); + default -> throw new IllegalStateException("Unexpected value 'channel': " + channel); } } + + private boolean doServicesContainChannel(List services, DistributionChannel.Value requiredChannel) { + return services.stream().anyMatch(service -> { + DistributionChannel annotation = service.getClass().getAnnotation(DistributionChannel.class); + return annotation != null && annotation.value() == requiredChannel; + }); + } + + private UpdateService getServiceForChannel(List services, DistributionChannel.Value requiredChannel) { + return services.stream().filter(service -> { + DistributionChannel annotation = service.getClass().getAnnotation(DistributionChannel.class); + return annotation != null && annotation.value() == requiredChannel; + }).findFirst().orElse(null); + } + } diff --git a/src/main/java/org/cryptomator/ui/fxapp/UpdateChecker.java b/src/main/java/org/cryptomator/ui/fxapp/UpdateChecker.java index 0854d9d90..07b401234 100644 --- a/src/main/java/org/cryptomator/ui/fxapp/UpdateChecker.java +++ b/src/main/java/org/cryptomator/ui/fxapp/UpdateChecker.java @@ -4,6 +4,7 @@ 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.common.DistributionChannel; import org.cryptomator.integrations.update.UpdateService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -56,9 +57,13 @@ public class UpdateChecker { public void automaticallyCheckForUpdatesIfEnabled() { if (!env.disableUpdateCheck() && settings.checkForUpdates.get()) { - if (updateChecker.isUpdateServiceAvailable()) { // prefer AppUpdateChecker - var x = updateChecker.checkForUpdates(UpdateService.DistributionChannel.LINUX_FLATPAK); - LOG.info("Retrieved version from Update Service {}", x); + if (updateChecker.isUpdateServiceAvailable(env.getBuildNumber())) { // prefer AppUpdateChecker + String version = ""; + switch (env.getBuildNumber().get()) { + case "flatpak-1" -> version = updateChecker.checkForUpdates(DistributionChannel.Value.LINUX_FLATPAK); + default -> LOG.error("Unexpected value 'buildNumber': {}", env.getBuildNumber().get()); + } + LOG.info("Retrieved version from Update Service {}", version); } else { // fallback is the "redirect user to website" approach LOG.info("Common \"redirect user to website\" approach"); startCheckingForUpdates(AUTO_CHECK_DELAY); From 89ce99deafd2a505d94a58614d2bb9c8d86e9d7d Mon Sep 17 00:00:00 2001 From: Ralph Plawetzki Date: Sun, 20 Jul 2025 08:23:50 +0200 Subject: [PATCH 06/34] Wire UpdateService --- src/main/java/module-info.java | 1 + .../common/updates/AppUpdateChecker.java | 10 ++- .../cryptomator/ui/fxapp/UpdateChecker.java | 73 ++++++++++++++++--- .../UpdatesPreferencesController.java | 16 ++++ .../resources/fxml/preferences_updates.fxml | 2 +- 5 files changed, 89 insertions(+), 13 deletions(-) diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 459d3c52d..3acbc041d 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -50,6 +50,7 @@ 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; diff --git a/src/main/java/org/cryptomator/common/updates/AppUpdateChecker.java b/src/main/java/org/cryptomator/common/updates/AppUpdateChecker.java index aaa90e737..238342e7d 100644 --- a/src/main/java/org/cryptomator/common/updates/AppUpdateChecker.java +++ b/src/main/java/org/cryptomator/common/updates/AppUpdateChecker.java @@ -35,7 +35,7 @@ public class AppUpdateChecker { } } - public String checkForUpdates(DistributionChannel.Value channel) { + public Object getUpdater(DistributionChannel.Value channel) { if (updateServices.isEmpty()) { LOG.error("No UpdateService found"); return null; @@ -47,7 +47,7 @@ public class AppUpdateChecker { LOG.error("Required service for channel LINUX_FLATPAK not available"); return null; } else { - return flatpakService.isUpdateAvailable(DistributionChannel.Value.LINUX_FLATPAK); + return flatpakService.getLatestReleaseChecker(DistributionChannel.Value.LINUX_FLATPAK); } } default -> throw new IllegalStateException("Unexpected value 'channel': " + channel); @@ -68,4 +68,10 @@ public class AppUpdateChecker { }).findFirst().orElse(null); } + public UpdateService getServiceForChannel(DistributionChannel.Value requiredChannel) { + return updateServices.stream().filter(service -> { + DistributionChannel annotation = service.getClass().getAnnotation(DistributionChannel.class); + return annotation != null && annotation.value() == requiredChannel; + }).findFirst().orElse(null); + } } diff --git a/src/main/java/org/cryptomator/ui/fxapp/UpdateChecker.java b/src/main/java/org/cryptomator/ui/fxapp/UpdateChecker.java index 07b401234..5e8f6eff7 100644 --- a/src/main/java/org/cryptomator/ui/fxapp/UpdateChecker.java +++ b/src/main/java/org/cryptomator/ui/fxapp/UpdateChecker.java @@ -5,7 +5,8 @@ import org.cryptomator.common.SemVerComparator; import org.cryptomator.common.settings.Settings; import org.cryptomator.common.updates.AppUpdateChecker; import org.cryptomator.integrations.common.DistributionChannel; -import org.cryptomator.integrations.update.UpdateService; +import org.cryptomator.integrations.update.UpdateFailedException; +import org.purejava.portal.rest.UpdateCheckerTask; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -22,6 +23,7 @@ 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; @FxApplicationScoped @@ -33,11 +35,13 @@ public class UpdateChecker { 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; @@ -51,6 +55,7 @@ public class UpdateChecker { 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; } @@ -58,14 +63,11 @@ public class UpdateChecker { public void automaticallyCheckForUpdatesIfEnabled() { if (!env.disableUpdateCheck() && settings.checkForUpdates.get()) { if (updateChecker.isUpdateServiceAvailable(env.getBuildNumber())) { // prefer AppUpdateChecker - String version = ""; switch (env.getBuildNumber().get()) { - case "flatpak-1" -> version = updateChecker.checkForUpdates(DistributionChannel.Value.LINUX_FLATPAK); + case "flatpak-1" -> startCheckingWithFlatpakUpdater((UpdateCheckerTask) updateChecker.getUpdater(DistributionChannel.Value.LINUX_FLATPAK), AUTO_CHECK_DELAY); default -> LOG.error("Unexpected value 'buildNumber': {}", env.getBuildNumber().get()); } - LOG.info("Retrieved version from Update Service {}", version); } else { // fallback is the "redirect user to website" approach - LOG.info("Common \"redirect user to website\" approach"); startCheckingForUpdates(AUTO_CHECK_DELAY); } } @@ -75,6 +77,11 @@ public class UpdateChecker { startCheckingForUpdates(Duration.ZERO); } + public void updateAppNow() throws UpdateFailedException { + var service = updateChecker.getServiceForChannel(DistributionChannel.Value.LINUX_FLATPAK); + service.triggerUpdate(); + } + private void startCheckingForUpdates(Duration initialDelay) { updateCheckerService.cancel(); updateCheckerService.reset(); @@ -85,11 +92,31 @@ 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); @@ -98,15 +125,26 @@ 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, CHECK_SUCCESSFUL, - CHECK_FAILED; + CHECK_FAILED } /* Observable Properties */ @@ -118,23 +156,38 @@ 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() { + public boolean isUpdateAvailable(StringProperty versionProperty) { String currentVersion = getCurrentVersion(); - String latestVersionString = latestVersion.get(); + String latestVersionString = versionProperty.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/preferences/UpdatesPreferencesController.java b/src/main/java/org/cryptomator/ui/preferences/UpdatesPreferencesController.java index f5a72290f..3c69db335 100644 --- a/src/main/java/org/cryptomator/ui/preferences/UpdatesPreferencesController.java +++ b/src/main/java/org/cryptomator/ui/preferences/UpdatesPreferencesController.java @@ -2,6 +2,7 @@ package org.cryptomator.ui.preferences; import org.cryptomator.common.Environment; import org.cryptomator.common.settings.Settings; +import org.cryptomator.integrations.update.UpdateFailedException; import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.fxapp.UpdateChecker; @@ -51,6 +52,7 @@ 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; @@ -73,6 +75,7 @@ public class UpdatesPreferencesController implements FxController { 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(); @@ -98,6 +101,11 @@ public class UpdatesPreferencesController implements FxController { updateChecker.checkForUpdatesNow(); } + @FXML + public void updateNow() throws UpdateFailedException { + updateChecker.updateAppNow(); + } + @FXML public void visitDownloadsPage() { application.getHostServices().showDocument(downloadsUri); @@ -174,10 +182,18 @@ 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; } diff --git a/src/main/resources/fxml/preferences_updates.fxml b/src/main/resources/fxml/preferences_updates.fxml index d0910949b..c0141f074 100644 --- a/src/main/resources/fxml/preferences_updates.fxml +++ b/src/main/resources/fxml/preferences_updates.fxml @@ -9,7 +9,6 @@ - @@ -53,5 +52,6 @@ + + + From 99e9e92f10971b3fc1b25fb7ecf4e26ae01568a2 Mon Sep 17 00:00:00 2001 From: Ralph Plawetzki Date: Tue, 29 Jul 2025 06:29:06 +0200 Subject: [PATCH 08/34] Fix progress bar due to JavaFX limitations --- .../cryptomator/ui/fxapp/UpdateChecker.java | 2 +- .../UpdatesPreferencesController.java | 10 +++++++--- .../resources/fxml/preferences_updates.fxml | 19 +++++++++++-------- 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/cryptomator/ui/fxapp/UpdateChecker.java b/src/main/java/org/cryptomator/ui/fxapp/UpdateChecker.java index b5c8579ba..6a957936b 100644 --- a/src/main/java/org/cryptomator/ui/fxapp/UpdateChecker.java +++ b/src/main/java/org/cryptomator/ui/fxapp/UpdateChecker.java @@ -101,7 +101,7 @@ public class UpdateChecker { LOG.debug("Update progess is at percentage: {} and has status: {}", progress.getProgress(), progress.getStatus()); if (progress.getStatus() == 0 || progress.getStatus() == 2) { - controller.flatpakProgressProperty().set(progress.getProgress() / 100.0); + Platform.runLater(() -> controller.flatpakProgressProperty().set(progress.getProgress() / 100.0)); } if (progress.getStatus() == 2 && progress.getProgress() == 100) { diff --git a/src/main/java/org/cryptomator/ui/preferences/UpdatesPreferencesController.java b/src/main/java/org/cryptomator/ui/preferences/UpdatesPreferencesController.java index 19ab9ea1c..c0e9a1489 100644 --- a/src/main/java/org/cryptomator/ui/preferences/UpdatesPreferencesController.java +++ b/src/main/java/org/cryptomator/ui/preferences/UpdatesPreferencesController.java @@ -27,6 +27,7 @@ import javafx.fxml.FXML; import javafx.scene.control.Button; import javafx.scene.control.CheckBox; import javafx.scene.control.ContentDisplay; +import javafx.scene.control.Label; import javafx.scene.control.ProgressBar; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; @@ -74,8 +75,7 @@ public class UpdatesPreferencesController implements FxController { /* FXML */ public CheckBox checkForUpdatesCheckbox; - @FXML - public Button flatpakUpdateButton; + public Label flatpakButtonLabel; @Inject UpdatesPreferencesController(Application application, Environment environment, ResourceBundle resourceBundle, Settings settings, UpdateChecker updateChecker, AppUpdateChecker appUpdateChecker, Environment env) { @@ -103,7 +103,7 @@ public class UpdatesPreferencesController implements FxController { public void initialize() { checkForUpdatesCheckbox.selectedProperty().bindBidirectional(settings.checkForUpdates); switch (env.getBuildNumber().get()) { - case "flatpak-1" -> flatpakUpdateButton.setText(appUpdateChecker.getServiceForChannel(DistributionChannel.Value.LINUX_FLATPAK).getDisplayName()); + case "flatpak-1" -> flatpakButtonLabel.setText(appUpdateChecker.getServiceForChannel(DistributionChannel.Value.LINUX_FLATPAK).getDisplayName()); default -> LOG.error("Unexpected value 'buildNumber': {}", env.getBuildNumber().get()); } @@ -243,4 +243,8 @@ public class UpdatesPreferencesController implements FxController { public DoubleProperty flatpakProgressProperty() { return flatpakProgress; } + + public double getFlatpakProgress() { + return flatpakProgress.get(); + } } diff --git a/src/main/resources/fxml/preferences_updates.fxml b/src/main/resources/fxml/preferences_updates.fxml index 96b40b82b..9f334bcfa 100644 --- a/src/main/resources/fxml/preferences_updates.fxml +++ b/src/main/resources/fxml/preferences_updates.fxml @@ -53,16 +53,19 @@ - - From 2ce7fee06d8cb819992c5716b06c778989d43771 Mon Sep 17 00:00:00 2001 From: Ralph Plawetzki Date: Wed, 30 Jul 2025 19:43:39 +0200 Subject: [PATCH 09/34] Log error messages --- .../java/org/cryptomator/ui/fxapp/UpdateChecker.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/cryptomator/ui/fxapp/UpdateChecker.java b/src/main/java/org/cryptomator/ui/fxapp/UpdateChecker.java index 6a957936b..bcc9a24bf 100644 --- a/src/main/java/org/cryptomator/ui/fxapp/UpdateChecker.java +++ b/src/main/java/org/cryptomator/ui/fxapp/UpdateChecker.java @@ -98,9 +98,19 @@ public class UpdateChecker { service.addProgressListener(new ProgressListener() { @Override public void onProgress(Progress progress) { - LOG.debug("Update progess is at percentage: {} and has status: {}", progress.getProgress(), progress.getStatus()); + + if (progress.getStatus() == 1) { + LOG.info("No update to install"); + return; + } + + if (progress.getStatus() == 3) { + LOG.info("Update failed: {} / {}", progress.getError(), progress.getErrorMessage()); + 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)); } From 79bb4a52152d7a46180c16282da6a7618a84abc4 Mon Sep 17 00:00:00 2001 From: Ralph Plawetzki Date: Sat, 2 Aug 2025 11:32:07 +0200 Subject: [PATCH 10/34] Correlate with API as suggested in a separate PoC: UpdateMechanism and UpdateProcess --- .../common/updates/AppUpdateChecker.java | 35 ++++++++++--------- .../cryptomator/ui/fxapp/UpdateChecker.java | 10 +++--- .../UpdatesPreferencesController.java | 6 ++-- 3 files changed, 26 insertions(+), 25 deletions(-) diff --git a/src/main/java/org/cryptomator/common/updates/AppUpdateChecker.java b/src/main/java/org/cryptomator/common/updates/AppUpdateChecker.java index 238342e7d..f35d6d757 100644 --- a/src/main/java/org/cryptomator/common/updates/AppUpdateChecker.java +++ b/src/main/java/org/cryptomator/common/updates/AppUpdateChecker.java @@ -1,6 +1,6 @@ package org.cryptomator.common.updates; -import org.cryptomator.integrations.common.DistributionChannel; +import org.cryptomator.integrations.common.DisplayName; import org.cryptomator.integrations.update.UpdateService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -12,6 +12,7 @@ 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 @@ -25,7 +26,7 @@ public class AppUpdateChecker { } switch (buildNumber.get()) { case "flatpak-1" -> { - return !updateServices.isEmpty() && doServicesContainChannel(updateServices, DistributionChannel.Value.LINUX_FLATPAK); + return !updateServices.isEmpty() && doServicesContainChannel(updateServices, DISPLAY_NAME_FLATPAK); } default -> { @@ -35,43 +36,43 @@ public class AppUpdateChecker { } } - public Object getUpdater(DistributionChannel.Value channel) { + public Object getUpdater(Optional buildNumber) { if (updateServices.isEmpty()) { LOG.error("No UpdateService found"); return null; } - switch (channel) { - case LINUX_FLATPAK -> { - var flatpakService = getServiceForChannel(updateServices, DistributionChannel.Value.LINUX_FLATPAK); + 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(DistributionChannel.Value.LINUX_FLATPAK); + return flatpakService.getLatestReleaseChecker(); } } - default -> throw new IllegalStateException("Unexpected value 'channel': " + channel); + default -> throw new IllegalStateException("Unexpected value 'buildNumber': " + buildNumber.get()); } } - private boolean doServicesContainChannel(List services, DistributionChannel.Value requiredChannel) { + private boolean doServicesContainChannel(List services, String displayName) { return services.stream().anyMatch(service -> { - DistributionChannel annotation = service.getClass().getAnnotation(DistributionChannel.class); - return annotation != null && annotation.value() == requiredChannel; + DisplayName annotation = service.getClass().getAnnotation(DisplayName.class); + return annotation != null && annotation.value().equals(displayName); }); } - private UpdateService getServiceForChannel(List services, DistributionChannel.Value requiredChannel) { + private UpdateService getServiceForChannel(List services, String displayName) { return services.stream().filter(service -> { - DistributionChannel annotation = service.getClass().getAnnotation(DistributionChannel.class); - return annotation != null && annotation.value() == requiredChannel; + DisplayName annotation = service.getClass().getAnnotation(DisplayName.class); + return annotation != null && annotation.value().equals(displayName); }).findFirst().orElse(null); } - public UpdateService getServiceForChannel(DistributionChannel.Value requiredChannel) { + public UpdateService getServiceForChannel(String displayName) { return updateServices.stream().filter(service -> { - DistributionChannel annotation = service.getClass().getAnnotation(DistributionChannel.class); - return annotation != null && annotation.value() == requiredChannel; + 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/ui/fxapp/UpdateChecker.java b/src/main/java/org/cryptomator/ui/fxapp/UpdateChecker.java index bcc9a24bf..28520d7a3 100644 --- a/src/main/java/org/cryptomator/ui/fxapp/UpdateChecker.java +++ b/src/main/java/org/cryptomator/ui/fxapp/UpdateChecker.java @@ -4,7 +4,6 @@ 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.common.DistributionChannel; import org.cryptomator.integrations.update.Progress; import org.cryptomator.integrations.update.ProgressListener; import org.cryptomator.integrations.update.UpdateFailedException; @@ -35,6 +34,7 @@ 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; @@ -80,7 +80,7 @@ public class UpdateChecker { private void decideOnUpdateChecker() { if (updateChecker.isUpdateServiceAvailable(env.getBuildNumber())) { // prefer AppUpdateChecker switch (env.getBuildNumber().get()) { - case "flatpak-1" -> startCheckingWithFlatpakUpdater((UpdateCheckerTask) updateChecker.getUpdater(DistributionChannel.Value.LINUX_FLATPAK), Duration.ZERO); + 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 @@ -89,12 +89,12 @@ public class UpdateChecker { } public void updateAppNow() throws UpdateFailedException { - var service = updateChecker.getServiceForChannel(DistributionChannel.Value.LINUX_FLATPAK); + var service = updateChecker.getServiceForChannel(DISPLAY_NAME_FLATPAK); service.triggerUpdate(); } public void terminateFlatpakOnUpdateCompleted(Runnable onComplete, UpdatesPreferencesController controller) { - var service = updateChecker.getServiceForChannel(DistributionChannel.Value.LINUX_FLATPAK); + var service = updateChecker.getServiceForChannel(DISPLAY_NAME_FLATPAK); service.addProgressListener(new ProgressListener() { @Override public void onProgress(Progress progress) { @@ -105,7 +105,7 @@ public class UpdateChecker { } if (progress.getStatus() == 3) { - LOG.info("Update failed: {} / {}", progress.getError(), progress.getErrorMessage()); + LOG.info("Update failed"); return; } diff --git a/src/main/java/org/cryptomator/ui/preferences/UpdatesPreferencesController.java b/src/main/java/org/cryptomator/ui/preferences/UpdatesPreferencesController.java index c0e9a1489..4382c63ef 100644 --- a/src/main/java/org/cryptomator/ui/preferences/UpdatesPreferencesController.java +++ b/src/main/java/org/cryptomator/ui/preferences/UpdatesPreferencesController.java @@ -3,7 +3,7 @@ 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.DistributionChannel; +import org.cryptomator.integrations.common.DisplayName; import org.cryptomator.integrations.update.UpdateFailedException; import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.fxapp.UpdateChecker; @@ -24,7 +24,6 @@ import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleDoubleProperty; import javafx.beans.value.ObservableValue; import javafx.fxml.FXML; -import javafx.scene.control.Button; import javafx.scene.control.CheckBox; import javafx.scene.control.ContentDisplay; import javafx.scene.control.Label; @@ -49,6 +48,7 @@ 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; @@ -103,7 +103,7 @@ public class UpdatesPreferencesController implements FxController { public void initialize() { checkForUpdatesCheckbox.selectedProperty().bindBidirectional(settings.checkForUpdates); switch (env.getBuildNumber().get()) { - case "flatpak-1" -> flatpakButtonLabel.setText(appUpdateChecker.getServiceForChannel(DistributionChannel.Value.LINUX_FLATPAK).getDisplayName()); + case "flatpak-1" -> flatpakButtonLabel.setText(appUpdateChecker.getServiceForChannel(DISPLAY_NAME_FLATPAK).getClass().getAnnotation(DisplayName.class).value()); default -> LOG.error("Unexpected value 'buildNumber': {}", env.getBuildNumber().get()); } From 8f4392711e67ee9ddd1c4ba0229712a36c5cc5af Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Fri, 8 Aug 2025 18:21:42 +0200 Subject: [PATCH 11/34] moved update API to `integrations-api` --- pom.xml | 2 +- src/main/java/module-info.java | 4 +- .../UpdatesPreferencesController.java | 10 +- .../updater/DownloadUpdateMechanism.java | 1 + .../updater/DownloadUpdateProcess.java | 165 ------------------ .../updater/MacOsDmgUpdateMechanism.java | 46 +++-- .../cryptomator/updater/UpdateMechanism.java | 29 --- .../cryptomator/updater/UpdateProcess.java | 51 ------ .../updater/MacOsDmgUpdateMechanismTest.java | 25 --- 9 files changed, 43 insertions(+), 290 deletions(-) delete mode 100644 src/main/java/org/cryptomator/updater/DownloadUpdateProcess.java delete mode 100644 src/main/java/org/cryptomator/updater/UpdateMechanism.java delete mode 100644 src/main/java/org/cryptomator/updater/UpdateProcess.java delete mode 100644 src/test/java/org/cryptomator/updater/MacOsDmgUpdateMechanismTest.java diff --git a/pom.xml b/pom.xml index caba8e342..48ae7e703 100644 --- a/pom.xml +++ b/pom.xml @@ -34,7 +34,7 @@ 2.9.0 - 1.6.0 + 1.7.0-SNAPSHOT 1.5.0 1.4.0 1.6.0 diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index c0d7d9499..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; @@ -21,7 +22,6 @@ import org.cryptomator.integrations.tray.TrayMenuController; import org.cryptomator.logging.LogbackConfiguratorFactory; import org.cryptomator.ui.traymenu.AwtTrayMenuController; import org.cryptomator.updater.MacOsDmgUpdateMechanism; -import org.cryptomator.updater.UpdateMechanism; open module org.cryptomator.desktop { requires static org.jetbrains.annotations; @@ -64,7 +64,7 @@ open module org.cryptomator.desktop { uses org.cryptomator.event.NotificationHandler; // opens org.cryptomator.updater to org.cryptomator.integrations.api; - provides UpdateMechanism with MacOsDmgUpdateMechanism; // TODO: move to integrations-mac + provides UpdateMechanism with MacOsDmgUpdateMechanism; provides TrayMenuController with AwtTrayMenuController; provides Configurator with LogbackConfiguratorFactory; diff --git a/src/main/java/org/cryptomator/ui/preferences/UpdatesPreferencesController.java b/src/main/java/org/cryptomator/ui/preferences/UpdatesPreferencesController.java index 54c1ac4d3..12be92241 100644 --- a/src/main/java/org/cryptomator/ui/preferences/UpdatesPreferencesController.java +++ b/src/main/java/org/cryptomator/ui/preferences/UpdatesPreferencesController.java @@ -2,10 +2,10 @@ 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.UpdateProcess; import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.fxapp.UpdateChecker; -import org.cryptomator.updater.UpdateMechanism; -import org.cryptomator.updater.UpdateProcess; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -139,7 +139,11 @@ public class UpdatesPreferencesController implements FxController { try { // TODO: check if all vaults closed? var restartProcess = updatePreparationTask.get().applyUpdate(); - assert restartProcess.isAlive(); + 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 diff --git a/src/main/java/org/cryptomator/updater/DownloadUpdateMechanism.java b/src/main/java/org/cryptomator/updater/DownloadUpdateMechanism.java index 10ca3b137..8434a9964 100644 --- a/src/main/java/org/cryptomator/updater/DownloadUpdateMechanism.java +++ b/src/main/java/org/cryptomator/updater/DownloadUpdateMechanism.java @@ -3,6 +3,7 @@ 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; diff --git a/src/main/java/org/cryptomator/updater/DownloadUpdateProcess.java b/src/main/java/org/cryptomator/updater/DownloadUpdateProcess.java deleted file mode 100644 index 4d158e8ba..000000000 --- a/src/main/java/org/cryptomator/updater/DownloadUpdateProcess.java +++ /dev/null @@ -1,165 +0,0 @@ -package org.cryptomator.updater; - -import java.io.FilterInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InterruptedIOException; -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.nio.channels.Channels; -import java.nio.channels.FileChannel; -import java.nio.file.Path; -import java.nio.file.StandardOpenOption; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.LongAdder; - -public abstract class DownloadUpdateProcess implements UpdateProcess { - - protected final Path workDir; - private final URI uri; - private final byte[] checksum; - private final AtomicLong totalBytes; - private final LongAdder loadedBytes = new LongAdder(); - private final Thread downloadThread; - private final CountDownLatch downloadCompleted = new CountDownLatch(1); - protected volatile IOException downloadException; - protected volatile boolean downloadSuccessful; - - /** - * Creates a new DownloadUpdateProcess instance. - * @param workdir The directory where the update will be downloaded to. Ideally, this should be a temporary directory that is cleaned up after the update process is complete. - * @param uri The URI from which the update will be downloaded. - * @param checksum (optional) The expected SHA-256 checksum of the downloaded file, can be null if not required. - * @param estDownloadSize The estimated size of the download in bytes. - */ - protected DownloadUpdateProcess(Path workdir, URI uri, byte[] checksum, long estDownloadSize) { - this.workDir = workdir; - this.uri = uri; - this.checksum = checksum; - this.totalBytes = new AtomicLong(estDownloadSize); - this.downloadThread = Thread.ofVirtual().start(this::download); - } - - @Override - public double preparationProgress() { - return (double) loadedBytes.sum() / totalBytes.get(); - } - - @Override - public void await() throws InterruptedException { - downloadCompleted.await(); - } - - @Override - public boolean await(long timeout, TimeUnit unit) throws InterruptedException { - return downloadCompleted.await(timeout, unit); - } - - @Override - public void cancel() { - downloadThread.interrupt(); - } - - private void download() { - try { - download("update.dmg"); - downloadSuccessful = true; - } catch (IOException e) { - // TODO: eventually handle this via structured concurrency? - downloadException = e; - } finally { - downloadCompleted.countDown(); - } - } - - /** - * Downloads the update from the given URI and saves it to the specified filename in the working directory. - * @param filename the name of the file to save the update as in the working directory - * @throws IOException indicating I/O errors during the download or file writing process or due to checksum mismatch - */ - protected void download(String filename) throws IOException { - var request = HttpRequest.newBuilder().uri(uri).GET().build(); - var downloadFile = workDir.resolve(filename); - try (HttpClient client = HttpClient.newBuilder().followRedirects(HttpClient.Redirect.ALWAYS).build()) { - // make download request - var response = client.send(request, HttpResponse.BodyHandlers.ofInputStream()); - if (response.statusCode() != 200) { - throw new IOException("Failed to download update, status code: " + response.statusCode()); - } - - // update totalBytes - response.headers().firstValueAsLong("Content-Length").ifPresent(totalBytes::set); - - // prepare checksum calculation - MessageDigest sha256; - try { - sha256 = MessageDigest.getInstance("SHA-256"); // Initialize SHA-256 digest, not used here but can be extended for checksum validation - } catch (NoSuchAlgorithmException e) { - throw new AssertionError("Every implementation of the Java platform is required to support [...] SHA-256", e); - } - - // write bytes to file - try (var in = new DownloadInputStream(response.body(), loadedBytes, sha256); - var src = Channels.newChannel(in); - var dst = FileChannel.open(downloadFile, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW)) { - dst.transferFrom(src, 0, totalBytes.get()); - } - - // verify checksum if provided - byte[] calculatedChecksum = sha256.digest(); - if (!MessageDigest.isEqual(calculatedChecksum, checksum)) { - throw new IOException("Checksum verification failed for downloaded file: " + filename); - } - - // post-download processing - postDownload(downloadFile); - } catch (InterruptedException e) { - throw new InterruptedIOException("Download interrupted"); - } - } - - protected void postDownload(Path downloadedFile) throws IOException { - // Default implementation does nothing, can be overridden by subclasses for specific post-download actions - } - - /** - * An InputStream decorator that counts the number of bytes read and updates a MessageDigest for checksum calculation. - */ - private static class DownloadInputStream extends FilterInputStream { - - private final LongAdder counter; - private final MessageDigest digest; - - protected DownloadInputStream(InputStream in, LongAdder counter, MessageDigest digest) { - super(in); - this.counter = counter; - this.digest = digest; - } - - @Override - public int read(byte[] b, int off, int len) throws IOException { - int n = super.read(b, off, len); - digest.update(b, off, n); - counter.add(n); - return n; - } - - @Override - public int read() throws IOException { - int b = super.read(); - if (b != -1) { - digest.update((byte) b); - counter.increment(); - } - return b; - } - - } - -} diff --git a/src/main/java/org/cryptomator/updater/MacOsDmgUpdateMechanism.java b/src/main/java/org/cryptomator/updater/MacOsDmgUpdateMechanism.java index 38af5d84f..6d288bbae 100644 --- a/src/main/java/org/cryptomator/updater/MacOsDmgUpdateMechanism.java +++ b/src/main/java/org/cryptomator/updater/MacOsDmgUpdateMechanism.java @@ -3,6 +3,9 @@ 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; @@ -24,9 +27,13 @@ public class MacOsDmgUpdateMechanism extends DownloadUpdateMechanism { private static final Logger LOG = LoggerFactory.getLogger(MacOsDmgUpdateMechanism.class); @Override - public UpdateProcess prepareUpdate() throws IOException { - Path workDir = Files.createTempDirectory("cryptomator-update"); - return new UpdateProcessImpl(workDir); + 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 { @@ -35,12 +42,12 @@ public class MacOsDmgUpdateMechanism extends DownloadUpdateMechanism { 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 workDir) { - super(workDir, UPDATE_URI, CHECKSUM,60_000_000L); // initially assume 60 MB for the update size + 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 downloadedFile) throws IOException { + 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 && @@ -57,29 +64,40 @@ public class MacOsDmgUpdateMechanism extends DownloadUpdateMechanism { 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 Process applyUpdate() throws IllegalStateException, IOException { - if (downloadException != null) { - throw new IllegalStateException("Downloading update failed", downloadException); - } else if (!downloadSuccessful) { + 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); } - // TODO: use /Applications/Cryptomator.app or ~/Applications/Cryptomator.app depending on the path of the current process (ProcessHandle.current().info().command()?) String script = """ while kill -0 ${CRYPTOMATOR_PID} 2> /dev/null; do sleep 0.5; done; - cp -R 'Cryptomator.app' '/Applications/Cryptomator.app'; - open -a '/Applications/Cryptomator.app' + 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())); - return processBuilder.start(); + processBuilder.environment().put("CRYPTOMATOR_INSTALL_PATH", installPath); + return processBuilder.start().toHandle(); } } diff --git a/src/main/java/org/cryptomator/updater/UpdateMechanism.java b/src/main/java/org/cryptomator/updater/UpdateMechanism.java deleted file mode 100644 index 540633fec..000000000 --- a/src/main/java/org/cryptomator/updater/UpdateMechanism.java +++ /dev/null @@ -1,29 +0,0 @@ -package org.cryptomator.updater; - -import org.cryptomator.integrations.common.NamedServiceProvider; -import org.jetbrains.annotations.Blocking; - -import javafx.concurrent.Task; -import java.io.IOException; - -public interface UpdateMechanism extends NamedServiceProvider { - - static UpdateMechanism get() { - return new MacOsDmgUpdateMechanism(); // TODO: IntegrationsLoader.load(UpdateMechanism.class).orElseThrow(); - } - - /** - * Checks whether an update is available. - * @return true if an update is available, false otherwise. - */ - @Blocking - boolean isUpdateAvailable(); - - /** - * Performs as much as possible to prepare the update. This may include downloading the update, checking signatures, etc. - * @return a new {@link Task} that can be used to monitor the progress of the update preparation. The task will complete when the preparation is done. - * @throws IOException I/O error during preparation, such as network issues or file access problems. - */ - UpdateProcess prepareUpdate() throws IOException; // TODO: exception types? - -} diff --git a/src/main/java/org/cryptomator/updater/UpdateProcess.java b/src/main/java/org/cryptomator/updater/UpdateProcess.java deleted file mode 100644 index 58c1b17a7..000000000 --- a/src/main/java/org/cryptomator/updater/UpdateProcess.java +++ /dev/null @@ -1,51 +0,0 @@ -package org.cryptomator.updater; - -import java.io.IOException; -import java.util.concurrent.TimeUnit; - -public interface UpdateProcess { - - /** - * A thread-safe method to check the progress of the update preparation. - * @return a value between 0.0 and 1.0 indicating the progress of the update preparation. - */ - double preparationProgress(); - - /** - * Cancels the update process and cleans up any resources that were used during the preparation. - */ - void cancel(); - - /** - * Blocks the current thread until the update preparation is complete or an error occurs. - *

- * If the preparation is already complete, this method returns immediately. - * - * @throws InterruptedException if the current thread is interrupted while waiting. - */ - void await() throws InterruptedException; - - /** - * Blocks the current thread until the update preparation is complete or an error occurs, or until the specified timeout expires. - *

- * If the preparation is already complete, this method returns immediately. - * - * @param timeout the maximum time to wait - * @param unit the time unit of the {@code timeout} argument - * @return true if the update is prepared - */ - boolean await(long timeout, TimeUnit unit) throws InterruptedException; - - /** - * Once the update preparation is complete, this method can be called to launch the external update process. - *

- * This method shall be called after making sure that the application is ready to be restarted, e.g. after locking all vaults. - * - * @return a {@link Process} that represents the external update process. - * @throws IllegalStateException if the update preparation is not complete or if the update process cannot be launched. - * @throws IOException if starting the update process fails - */ - Process applyUpdate() throws IllegalStateException, IOException; - - -} diff --git a/src/test/java/org/cryptomator/updater/MacOsDmgUpdateMechanismTest.java b/src/test/java/org/cryptomator/updater/MacOsDmgUpdateMechanismTest.java deleted file mode 100644 index 3ec3ffc77..000000000 --- a/src/test/java/org/cryptomator/updater/MacOsDmgUpdateMechanismTest.java +++ /dev/null @@ -1,25 +0,0 @@ -package org.cryptomator.updater; - -import java.io.IOException; -import java.util.concurrent.TimeUnit; - -class MacOsDmgUpdateMechanismTest { - - public static void main(String args[]) throws InterruptedException, IOException { - UpdateMechanism updateMechanism = new MacOsDmgUpdateMechanism(); - if (updateMechanism.isUpdateAvailable()) { - System.out.println("Update is available."); - } - var updateProcess = updateMechanism.prepareUpdate(); - do { - double percentage = updateProcess.preparationProgress() * 100.0; - System.out.printf("\rPreparing update: %.2f%%", percentage); - } while (!updateProcess.await(100, TimeUnit.MILLISECONDS)); - System.out.println("\nUpdate ready..."); - Process p = updateProcess.applyUpdate(); - p.isAlive(); - System.out.println("Update running, exiting..."); - // exit. - } - -} \ No newline at end of file From cdcd43a80583321cc4903b6e7979474862e6da84 Mon Sep 17 00:00:00 2001 From: Armin Schrenk Date: Thu, 25 Sep 2025 12:27:44 +0200 Subject: [PATCH 12/34] first draft for JDK 25 migration (including comments) --- .github/workflows/appimage.yml | 2 +- .github/workflows/build.yml | 2 +- .github/workflows/check-jdk-updates.yml | 2 +- .github/workflows/debian.yml | 6 ++-- .github/workflows/dependency-check.yml | 2 +- .github/workflows/get-version.yml | 2 +- .github/workflows/mac-dmg-x64.yml | 2 +- .github/workflows/mac-dmg.yml | 2 +- .github/workflows/pullrequest.yml | 2 +- .github/workflows/release-check.yml | 2 +- .github/workflows/win-exe.yml | 6 ++-- .idea/compiler.xml | 39 +++++++++++++++++++++---- .idea/misc.xml | 2 +- dist/linux/debian/control | 2 +- dist/linux/debian/rules | 2 +- pom.xml | 2 +- 16 files changed, 52 insertions(+), 25 deletions(-) diff --git a/.github/workflows/appimage.yml b/.github/workflows/appimage.yml index 40ea6638f..44fd774e0 100644 --- a/.github/workflows/appimage.yml +++ b/.github/workflows/appimage.yml @@ -19,7 +19,7 @@ on: env: JAVA_DIST: 'temurin' - JAVA_VERSION: '24.0.1+9' + JAVA_VERSION: '25.0.0+36' jobs: get-version: diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0b5a1f6c9..2ded79b7d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,7 +11,7 @@ on: env: JAVA_DIST: 'temurin' - JAVA_VERSION: 24 + JAVA_VERSION: 25 defaults: run: diff --git a/.github/workflows/check-jdk-updates.yml b/.github/workflows/check-jdk-updates.yml index 8d40a6892..967b2ed80 100644 --- a/.github/workflows/check-jdk-updates.yml +++ b/.github/workflows/check-jdk-updates.yml @@ -6,7 +6,7 @@ on: workflow_dispatch: env: - JDK_VERSION: '24.0.1+9' + JAVA_VERSION: '25.0.0+36' JDK_VENDOR: temurin RUNTIME_VERSION_HELPER: > public class Test { diff --git a/.github/workflows/debian.yml b/.github/workflows/debian.yml index 6ea0d7442..2fc761230 100644 --- a/.github/workflows/debian.yml +++ b/.github/workflows/debian.yml @@ -23,9 +23,9 @@ on: env: JAVA_DIST: 'temurin' - JAVA_VERSION: '24.0.1+9' - COFFEELIBS_JDK: 24 - COFFEELIBS_JDK_VERSION: '24.0.1+9-0ppa3' + JAVA_VERSION: '25.0.0+36' + COFFEELIBS_JDK: 25 + COFFEELIBS_JDK_VERSION: '25.0.0+36-0ppa3' #TODO: update coffeelibs OPENJFX_JMODS_AMD64: 'https://download2.gluonhq.com/openjfx/24.0.1/openjfx-24.0.1_linux-x64_bin-jmods.zip' OPENJFX_JMODS_AMD64_HASH: '425fac742b9fbd095b2ce868cff82d1024620f747c94a7144d0a4879e756146c' OPENJFX_JMODS_AARCH64: 'https://download2.gluonhq.com/openjfx/24.0.1/openjfx-24.0.1_linux-aarch64_bin-jmods.zip' diff --git a/.github/workflows/dependency-check.yml b/.github/workflows/dependency-check.yml index b44604490..6db100254 100644 --- a/.github/workflows/dependency-check.yml +++ b/.github/workflows/dependency-check.yml @@ -11,7 +11,7 @@ jobs: with: runner-os: 'ubuntu-latest' java-distribution: 'temurin' - java-version: 24 + java-version: 25 check-command: 'mvn -B validate -Pdependency-check -Djavafx.platform=linux' secrets: nvd-api-key: ${{ secrets.NVD_API_KEY }} diff --git a/.github/workflows/get-version.yml b/.github/workflows/get-version.yml index d24b5692c..7fc1c09b5 100644 --- a/.github/workflows/get-version.yml +++ b/.github/workflows/get-version.yml @@ -23,7 +23,7 @@ on: env: JAVA_DIST: 'temurin' - JAVA_VERSION: 24 + JAVA_VERSION: 25 jobs: determine-version: diff --git a/.github/workflows/mac-dmg-x64.yml b/.github/workflows/mac-dmg-x64.yml index 1fd251ac4..193c6788f 100644 --- a/.github/workflows/mac-dmg-x64.yml +++ b/.github/workflows/mac-dmg-x64.yml @@ -24,7 +24,7 @@ on: env: JAVA_DIST: 'temurin' - JAVA_VERSION: '24.0.1+9' + JAVA_VERSION: '25.0.0+36' jobs: get-version: diff --git a/.github/workflows/mac-dmg.yml b/.github/workflows/mac-dmg.yml index 953780ae6..1d6b3dc66 100644 --- a/.github/workflows/mac-dmg.yml +++ b/.github/workflows/mac-dmg.yml @@ -22,7 +22,7 @@ on: env: JAVA_DIST: 'temurin' - JAVA_VERSION: '24.0.1+9' + JAVA_VERSION: '25.0.0+36' jobs: get-version: diff --git a/.github/workflows/pullrequest.yml b/.github/workflows/pullrequest.yml index 98da25f91..0047fc37e 100644 --- a/.github/workflows/pullrequest.yml +++ b/.github/workflows/pullrequest.yml @@ -5,7 +5,7 @@ on: env: JAVA_DIST: 'temurin' - JAVA_VERSION: 24 + JAVA_VERSION: 25 defaults: run: diff --git a/.github/workflows/release-check.yml b/.github/workflows/release-check.yml index 3c3643a64..ab0c1c8e8 100644 --- a/.github/workflows/release-check.yml +++ b/.github/workflows/release-check.yml @@ -12,7 +12,7 @@ defaults: env: JAVA_DIST: 'temurin' - JAVA_VERSION: 23 + JAVA_VERSION: 25 jobs: check-preconditions: diff --git a/.github/workflows/win-exe.yml b/.github/workflows/win-exe.yml index 08e19321e..0fa4a42c5 100644 --- a/.github/workflows/win-exe.yml +++ b/.github/workflows/win-exe.yml @@ -51,13 +51,13 @@ jobs: include: - arch: x64 os: windows-latest - java-dist: 'zulu' - java-version: '24.0.1+9' + java-dist: 'zulu' #TODO: is finally temurin possible? + java-version: '25.0.0+36' java-package: 'jdk' - arch: arm64 os: windows-11-arm java-dist: 'liberica' - java-version: '24.0.1+11' + java-version: '24.0.1+11' #TODO: which distro to use here? java-package: 'jdk+fx' #This is needed, as liberica contains JFX 24 Jmods for Windows ARM64 steps: - uses: actions/checkout@v5 diff --git a/.idea/compiler.xml b/.idea/compiler.xml index 1256745d3..28d93ef1d 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -14,15 +14,15 @@ - + \ No newline at end of file diff --git a/dist/linux/debian/control b/dist/linux/debian/control index b0c2c6725..009d73ff5 100644 --- a/dist/linux/debian/control +++ b/dist/linux/debian/control @@ -2,7 +2,7 @@ Source: cryptomator Maintainer: Cryptobot Section: utils Priority: optional -Build-Depends: debhelper (>=10), coffeelibs-jdk-24 (>= 24.0.1+9-0ppa3), libgtk-3-0, libxxf86vm1, libgl1 +Build-Depends: debhelper (>=10), coffeelibs-jdk-25 (>= 24.0.1+9-0ppa3), libgtk-3-0, libxxf86vm1, libgl1 Standards-Version: 4.5.0 Homepage: https://cryptomator.org Vcs-Git: https://github.com/cryptomator/cryptomator.git diff --git a/dist/linux/debian/rules b/dist/linux/debian/rules index f69f4a789..d4edb1f79 100755 --- a/dist/linux/debian/rules +++ b/dist/linux/debian/rules @@ -4,7 +4,7 @@ # Uncomment this to turn on verbose mode. #export DH_VERBOSE=1 -JAVA_HOME = /usr/lib/jvm/java-24-coffeelibs +JAVA_HOME = /usr/lib/jvm/java-25-coffeelibs DEB_BUILD_ARCH ?= $(shell dpkg-architecture -qDEB_BUILD_ARCH) ifeq ($(DEB_BUILD_ARCH),amd64) JMODS_PATH = jmods/amd64:${JAVA_HOME}/jmods diff --git a/pom.xml b/pom.xml index d53108857..e05dd809d 100644 --- a/pom.xml +++ b/pom.xml @@ -26,7 +26,7 @@ UTF-8 - 24 + 25 From 4bb5a3f10d643021d6fa9ea76bbfacacaa8dacbc Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Fri, 17 Oct 2025 12:12:44 +0200 Subject: [PATCH 13/34] fix unresolvable version in setup-java action --- .github/workflows/appimage.yml | 2 +- .github/workflows/check-jdk-updates.yml | 2 +- .github/workflows/debian.yml | 2 +- .github/workflows/mac-dmg-x64.yml | 2 +- .github/workflows/mac-dmg.yml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/appimage.yml b/.github/workflows/appimage.yml index 44fd774e0..b1a426cfd 100644 --- a/.github/workflows/appimage.yml +++ b/.github/workflows/appimage.yml @@ -19,7 +19,7 @@ on: env: JAVA_DIST: 'temurin' - JAVA_VERSION: '25.0.0+36' + JAVA_VERSION: '25.0.0' jobs: get-version: diff --git a/.github/workflows/check-jdk-updates.yml b/.github/workflows/check-jdk-updates.yml index 967b2ed80..d926f6630 100644 --- a/.github/workflows/check-jdk-updates.yml +++ b/.github/workflows/check-jdk-updates.yml @@ -6,7 +6,7 @@ on: workflow_dispatch: env: - JAVA_VERSION: '25.0.0+36' + JAVA_VERSION: '25.0.0' JDK_VENDOR: temurin RUNTIME_VERSION_HELPER: > public class Test { diff --git a/.github/workflows/debian.yml b/.github/workflows/debian.yml index 2fc761230..810da7161 100644 --- a/.github/workflows/debian.yml +++ b/.github/workflows/debian.yml @@ -23,7 +23,7 @@ on: env: JAVA_DIST: 'temurin' - JAVA_VERSION: '25.0.0+36' + JAVA_VERSION: '25.0.0' COFFEELIBS_JDK: 25 COFFEELIBS_JDK_VERSION: '25.0.0+36-0ppa3' #TODO: update coffeelibs OPENJFX_JMODS_AMD64: 'https://download2.gluonhq.com/openjfx/24.0.1/openjfx-24.0.1_linux-x64_bin-jmods.zip' diff --git a/.github/workflows/mac-dmg-x64.yml b/.github/workflows/mac-dmg-x64.yml index 193c6788f..c54a25a50 100644 --- a/.github/workflows/mac-dmg-x64.yml +++ b/.github/workflows/mac-dmg-x64.yml @@ -24,7 +24,7 @@ on: env: JAVA_DIST: 'temurin' - JAVA_VERSION: '25.0.0+36' + JAVA_VERSION: '25.0.0' jobs: get-version: diff --git a/.github/workflows/mac-dmg.yml b/.github/workflows/mac-dmg.yml index 1d6b3dc66..fa47cd405 100644 --- a/.github/workflows/mac-dmg.yml +++ b/.github/workflows/mac-dmg.yml @@ -22,7 +22,7 @@ on: env: JAVA_DIST: 'temurin' - JAVA_VERSION: '25.0.0+36' + JAVA_VERSION: '25.0.0' jobs: get-version: From 43a1f00bea152611de17738e82ebdb91eb6ecc09 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Fri, 17 Oct 2025 12:47:13 +0200 Subject: [PATCH 14/34] switch to official `openjdk-25-jdk` ubuntu package see https://packages.ubuntu.com/search?suite=jammy&arch=any&searchon=names&keywords=openjdk-25 --- .github/workflows/debian.yml | 9 +++++---- dist/linux/debian/control | 2 +- dist/linux/debian/rules | 3 ++- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/workflows/debian.yml b/.github/workflows/debian.yml index f4ae6b439..7d07cc948 100644 --- a/.github/workflows/debian.yml +++ b/.github/workflows/debian.yml @@ -24,8 +24,7 @@ on: env: JAVA_DIST: 'temurin' JAVA_VERSION: '25.0.0' - COFFEELIBS_JDK: 25 - COFFEELIBS_JDK_VERSION: '25.0.0+36-0ppa1' + DEB_BUILD_DEPENDS: 'debhelper (>=10), openjdk-25-jdk (>= 25+36), libgtk-3-0 (>= 3.20.0), libxxf86vm1, libgl1' OPENJFX_JMODS_AMD64: 'https://download2.gluonhq.com/openjfx/25/openjfx-25_linux-x64_bin-jmods.zip' OPENJFX_JMODS_AMD64_HASH: '96e520f48610d8ffb94ca30face1f11ffe8a977ddc1c4ff80b1a9e9f048bd94e' OPENJFX_JMODS_AARCH64: 'https://download2.gluonhq.com/openjfx/25/openjfx-25_linux-aarch64_bin-jmods.zip' @@ -55,9 +54,11 @@ jobs: fi - name: Install build tools run: | - sudo add-apt-repository ppa:coffeelibs/openjdk sudo apt-get update - sudo apt-get install debhelper devscripts dput coffeelibs-jdk-${{ env.COFFEELIBS_JDK }}=${{ env.COFFEELIBS_JDK_VERSION }} + sudo apt-get install devscripts dput + sudo apt-get satisfy "${DEB_BUILD_DEPENDS}" + env: + DEB_BUILD_DEPENDS: ${{ env.DEB_BUILD_DEPENDS }} - name: Setup Java uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0 with: diff --git a/dist/linux/debian/control b/dist/linux/debian/control index c525fa019..713f03972 100644 --- a/dist/linux/debian/control +++ b/dist/linux/debian/control @@ -2,7 +2,7 @@ Source: cryptomator Maintainer: Cryptobot Section: utils Priority: optional -Build-Depends: debhelper (>=10), coffeelibs-jdk-25 (>= 25.0.0+36-0ppa1), libgtk-3-0 (>= 3.20.0), libxxf86vm1, libgl1 +Build-Depends: debhelper (>=10), openjdk-25-jdk (>= 25+36), libgtk-3-0 (>= 3.20.0), libxxf86vm1, libgl1 Standards-Version: 4.5.0 Homepage: https://cryptomator.org Vcs-Git: https://github.com/cryptomator/cryptomator.git diff --git a/dist/linux/debian/rules b/dist/linux/debian/rules index 0033d048f..456a97e89 100755 --- a/dist/linux/debian/rules +++ b/dist/linux/debian/rules @@ -4,11 +4,12 @@ # Uncomment this to turn on verbose mode. #export DH_VERBOSE=1 -JAVA_HOME = /usr/lib/jvm/java-25-coffeelibs DEB_BUILD_ARCH ?= $(shell dpkg-architecture -qDEB_BUILD_ARCH) ifeq ($(DEB_BUILD_ARCH),amd64) +JAVA_HOME = /usr/lib/jvm/java-25-openjdk-amd64 JMODS_PATH = jmods/amd64:${JAVA_HOME}/jmods else ifeq ($(DEB_BUILD_ARCH),arm64) +JAVA_HOME = /usr/lib/jvm/java-25-openjdk-arm64 JMODS_PATH = jmods/aarch64:${JAVA_HOME}/jmods endif From 8a441152342804a44a3ee89f4ab706e813a9c0c2 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Fri, 17 Oct 2025 15:25:18 +0200 Subject: [PATCH 15/34] update jacoco to support JDK 25 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 52097c539..021696864 100644 --- a/pom.xml +++ b/pom.xml @@ -62,7 +62,7 @@ 26.0.2-1 12.1.5 - 0.8.13 + 0.8.14 2.7.0 1.4.0 3.14.1 From 59560193ee1e1e5b14bf60bff288b827c9891388 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Tue, 21 Oct 2025 11:32:45 +0200 Subject: [PATCH 16/34] adjust to new multi-step update API --- .idea/runConfigurations/Cryptomator_macOS.xml | 2 +- dist/mac/dmg/build.sh | 1 + .../UpdatesPreferencesController.java | 88 ++++++++++--------- .../updater/DownloadUpdateMechanism.java | 11 ++- .../updater/FallbackUpdateMechanism.java | 57 ++++++++++++ .../updater/MacOsDmgUpdateMechanism.java | 53 +++++++---- .../cryptomator/updater/UpdateService.java | 59 +++++++++++++ .../resources/fxml/preferences_updates.fxml | 16 ++-- 8 files changed, 217 insertions(+), 70 deletions(-) create mode 100644 src/main/java/org/cryptomator/updater/FallbackUpdateMechanism.java create mode 100644 src/main/java/org/cryptomator/updater/UpdateService.java diff --git a/.idea/runConfigurations/Cryptomator_macOS.xml b/.idea/runConfigurations/Cryptomator_macOS.xml index c777434a2..bdd57a54a 100644 --- a/.idea/runConfigurations/Cryptomator_macOS.xml +++ b/.idea/runConfigurations/Cryptomator_macOS.xml @@ -5,7 +5,7 @@ + + + Central Portal Snapshots + central-portal-snapshots + https://central.sonatype.com/repository/maven-snapshots/ + + false + + + true + + + + diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 8db372c7a..c4a1bfe58 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -21,7 +21,6 @@ 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; @@ -64,9 +63,6 @@ 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/updater/DownloadUpdateMechanism.java b/src/main/java/org/cryptomator/updater/DownloadUpdateMechanism.java deleted file mode 100644 index dc9679d36..000000000 --- a/src/main/java/org/cryptomator/updater/DownloadUpdateMechanism.java +++ /dev/null @@ -1,78 +0,0 @@ -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; -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 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 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()); - if (response.statusCode() != 200) { - throw new RuntimeException("Failed to fetch release: " + response.statusCode()); - } - 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 DownloadUpdateInfo checkForUpdate(String currentVersion, LatestVersionResponse response); - - @JsonIgnoreProperties(ignoreUnknown = true) - public record LatestVersionResponse( - @JsonProperty("latestVersion") LatestVersion latestVersion, - @JsonProperty("assets") List 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( - @JsonProperty("name") String name, - @JsonProperty("digest") String digest, - @JsonProperty("size") long size, - @JsonProperty("downloadUrl") String downloadUrl - ) {} - -} diff --git a/src/main/java/org/cryptomator/updater/MacOsDmgUpdateMechanism.java b/src/main/java/org/cryptomator/updater/MacOsDmgUpdateMechanism.java deleted file mode 100644 index 91205b610..000000000 --- a/src/main/java/org/cryptomator/updater/MacOsDmgUpdateMechanism.java +++ /dev/null @@ -1,137 +0,0 @@ -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.DownloadUpdateStep; -import org.cryptomator.integrations.update.UpdateFailedException; -import org.cryptomator.integrations.update.UpdateMechanism; -import org.cryptomator.integrations.update.UpdateStep; -import org.jetbrains.annotations.Nullable; -import org.slf4j.Logger; -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; - - -@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 - DownloadUpdateInfo checkForUpdate(String currentVersion, LatestVersionResponse response) { - String suffix = switch (System.getProperty("os.arch")) { - case "aarch64", "arm64" -> "arm64.dmg"; - default -> "x64.dmg"; - }; - 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(DownloadUpdateInfo updateInfo) throws UpdateFailedException { - try { - Path workDir = Files.createTempDirectory("cryptomator-update"); - return new UpdateProcessImpl(workDir, updateInfo); - } catch (IOException e) { - throw new UpdateFailedException("Failed to create temporary directory for update", e); - } - } - - private static class UpdateProcessImpl extends DownloadUpdateStep { - - private final Path workDir; - - public UpdateProcessImpl(Path workDir, DownloadUpdateInfo updateInfo) { - var destination = workDir.resolve("update.dmg"); - 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; - } - - @Override - public @Nullable UpdateStep nextStep() throws IllegalStateException, IOException { - if (!isDone()) { - throw new IllegalStateException("Update not yet downloaded"); - } else if (downloadException != null) { - throw new UpdateFailedException("Download failed", downloadException); - } - return UpdateStep.of("Mounting...", this::mount); - } - - private UpdateStep mount() 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) { - Thread.currentThread().interrupt(); - throw new InterruptedIOException("Failed to extract DMG, interrupted"); - } - return UpdateStep.of("Restarting...", this::restart); - } - - public UpdateStep restart() throws IllegalStateException, IOException { - 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); - } - LOG.info("Restarting to apply Update in {} now...", workDir); - String script = """ - while kill -0 ${CRYPTOMATOR_PID} 2> /dev/null; do sleep 0.5; done; - 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}"; - """; - 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())); - processBuilder.environment().put("CRYPTOMATOR_INSTALL_PATH", installPath); - processBuilder.start(); - - return UpdateStep.EXIT; - } - } - -} From 754e53d8dbec17f2a00498d2e225fd601234e373 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Sat, 8 Nov 2025 10:18:40 +0100 Subject: [PATCH 30/34] cleanup --- src/main/java/module-info.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index c4a1bfe58..f1f2aa5c6 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -1,5 +1,4 @@ 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; @@ -52,7 +51,6 @@ open module org.cryptomator.desktop { requires org.slf4j; requires org.apache.commons.lang3; requires com.github.benmanes.caffeine; - requires com.fasterxml.jackson.annotation; /* dagger bs */ requires jakarta.inject; From 7a1cd9026cf8d1f865fc9c0290dd440f712281fd Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Sat, 8 Nov 2025 10:24:11 +0100 Subject: [PATCH 31/34] use dmg update mechanism --- .github/workflows/mac-dmg-x64.yml | 1 + .github/workflows/mac-dmg.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/mac-dmg-x64.yml b/.github/workflows/mac-dmg-x64.yml index b0af671a6..6a3fda580 100644 --- a/.github/workflows/mac-dmg-x64.yml +++ b/.github/workflows/mac-dmg-x64.yml @@ -136,6 +136,7 @@ jobs: --java-options "-Dcryptomator.integrationsMac.keychainServiceName=\"Cryptomator\"" --java-options "-Dcryptomator.mountPointsDir=\"@{userhome}/Library/Application Support/Cryptomator/mnt\"" --java-options "-Dcryptomator.showTrayIcon=true" + --java-options "-Dcryptomator.updateMechanism=org.cryptomator.macos.update.DmgUpdateMechanism" \ --java-options "-Dcryptomator.buildNumber=\"dmg-${{ needs.get-version.outputs.revNum }}\"" --mac-package-identifier org.cryptomator --resource-dir dist/mac/resources diff --git a/.github/workflows/mac-dmg.yml b/.github/workflows/mac-dmg.yml index cc1af8a78..794572af4 100644 --- a/.github/workflows/mac-dmg.yml +++ b/.github/workflows/mac-dmg.yml @@ -134,6 +134,7 @@ jobs: --java-options "-Dcryptomator.integrationsMac.keychainServiceName=\"Cryptomator\"" --java-options "-Dcryptomator.mountPointsDir=\"@{userhome}/Library/Application Support/Cryptomator/mnt\"" --java-options "-Dcryptomator.showTrayIcon=true" + --java-options "-Dcryptomator.updateMechanism=org.cryptomator.macos.update.DmgUpdateMechanism" \ --java-options "-Dcryptomator.buildNumber=\"dmg-${{ needs.get-version.outputs.revNum }}\"" --java-options "-XX:ErrorFile=/cryptomator/cryptomator_crash.log" --mac-package-identifier org.cryptomator From 48298bb161d742ea47353dc1d583ccdae81b1722 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Sat, 8 Nov 2025 10:37:10 +0100 Subject: [PATCH 32/34] fix syntax error --- .github/workflows/mac-dmg-x64.yml | 2 +- .github/workflows/mac-dmg.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/mac-dmg-x64.yml b/.github/workflows/mac-dmg-x64.yml index 6a3fda580..59f9c6014 100644 --- a/.github/workflows/mac-dmg-x64.yml +++ b/.github/workflows/mac-dmg-x64.yml @@ -135,7 +135,7 @@ jobs: --java-options "-Dcryptomator.ipcSocketPath=\"@{userhome}/Library/Application Support/Cryptomator/ipc.socket\"" --java-options "-Dcryptomator.integrationsMac.keychainServiceName=\"Cryptomator\"" --java-options "-Dcryptomator.mountPointsDir=\"@{userhome}/Library/Application Support/Cryptomator/mnt\"" - --java-options "-Dcryptomator.showTrayIcon=true" + --java-options "-Dcryptomator.showTrayIcon=true" \ --java-options "-Dcryptomator.updateMechanism=org.cryptomator.macos.update.DmgUpdateMechanism" \ --java-options "-Dcryptomator.buildNumber=\"dmg-${{ needs.get-version.outputs.revNum }}\"" --mac-package-identifier org.cryptomator diff --git a/.github/workflows/mac-dmg.yml b/.github/workflows/mac-dmg.yml index 794572af4..1588c93c4 100644 --- a/.github/workflows/mac-dmg.yml +++ b/.github/workflows/mac-dmg.yml @@ -133,7 +133,7 @@ jobs: --java-options "-Dcryptomator.ipcSocketPath=\"@{userhome}/Library/Application Support/Cryptomator/ipc.socket\"" --java-options "-Dcryptomator.integrationsMac.keychainServiceName=\"Cryptomator\"" --java-options "-Dcryptomator.mountPointsDir=\"@{userhome}/Library/Application Support/Cryptomator/mnt\"" - --java-options "-Dcryptomator.showTrayIcon=true" + --java-options "-Dcryptomator.showTrayIcon=true" \ --java-options "-Dcryptomator.updateMechanism=org.cryptomator.macos.update.DmgUpdateMechanism" \ --java-options "-Dcryptomator.buildNumber=\"dmg-${{ needs.get-version.outputs.revNum }}\"" --java-options "-XX:ErrorFile=/cryptomator/cryptomator_crash.log" From f91cc2374c1a83c406a0ae8d97ec94da3a48e28f Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Sat, 8 Nov 2025 10:48:29 +0100 Subject: [PATCH 33/34] fix syntax error (really!) --- .github/workflows/mac-dmg-x64.yml | 4 ++-- .github/workflows/mac-dmg.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/mac-dmg-x64.yml b/.github/workflows/mac-dmg-x64.yml index 59f9c6014..197c0e2e3 100644 --- a/.github/workflows/mac-dmg-x64.yml +++ b/.github/workflows/mac-dmg-x64.yml @@ -135,8 +135,8 @@ jobs: --java-options "-Dcryptomator.ipcSocketPath=\"@{userhome}/Library/Application Support/Cryptomator/ipc.socket\"" --java-options "-Dcryptomator.integrationsMac.keychainServiceName=\"Cryptomator\"" --java-options "-Dcryptomator.mountPointsDir=\"@{userhome}/Library/Application Support/Cryptomator/mnt\"" - --java-options "-Dcryptomator.showTrayIcon=true" \ - --java-options "-Dcryptomator.updateMechanism=org.cryptomator.macos.update.DmgUpdateMechanism" \ + --java-options "-Dcryptomator.showTrayIcon=true" + --java-options "-Dcryptomator.updateMechanism=org.cryptomator.macos.update.DmgUpdateMechanism" --java-options "-Dcryptomator.buildNumber=\"dmg-${{ needs.get-version.outputs.revNum }}\"" --mac-package-identifier org.cryptomator --resource-dir dist/mac/resources diff --git a/.github/workflows/mac-dmg.yml b/.github/workflows/mac-dmg.yml index 1588c93c4..864963db5 100644 --- a/.github/workflows/mac-dmg.yml +++ b/.github/workflows/mac-dmg.yml @@ -133,8 +133,8 @@ jobs: --java-options "-Dcryptomator.ipcSocketPath=\"@{userhome}/Library/Application Support/Cryptomator/ipc.socket\"" --java-options "-Dcryptomator.integrationsMac.keychainServiceName=\"Cryptomator\"" --java-options "-Dcryptomator.mountPointsDir=\"@{userhome}/Library/Application Support/Cryptomator/mnt\"" - --java-options "-Dcryptomator.showTrayIcon=true" \ - --java-options "-Dcryptomator.updateMechanism=org.cryptomator.macos.update.DmgUpdateMechanism" \ + --java-options "-Dcryptomator.showTrayIcon=true" + --java-options "-Dcryptomator.updateMechanism=org.cryptomator.macos.update.DmgUpdateMechanism" --java-options "-Dcryptomator.buildNumber=\"dmg-${{ needs.get-version.outputs.revNum }}\"" --java-options "-XX:ErrorFile=/cryptomator/cryptomator_crash.log" --mac-package-identifier org.cryptomator From 7a2944cbea0526adb0155b7c73b4885ec3c42da2 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Mon, 24 Nov 2025 14:39:17 +0100 Subject: [PATCH 34/34] bump integrations version --- pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index b720dbd77..24f35de0a 100644 --- a/pom.xml +++ b/pom.xml @@ -34,10 +34,10 @@ 2.9.0 - 1.8.0-SNAPSHOT + 1.8.0-beta1 1.5.1 - 1.5.0-SNAPSHOT - 1.7.0-SNAPSHOT + 1.5.0-beta1 + 1.7.0-beta1 5.1.0 3.0.0