Skip to content

Commit 6e3bb15

Browse files
share classes for DownloadUpdateMechanism
1 parent 5856f28 commit 6e3bb15

8 files changed

Lines changed: 178 additions & 10 deletions

File tree

pom.xml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
<jdk.version>21</jdk.version>
3131

3232
<slf4j.version>2.0.17</slf4j.version>
33+
<jackson.version>2.20.0</jackson.version>
3334
<jetbrains-annotation.version>26.0.2-1</jetbrains-annotation.version>
3435

3536
<!-- Test dependencies -->
@@ -59,6 +60,16 @@
5960
<artifactId>slf4j-api</artifactId>
6061
<version>${slf4j.version}</version>
6162
</dependency>
63+
<dependency>
64+
<groupId>com.fasterxml.jackson.core</groupId>
65+
<artifactId>jackson-databind</artifactId>
66+
<version>${jackson.version}</version>
67+
</dependency>
68+
<dependency>
69+
<groupId>com.fasterxml.jackson.datatype</groupId>
70+
<artifactId>jackson-datatype-jsr310</artifactId>
71+
<version>${jackson.version}</version>
72+
</dependency>
6273

6374
<dependency>
6475
<groupId>org.jetbrains</groupId>

src/main/java/module-info.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
module org.cryptomator.integrations.api {
1313
requires static org.jetbrains.annotations;
1414
requires org.slf4j;
15+
requires com.fasterxml.jackson.databind;
16+
requires com.fasterxml.jackson.datatype.jsr310;
1517
requires java.net.http;
1618

1719
exports org.cryptomator.integrations.autostart;
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package org.cryptomator.integrations;
2+
3+
import java.util.ResourceBundle;
4+
5+
public enum Localization {
6+
INSTANCE;
7+
8+
private final ResourceBundle resourceBundle = ResourceBundle.getBundle("IntegrationsApi");
9+
10+
public static ResourceBundle get() {
11+
return INSTANCE.resourceBundle;
12+
}
13+
14+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package org.cryptomator.integrations.update;
2+
3+
public record DownloadUpdateInfo(
4+
DownloadUpdateMechanism updateMechanism,
5+
String version,
6+
DownloadUpdateMechanism.Asset asset
7+
) implements UpdateInfo<DownloadUpdateInfo> {
8+
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package org.cryptomator.integrations.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.jetbrains.annotations.Blocking;
7+
import org.jetbrains.annotations.NotNull;
8+
import org.jetbrains.annotations.Nullable;
9+
import org.slf4j.Logger;
10+
import org.slf4j.LoggerFactory;
11+
12+
import java.io.IOException;
13+
import java.io.InputStream;
14+
import java.net.URI;
15+
import java.net.http.HttpClient;
16+
import java.net.http.HttpRequest;
17+
import java.net.http.HttpResponse;
18+
import java.nio.file.Files;
19+
import java.nio.file.Path;
20+
import java.util.HexFormat;
21+
import java.util.List;
22+
23+
public abstract class DownloadUpdateMechanism implements UpdateMechanism<DownloadUpdateInfo> {
24+
25+
private static final Logger LOG = LoggerFactory .getLogger(DownloadUpdateMechanism.class);
26+
private static final String LATEST_VERSION_API_URL = "https://api.cryptomator.org/connect/apps/desktop/latest-version?format=1";
27+
private static final ObjectMapper MAPPER = new ObjectMapper();
28+
29+
@Override
30+
public DownloadUpdateInfo checkForUpdate(String currentVersion, HttpClient httpClient) {
31+
try {
32+
HttpRequest request = HttpRequest.newBuilder().uri(URI.create(LATEST_VERSION_API_URL)).build();
33+
HttpResponse<InputStream> response = httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream());
34+
if (response.statusCode() != 200) {
35+
throw new RuntimeException("Failed to fetch release: " + response.statusCode());
36+
}
37+
var release = MAPPER.readValue(response.body(), LatestVersionResponse.class);
38+
return checkForUpdate(currentVersion, release);
39+
} catch (InterruptedException e) {
40+
Thread.currentThread().interrupt();
41+
LOG.debug("Update check interrupted.");
42+
return null;
43+
} catch (IOException e) {
44+
LOG.warn("Update check failed", e);
45+
return null;
46+
}
47+
}
48+
49+
/**
50+
* Returns the first step to prepare the update. This downloads the {@link DownloadUpdateInfo#asset() asset} to a temporary location and verifies its checksum.
51+
* @param updateInfo The {@link DownloadUpdateInfo} retrieved from {@link #checkForUpdate(String, HttpClient)}.
52+
* @return a new {@link UpdateStep} that can be used to monitor the download progress.
53+
* @throws UpdateFailedException When failing to prepare a temporary download location.
54+
*/
55+
@Override
56+
public UpdateStep firstStep(DownloadUpdateInfo updateInfo) throws UpdateFailedException {
57+
try {
58+
Path workDir = Files.createTempDirectory("cryptomator-update");
59+
return new FirstStep(workDir, updateInfo);
60+
} catch (IOException e) {
61+
throw new UpdateFailedException("Failed to create temporary directory for update", e);
62+
}
63+
}
64+
65+
/**
66+
* Second step that is executed after the download has completed in the {@link #firstStep(DownloadUpdateInfo) first step}.
67+
* @param workDir A temporary working directory to which the asset has been downloaded.
68+
* @param assetPath The path of the downloaded asset.
69+
* @param updateInfo The {@link DownloadUpdateInfo} representing the update.
70+
* @return The next step of the update process.
71+
* @throws IllegalStateException if preconditions aren't met.
72+
* @throws IOException indicating an error preventing the next step from starting.
73+
* @implSpec The returned {@link UpdateStep} must either be stateless or a new instance must be returned on each call.
74+
*/
75+
public abstract UpdateStep secondStep(Path workDir, Path assetPath, DownloadUpdateInfo updateInfo) throws IllegalStateException, IOException;
76+
77+
@Nullable
78+
@Blocking
79+
protected abstract DownloadUpdateInfo checkForUpdate(String currentVersion, LatestVersionResponse response);
80+
81+
@JsonIgnoreProperties(ignoreUnknown = true)
82+
public record LatestVersionResponse(
83+
@JsonProperty("latestVersion") LatestVersion latestVersion,
84+
@JsonProperty("assets") List<Asset> assets
85+
) {}
86+
87+
@JsonIgnoreProperties(ignoreUnknown = true)
88+
public record LatestVersion(
89+
@JsonProperty("mac") String macVersion,
90+
@JsonProperty("win") String winVersion,
91+
@JsonProperty("linux") String linuxVersion
92+
) {}
93+
94+
@JsonIgnoreProperties(ignoreUnknown = true)
95+
public record Asset(
96+
@JsonProperty("name") String name,
97+
@JsonProperty("digest") String digest, // TODO: verify this starts with "sha256:"?
98+
@JsonProperty("size") long size,
99+
@JsonProperty("downloadUrl") String downloadUrl
100+
) {}
101+
102+
private class FirstStep extends DownloadUpdateStep {
103+
private final Path workDir;
104+
private final DownloadUpdateInfo updateInfo;
105+
106+
public FirstStep(Path workDir, DownloadUpdateInfo updateInfo) {
107+
super(URI.create(updateInfo.asset().downloadUrl),
108+
workDir.resolve(updateInfo.asset().name),
109+
HexFormat.of().withLowerCase().parseHex(updateInfo.asset().digest.substring(7)), // remove "sha256:" prefix
110+
updateInfo.asset().size);
111+
this.workDir = workDir;
112+
this.updateInfo = updateInfo;
113+
}
114+
115+
@Override
116+
public @Nullable UpdateStep nextStep() throws IllegalStateException, IOException {
117+
if (!isDone()) {
118+
throw new IllegalStateException("Download not yet completed.");
119+
} else if (downloadException != null) {
120+
throw new UpdateFailedException("Download failed.", downloadException);
121+
}
122+
return secondStep(workDir, destination, updateInfo);
123+
}
124+
}
125+
126+
}

src/main/java/org/cryptomator/integrations/update/DownloadUpdateStep.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package org.cryptomator.integrations.update;
22

3+
import org.cryptomator.integrations.Localization;
4+
35
import java.io.FilterInputStream;
46
import java.io.IOException;
57
import java.io.InputStream;
@@ -48,9 +50,9 @@ protected DownloadUpdateStep(URI source, Path destination, byte[] checksum, long
4850
@Override
4951
public String description() {
5052
return switch (downloadThread.getState()) {
51-
case NEW -> "Download... ";
52-
case TERMINATED -> "Downloaded.";
53-
default -> "Downloading... %1.0f%%".formatted(preparationProgress() * 100);
53+
case NEW -> Localization.get().getString("org.cryptomator.api.update.download.new");
54+
case TERMINATED -> Localization.get().getString("org.cryptomator.api.update.download.done");
55+
default -> Localization.get().getString("org.cryptomator.api.update.download.progress").formatted(preparationProgress() * 100);
5456
};
5557
}
5658

src/main/java/org/cryptomator/integrations/update/UpdateStep.java

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.cryptomator.integrations.update;
22

3+
import org.cryptomator.integrations.Localization;
34
import org.jetbrains.annotations.ApiStatus;
45
import org.jetbrains.annotations.NonBlocking;
56
import org.jetbrains.annotations.Nullable;
@@ -18,12 +19,12 @@ public interface UpdateStep {
1819
* <p>
1920
* This step can be returned as the last step of the update process, usually immediately after a restart has been scheduled.
2021
*/
21-
UpdateStep EXIT = new NoopUpdateStep("Exiting...");
22+
UpdateStep EXIT = new NoopUpdateStep(Localization.get().getString("org.cryptomator.api.update.updateStep.EXIT"));
2223

2324
/**
2425
* A magic constant indicating that the update process shall be retried.
2526
*/
26-
UpdateStep RETRY = new NoopUpdateStep("Retry");
27+
UpdateStep RETRY = new NoopUpdateStep(Localization.get().getString("org.cryptomator.api.update.updateStep.RETRY"));
2728

2829

2930
static UpdateStep of(String name, Callable<UpdateStep> nextStep) {
@@ -95,13 +96,11 @@ default boolean isDone() {
9596
}
9697

9798
/**
98-
* Once the update preparation is complete, this method can be called to launch the external update process.
99-
* <p>
100-
* This method shall be called after making sure that the application is ready to be restarted, e.g. after locking all vaults.
99+
* After running this step to completion, this method returns the next step of the update process.
101100
*
102101
* @return the next {@link UpdateStep step} of the update process or <code>null</code> if this was the final step.
103-
* @throws IllegalStateException if the update preparation is not complete or if the update process cannot be launched.
104-
* @throws IOException if the update preparation failed
102+
* @throws IllegalStateException if this step didn't complete yet or other preconditions aren't met.
103+
* @throws IOException indicating an error before reaching the next step, e.g. during execution of this step.
105104
* @implSpec The returned {@link UpdateStep} must either be stateless or a new instance must be returned on each call.
106105
*/
107106
@Nullable
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
org.cryptomator.api.update.download.new=Download...
2+
org.cryptomator.api.update.download.progress=Downloading... %1.0f%%
3+
org.cryptomator.api.update.download.done=Downloaded.
4+
5+
org.cryptomator.api.update.updateStep.EXIT=Exiting...
6+
org.cryptomator.api.update.updateStep.RETRY=Retry

0 commit comments

Comments
 (0)