Skip to content

Commit 857c812

Browse files
committed
Merge branch 'develop' into new-secret-service
2 parents ca6b4d8 + 2060d55 commit 857c812

File tree

7 files changed

+292
-8
lines changed

7 files changed

+292
-8
lines changed

.github/workflows/build.yml

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,14 @@ jobs:
5454
server-id: central
5555
server-username: MAVEN_CENTRAL_USERNAME
5656
server-password: MAVEN_CENTRAL_PASSWORD
57-
- name: Verify project version matches tag
57+
- name: Ensure to use tagged version
5858
if: startsWith(github.ref, 'refs/tags/')
59+
run: mvn versions:set --file ./pom.xml -DnewVersion=${GITHUB_REF##*/}
60+
- name: Verify project version is -SNAPSHOT
61+
if: startsWith(github.ref, 'refs/tags/') == false
5962
run: |
6063
PROJECT_VERSION=$(mvn help:evaluate "-Dexpression=project.version" -q -DforceStdout)
61-
test "$PROJECT_VERSION" = "${GITHUB_REF##*/}"
64+
test "${PROJECT_VERSION: -9}" = "-SNAPSHOT"
6265
- name: Deploy to Maven Central
6366
run: mvn deploy -B -DskipTests -Psign,deploy-central --no-transfer-progress
6467
env:
@@ -82,11 +85,14 @@ jobs:
8285
java-version: ${{ env.JAVA_VERSION }}
8386
distribution: 'temurin'
8487
cache: 'maven'
85-
- name: Verify project version matches tag
88+
- name: Ensure to use tagged version
8689
if: startsWith(github.ref, 'refs/tags/')
90+
run: mvn versions:set --file ./pom.xml -DnewVersion=${GITHUB_REF##*/}
91+
- name: Verify project version is -SNAPSHOT
92+
if: startsWith(github.ref, 'refs/tags/') == false
8793
run: |
8894
PROJECT_VERSION=$(mvn help:evaluate "-Dexpression=project.version" -q -DforceStdout)
89-
test "$PROJECT_VERSION" = "${GITHUB_REF##*/}"
95+
test "${PROJECT_VERSION: -9}" = "-SNAPSHOT"
9096
- name: Deploy to GitHub Packages
9197
run: mvn deploy -B -DskipTests -Psign,deploy-github --no-transfer-progress
9298
env:

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ Changes to prior versions can be found on the [Github release page](https://gith
99

1010
## [Unreleased](https://github.com/cryptomator/integrations-linux/compare/1.6.1...HEAD)
1111

12+
### Added
13+
* Flatpak Update Mechanism (#117)
14+
15+
### Changed
1216
* Require JDK 25
1317

1418
## [1.6.1](https://github.com/cryptomator/integrations-linux/releases/tag/1.6.1) - 2025-09-17

pom.xml

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,13 @@
3939
<project.jdk.version>25</project.jdk.version>
4040

4141
<!-- runtime dependencies -->
42-
43-
<api.version>1.8.0-SNAPSHOT</api.version>
42+
<api.version>1.8.0-beta1</api.version>
43+
<slf4j.version>2.0.17</slf4j.version>
44+
<jackson.version>2.20.0</jackson.version>
4445
<secret-service.version>2.0.1-alpha</secret-service.version>
4546
<kdewallet.version>1.4.0</kdewallet.version>
4647
<secret-service-02.version>1.0.1</secret-service-02.version>
47-
<slf4j.version>2.0.17</slf4j.version>
48+
<flatpakupdateportal.version>1.1.0</flatpakupdateportal.version>
4849
<appindicator.version>1.4.2</appindicator.version>
4950

5051
<!-- test dependencies -->
@@ -87,6 +88,12 @@
8788
<artifactId>slf4j-api</artifactId>
8889
<version>${slf4j.version}</version>
8990
</dependency>
91+
<dependency>
92+
<groupId>com.fasterxml.jackson.core</groupId>
93+
<artifactId>jackson-databind</artifactId>
94+
<version>${jackson.version}</version>
95+
</dependency>
96+
9097
<dependency>
9198
<groupId>de.swiesend</groupId>
9299
<artifactId>secret-service</artifactId>
@@ -108,6 +115,11 @@
108115
<artifactId>libappindicator-gtk3-java-minimal</artifactId>
109116
<version>${appindicator.version}</version>
110117
</dependency>
118+
<dependency>
119+
<groupId>org.purejava</groupId>
120+
<artifactId>flatpak-update-portal</artifactId>
121+
<version>${flatpakupdateportal.version}</version>
122+
</dependency>
111123
<dependency>
112124
<groupId>org.junit.jupiter</groupId>
113125
<artifactId>junit-jupiter</artifactId>

src/main/java/module-info.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import org.cryptomator.integrations.quickaccess.QuickAccessService;
44
import org.cryptomator.integrations.revealpath.RevealPathService;
55
import org.cryptomator.integrations.tray.TrayMenuController;
6+
import org.cryptomator.integrations.update.UpdateMechanism;
67
import org.cryptomator.linux.autostart.FreedesktopAutoStartService;
78
import org.cryptomator.linux.keychain.GnomeKeyringKeychainAccess;
89
import org.cryptomator.linux.keychain.KDEWalletKeychainAccess;
@@ -11,24 +12,30 @@
1112
import org.cryptomator.linux.quickaccess.NautilusBookmarks;
1213
import org.cryptomator.linux.revealpath.DBusSendRevealPathService;
1314
import org.cryptomator.linux.tray.AppindicatorTrayMenuController;
15+
import org.cryptomator.linux.update.FlatpakUpdater;
1416

1517
module org.cryptomator.integrations.linux {
1618
requires org.cryptomator.integrations.api;
1719
requires org.slf4j;
1820
requires org.freedesktop.dbus;
1921
requires org.purejava.appindicator;
2022
requires org.purejava.kwallet;
23+
requires org.purejava.portal;
2124
requires de.swiesend.secretservice;
2225
requires org.purejava.secret;
2326
requires java.xml;
27+
requires java.net.http;
28+
requires com.fasterxml.jackson.databind;
2429

2530
provides AutoStartProvider with FreedesktopAutoStartService;
2631
provides KeychainAccessProvider with SecretServiceKeychainAccess, GnomeKeyringKeychainAccess, KDEWalletKeychainAccess;
2732
provides RevealPathService with DBusSendRevealPathService;
2833
provides TrayMenuController with AppindicatorTrayMenuController;
2934
provides QuickAccessService with NautilusBookmarks, DolphinPlaces;
35+
provides UpdateMechanism with FlatpakUpdater;
3036

3137
opens org.cryptomator.linux.tray to org.cryptomator.integrations.api;
3238
opens org.cryptomator.linux.quickaccess to org.cryptomator.integrations.api;
3339
opens org.cryptomator.linux.autostart to org.cryptomator.integrations.api;
34-
}
40+
opens org.cryptomator.linux.update to org.cryptomator.integrations.api, com.fasterxml.jackson.databind;
41+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package org.cryptomator.linux.update;
2+
3+
import org.cryptomator.integrations.update.UpdateInfo;
4+
import org.cryptomator.integrations.update.UpdateMechanism;
5+
6+
public record FlatpakUpdateInfo(String version, UpdateMechanism<FlatpakUpdateInfo> updateMechanism) implements UpdateInfo<FlatpakUpdateInfo> {
7+
}
Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
package org.cryptomator.linux.update;
2+
3+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
4+
import com.fasterxml.jackson.annotation.JsonProperty;
5+
import com.fasterxml.jackson.databind.ObjectMapper;
6+
import org.cryptomator.integrations.common.CheckAvailability;
7+
import org.cryptomator.integrations.common.DisplayName;
8+
import org.cryptomator.integrations.common.OperatingSystem;
9+
import org.cryptomator.integrations.update.UpdateFailedException;
10+
import org.cryptomator.integrations.update.UpdateMechanism;
11+
import org.cryptomator.integrations.update.UpdateStep;
12+
import org.freedesktop.dbus.FileDescriptor;
13+
import org.freedesktop.dbus.exceptions.DBusException;
14+
import org.freedesktop.dbus.types.UInt32;
15+
import org.freedesktop.dbus.types.Variant;
16+
import org.purejava.portal.Flatpak;
17+
import org.purejava.portal.FlatpakSpawnFlag;
18+
import org.purejava.portal.UpdatePortal;
19+
import org.purejava.portal.Util;
20+
import org.slf4j.Logger;
21+
import org.slf4j.LoggerFactory;
22+
23+
import java.io.IOException;
24+
import java.net.URI;
25+
import java.net.http.HttpClient;
26+
import java.net.http.HttpRequest;
27+
import java.net.http.HttpResponse;
28+
import java.util.Collections;
29+
import java.util.Comparator;
30+
import java.util.List;
31+
import java.util.Map;
32+
import java.util.concurrent.CountDownLatch;
33+
import java.util.concurrent.TimeUnit;
34+
35+
@CheckAvailability
36+
@DisplayName("Update via Flatpak update")
37+
@OperatingSystem(OperatingSystem.Value.LINUX)
38+
public class FlatpakUpdater implements UpdateMechanism<FlatpakUpdateInfo> {
39+
40+
private static final Logger LOG = LoggerFactory.getLogger(FlatpakUpdater.class);
41+
private static final String FLATHUB_API_BASE_URL = "https://flathub.org/api/v2/appstream/";
42+
private static final String APP_NAME = "org.cryptomator.Cryptomator";
43+
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
44+
45+
private final UpdatePortal portal;
46+
47+
public FlatpakUpdater() {
48+
this.portal = new UpdatePortal();
49+
portal.CreateUpdateMonitor(UpdatePortal.OPTIONS_DUMMY);
50+
}
51+
52+
@CheckAvailability
53+
public boolean isSupported() {
54+
return portal.isAvailable();
55+
}
56+
57+
@Override
58+
public FlatpakUpdateInfo checkForUpdate(String currentVersion, HttpClient httpClient) throws UpdateFailedException {
59+
var uri = URI.create(FLATHUB_API_BASE_URL + APP_NAME);
60+
var request = HttpRequest.newBuilder(uri).GET().build();
61+
try {
62+
var response = httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream());
63+
if (response.statusCode() != 200) {
64+
LOG.warn("GET {} resulted in status {}", uri, response.statusCode());
65+
return null;
66+
} else {
67+
var appstream = OBJECT_MAPPER.reader().readValue(response.body(), AppstreamResponse.class);
68+
var updateVersion = appstream.releases().stream()
69+
.filter(release -> "stable".equalsIgnoreCase(release.type))
70+
.max(Comparator.comparing(AppstreamReleases::timestamp)) // we're interested in the newest stable release
71+
.map(AppstreamReleases::version)
72+
.orElse("0.0.0"); // fallback should always be smaller than current version
73+
74+
75+
// FIXME: remove this block! see https://github.com/cryptomator/cryptomator/issues/4058
76+
if (currentVersion.startsWith("1.18.0-beta")) {
77+
return new FlatpakUpdateInfo(updateVersion, this);
78+
}
79+
// END FIXME
80+
81+
82+
if (UpdateMechanism.isUpdateAvailable(updateVersion, currentVersion)) {
83+
return new FlatpakUpdateInfo(updateVersion, this);
84+
} else {
85+
return null;
86+
}
87+
}
88+
} catch (IOException e) {
89+
throw new UpdateFailedException("Check for updates failed.", e);
90+
} catch (InterruptedException e) {
91+
Thread.currentThread().interrupt();
92+
LOG.warn("Update check interrupted", e);
93+
return null;
94+
}
95+
}
96+
97+
@JsonIgnoreProperties(ignoreUnknown = true)
98+
public record AppstreamResponse(
99+
@JsonProperty("releases") List<AppstreamReleases> releases
100+
) {}
101+
102+
@JsonIgnoreProperties(ignoreUnknown = true)
103+
public record AppstreamReleases(
104+
@JsonProperty("timestamp") long timestamp,
105+
@JsonProperty("version") String version,
106+
@JsonProperty("type") String type
107+
) {}
108+
109+
@Override
110+
public UpdateStep firstStep(FlatpakUpdateInfo updateInfo) throws UpdateFailedException {
111+
var monitorPath = portal.CreateUpdateMonitor(UpdatePortal.OPTIONS_DUMMY);
112+
if (monitorPath == null) {
113+
throw new UpdateFailedException("Failed to create UpdateMonitor on DBus");
114+
}
115+
116+
return new FlatpakUpdateStep(portal.getUpdateMonitor(monitorPath.toString()));
117+
}
118+
119+
private class FlatpakUpdateStep implements UpdateStep {
120+
121+
private final CountDownLatch latch = new CountDownLatch(1);
122+
private final Flatpak.UpdateMonitor monitor;
123+
private volatile double progress = 0.0;
124+
private volatile UpdateFailedException error;
125+
private AutoCloseable signalHandler;
126+
127+
private FlatpakUpdateStep(Flatpak.UpdateMonitor monitor) {
128+
this.monitor = monitor;
129+
}
130+
131+
@Override
132+
public String description() {
133+
return "Updating via Flatpak... %1.0f%%".formatted(preparationProgress() * 100);
134+
}
135+
136+
@Override
137+
public void start() {
138+
try {
139+
this.signalHandler = portal.getDBusConnection().addSigHandler(Flatpak.UpdateMonitor.Progress.class, this::handleProgressSignal);
140+
} catch (DBusException e) {
141+
LOG.error("DBus error", e);
142+
latch.countDown();
143+
}
144+
portal.updateApp("x11:0", monitor, UpdatePortal.OPTIONS_DUMMY);
145+
}
146+
147+
private void handleProgressSignal(Flatpak.UpdateMonitor.Progress signal) {
148+
int status = ((UInt32) signal.info.get("status").getValue()).intValue();
149+
switch (status) {
150+
case 0 -> { // In progress
151+
Variant<?> progressVariant = signal.info.get("progress");
152+
if (progressVariant != null) {
153+
progress = ((UInt32) progressVariant.getValue()).doubleValue() / 100.0; // progress reported as int in range [0, 100]
154+
}
155+
}
156+
case 1 -> { // No update available
157+
error = new UpdateFailedException("No update available");
158+
latch.countDown();
159+
}
160+
case 2 -> { // Update complete
161+
progress = 1.0;
162+
latch.countDown();
163+
}
164+
case 3 -> { // Update failed
165+
error = new UpdateFailedException("Update preparation failed");
166+
latch.countDown();
167+
}
168+
default -> {
169+
error = new UpdateFailedException("Unknown update status " + status);
170+
latch.countDown();
171+
}
172+
}
173+
}
174+
175+
private void stopReceivingSignals() {
176+
if (signalHandler != null) {
177+
try {
178+
signalHandler.close();
179+
} catch (Exception e) {
180+
LOG.error("Failed to close signal handler", e);
181+
}
182+
signalHandler = null;
183+
}
184+
}
185+
186+
@Override
187+
public double preparationProgress() {
188+
return progress;
189+
}
190+
191+
@Override
192+
public void cancel() {
193+
portal.cancelUpdateMonitor(monitor);
194+
stopReceivingSignals();
195+
portal.close(); // TODO: is this right? belongs to parent class. update can not be retried afterwards. or should each process have its own portal instance?
196+
error = new UpdateFailedException("Update cancelled by user");
197+
}
198+
199+
@Override
200+
public void await() throws InterruptedException {
201+
latch.await();
202+
}
203+
204+
@Override
205+
public boolean await(long timeout, TimeUnit unit) throws InterruptedException {
206+
return latch.await(timeout, unit);
207+
}
208+
209+
@Override
210+
public boolean isDone() {
211+
try {
212+
return latch.await(0, TimeUnit.MILLISECONDS);
213+
} catch (InterruptedException e) {
214+
Thread.currentThread().interrupt();
215+
return false;
216+
}
217+
}
218+
219+
@Override
220+
public UpdateStep nextStep() throws IllegalStateException, IOException {
221+
return UpdateStep.of("Restarting application", this::applyUpdate);
222+
}
223+
224+
public UpdateStep applyUpdate() throws IllegalStateException, IOException {
225+
if (!isDone()) {
226+
throw new IllegalStateException("Update preparation is not complete");
227+
}
228+
stopReceivingSignals();
229+
if (error != null) {
230+
throw error;
231+
}
232+
233+
// spawn new Cryptomator process:
234+
var cwdPath = Util.stringToByteList(System.getProperty("user.dir"));
235+
List<List<Byte>> argv = List.of(
236+
Util.stringToByteList(APP_NAME));
237+
Map<UInt32, FileDescriptor> fds = Collections.emptyMap();
238+
Map<String, String> envs = Map.of();
239+
UInt32 flags = new UInt32(FlatpakSpawnFlag.LATEST_VERSION.getValue());
240+
Map<String, Variant<?>> options = UpdatePortal.OPTIONS_DUMMY;
241+
var pid = portal.Spawn(cwdPath, argv, fds, envs, flags, options).longValue();
242+
LOG.info("Spawned updated Cryptomator process with PID {}", pid);
243+
return UpdateStep.EXIT;
244+
}
245+
}
246+
247+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
org.cryptomator.linux.update.FlatpakUpdater

0 commit comments

Comments
 (0)