Skip to content
Open
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
88f7231
Invent UpdateService
purejava Jun 28, 2025
a501a2b
Have multiple UpdateServices
purejava Jul 12, 2025
6af0aa8
Invent @DistributionChannel
purejava Jul 13, 2025
a2a6f98
Drop isUpdateAvailable in favor of getLatestReleaseChecker
purejava Jul 20, 2025
f3f3c35
Add spawnApp()
purejava Jul 20, 2025
5f29005
Add listener interfaces
purejava Jul 20, 2025
28680db
Implement missing signals
purejava Jul 29, 2025
eaa63e5
Correlate with API as suggested in a separate PoC: UpdateMechanism an…
purejava Aug 2, 2025
84076df
Reduce API surface
overheadhunter Aug 8, 2025
e875adb
Merge branch 'develop' into update-mechanism
overheadhunter Aug 8, 2025
dde78a1
API refinement
overheadhunter Aug 9, 2025
0f765d6
added test case
overheadhunter Aug 9, 2025
5dadcbe
multi-step update api
overheadhunter Oct 21, 2025
61de9f3
add magic constants for UI to react to
overheadhunter Oct 21, 2025
ffc3666
update update mechanism
overheadhunter Nov 2, 2025
2493753
Allow UpdateInfo to carry mechanism-specific data
overheadhunter Nov 4, 2025
b0d9fe4
improve type safety
overheadhunter Nov 4, 2025
41d8e4c
delete unused class
overheadhunter Nov 6, 2025
a052dd0
Merge branch 'develop' into update-mechanism
overheadhunter Nov 6, 2025
5856f28
append CHANGELOG
overheadhunter Nov 6, 2025
6e3bb15
share classes for `DownloadUpdateMechanism`
overheadhunter Nov 6, 2025
4dbfb11
Merge branch 'develop' into feature/update-mechanism
overheadhunter Nov 7, 2025
d75729e
apply suggestions from code review
overheadhunter Nov 12, 2025
73332c8
no need for java.time.* deserialization
overheadhunter Nov 12, 2025
a522f36
implement more suggestions from code review
overheadhunter Nov 12, 2025
0fa2e52
update changelog
overheadhunter Nov 17, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
The changelog starts with version 1.7.0.
Changes to prior versions can be found on the [Github release page](https://github.com/cryptomator/integrations-api/releases).

## [Unreleased]
## [Unreleased](https://github.com/cryptomator/integrations-api/compare/1.7.0...HEAD)

No changes yet.
### Added

## [1.7.0] - 2025-09-17
* Experimental [Update API](https://github.com/cryptomator/integrations-api/blob/a052dd06a38f5410f6d9c9c7061c036efee83480/src/main/java/org/cryptomator/integrations/update/UpdateMechanism.java)

## [1.7.0](https://github.com/cryptomator/integrations-api/releases/tag/1.7.0) - 2025-09-17

### Changed

Expand Down
11 changes: 11 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
<jdk.version>25</jdk.version>

<slf4j.version>2.0.17</slf4j.version>
<jackson.version>2.20.0</jackson.version>
<jetbrains-annotation.version>26.0.2-1</jetbrains-annotation.version>

<!-- Test dependencies -->
Expand Down Expand Up @@ -59,6 +60,16 @@
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>${jackson.version}</version>
</dependency>

<dependency>
<groupId>org.jetbrains</groupId>
Expand Down
6 changes: 6 additions & 0 deletions src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,15 @@
import org.cryptomator.integrations.keychain.KeychainAccessProvider;
import org.cryptomator.integrations.tray.TrayIntegrationProvider;
import org.cryptomator.integrations.uiappearance.UiAppearanceProvider;
import org.cryptomator.integrations.update.UpdateMechanism;


module org.cryptomator.integrations.api {
requires static org.jetbrains.annotations;
requires org.slf4j;
requires com.fasterxml.jackson.databind;
requires com.fasterxml.jackson.datatype.jsr310;
Copy link

Copilot AI Nov 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The module requires 'com.fasterxml.jackson.datatype.jsr310' but this dependency doesn't appear to be used in any of the new code. Consider removing this dependency if it's not needed.

Suggested change
requires com.fasterxml.jackson.datatype.jsr310;

Copilot uses AI. Check for mistakes.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed in 73332c8

requires java.net.http;

exports org.cryptomator.integrations.autostart;
exports org.cryptomator.integrations.common;
Expand All @@ -20,6 +24,7 @@
exports org.cryptomator.integrations.tray;
exports org.cryptomator.integrations.uiappearance;
exports org.cryptomator.integrations.quickaccess;
exports org.cryptomator.integrations.update;

uses AutoStartProvider;
uses KeychainAccessProvider;
Expand All @@ -29,4 +34,5 @@
uses TrayMenuController;
uses UiAppearanceProvider;
uses QuickAccessService;
uses UpdateMechanism;
}
14 changes: 14 additions & 0 deletions src/main/java/org/cryptomator/integrations/Localization.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.cryptomator.integrations;

import java.util.ResourceBundle;

public enum Localization {
INSTANCE;

private final ResourceBundle resourceBundle = ResourceBundle.getBundle("IntegrationsApi");

public static ResourceBundle get() {
return INSTANCE.resourceBundle;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,19 @@ public static <T> Optional<T> load(Class<T> clazz) {
return loadAll(clazz).findFirst();
}

/**
* Loads a specific service provider by its implementation class name.
* @param clazz Service class
* @param implementationClassName fully qualified class name of the implementation
* @return Optional of the service provider if found
* @param <T> Type of the service
*/
public static <T> Optional<T> loadSpecific(Class<T> clazz, String implementationClassName) {
return ServiceLoader.load(clazz, ClassLoaderFactory.forPluginDir()).stream()
.filter(provider -> provider.type().getName().equals(implementationClassName))
.map(ServiceLoader.Provider::get)
.findAny();
}

/**
* Loads all suited service providers ordered by priority in descending order.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.cryptomator.integrations.update;

public record DownloadUpdateInfo(
DownloadUpdateMechanism updateMechanism,
String version,
DownloadUpdateMechanism.Asset asset
) implements UpdateInfo<DownloadUpdateInfo> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package org.cryptomator.integrations.update;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.jetbrains.annotations.Blocking;
import org.jetbrains.annotations.NotNull;
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.nio.file.Files;
import java.nio.file.Path;
import java.util.HexFormat;
import java.util.List;

public abstract class DownloadUpdateMechanism implements UpdateMechanism<DownloadUpdateInfo> {

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();

@Override
public DownloadUpdateInfo checkForUpdate(String currentVersion, HttpClient httpClient) {
try {
HttpRequest request = HttpRequest.newBuilder().uri(URI.create(LATEST_VERSION_API_URL)).build();
HttpResponse<InputStream> response = httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream());
if (response.statusCode() != 200) {
LOG.warn("Failed to fetch release: HTTP {}", response.statusCode());
return null;
}
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;
}
}

/**
* Returns the first step to prepare the update. This downloads the {@link DownloadUpdateInfo#asset() asset} to a temporary location and verifies its checksum.
* @param updateInfo The {@link DownloadUpdateInfo} retrieved from {@link #checkForUpdate(String, HttpClient)}.
* @return a new {@link UpdateStep} that can be used to monitor the download progress.
* @throws UpdateFailedException When failing to prepare a temporary download location.
*/
@Override
public UpdateStep firstStep(DownloadUpdateInfo updateInfo) throws UpdateFailedException {
try {
Path workDir = Files.createTempDirectory("cryptomator-update");
return new FirstStep(workDir, updateInfo);
} catch (IOException e) {
throw new UpdateFailedException("Failed to create temporary directory for update", e);
}
}

/**
* Second step that is executed after the download has completed in the {@link #firstStep(DownloadUpdateInfo) first step}.
* @param workDir A temporary working directory to which the asset has been downloaded.
* @param assetPath The path of the downloaded asset.
* @param updateInfo The {@link DownloadUpdateInfo} representing the update.
* @return The next step of the update process.
* @throws IllegalStateException if preconditions aren't met.
* @throws IOException indicating an error preventing the next step from starting.
* @implSpec The returned {@link UpdateStep} must either be stateless or a new instance must be returned on each call.
*/
public abstract UpdateStep secondStep(Path workDir, Path assetPath, DownloadUpdateInfo updateInfo) throws IllegalStateException, IOException;

@Nullable
@Blocking
protected abstract DownloadUpdateInfo checkForUpdate(String currentVersion, LatestVersionResponse response);

@JsonIgnoreProperties(ignoreUnknown = true)
public record LatestVersionResponse(
@JsonProperty("latestVersion") LatestVersion latestVersion,
@JsonProperty("assets") List<Asset> 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
) {}

private class FirstStep extends DownloadUpdateStep {
private final Path workDir;
private final DownloadUpdateInfo updateInfo;

public FirstStep(Path workDir, DownloadUpdateInfo updateInfo) {
var uri = URI.create(updateInfo.asset().downloadUrl);
var destination = workDir.resolve(updateInfo.asset().name);
var digest = updateInfo.asset().digest().startsWith("sha256:")
? HexFormat.of().withLowerCase().parseHex(updateInfo.asset().digest.substring(7)) // remove "sha256:" prefix
: null;
var size = updateInfo.asset().size;
super(uri, destination, digest, size);
this.workDir = workDir;
this.updateInfo = updateInfo;
}

@Override
public @Nullable UpdateStep nextStep() throws IllegalStateException, IOException {
if (!isDone()) {
throw new IllegalStateException("Download not yet completed.");
} else if (downloadException != null) {
throw new UpdateFailedException("Download failed.", downloadException);
}
return secondStep(workDir, destination, updateInfo);
}
}

}
Loading