diff --git a/pom.xml b/pom.xml
index cbee9a8..1481f4a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -32,7 +32,7 @@
25
- 1.7.0
+ 1.8.0-SNAPSHOT
2.0.17
@@ -63,6 +63,20 @@
+
+
+ Central Portal Snapshots
+ central-portal-snapshots
+ https://central.sonatype.com/repository/maven-snapshots/
+
+ false
+
+
+ true
+
+
+
+
org.cryptomator
diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java
index 889f37f..25c4ab3 100644
--- a/src/main/java/module-info.java
+++ b/src/main/java/module-info.java
@@ -3,12 +3,14 @@
import org.cryptomator.integrations.revealpath.RevealPathService;
import org.cryptomator.integrations.tray.TrayIntegrationProvider;
import org.cryptomator.integrations.uiappearance.UiAppearanceProvider;
+import org.cryptomator.integrations.update.UpdateMechanism;
import org.cryptomator.macos.autostart.MacAutoStartProvider;
import org.cryptomator.macos.keychain.MacSystemKeychainAccess;
import org.cryptomator.macos.keychain.TouchIdKeychainAccess;
import org.cryptomator.macos.revealpath.OpenCmdRevealPathService;
import org.cryptomator.macos.tray.MacTrayIntegrationProvider;
import org.cryptomator.macos.uiappearance.MacUiAppearanceProvider;
+import org.cryptomator.macos.update.DmgUpdateMechanism;
module org.cryptomator.integrations.mac {
requires org.cryptomator.integrations.api;
@@ -19,4 +21,5 @@
provides RevealPathService with OpenCmdRevealPathService;
provides TrayIntegrationProvider with MacTrayIntegrationProvider;
provides UiAppearanceProvider with MacUiAppearanceProvider;
+ provides UpdateMechanism with DmgUpdateMechanism;
}
\ No newline at end of file
diff --git a/src/main/java/org/cryptomator/macos/update/DmgUpdateMechanism.java b/src/main/java/org/cryptomator/macos/update/DmgUpdateMechanism.java
new file mode 100644
index 0000000..49273bf
--- /dev/null
+++ b/src/main/java/org/cryptomator/macos/update/DmgUpdateMechanism.java
@@ -0,0 +1,106 @@
+package org.cryptomator.macos.update;
+
+import org.cryptomator.integrations.common.LocalizedDisplayName;
+import org.cryptomator.integrations.common.OperatingSystem;
+import org.cryptomator.integrations.update.DownloadUpdateInfo;
+import org.cryptomator.integrations.update.DownloadUpdateMechanism;
+import org.cryptomator.integrations.update.UpdateFailedException;
+import org.cryptomator.integrations.update.UpdateMechanism;
+import org.cryptomator.integrations.update.UpdateStep;
+import org.cryptomator.macos.common.Localization;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.util.List;
+import java.util.UUID;
+
+@OperatingSystem(OperatingSystem.Value.MAC)
+@LocalizedDisplayName(bundle = "MacIntegrationsBundle", key = "org.cryptomator.macos.update.dmg.displayName")
+public class DmgUpdateMechanism extends DownloadUpdateMechanism {
+
+ private static final Logger LOG = LoggerFactory.getLogger(DmgUpdateMechanism.class);
+
+ @Override
+ protected 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 DownloadUpdateInfo(this, updateVersion, asset);
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public UpdateStep secondStep(Path workDir, Path assetPath, DownloadUpdateInfo updateInfo) {
+ return UpdateStep.of(Localization.get().getString("org.cryptomator.macos.update.dmg.unpacking"), () -> this.unpack(workDir, assetPath));
+ }
+
+ private UpdateStep unpack(Path workDir, Path assetPath) throws IOException {
+ // Extract Cryptomator.app from the .dmg file
+ String script = """
+ hdiutil attach "${DMG_PATH}" -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("/bin/zsh", "-c", script);
+ var processBuilder = new ProcessBuilder(command);
+ processBuilder.directory(workDir.toFile());
+ processBuilder.environment().put("DMG_PATH", assetPath.toString());
+ 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(Localization.get().getString("org.cryptomator.macos.update.dmg.restarting"), () -> this.restart(workDir));
+ }
+
+ public UpdateStep restart(Path workDir) 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.2; 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 "${CRYPTOMATOR_INSTALL_PATH}";
+ """;
+ Files.writeString(workDir.resolve("install.sh"), script, StandardCharsets.US_ASCII, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW);
+ var command = List.of("/bin/zsh", "-c", "/usr/bin/nohup zsh 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;
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/resources/MacIntegrationsBundle.properties b/src/main/resources/MacIntegrationsBundle.properties
index 7b3099c..55214da 100644
--- a/src/main/resources/MacIntegrationsBundle.properties
+++ b/src/main/resources/MacIntegrationsBundle.properties
@@ -1,2 +1,5 @@
org.cryptomator.macos.keychain.displayName=macOS Keychain
-org.cryptomator.macos.keychain.touchIdDisplayName=Touch ID
\ No newline at end of file
+org.cryptomator.macos.keychain.touchIdDisplayName=Touch ID
+org.cryptomator.macos.update.dmg.displayName=Download .dmg file
+org.cryptomator.macos.update.dmg.unpacking=Unpacking...
+org.cryptomator.macos.update.dmg.restarting=Restarting...
\ No newline at end of file