mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-05-22 04:31:27 +00:00
moved update API to integrations-api
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import ch.qos.logback.classic.spi.Configurator;
|
||||
import org.cryptomator.integrations.update.UpdateMechanism;
|
||||
import org.cryptomator.networking.SSLContextWithPKCS12TrustStore;
|
||||
import org.cryptomator.common.locationpresets.DropboxLinuxLocationPresetsProvider;
|
||||
import org.cryptomator.common.locationpresets.DropboxMacLocationPresetsProvider;
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 <code>true</code> if an update is available, <code>false</code> 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?
|
||||
|
||||
}
|
||||
@@ -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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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;
|
||||
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user