mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-05-14 08:41:28 +00:00
Merge pull request #3948 from purejava/feature/app-update
Implement new update API
This commit is contained in:
1
.github/workflows/mac-dmg-x64.yml
vendored
1
.github/workflows/mac-dmg-x64.yml
vendored
@@ -136,6 +136,7 @@ jobs:
|
||||
--java-options "-Dcryptomator.integrationsMac.keychainServiceName=\"Cryptomator\""
|
||||
--java-options "-Dcryptomator.mountPointsDir=\"@{userhome}/Library/Application Support/Cryptomator/mnt\""
|
||||
--java-options "-Dcryptomator.showTrayIcon=true"
|
||||
--java-options "-Dcryptomator.updateMechanism=org.cryptomator.macos.update.DmgUpdateMechanism"
|
||||
--java-options "-Dcryptomator.buildNumber=\"dmg-${{ needs.get-version.outputs.revNum }}\""
|
||||
--mac-package-identifier org.cryptomator
|
||||
--resource-dir dist/mac/resources
|
||||
|
||||
1
.github/workflows/mac-dmg.yml
vendored
1
.github/workflows/mac-dmg.yml
vendored
@@ -134,6 +134,7 @@ jobs:
|
||||
--java-options "-Dcryptomator.integrationsMac.keychainServiceName=\"Cryptomator\""
|
||||
--java-options "-Dcryptomator.mountPointsDir=\"@{userhome}/Library/Application Support/Cryptomator/mnt\""
|
||||
--java-options "-Dcryptomator.showTrayIcon=true"
|
||||
--java-options "-Dcryptomator.updateMechanism=org.cryptomator.macos.update.DmgUpdateMechanism"
|
||||
--java-options "-Dcryptomator.buildNumber=\"dmg-${{ needs.get-version.outputs.revNum }}\""
|
||||
--java-options "-XX:ErrorFile=/cryptomator/cryptomator_crash.log"
|
||||
--mac-package-identifier org.cryptomator
|
||||
|
||||
8
.idea/inspectionProfiles/Project_Default.xml
generated
8
.idea/inspectionProfiles/Project_Default.xml
generated
@@ -1,10 +1,8 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="SpellCheckingInspection" enabled="true" level="TYPO" enabled_by_default="true">
|
||||
<option name="processCode" value="true" />
|
||||
<option name="processLiterals" value="true" />
|
||||
<option name="processComments" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="Deprecation" enabled="true" level="WARNING" enabled_by_default="true" editorAttributes="DEPRECATED_ATTRIBUTES" />
|
||||
<inspection_tool class="MarkedForRemoval" enabled="true" level="WARNING" enabled_by_default="true" editorAttributes="MARKED_FOR_REMOVAL_ATTRIBUTES" />
|
||||
<inspection_tool class="RedundantScheduledForRemovalAnnotation" enabled="true" level="WARNING" enabled_by_default="true" editorAttributes="MARKED_FOR_REMOVAL_ATTRIBUTES" />
|
||||
</profile>
|
||||
</component>
|
||||
2
.idea/runConfigurations/Cryptomator_macOS.xml
generated
2
.idea/runConfigurations/Cryptomator_macOS.xml
generated
@@ -5,7 +5,7 @@
|
||||
</envs>
|
||||
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
|
||||
<module name="cryptomator" />
|
||||
<option name="VM_PARAMETERS" value="-Dapple.awt.enableTemplateImages=true -Dcryptomator.settingsPath="@{userhome}/Library/Application Support/Cryptomator/settings.json" -Dcryptomator.p12Path="@{userhome}/Library/Application Support/Cryptomator/key.p12" -Dcryptomator.ipcSocketPath="@{userhome}/Library/Application Support/Cryptomator/ipc.socket" -Dcryptomator.logDir="@{userhome}/Library/Logs/Cryptomator" -Dcryptomator.pluginDir="@{userhome}/Library/Application Support/Cryptomator/Plugins" -Dcryptomator.mountPointsDir="@{userhome}/Cryptomator" -Dcryptomator.showTrayIcon=true -Dcryptomator.integrationsMac.keychainServiceName=Cryptomator -Xss2m -Xmx512m -ea --enable-preview --enable-native-access=org.cryptomator.jfuse.mac,javafx.graphics" />
|
||||
<option name="VM_PARAMETERS" value="-Dapple.awt.enableTemplateImages=true -Dcryptomator.settingsPath="@{userhome}/Library/Application Support/Cryptomator/settings.json" -Dcryptomator.p12Path="@{userhome}/Library/Application Support/Cryptomator/key.p12" -Dcryptomator.ipcSocketPath="@{userhome}/Library/Application Support/Cryptomator/ipc.socket" -Dcryptomator.logDir="@{userhome}/Library/Logs/Cryptomator" -Dcryptomator.pluginDir="@{userhome}/Library/Application Support/Cryptomator/Plugins" -Dcryptomator.mountPointsDir="@{userhome}/Cryptomator" -Dcryptomator.showTrayIcon=true -Dcryptomator.integrationsMac.keychainServiceName=Cryptomator -Dcryptomator.updateMechanism=org.cryptomator.macos.update.DmgUpdateMechanism -Xss2m -Xmx512m -ea --enable-preview --enable-native-access=org.cryptomator.jfuse.mac,javafx.graphics" />
|
||||
<method v="2">
|
||||
<option name="Make" enabled="true" />
|
||||
</method>
|
||||
|
||||
1
dist/mac/dmg/build.sh
vendored
1
dist/mac/dmg/build.sh
vendored
@@ -123,6 +123,7 @@ ${JAVA_HOME}/bin/jpackage \
|
||||
--java-options "-Dcryptomator.integrationsMac.keychainServiceName=\"${APP_NAME}\"" \
|
||||
--java-options "-Dcryptomator.mountPointsDir=\"@{userhome}/Library/Application Support${APP_NAME}/mnt\"" \
|
||||
--java-options "-Dcryptomator.showTrayIcon=true" \
|
||||
--java-options "-Dcryptomator.updateMechanism=org.cryptomator.macos.update.DmgUpdateMechanism" \
|
||||
--java-options "-Dcryptomator.buildNumber=\"dmg-${REVISION_NO}\"" \
|
||||
--mac-package-identifier ${PACKAGE_IDENTIFIER} \
|
||||
--resource-dir ../resources
|
||||
|
||||
20
pom.xml
20
pom.xml
@@ -34,10 +34,10 @@
|
||||
|
||||
<!-- cryptomator dependencies -->
|
||||
<cryptomator.cryptofs.version>2.9.0</cryptomator.cryptofs.version>
|
||||
<cryptomator.integrations.version>1.7.0</cryptomator.integrations.version>
|
||||
<cryptomator.integrations.version>1.8.0-beta1</cryptomator.integrations.version>
|
||||
<cryptomator.integrations.win.version>1.5.1</cryptomator.integrations.win.version>
|
||||
<cryptomator.integrations.mac.version>1.4.1</cryptomator.integrations.mac.version>
|
||||
<cryptomator.integrations.linux.version>1.6.1</cryptomator.integrations.linux.version>
|
||||
<cryptomator.integrations.mac.version>1.5.0-beta1</cryptomator.integrations.mac.version>
|
||||
<cryptomator.integrations.linux.version>1.7.0-beta1</cryptomator.integrations.linux.version>
|
||||
<cryptomator.fuse.version>5.1.0</cryptomator.fuse.version>
|
||||
<cryptomator.webdav.version>3.0.0</cryptomator.webdav.version>
|
||||
|
||||
@@ -75,6 +75,20 @@
|
||||
<surefire.jacoco.args></surefire.jacoco.args>
|
||||
</properties>
|
||||
|
||||
<repositories>
|
||||
<repository>
|
||||
<name>Central Portal Snapshots</name>
|
||||
<id>central-portal-snapshots</id>
|
||||
<url>https://central.sonatype.com/repository/maven-snapshots/</url>
|
||||
<releases>
|
||||
<enabled>false</enabled>
|
||||
</releases>
|
||||
<snapshots>
|
||||
<enabled>true</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
<dependencies>
|
||||
<!-- Cryptomator Libs -->
|
||||
<dependency>
|
||||
|
||||
@@ -50,12 +50,12 @@ open module org.cryptomator.desktop {
|
||||
requires io.github.coffeelibs.tinyoauth2client;
|
||||
requires org.slf4j;
|
||||
requires org.apache.commons.lang3;
|
||||
requires com.github.benmanes.caffeine;
|
||||
|
||||
/* dagger bs */
|
||||
requires jakarta.inject;
|
||||
requires static javax.inject;
|
||||
requires java.compiler;
|
||||
requires com.github.benmanes.caffeine;
|
||||
|
||||
uses org.cryptomator.common.locationpresets.LocationPresetsProvider;
|
||||
uses SSLContextProvider;
|
||||
|
||||
@@ -74,13 +74,6 @@ public abstract class CommonsModule {
|
||||
return new MasterkeyFileAccess(Constants.PEPPER, csprng);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@Named("SemVer")
|
||||
static Comparator<String> providesSemVerComparator() {
|
||||
return new SemVerComparator();
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
static Optional<RevealPathService> provideRevealPathService() {
|
||||
|
||||
@@ -124,6 +124,15 @@ public class Environment {
|
||||
return Optional.ofNullable(System.getProperty(BUILD_NUMBER_PROP_NAME));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the app version concatenated with the build number (if defined).
|
||||
*
|
||||
* @return version string formatted like {@code 1.2.3-4567} or {@code 1.2.3} if no build number is defined.
|
||||
*/
|
||||
public String getAppVersionWithBuildNumber() {
|
||||
return getAppVersion() + getBuildNumber().map("-"::concat).orElse("");
|
||||
}
|
||||
|
||||
public Optional<Path> getPluginDir() {
|
||||
return getPath(PLUGIN_DIR_PROP_NAME);
|
||||
}
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2016, 2017 Sebastian Stenzel and others.
|
||||
* All rights reserved.
|
||||
* This program and the accompanying materials are made available under the terms of the accompanying LICENSE file.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.common;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.Comparator;
|
||||
|
||||
/**
|
||||
* Compares version strings according to <a href="http://semver.org/spec/v2.0.0.html">SemVer 2.0.0</a>.
|
||||
*/
|
||||
public class SemVerComparator implements Comparator<String> {
|
||||
|
||||
private static final char VERSION_SEP = '.'; // http://semver.org/spec/v2.0.0.html#spec-item-2
|
||||
private static final String PRE_RELEASE_SEP = "-"; // http://semver.org/spec/v2.0.0.html#spec-item-9
|
||||
private static final String BUILD_SEP = "+"; // http://semver.org/spec/v2.0.0.html#spec-item-10
|
||||
|
||||
@Override
|
||||
public int compare(String version1, String version2) {
|
||||
// "Build metadata SHOULD be ignored when determining version precedence.
|
||||
// Thus two versions that differ only in the build metadata, have the same precedence."
|
||||
String v1WithoutBuildMetadata = StringUtils.substringBefore(version1, BUILD_SEP);
|
||||
String v2WithoutBuildMetadata = StringUtils.substringBefore(version2, BUILD_SEP);
|
||||
|
||||
if (v1WithoutBuildMetadata.equals(v2WithoutBuildMetadata)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
String v1MajorMinorPatch = StringUtils.substringBefore(v1WithoutBuildMetadata, PRE_RELEASE_SEP);
|
||||
String v2MajorMinorPatch = StringUtils.substringBefore(v2WithoutBuildMetadata, PRE_RELEASE_SEP);
|
||||
String v1PreReleaseVersion = StringUtils.substringAfter(v1WithoutBuildMetadata, PRE_RELEASE_SEP);
|
||||
String v2PreReleaseVersion = StringUtils.substringAfter(v2WithoutBuildMetadata, PRE_RELEASE_SEP);
|
||||
return compare(v1MajorMinorPatch, v1PreReleaseVersion, v2MajorMinorPatch, v2PreReleaseVersion);
|
||||
}
|
||||
|
||||
private int compare(String v1MajorMinorPatch, String v1PreReleaseVersion, String v2MajorMinorPatch, String v2PreReleaseVersion) {
|
||||
int comparisonResult = compareNumericallyThenLexicographically(v1MajorMinorPatch, v2MajorMinorPatch);
|
||||
if (comparisonResult == 0) {
|
||||
if (v1PreReleaseVersion.isEmpty()) {
|
||||
return 1; // 1.0.0 > 1.0.0-BETA
|
||||
} else if (v2PreReleaseVersion.isEmpty()) {
|
||||
return -1; // 1.0.0-BETA < 1.0.0
|
||||
} else {
|
||||
return compareNumericallyThenLexicographically(v1PreReleaseVersion, v2PreReleaseVersion);
|
||||
}
|
||||
} else {
|
||||
return comparisonResult;
|
||||
}
|
||||
}
|
||||
|
||||
private int compareNumericallyThenLexicographically(String version1, String version2) {
|
||||
final String[] vComps1 = StringUtils.split(version1, VERSION_SEP);
|
||||
final String[] vComps2 = StringUtils.split(version2, VERSION_SEP);
|
||||
final int commonCompCount = Math.min(vComps1.length, vComps2.length);
|
||||
|
||||
for (int i = 0; i < commonCompCount; i++) {
|
||||
int subversionComparisonResult = 0;
|
||||
try {
|
||||
final int v1 = Integer.parseInt(vComps1[i]);
|
||||
final int v2 = Integer.parseInt(vComps2[i]);
|
||||
subversionComparisonResult = v1 - v2;
|
||||
} catch (NumberFormatException ex) {
|
||||
// ok, lets compare this fragment lexicographically
|
||||
subversionComparisonResult = vComps1[i].compareTo(vComps2[i]);
|
||||
}
|
||||
if (subversionComparisonResult != 0) {
|
||||
return subversionComparisonResult;
|
||||
}
|
||||
}
|
||||
|
||||
// all in common so far? longest version string is considered the higher version:
|
||||
return vComps1.length - vComps2.length;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -25,10 +25,8 @@ import javafx.beans.property.StringProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.NodeOrientation;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.time.Instant;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class Settings {
|
||||
|
||||
@@ -53,6 +51,7 @@ public class Settings {
|
||||
static final String DEFAULT_USER_INTERFACE_ORIENTATION = NodeOrientation.LEFT_TO_RIGHT.name();
|
||||
public static final Instant DEFAULT_TIMESTAMP = Instant.parse("2000-01-01T00:00:00Z");
|
||||
|
||||
private final SettingsProvider provider;
|
||||
public final ObservableList<VaultSettings> directories;
|
||||
public final BooleanProperty startHidden;
|
||||
public final BooleanProperty autoCloseVaults;
|
||||
@@ -78,13 +77,12 @@ public class Settings {
|
||||
public final ObjectProperty<Instant> lastUpdateCheckReminder;
|
||||
public final ObjectProperty<Instant> lastSuccessfulUpdateCheck;
|
||||
public final ObjectProperty<Path> previouslyUsedVaultDirectory;
|
||||
public final StringProperty lastUpdateAttemptedByVersion;
|
||||
|
||||
private Consumer<Settings> saveCmd;
|
||||
|
||||
public static Settings create(Environment env) {
|
||||
public static Settings create(SettingsProvider provider, Environment env) {
|
||||
var defaults = new SettingsJson();
|
||||
defaults.showTrayIcon = env.showTrayIcon();
|
||||
return new Settings(defaults);
|
||||
return new Settings(provider, defaults);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -92,7 +90,8 @@ public class Settings {
|
||||
*
|
||||
* @param json The parsed settings.json
|
||||
*/
|
||||
Settings(SettingsJson json) {
|
||||
Settings(SettingsProvider provider, SettingsJson json) {
|
||||
this.provider = provider;
|
||||
this.directories = FXCollections.observableArrayList(VaultSettings::observables);
|
||||
this.startHidden = new SimpleBooleanProperty(this, "startHidden", json.startHidden);
|
||||
this.autoCloseVaults = new SimpleBooleanProperty(this, "autoCloseVaults", json.autoCloseVaults);
|
||||
@@ -118,6 +117,7 @@ public class Settings {
|
||||
this.lastUpdateCheckReminder = new SimpleObjectProperty<>(this, "lastUpdateCheckReminder", json.lastReminderForUpdateCheck);
|
||||
this.lastSuccessfulUpdateCheck = new SimpleObjectProperty<>(this, "lastSuccessfulUpdateCheck", json.lastSuccessfulUpdateCheck);
|
||||
this.previouslyUsedVaultDirectory = new SimpleObjectProperty<>(this, "previouslyUsedVaultDirectory", json.previouslyUsedVaultDirectory);
|
||||
this.lastUpdateAttemptedByVersion = new SimpleStringProperty(this, "lastUpdateAttemptedByVersion", json.lastUpdateAttemptedByVersion);
|
||||
|
||||
this.directories.addAll(json.directories.stream().map(VaultSettings::new).toList());
|
||||
|
||||
@@ -148,6 +148,7 @@ public class Settings {
|
||||
lastUpdateCheckReminder.addListener(this::somethingChanged);
|
||||
lastSuccessfulUpdateCheck.addListener(this::somethingChanged);
|
||||
previouslyUsedVaultDirectory.addListener(this::somethingChanged);
|
||||
lastUpdateAttemptedByVersion.addListener(this::somethingChanged);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@@ -210,6 +211,7 @@ public class Settings {
|
||||
json.lastReminderForUpdateCheck = lastUpdateCheckReminder.get();
|
||||
json.lastSuccessfulUpdateCheck = lastSuccessfulUpdateCheck.get();
|
||||
json.previouslyUsedVaultDirectory = previouslyUsedVaultDirectory.get();
|
||||
json.lastUpdateAttemptedByVersion = lastUpdateAttemptedByVersion.get();
|
||||
return json;
|
||||
}
|
||||
|
||||
@@ -222,20 +224,12 @@ public class Settings {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// TODO rename to setChangeListener
|
||||
void setSaveCmd(Consumer<Settings> saveCmd) {
|
||||
this.saveCmd = saveCmd;
|
||||
}
|
||||
|
||||
private void somethingChanged(@SuppressWarnings("unused") Observable observable) {
|
||||
this.save();
|
||||
provider.scheduleSave(this);
|
||||
}
|
||||
|
||||
void save() {
|
||||
if (saveCmd != null) {
|
||||
saveCmd.accept(this);
|
||||
}
|
||||
public void saveNow() {
|
||||
provider.saveNow(this);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -96,4 +96,7 @@ class SettingsJson {
|
||||
|
||||
@JsonProperty("previouslyUsedVaultDirectory")
|
||||
Path previouslyUsedVaultDirectory;
|
||||
|
||||
@JsonProperty("lastUpdateAttemptedByVersion")
|
||||
String lastUpdateAttemptedByVersion;
|
||||
}
|
||||
|
||||
@@ -26,7 +26,9 @@ import java.nio.file.NoSuchFileException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@@ -61,8 +63,7 @@ public class SettingsProvider implements Supplier<Settings> {
|
||||
Settings settings = env.getSettingsPath() //
|
||||
.flatMap(this::tryLoad) //
|
||||
.findFirst() //
|
||||
.orElseGet(() -> Settings.create(env));
|
||||
settings.setSaveCmd(this::scheduleSave);
|
||||
.orElseGet(() -> Settings.create(this, env));
|
||||
return settings;
|
||||
}
|
||||
|
||||
@@ -71,7 +72,7 @@ public class SettingsProvider implements Supplier<Settings> {
|
||||
try (InputStream in = Files.newInputStream(path, StandardOpenOption.READ)) {
|
||||
var json = JSON.reader().readValue(in, SettingsJson.class);
|
||||
LOG.info("Settings loaded from {}", path);
|
||||
var settings = new Settings(json);
|
||||
var settings = new Settings(this, json);
|
||||
return Stream.of(settings);
|
||||
} catch (JacksonException e) {
|
||||
LOG.warn("Failed to parse json file {}", path, e);
|
||||
@@ -84,19 +85,33 @@ public class SettingsProvider implements Supplier<Settings> {
|
||||
}
|
||||
}
|
||||
|
||||
private void scheduleSave(Settings settings) {
|
||||
if (settings == null) {
|
||||
return;
|
||||
void saveNow(Settings settings) {
|
||||
try {
|
||||
scheduleSave(settings, 0L).get();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
LOG.error("Saving settings was interrupted.", e);
|
||||
} catch (ExecutionException e) {
|
||||
LOG.error("Unexpected exception while saving.", e);
|
||||
}
|
||||
final Optional<Path> settingsPath = env.getSettingsPath().findFirst(); // always save to preferred (first) path
|
||||
settingsPath.ifPresent(path -> {
|
||||
Runnable saveCommand = () -> this.save(settings, path);
|
||||
ScheduledFuture<?> scheduledTask = scheduler.schedule(saveCommand, SAVE_DELAY_MS, TimeUnit.MILLISECONDS);
|
||||
ScheduledFuture<?> previouslyScheduledTask = scheduledSaveCmd.getAndSet(scheduledTask);
|
||||
if (previouslyScheduledTask != null) {
|
||||
previouslyScheduledTask.cancel(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void scheduleSave(Settings settings) {
|
||||
scheduleSave(settings, SAVE_DELAY_MS);
|
||||
}
|
||||
|
||||
private Future<?> scheduleSave(Settings settings, long delayMillis) {
|
||||
if (settings == null) {
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
final Path settingsPath = env.getSettingsPath().findFirst().orElseThrow(); // always save to preferred (first) path
|
||||
Runnable saveCommand = () -> this.save(settings, settingsPath);
|
||||
ScheduledFuture<?> scheduledTask = scheduler.schedule(saveCommand, delayMillis, TimeUnit.MILLISECONDS);
|
||||
ScheduledFuture<?> previouslyScheduledTask = scheduledSaveCmd.getAndSet(scheduledTask);
|
||||
if (previouslyScheduledTask != null) {
|
||||
previouslyScheduledTask.cancel(false);
|
||||
}
|
||||
return scheduledTask;
|
||||
}
|
||||
|
||||
private void save(Settings settings, Path settingsPath) {
|
||||
@@ -107,7 +122,7 @@ public class SettingsProvider implements Supplier<Settings> {
|
||||
Path tmpPath = settingsPath.resolveSibling(settingsPath.getFileName().toString() + ".tmp");
|
||||
try (OutputStream out = Files.newOutputStream(tmpPath, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE)) {
|
||||
var jsonObj = settings.serialized();
|
||||
jsonObj.writtenByVersion = env.getAppVersion() + env.getBuildNumber().map("-"::concat).orElse("");
|
||||
jsonObj.writtenByVersion = env.getAppVersionWithBuildNumber();
|
||||
JSON.writerWithDefaultPrettyPrinter().writeValue(out, jsonObj);
|
||||
}
|
||||
Files.move(tmpPath, settingsPath, StandardCopyOption.REPLACE_EXISTING);
|
||||
|
||||
@@ -26,7 +26,7 @@ import javafx.scene.image.Image;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
@Module(includes = {UpdateCheckerModule.class}, subcomponents = {TrayMenuComponent.class, //
|
||||
@Module(subcomponents = {TrayMenuComponent.class, //
|
||||
DecryptNameComponent.class, //
|
||||
MainWindowComponent.class, //
|
||||
PreferencesComponent.class, //
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
package org.cryptomator.ui.fxapp;
|
||||
|
||||
import org.cryptomator.common.Environment;
|
||||
import org.cryptomator.common.SemVerComparator;
|
||||
import org.cryptomator.common.settings.Settings;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyStringProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.concurrent.ScheduledService;
|
||||
import javafx.concurrent.Worker;
|
||||
import javafx.concurrent.WorkerStateEvent;
|
||||
import javafx.util.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Comparator;
|
||||
|
||||
@FxApplicationScoped
|
||||
public class UpdateChecker {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(UpdateChecker.class);
|
||||
private static final Duration AUTO_CHECK_DELAY = Duration.seconds(5);
|
||||
|
||||
private final Environment env;
|
||||
private final Settings settings;
|
||||
private final StringProperty latestVersion = new SimpleStringProperty();
|
||||
private final ScheduledService<String> updateCheckerService;
|
||||
private final ObjectProperty<UpdateCheckState> state = new SimpleObjectProperty<>(UpdateCheckState.NOT_CHECKED);
|
||||
private final ObjectProperty<Instant> lastSuccessfulUpdateCheck;
|
||||
private final Comparator<String> versionComparator = new SemVerComparator();
|
||||
private final BooleanBinding updateAvailable;
|
||||
private final BooleanBinding checkFailed;
|
||||
|
||||
@Inject
|
||||
UpdateChecker(Settings settings, //
|
||||
Environment env, //
|
||||
ScheduledService<String> updateCheckerService) {
|
||||
this.env = env;
|
||||
this.settings = settings;
|
||||
this.updateCheckerService = updateCheckerService;
|
||||
this.lastSuccessfulUpdateCheck = settings.lastSuccessfulUpdateCheck;
|
||||
this.updateAvailable = Bindings.createBooleanBinding(this::isUpdateAvailable, latestVersion);
|
||||
this.checkFailed = Bindings.equal(UpdateCheckState.CHECK_FAILED, state);
|
||||
}
|
||||
|
||||
public void automaticallyCheckForUpdatesIfEnabled() {
|
||||
if (!env.disableUpdateCheck() && settings.checkForUpdates.get()) {
|
||||
startCheckingForUpdates(AUTO_CHECK_DELAY);
|
||||
}
|
||||
}
|
||||
|
||||
public void checkForUpdatesNow() {
|
||||
startCheckingForUpdates(Duration.ZERO);
|
||||
}
|
||||
|
||||
private void startCheckingForUpdates(Duration initialDelay) {
|
||||
updateCheckerService.cancel();
|
||||
updateCheckerService.reset();
|
||||
updateCheckerService.setDelay(initialDelay);
|
||||
updateCheckerService.setOnRunning(this::checkStarted);
|
||||
updateCheckerService.setOnSucceeded(this::checkSucceeded);
|
||||
updateCheckerService.setOnFailed(this::checkFailed);
|
||||
updateCheckerService.start();
|
||||
}
|
||||
|
||||
private void checkStarted(WorkerStateEvent event) {
|
||||
LOG.debug("Checking for updates...");
|
||||
state.set(UpdateCheckState.IS_CHECKING);
|
||||
}
|
||||
|
||||
private void checkSucceeded(WorkerStateEvent event) {
|
||||
var latestVersionString = updateCheckerService.getValue();
|
||||
LOG.info("Current version: {}, latest version: {}", getCurrentVersion(), latestVersionString);
|
||||
lastSuccessfulUpdateCheck.set(Instant.now());
|
||||
latestVersion.set(latestVersionString);
|
||||
state.set(UpdateCheckState.CHECK_SUCCESSFUL);
|
||||
}
|
||||
|
||||
private void checkFailed(WorkerStateEvent event) {
|
||||
state.set(UpdateCheckState.CHECK_FAILED);
|
||||
}
|
||||
|
||||
public enum UpdateCheckState {
|
||||
NOT_CHECKED,
|
||||
IS_CHECKING,
|
||||
CHECK_SUCCESSFUL,
|
||||
CHECK_FAILED;
|
||||
}
|
||||
|
||||
/* Observable Properties */
|
||||
public BooleanBinding checkingForUpdatesProperty() {
|
||||
return updateCheckerService.stateProperty().isEqualTo(Worker.State.RUNNING);
|
||||
}
|
||||
|
||||
public ReadOnlyStringProperty latestVersionProperty() {
|
||||
return latestVersion;
|
||||
}
|
||||
|
||||
public BooleanBinding updateAvailableProperty() {
|
||||
return updateAvailable;
|
||||
}
|
||||
|
||||
public BooleanBinding checkFailedProperty() {
|
||||
return checkFailed;
|
||||
}
|
||||
|
||||
public boolean isUpdateAvailable() {
|
||||
String currentVersion = getCurrentVersion();
|
||||
String latestVersionString = latestVersion.get();
|
||||
|
||||
if (currentVersion == null || latestVersionString == null) {
|
||||
return false;
|
||||
} else {
|
||||
return versionComparator.compare(currentVersion, latestVersionString) < 0;
|
||||
}
|
||||
}
|
||||
|
||||
public ObjectProperty<Instant> lastSuccessfulUpdateCheckProperty() {
|
||||
return lastSuccessfulUpdateCheck;
|
||||
}
|
||||
|
||||
public ObjectProperty<UpdateCheckState> updateCheckStateProperty() {
|
||||
return state;
|
||||
}
|
||||
|
||||
public String getCurrentVersion() {
|
||||
return env.getAppVersion();
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
package org.cryptomator.ui.fxapp;
|
||||
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.common.Environment;
|
||||
import org.cryptomator.common.settings.Settings;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Named;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
import javafx.concurrent.ScheduledService;
|
||||
import javafx.concurrent.Task;
|
||||
import javafx.util.Duration;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
|
||||
@Module
|
||||
public abstract class UpdateCheckerModule {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(UpdateCheckerModule.class);
|
||||
|
||||
private static final URI LATEST_VERSION_URI = URI.create("https://api.cryptomator.org/desktop/latest-version.json");
|
||||
private static final Duration UPDATE_CHECK_INTERVAL = Duration.hours(3);
|
||||
private static final Duration DISABLED_UPDATE_CHECK_INTERVAL = Duration.hours(100000); // Duration.INDEFINITE leads to overflows...
|
||||
|
||||
@Provides
|
||||
@FxApplicationScoped
|
||||
static Optional<HttpClient> provideHttpClient() {
|
||||
try {
|
||||
return Optional.of(HttpClient.newBuilder() //
|
||||
.followRedirects(HttpClient.Redirect.NORMAL) // from version 1.6.11 onwards, Cryptomator can follow redirects, in case this URL ever changes
|
||||
.build());
|
||||
} catch (UncheckedIOException e) {
|
||||
LOG.error("HttpClient for update check cannot be created.", e);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FxApplicationScoped
|
||||
static HttpRequest provideCheckForUpdatesRequest(Environment env) {
|
||||
String userAgent = String.format("Cryptomator VersionChecker/%s %s %s (%s)", //
|
||||
env.getAppVersion(), //
|
||||
SystemUtils.OS_NAME, //
|
||||
SystemUtils.OS_VERSION, //
|
||||
SystemUtils.OS_ARCH); //
|
||||
return HttpRequest.newBuilder() //
|
||||
.uri(LATEST_VERSION_URI) //
|
||||
.header("User-Agent", userAgent) //
|
||||
.timeout(java.time.Duration.ofSeconds(10))
|
||||
.build();
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Named("checkForUpdatesInterval")
|
||||
@FxApplicationScoped
|
||||
static ObjectBinding<Duration> provideCheckForUpdateInterval(Settings settings) {
|
||||
return Bindings.when(settings.checkForUpdates).then(UPDATE_CHECK_INTERVAL).otherwise(DISABLED_UPDATE_CHECK_INTERVAL);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FxApplicationScoped
|
||||
static ScheduledService<String> provideCheckForUpdatesService(ExecutorService executor, Optional<HttpClient> httpClient, HttpRequest checkForUpdatesRequest, @Named("checkForUpdatesInterval") ObjectBinding<Duration> period) {
|
||||
ScheduledService<String> service = new ScheduledService<>() {
|
||||
@Override
|
||||
protected Task<String> createTask() {
|
||||
if (httpClient.isPresent()) {
|
||||
return new UpdateCheckerTask(httpClient.get(), checkForUpdatesRequest);
|
||||
} else {
|
||||
return new Task<>() {
|
||||
@Override
|
||||
protected String call() {
|
||||
throw new NullPointerException("No HttpClient present.");
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
service.setOnFailed(event -> LOG.error("Failed to execute update service", service.getException()));
|
||||
service.setExecutor(executor);
|
||||
service.periodProperty().bind(period);
|
||||
return service;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
package org.cryptomator.ui.fxapp;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.google.common.io.ByteStreams;
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javafx.concurrent.Task;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
|
||||
public class UpdateCheckerTask extends Task<String> {
|
||||
|
||||
private static final ObjectMapper JSON = new ObjectMapper();
|
||||
private static final Logger LOG = LoggerFactory.getLogger(UpdateCheckerTask.class);
|
||||
|
||||
private static final long MAX_RESPONSE_SIZE = 10L * 1024; // 10kb should be sufficient. protect against flooding
|
||||
|
||||
private final HttpClient httpClient;
|
||||
private final HttpRequest checkForUpdatesRequest;
|
||||
|
||||
UpdateCheckerTask(HttpClient httpClient, HttpRequest checkForUpdatesRequest) {
|
||||
this.httpClient = httpClient;
|
||||
this.checkForUpdatesRequest = checkForUpdatesRequest;
|
||||
|
||||
setOnFailed(event -> LOG.error("Failed to check for updates", getException()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String call() throws IOException, InterruptedException {
|
||||
HttpResponse<InputStream> response = httpClient.send(checkForUpdatesRequest, HttpResponse.BodyHandlers.ofInputStream());
|
||||
if (response.statusCode() == 200) {
|
||||
return processBody(response);
|
||||
} else {
|
||||
throw new IOException("Unexpected HTTP response code " + response.statusCode());
|
||||
}
|
||||
}
|
||||
|
||||
private String processBody(HttpResponse<InputStream> response) throws IOException {
|
||||
try (InputStream in = response.body(); //
|
||||
InputStream limitedIn = ByteStreams.limit(in, MAX_RESPONSE_SIZE)) {
|
||||
var json = JSON.reader().readTree(limitedIn);
|
||||
if (SystemUtils.IS_OS_MAC_OSX) {
|
||||
return json.get("mac").asText();
|
||||
} else if (SystemUtils.IS_OS_WINDOWS) {
|
||||
return json.get("win").asText();
|
||||
} else if (SystemUtils.IS_OS_LINUX) {
|
||||
return json.get("linux").asText();
|
||||
} else {
|
||||
throw new IllegalStateException("Unsupported operating system");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.common.vaults.VaultListManager;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.fxapp.FxApplicationWindows;
|
||||
import org.cryptomator.ui.fxapp.UpdateChecker;
|
||||
import org.cryptomator.updater.UpdateChecker;
|
||||
import org.cryptomator.ui.preferences.SelectedPreferencesTab;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -3,7 +3,7 @@ package org.cryptomator.ui.preferences;
|
||||
import com.google.common.io.CharStreams;
|
||||
import org.cryptomator.common.Environment;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.fxapp.UpdateChecker;
|
||||
import org.cryptomator.updater.UpdateChecker;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ package org.cryptomator.ui.preferences;
|
||||
|
||||
import org.cryptomator.common.Environment;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.fxapp.UpdateChecker;
|
||||
import org.cryptomator.updater.UpdateChecker;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
|
||||
@@ -2,25 +2,37 @@ package org.cryptomator.ui.preferences;
|
||||
|
||||
import org.cryptomator.common.Environment;
|
||||
import org.cryptomator.common.settings.Settings;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.integrations.update.UpdateStep;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.fxapp.UpdateChecker;
|
||||
import org.cryptomator.ui.common.VaultService;
|
||||
import org.cryptomator.updater.UpdateChecker;
|
||||
import org.cryptomator.updater.FallbackUpdateInfo;
|
||||
import org.cryptomator.updater.UpdateService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.animation.PauseTransition;
|
||||
import javafx.application.Application;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.binding.BooleanExpression;
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
import javafx.beans.binding.StringBinding;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ReadOnlyStringProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.concurrent.Worker;
|
||||
import javafx.concurrent.WorkerStateEvent;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.CheckBox;
|
||||
import javafx.scene.control.ContentDisplay;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
@@ -34,73 +46,64 @@ import java.util.ResourceBundle;
|
||||
@PreferencesScoped
|
||||
public class UpdatesPreferencesController implements FxController {
|
||||
|
||||
private static final String DOWNLOADS_URI_TEMPLATE = "https://cryptomator.org/downloads/" //
|
||||
+ "?utm_source=cryptomator-desktop" //
|
||||
+ "&utm_medium=update-notification&" //
|
||||
+ "utm_campaign=app-update-%s";
|
||||
private static final Logger LOG = LoggerFactory.getLogger(UpdatesPreferencesController.class);
|
||||
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).withLocale(Locale.getDefault());
|
||||
|
||||
private final Application application;
|
||||
private final Environment environment;
|
||||
private final ResourceBundle resourceBundle;
|
||||
private final Settings settings;
|
||||
private final UpdateChecker updateChecker;
|
||||
private final ObjectBinding<ContentDisplay> checkForUpdatesButtonState;
|
||||
private final ReadOnlyStringProperty latestVersion;
|
||||
private final ObservableValue<Instant> lastSuccessfulUpdateCheck;
|
||||
private final StringBinding lastUpdateCheckMessage;
|
||||
private final UpdateService updateService;
|
||||
private final ObservableList<Vault> unlockedVaults;
|
||||
private final VaultService vaultService;
|
||||
private final ObjectBinding<Worker<?>> worker;
|
||||
private final BooleanExpression running;
|
||||
private final StringBinding updateButtonTitle;
|
||||
private final ObjectBinding<ContentDisplay> updateButtonState;
|
||||
private final ObservableValue<String> timeDifferenceMessage;
|
||||
private final String currentVersion;
|
||||
private final BooleanBinding updateAvailable;
|
||||
private final BooleanBinding checkFailed;
|
||||
private final StringBinding lastUpdateCheckMessage;
|
||||
private final BooleanBinding prohibitUpdateWhileUnlocked;
|
||||
private final BooleanBinding updateButtonDisabled;
|
||||
private final StringProperty errorMessage = new SimpleStringProperty("");
|
||||
private final BooleanProperty upToDateLabelVisible = new SimpleBooleanProperty(false);
|
||||
private final DateTimeFormatter formatter;
|
||||
private final BooleanBinding upToDate;
|
||||
private final String downloadsUri;
|
||||
|
||||
/* FXML */
|
||||
public CheckBox checkForUpdatesCheckbox;
|
||||
|
||||
@Inject
|
||||
UpdatesPreferencesController(Application application, Environment environment, ResourceBundle resourceBundle, Settings settings, UpdateChecker updateChecker) {
|
||||
UpdatesPreferencesController(Application application, Environment environment, ResourceBundle resourceBundle, Settings settings, UpdateChecker updateChecker, ObservableList<Vault> vaults, VaultService vaultService) {
|
||||
this.application = application;
|
||||
this.environment = environment;
|
||||
this.resourceBundle = resourceBundle;
|
||||
this.settings = settings;
|
||||
this.updateChecker = updateChecker;
|
||||
this.checkForUpdatesButtonState = Bindings.when(updateChecker.checkingForUpdatesProperty()).then(ContentDisplay.LEFT).otherwise(ContentDisplay.TEXT_ONLY);
|
||||
this.latestVersion = updateChecker.latestVersionProperty();
|
||||
this.lastSuccessfulUpdateCheck = updateChecker.lastSuccessfulUpdateCheckProperty();
|
||||
this.timeDifferenceMessage = Bindings.createStringBinding(this::getTimeDifferenceMessage, lastSuccessfulUpdateCheck);
|
||||
this.currentVersion = environment.getAppVersion();
|
||||
this.updateAvailable = updateChecker.updateAvailableProperty();
|
||||
this.formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).withLocale(Locale.getDefault());
|
||||
this.upToDate = updateChecker.updateCheckStateProperty().isEqualTo(UpdateChecker.UpdateCheckState.CHECK_SUCCESSFUL).and(latestVersion.isEqualTo(currentVersion));
|
||||
this.checkFailed = updateChecker.checkFailedProperty();
|
||||
this.lastUpdateCheckMessage = Bindings.createStringBinding(this::getLastUpdateCheckMessage, lastSuccessfulUpdateCheck);
|
||||
this.downloadsUri = DOWNLOADS_URI_TEMPLATE.formatted(URLEncoder.encode(currentVersion, StandardCharsets.US_ASCII));
|
||||
this.updateService = new UpdateService(updateChecker.updateProperty());
|
||||
this.unlockedVaults = vaults.filtered(Vault::isUnlocked);
|
||||
this.vaultService = vaultService;
|
||||
this.worker = Bindings.when(updateChecker.updateAvailableProperty()).<Worker<?>>then(this.updateService).otherwise(this.updateChecker);
|
||||
this.running = Bindings.createBooleanBinding(this::isRunning, updateService.stateProperty(), updateChecker.stateProperty());
|
||||
this.updateButtonTitle = Bindings.createStringBinding(this::getUpdateButtonTitle, worker, updateService.stateProperty(), updateService.messageProperty());
|
||||
this.updateButtonState = Bindings.createObjectBinding(this::getUpdateButtonState, updateChecker.stateProperty(), updateService.stateProperty());
|
||||
this.timeDifferenceMessage = Bindings.createStringBinding(this::getTimeDifferenceMessage, updateChecker.lastSuccessfulUpdateCheckProperty());
|
||||
this.lastUpdateCheckMessage = Bindings.createStringBinding(this::getLastUpdateCheckMessage, updateChecker.lastSuccessfulUpdateCheckProperty());
|
||||
this.prohibitUpdateWhileUnlocked = Bindings.createBooleanBinding(this::isProhibitUpdateWhileUnlocked, unlockedVaults, updateChecker.updateProperty());
|
||||
this.updateButtonDisabled = Bindings.when(worker.isEqualTo(updateChecker)).then(running).otherwise(prohibitUpdateWhileUnlocked.or(running));
|
||||
}
|
||||
|
||||
public void initialize() {
|
||||
checkForUpdatesCheckbox.selectedProperty().bindBidirectional(settings.checkForUpdates);
|
||||
|
||||
upToDate.addListener((_, _, newVal) -> {
|
||||
if (newVal) {
|
||||
updateChecker.updateAvailableProperty().addListener((_, _, hasUpdate) -> {
|
||||
if (!hasUpdate) {
|
||||
upToDateLabelVisible.set(true);
|
||||
PauseTransition delay = new PauseTransition(javafx.util.Duration.seconds(5));
|
||||
delay.setOnFinished(_ -> upToDateLabelVisible.set(false));
|
||||
delay.play();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void checkNow() {
|
||||
updateChecker.checkForUpdatesNow();
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void visitDownloadsPage() {
|
||||
application.getHostServices().showDocument(downloadsUri);
|
||||
updateChecker.setOnFailed(this::checkFailed);
|
||||
updateService.setOnSucceeded(this::updateSucceeded);
|
||||
updateService.setOnFailed(this::updateFailed);
|
||||
}
|
||||
|
||||
@FXML
|
||||
@@ -108,38 +111,104 @@ public class UpdatesPreferencesController implements FxController {
|
||||
environment.getLogDir().ifPresent(logDirPath -> application.getHostServices().showDocument(logDirPath.toUri().toString()));
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void startWork() {
|
||||
if (worker.get().equals(updateChecker)) {
|
||||
updateChecker.checkForUpdatesNow();
|
||||
} else if (!unlockedVaults.isEmpty()) {
|
||||
LOG.warn("Cannot start update due to unlocked vaults.");
|
||||
} else if (worker.get().equals(updateService)) {
|
||||
LOG.info("User started update to version {}", updateChecker.getUpdate().version());
|
||||
updateService.start();
|
||||
}
|
||||
}
|
||||
|
||||
private void checkFailed(WorkerStateEvent workerStateEvent) {
|
||||
assert workerStateEvent.getSource() == updateChecker;
|
||||
LOG.error("Update check failed.", updateChecker.getException());
|
||||
errorMessage.set(resourceBundle.getString("preferences.updates.checkFailed"));
|
||||
}
|
||||
|
||||
private void updateSucceeded(WorkerStateEvent workerStateEvent) {
|
||||
assert workerStateEvent.getSource() == updateService;
|
||||
var lastStep = updateService.getValue();
|
||||
if (lastStep == UpdateStep.EXIT) {
|
||||
// Record that this version attempted an update, so next launch can choose fallback if needed
|
||||
settings.lastUpdateAttemptedByVersion.set(environment.getAppVersionWithBuildNumber());
|
||||
settings.saveNow();
|
||||
LOG.info("Exiting app to update...");
|
||||
Platform.exit();
|
||||
} else if (lastStep == UpdateStep.RETRY) {
|
||||
updateService.reset();
|
||||
} else {
|
||||
LOG.info("Update succeeded.");
|
||||
}
|
||||
}
|
||||
|
||||
private void updateFailed(WorkerStateEvent workerStateEvent) {
|
||||
assert workerStateEvent.getSource() == updateService;
|
||||
LOG.error("Update failed.", updateService.getException());
|
||||
updateService.reset();
|
||||
errorMessage.set(resourceBundle.getString("preferences.updates.updateFailed"));
|
||||
// try fallback mechanism:
|
||||
updateChecker.recheckWithFallbackMechanism();
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void lockAllGracefully() {
|
||||
vaultService.lockAll(unlockedVaults, false);
|
||||
}
|
||||
|
||||
/* Observable Properties */
|
||||
|
||||
public ObjectBinding<ContentDisplay> checkForUpdatesButtonStateProperty() {
|
||||
return checkForUpdatesButtonState;
|
||||
public UpdateChecker getUpdateChecker() {
|
||||
return updateChecker;
|
||||
}
|
||||
|
||||
public ContentDisplay getCheckForUpdatesButtonState() {
|
||||
return checkForUpdatesButtonState.get();
|
||||
public ObjectBinding<Worker<?>> workerProperty() {
|
||||
return worker;
|
||||
}
|
||||
|
||||
public ReadOnlyStringProperty latestVersionProperty() {
|
||||
return latestVersion;
|
||||
public Worker<?> getWorker() {
|
||||
return worker.get();
|
||||
}
|
||||
|
||||
public String getLatestVersion() {
|
||||
return latestVersion.get();
|
||||
public BooleanExpression runningProperty() {
|
||||
return running;
|
||||
}
|
||||
|
||||
public String getCurrentVersion() {
|
||||
return currentVersion;
|
||||
public boolean isRunning() {
|
||||
return updateChecker.getState() == Worker.State.RUNNING || updateService.getState() == Worker.State.RUNNING;
|
||||
}
|
||||
|
||||
public StringBinding lastUpdateCheckMessageProperty() {
|
||||
return lastUpdateCheckMessage;
|
||||
public StringBinding updateButtonTitleProperty() {
|
||||
return updateButtonTitle;
|
||||
}
|
||||
|
||||
public String getLastUpdateCheckMessage() {
|
||||
Instant lastCheck = lastSuccessfulUpdateCheck.getValue();
|
||||
if (lastCheck != null && !lastCheck.equals(Settings.DEFAULT_TIMESTAMP)) {
|
||||
return formatter.format(LocalDateTime.ofInstant(lastCheck, ZoneId.systemDefault()));
|
||||
public String getUpdateButtonTitle() {
|
||||
if (worker.get() == updateChecker) {
|
||||
return resourceBundle.getString("preferences.updates.checkNowBtn");
|
||||
} else {
|
||||
return "-";
|
||||
return switch (updateService.getState()) {
|
||||
case READY -> updateChecker.getUpdate().updateMechanism().getName();
|
||||
case SCHEDULED, RUNNING -> updateService.getMessage();
|
||||
case SUCCEEDED -> resourceBundle.getString("generic.button.done");
|
||||
case FAILED, CANCELLED -> "failed"; // should never be visible
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public ObjectBinding<ContentDisplay> updateButtonStateProperty() {
|
||||
return updateButtonState;
|
||||
}
|
||||
|
||||
public ContentDisplay getUpdateButtonState() {
|
||||
if (updateService.isRunning()) { // isRunning() covers RUNNING and SCHEDULED states
|
||||
return ContentDisplay.BOTTOM;
|
||||
} else if (updateChecker.getState() == Worker.State.RUNNING) {
|
||||
return ContentDisplay.LEFT;
|
||||
} else {
|
||||
return ContentDisplay.TEXT_ONLY;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,7 +217,7 @@ public class UpdatesPreferencesController implements FxController {
|
||||
}
|
||||
|
||||
public String getTimeDifferenceMessage() {
|
||||
var lastSuccessCheck = lastSuccessfulUpdateCheck.getValue();
|
||||
var lastSuccessCheck = updateChecker.getLastSuccessfulUpdateCheck();
|
||||
var duration = Duration.between(lastSuccessCheck, Instant.now());
|
||||
var hours = duration.toHours();
|
||||
if (lastSuccessCheck.equals(Settings.DEFAULT_TIMESTAMP)) {
|
||||
@@ -162,6 +231,44 @@ public class UpdatesPreferencesController implements FxController {
|
||||
}
|
||||
}
|
||||
|
||||
public StringBinding lastUpdateCheckMessageProperty() {
|
||||
return lastUpdateCheckMessage;
|
||||
}
|
||||
|
||||
public String getLastUpdateCheckMessage() {
|
||||
Instant lastCheck = updateChecker.getLastSuccessfulUpdateCheck();
|
||||
if (lastCheck != null && !lastCheck.equals(Settings.DEFAULT_TIMESTAMP)) {
|
||||
return FORMATTER.format(LocalDateTime.ofInstant(lastCheck, ZoneId.systemDefault()));
|
||||
} else {
|
||||
return "-";
|
||||
}
|
||||
}
|
||||
|
||||
public String getErrorMessage() {
|
||||
return errorMessage.get();
|
||||
}
|
||||
|
||||
public ReadOnlyStringProperty errorMessageProperty() {
|
||||
return errorMessage;
|
||||
}
|
||||
|
||||
public boolean isProhibitUpdateWhileUnlocked() {
|
||||
// If the result of the last update check was from the fallback mechanism, we don't need to show the warning
|
||||
return !unlockedVaults.isEmpty() && !FallbackUpdateInfo.class.isInstance(updateChecker.getUpdate());
|
||||
}
|
||||
|
||||
public BooleanBinding prohibitUpdateWhileUnlockedProperty() {
|
||||
return prohibitUpdateWhileUnlocked;
|
||||
}
|
||||
|
||||
public boolean isUpdateButtonDisabled() {
|
||||
return updateButtonDisabled.get();
|
||||
}
|
||||
|
||||
public BooleanBinding updateButtonDisabledProperty() {
|
||||
return updateButtonDisabled;
|
||||
}
|
||||
|
||||
public BooleanProperty upToDateLabelVisibleProperty() {
|
||||
return upToDateLabelVisible;
|
||||
}
|
||||
@@ -170,20 +277,4 @@ public class UpdatesPreferencesController implements FxController {
|
||||
return upToDateLabelVisible.get();
|
||||
}
|
||||
|
||||
public BooleanBinding updateAvailableProperty() {
|
||||
return updateAvailable;
|
||||
}
|
||||
|
||||
public boolean isUpdateAvailable() {
|
||||
return updateAvailable.get();
|
||||
}
|
||||
|
||||
public BooleanBinding checkFailedProperty() {
|
||||
return checkFailed;
|
||||
}
|
||||
|
||||
public boolean isCheckFailed() {
|
||||
return checkFailed.getValue();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package org.cryptomator.ui.updatereminder;
|
||||
|
||||
import org.cryptomator.common.settings.Settings;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.fxapp.UpdateChecker;
|
||||
import org.cryptomator.updater.UpdateChecker;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.fxml.FXML;
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
package org.cryptomator.updater;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLParameters;
|
||||
import java.io.IOException;
|
||||
import java.net.Authenticator;
|
||||
import java.net.CookieHandler;
|
||||
import java.net.ProxySelector;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.time.Duration;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
abstract class DelegatingHttpClient extends HttpClient {
|
||||
|
||||
private final HttpClient delegate;
|
||||
|
||||
public DelegatingHttpClient(HttpClient delegate) {
|
||||
this.delegate = Objects.requireNonNull(delegate, "delegate must not be null");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<CookieHandler> cookieHandler() {
|
||||
return delegate.cookieHandler();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Duration> connectTimeout() {
|
||||
return delegate.connectTimeout();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Redirect followRedirects() {
|
||||
return delegate.followRedirects();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<ProxySelector> proxy() {
|
||||
return delegate.proxy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public SSLContext sslContext() {
|
||||
return delegate.sslContext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public SSLParameters sslParameters() {
|
||||
return delegate.sslParameters();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Authenticator> authenticator() {
|
||||
return delegate.authenticator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Version version() {
|
||||
return delegate.version();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Executor> executor() {
|
||||
return delegate.executor();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> HttpResponse<T> send(HttpRequest request, HttpResponse.BodyHandler<T> responseBodyHandler) throws IOException, InterruptedException {
|
||||
return delegate.send(request, responseBodyHandler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> CompletableFuture<HttpResponse<T>> sendAsync(HttpRequest request, HttpResponse.BodyHandler<T> responseBodyHandler) {
|
||||
return delegate.sendAsync(request, responseBodyHandler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> CompletableFuture<HttpResponse<T>> sendAsync(HttpRequest request, HttpResponse.BodyHandler<T> responseBodyHandler, HttpResponse.PushPromiseHandler<T> pushPromiseHandler) {
|
||||
return delegate.sendAsync(request, responseBodyHandler, pushPromiseHandler);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package org.cryptomator.updater;
|
||||
|
||||
import org.cryptomator.integrations.update.UpdateInfo;
|
||||
import org.cryptomator.integrations.update.UpdateMechanism;
|
||||
|
||||
public record FallbackUpdateInfo(String version, UpdateMechanism<FallbackUpdateInfo> updateMechanism) implements UpdateInfo<FallbackUpdateInfo> {}
|
||||
@@ -0,0 +1,105 @@
|
||||
package org.cryptomator.updater;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.common.Environment;
|
||||
import org.cryptomator.integrations.common.LocalizedDisplayName;
|
||||
import org.cryptomator.integrations.update.UpdateMechanism;
|
||||
import org.cryptomator.integrations.update.UpdateStep;
|
||||
import org.cryptomator.ui.fxapp.FxApplicationScoped;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.application.Application;
|
||||
import javafx.application.Platform;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.net.URLEncoder;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
@FxApplicationScoped
|
||||
@LocalizedDisplayName(bundle = "i18n.strings", key = "preferences.updates.visitDownloadPage")
|
||||
public class FallbackUpdateMechanism implements UpdateMechanism<FallbackUpdateInfo> {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(FallbackUpdateMechanism.class);
|
||||
private static final String LATEST_VERSION_API_URL = "https://api.cryptomator.org/connect/apps/desktop/latest-version";
|
||||
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||
private static final String DOWNLOADS_URI_TEMPLATE = "https://cryptomator.org/downloads/" //
|
||||
+ "?utm_source=cryptomator-desktop" //
|
||||
+ "&utm_medium=update-notification&" //
|
||||
+ "utm_campaign=app-update-%s";
|
||||
|
||||
private final Application app;
|
||||
private final Environment env;
|
||||
|
||||
@Inject
|
||||
public FallbackUpdateMechanism(Application app, Environment env) {
|
||||
this.app = app;
|
||||
this.env = env;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FallbackUpdateInfo 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) {
|
||||
throw new RuntimeException("Failed to fetch release: " + response.statusCode());
|
||||
}
|
||||
var release = MAPPER.readValue(response.body(), LatestVersion.class);
|
||||
var updateVersion = release.versionForCurrentOS();
|
||||
if (UpdateMechanism.isUpdateAvailable(updateVersion, currentVersion)) {
|
||||
return new FallbackUpdateInfo(updateVersion, this);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
LOG.warn("Update check interrupted", e);
|
||||
return null;
|
||||
} catch (IOException e) {
|
||||
LOG.warn("Update check failed", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public UpdateStep firstStep(FallbackUpdateInfo updateInfo) {
|
||||
return UpdateStep.of("Go to download page", this::openDownloadPage); // TODO localize
|
||||
}
|
||||
|
||||
private UpdateStep openDownloadPage() {
|
||||
var downloadUrl = DOWNLOADS_URI_TEMPLATE.formatted(URLEncoder.encode(env.getAppVersion(), StandardCharsets.US_ASCII));
|
||||
Platform.runLater(() -> {
|
||||
app.getHostServices().showDocument(downloadUrl);
|
||||
});
|
||||
return UpdateStep.RETRY; // allow running this "update mechanism" as many times as the user wants
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public record LatestVersion(
|
||||
@JsonProperty("mac") String macVersion,
|
||||
@JsonProperty("win") String winVersion,
|
||||
@JsonProperty("linux") String linuxVersion
|
||||
) {
|
||||
public String versionForCurrentOS() {
|
||||
if (SystemUtils.IS_OS_MAC_OSX) {
|
||||
return macVersion;
|
||||
} else if (SystemUtils.IS_OS_WINDOWS) {
|
||||
return winVersion;
|
||||
} else if (SystemUtils.IS_OS_LINUX) {
|
||||
return linuxVersion;
|
||||
} else {
|
||||
throw new IllegalStateException("Unsupported operating system");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
198
src/main/java/org/cryptomator/updater/UpdateChecker.java
Normal file
198
src/main/java/org/cryptomator/updater/UpdateChecker.java
Normal file
@@ -0,0 +1,198 @@
|
||||
package org.cryptomator.updater;
|
||||
|
||||
import org.cryptomator.common.Environment;
|
||||
import org.cryptomator.common.settings.Settings;
|
||||
import org.cryptomator.integrations.update.UpdateFailedException;
|
||||
import org.cryptomator.integrations.update.UpdateInfo;
|
||||
import org.cryptomator.integrations.update.UpdateMechanism;
|
||||
import org.cryptomator.ui.fxapp.FxApplicationScoped;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
import javafx.beans.binding.StringExpression;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.concurrent.ScheduledService;
|
||||
import javafx.concurrent.Task;
|
||||
import javafx.util.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
@FxApplicationScoped
|
||||
public class UpdateChecker extends ScheduledService<UpdateInfo<?>> {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(UpdateChecker.class);
|
||||
private static final Duration AUTO_CHECK_DELAY = Duration.seconds(5);
|
||||
private static final Duration UPDATE_CHECK_INTERVAL = Duration.hours(3);
|
||||
private static final Duration DISABLED_UPDATE_CHECK_INTERVAL = Duration.hours(100000); // Duration.INDEFINITE leads to overflows...
|
||||
|
||||
public enum UpdateCheckState {
|
||||
NOT_CHECKED,
|
||||
IS_CHECKING,
|
||||
CHECK_SUCCESSFUL,
|
||||
CHECK_FAILED
|
||||
}
|
||||
|
||||
private final Environment env;
|
||||
private final Settings settings;
|
||||
private final ObjectProperty<Instant> lastSuccessfulUpdateCheck;
|
||||
private final ObjectProperty<UpdateInfo<?>> update = new SimpleObjectProperty<>();
|
||||
private final StringExpression latestVersion = StringExpression.stringExpression(update.map(UpdateInfo::version));
|
||||
private final BooleanBinding updateAvailable = update.isNotNull();
|
||||
private final ObjectBinding<UpdateCheckState> updateState = Bindings.createObjectBinding(this::getUpdateCheckState, stateProperty());
|
||||
private final BooleanBinding checkFailed = Bindings.equal(UpdateCheckState.CHECK_FAILED, updateState);
|
||||
private final UpdateMechanism<?> fallbackUpdateMechanism;
|
||||
private UpdateMechanism<?> updateMechanism;
|
||||
|
||||
@Inject
|
||||
UpdateChecker(Settings settings, //
|
||||
Environment env,
|
||||
FallbackUpdateMechanism fallbackUpdateMechanism) {
|
||||
this.env = env;
|
||||
this.settings = settings;
|
||||
this.lastSuccessfulUpdateCheck = settings.lastSuccessfulUpdateCheck;
|
||||
this.fallbackUpdateMechanism = fallbackUpdateMechanism;
|
||||
|
||||
// Prefer the safer fallback mechanism if the last update attempt was already made by this app version
|
||||
var currentVersion = env.getAppVersionWithBuildNumber();
|
||||
var lastAttemptedBy = settings.lastUpdateAttemptedByVersion.get();
|
||||
if (currentVersion != null && currentVersion.equals(lastAttemptedBy)) {
|
||||
this.updateMechanism = fallbackUpdateMechanism; // immediately use fallback mechanism
|
||||
} else {
|
||||
this.updateMechanism = UpdateMechanism.get().orElse(fallbackUpdateMechanism);
|
||||
}
|
||||
|
||||
setExecutor(Executors.newVirtualThreadPerTaskExecutor());
|
||||
periodProperty().bind(Bindings.when(settings.checkForUpdates).then(UPDATE_CHECK_INTERVAL).otherwise(DISABLED_UPDATE_CHECK_INTERVAL));
|
||||
}
|
||||
|
||||
public void automaticallyCheckForUpdatesIfEnabled() {
|
||||
if (!env.disableUpdateCheck() && settings.checkForUpdates.get()) {
|
||||
startCheckingForUpdates(AUTO_CHECK_DELAY);
|
||||
}
|
||||
}
|
||||
|
||||
public void recheckWithFallbackMechanism() {
|
||||
if (updateMechanism == fallbackUpdateMechanism) {
|
||||
return; // already using fallback mechanism
|
||||
}
|
||||
updateMechanism = fallbackUpdateMechanism;
|
||||
checkForUpdatesNow();
|
||||
}
|
||||
|
||||
public void checkForUpdatesNow() {
|
||||
startCheckingForUpdates(Duration.ZERO);
|
||||
}
|
||||
|
||||
private void startCheckingForUpdates(Duration initialDelay) {
|
||||
cancel();
|
||||
reset();
|
||||
setDelay(initialDelay);
|
||||
start();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void succeeded() {
|
||||
var updateInfo = getValue();
|
||||
super.succeeded(); // this will nil the value property!
|
||||
lastSuccessfulUpdateCheck.set(Instant.now());
|
||||
if (updateInfo != null) {
|
||||
LOG.info("Current version: {}, latest version: {}", getCurrentVersion(), updateInfo.version());
|
||||
update.set(updateInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Task<UpdateInfo<?>> createTask() {
|
||||
return new UpdateCheckTask();
|
||||
}
|
||||
|
||||
/* Observable Properties */
|
||||
|
||||
public UpdateInfo<?> getUpdate() {
|
||||
return update.get();
|
||||
}
|
||||
|
||||
public ObjectProperty<UpdateInfo<?>> updateProperty() {
|
||||
return update;
|
||||
}
|
||||
|
||||
public String getLatestVersion() {
|
||||
return latestVersion.get();
|
||||
}
|
||||
|
||||
public StringExpression latestVersionProperty() {
|
||||
return latestVersion;
|
||||
}
|
||||
|
||||
public boolean isUpdateAvailable() {
|
||||
return updateAvailable.get();
|
||||
}
|
||||
|
||||
public BooleanBinding updateAvailableProperty() {
|
||||
return updateAvailable;
|
||||
}
|
||||
|
||||
public boolean isCheckFailed() {
|
||||
return checkFailed.get();
|
||||
}
|
||||
|
||||
public BooleanBinding checkFailedProperty() {
|
||||
return checkFailed;
|
||||
}
|
||||
|
||||
public Instant getLastSuccessfulUpdateCheck() {
|
||||
return lastSuccessfulUpdateCheck.get();
|
||||
}
|
||||
|
||||
public ObjectProperty<Instant> lastSuccessfulUpdateCheckProperty() {
|
||||
return lastSuccessfulUpdateCheck;
|
||||
}
|
||||
|
||||
public ObjectBinding<UpdateCheckState> updateCheckStateProperty() {
|
||||
return updateState;
|
||||
}
|
||||
|
||||
private UpdateCheckState getUpdateCheckState() {
|
||||
return switch (getState()) {
|
||||
case READY -> UpdateCheckState.NOT_CHECKED;
|
||||
case SCHEDULED, RUNNING -> UpdateCheckState.IS_CHECKING;
|
||||
case SUCCEEDED -> UpdateCheckState.CHECK_SUCCESSFUL;
|
||||
case FAILED, CANCELLED -> UpdateCheckState.CHECK_FAILED;
|
||||
};
|
||||
}
|
||||
|
||||
public String getCurrentVersion() {
|
||||
return env.getAppVersion();
|
||||
}
|
||||
|
||||
private class UpdateCheckTask extends Task<UpdateInfo<?>> {
|
||||
|
||||
@Override
|
||||
protected UpdateInfo<?> call() {
|
||||
try (var httpClient = new UpdateCheckerHttpClient(env)) {
|
||||
var result = updateMechanism.checkForUpdate(env.getAppVersion(), httpClient);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
} catch (UpdateFailedException e) {
|
||||
LOG.error("Update check using {} failed.", updateMechanism.getClass(), e);
|
||||
}
|
||||
if (updateMechanism == fallbackUpdateMechanism) {
|
||||
return null;
|
||||
}
|
||||
LOG.debug("Trying fallback update check...");
|
||||
try (var httpClient = new UpdateCheckerHttpClient(env)) {
|
||||
return fallbackUpdateMechanism.checkForUpdate(env.getAppVersion(), httpClient);
|
||||
} catch (UpdateFailedException e) {
|
||||
LOG.error("Fallback update check failed.", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package org.cryptomator.updater;
|
||||
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.common.Environment;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.ProxySelector;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public class UpdateCheckerHttpClient extends DelegatingHttpClient {
|
||||
|
||||
private final String userAgent;
|
||||
|
||||
public UpdateCheckerHttpClient(Environment env) {
|
||||
var delegate = HttpClient.newBuilder() //
|
||||
.followRedirects(HttpClient.Redirect.NORMAL) // from version 1.6.11 onwards, Cryptomator can follow redirects, in case this URL ever changes
|
||||
.proxy(ProxySelector.getDefault()).build();
|
||||
super(delegate);
|
||||
this.userAgent = String.format("Cryptomator VersionChecker/%s %s %s (%s)", env.getAppVersion(), SystemUtils.OS_NAME, SystemUtils.OS_VERSION, SystemUtils.OS_ARCH);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> HttpResponse<T> send(HttpRequest request, HttpResponse.BodyHandler<T> responseBodyHandler) throws IOException, InterruptedException {
|
||||
return super.send(decorateRequest(request), responseBodyHandler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> CompletableFuture<HttpResponse<T>> sendAsync(HttpRequest request, HttpResponse.BodyHandler<T> responseBodyHandler) {
|
||||
return super.sendAsync(decorateRequest(request), responseBodyHandler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> CompletableFuture<HttpResponse<T>> sendAsync(HttpRequest request, HttpResponse.BodyHandler<T> responseBodyHandler, HttpResponse.PushPromiseHandler<T> pushPromiseHandler) {
|
||||
return super.sendAsync(decorateRequest(request), responseBodyHandler, pushPromiseHandler);
|
||||
}
|
||||
|
||||
private HttpRequest decorateRequest(HttpRequest request) {
|
||||
return HttpRequest.newBuilder(request, (_, _) -> true) //
|
||||
.header("User-Agent", this.userAgent) //
|
||||
.timeout(Duration.ofSeconds(10)) //
|
||||
.build();
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
81
src/main/java/org/cryptomator/updater/UpdateService.java
Normal file
81
src/main/java/org/cryptomator/updater/UpdateService.java
Normal file
@@ -0,0 +1,81 @@
|
||||
package org.cryptomator.updater;
|
||||
|
||||
import org.cryptomator.integrations.update.UpdateInfo;
|
||||
import org.cryptomator.integrations.update.UpdateMechanism;
|
||||
import org.cryptomator.integrations.update.UpdateStep;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.concurrent.Service;
|
||||
import javafx.concurrent.Task;
|
||||
import java.io.IOException;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* A service that performs all update steps provided by the given {@link UpdateMechanism} in sequence.
|
||||
*/
|
||||
public class UpdateService extends Service<UpdateStep> {
|
||||
|
||||
private final BooleanBinding updateFailed = Bindings.equal(State.FAILED, stateProperty());
|
||||
|
||||
private ObservableValue<UpdateInfo<?>> updateInfo;
|
||||
|
||||
public UpdateService(ObservableValue<UpdateInfo<?>> updateInfo) {
|
||||
setExecutor(Executors.newVirtualThreadPerTaskExecutor());
|
||||
this.updateInfo = updateInfo;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Task<UpdateStep> createTask() {
|
||||
return new RunAllStepsTask(updateInfo.getValue());
|
||||
}
|
||||
|
||||
private static class RunAllStepsTask extends Task<UpdateStep> {
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
private final UpdateInfo updateInfo;
|
||||
|
||||
public RunAllStepsTask(UpdateInfo<?> updateInfo) {
|
||||
this.updateInfo = Objects.requireNonNull(updateInfo);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected UpdateStep call() throws IOException {
|
||||
try {
|
||||
UpdateStep step = updateInfo.useToPrepareFirstStep();
|
||||
UpdateStep lastStep;
|
||||
do {
|
||||
step.start();
|
||||
observeAndWaitFor(step);
|
||||
lastStep = step;
|
||||
step = step.nextStep();
|
||||
} while (step != null);
|
||||
return lastStep;
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new InterruptedIOException("Update interrupted");
|
||||
}
|
||||
}
|
||||
|
||||
private void observeAndWaitFor(UpdateStep step) throws InterruptedException {
|
||||
do {
|
||||
updateProgress(step.preparationProgress(), 1.0);
|
||||
updateMessage(step.description());
|
||||
} while (!step.await(100, TimeUnit.MILLISECONDS));
|
||||
}
|
||||
}
|
||||
|
||||
/* Observable Properties */
|
||||
|
||||
public boolean isUpdateFailed() {
|
||||
return updateFailed.get();
|
||||
}
|
||||
|
||||
public BooleanBinding updateFailedProperty() {
|
||||
return updateFailed;
|
||||
}
|
||||
}
|
||||
@@ -3,55 +3,73 @@
|
||||
<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
|
||||
<?import org.cryptomator.ui.controls.FontAwesome5Spinner?>
|
||||
<?import org.cryptomator.ui.controls.FormattedLabel?>
|
||||
<?import org.cryptomator.ui.controls.FormattedString?>
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.Button?>
|
||||
<?import javafx.scene.control.CheckBox?>
|
||||
<?import javafx.scene.control.Hyperlink?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.layout.HBox?>
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
<?import javafx.scene.control.ProgressBar?>
|
||||
<?import javafx.scene.control.Tooltip?>
|
||||
<?import javafx.scene.text.TextFlow?>
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
<?import javafx.scene.text.Text?>
|
||||
<?import javafx.scene.text.TextFlow?>
|
||||
<VBox xmlns:fx="http://javafx.com/fxml"
|
||||
xmlns="http://javafx.com/javafx"
|
||||
fx:controller="org.cryptomator.ui.preferences.UpdatesPreferencesController"
|
||||
spacing="12">
|
||||
<fx:define>
|
||||
<FormattedString fx:id="linkLabel" format="%preferences.updates.updateAvailable" arg1="${controller.latestVersion}"/>
|
||||
</fx:define>
|
||||
<padding>
|
||||
<Insets topRightBottomLeft="24"/>
|
||||
</padding>
|
||||
<FormattedLabel format="%preferences.updates.currentVersion" arg1="${controller.currentVersion}" textAlignment="CENTER" wrapText="true"/>
|
||||
<FormattedLabel format="%preferences.updates.currentVersion" arg1="${controller.updateChecker.currentVersion}" textAlignment="CENTER" wrapText="true"/>
|
||||
|
||||
<CheckBox fx:id="checkForUpdatesCheckbox" text="%preferences.updates.autoUpdateCheck"/>
|
||||
|
||||
<VBox alignment="CENTER" spacing="12">
|
||||
<Button text="%preferences.updates.checkNowBtn" defaultButton="true" onAction="#checkNow" contentDisplay="${controller.checkForUpdatesButtonState}">
|
||||
<FormattedLabel format="%preferences.updates.updateAvailable" arg1="${controller.updateChecker.latestVersion}" textAlignment="CENTER" wrapText="true" visible="${controller.updateChecker.updateAvailable}"/>
|
||||
|
||||
<Button text="${controller.updateButtonTitle}" defaultButton="true" onAction="#startWork" disable="${controller.updateButtonDisabled}" contentDisplay="${controller.updateButtonState}">
|
||||
<graphic>
|
||||
<FontAwesome5Spinner glyphSize="12"/>
|
||||
<VBox spacing="5" alignment="CENTER">
|
||||
<ProgressBar maxWidth="200"
|
||||
maxHeight="12"
|
||||
visible="${controller.running && controller.worker.progress != -1.0}"
|
||||
managed="${controller.running && controller.worker.progress != -1.0}"
|
||||
progress="${controller.worker.progress}"/>
|
||||
<FontAwesome5Spinner glyphSize="12"
|
||||
visible="${controller.running && controller.worker.progress == -1.0}"
|
||||
managed="${controller.running && controller.worker.progress == -1.0}"/>
|
||||
</VBox>
|
||||
</graphic>
|
||||
</Button>
|
||||
|
||||
<TextFlow styleClass="text-flow" textAlignment="CENTER" visible="${controller.checkFailed}" managed="${controller.checkFailed}">
|
||||
<TextFlow styleClass="text-flow" textAlignment="CENTER" visible="${controller.prohibitUpdateWhileUnlocked}" managed="${controller.prohibitUpdateWhileUnlocked}">
|
||||
<FontAwesome5IconView glyphSize="12" styleClass="glyph-icon-primary" glyph="LOCK_OPEN"/>
|
||||
<Text text=" "/>
|
||||
<Text text="%preferences.updates.prohibitedDueToUnlockedVaults.1"/>
|
||||
<Text text=" "/>
|
||||
<Hyperlink styleClass="hyperlink-underline" text="%preferences.updates.prohibitedDueToUnlockedVaults.2" onAction="#lockAllGracefully"/>
|
||||
<Text text=" "/>
|
||||
<Text text="%preferences.updates.prohibitedDueToUnlockedVaults.3"/>
|
||||
</TextFlow>
|
||||
|
||||
<TextFlow styleClass="text-flow" textAlignment="CENTER" visible="${!controller.errorMessage.empty}" managed="${!controller.errorMessage.empty}">
|
||||
<FontAwesome5IconView glyphSize="12" styleClass="glyph-icon-orange" glyph="EXCLAMATION_TRIANGLE"/>
|
||||
<Text text=" "/>
|
||||
<Text text="%preferences.updates.checkFailed"/>
|
||||
<Text text="${controller.errorMessage}"/>
|
||||
<Text text=" "/>
|
||||
<Hyperlink styleClass="hyperlink-underline" text="%preferences.general.debugDirectory" onAction="#showLogfileDirectory"/>
|
||||
</TextFlow>
|
||||
<FormattedLabel format="%preferences.updates.lastUpdateCheck" arg1="${controller.timeDifferenceMessage}" textAlignment="CENTER" wrapText="true">
|
||||
|
||||
<FormattedLabel format="%preferences.updates.lastUpdateCheck" arg1="${controller.timeDifferenceMessage}" textAlignment="CENTER" wrapText="true" visible="${!controller.updateChecker.updateAvailable}" managed="${!controller.updateChecker.updateAvailable}">
|
||||
<tooltip>
|
||||
<Tooltip text="${controller.lastUpdateCheckMessage}" showDelay="10ms"/>
|
||||
</tooltip>
|
||||
</FormattedLabel>
|
||||
|
||||
<Label text="%preferences.updates.upToDate" visible="${controller.upToDateLabelVisible}" managed="${controller.upToDateLabelVisible}">
|
||||
<graphic>
|
||||
<FontAwesome5IconView glyphSize="12" styleClass="glyph-icon-primary" glyph="CHECK"/>
|
||||
</graphic>
|
||||
</Label>
|
||||
<Hyperlink text="${linkLabel.value}" onAction="#visitDownloadsPage" textAlignment="CENTER" wrapText="true" styleClass="hyperlink-underline" visible="${controller.updateAvailable}" managed="${controller.updateAvailable}"/>
|
||||
</VBox>
|
||||
</VBox>
|
||||
|
||||
@@ -330,8 +330,13 @@ preferences.updates.lastUpdateCheck.never=never
|
||||
preferences.updates.lastUpdateCheck.recently=recently
|
||||
preferences.updates.lastUpdateCheck.daysAgo=%s days ago
|
||||
preferences.updates.lastUpdateCheck.hoursAgo=%s hours ago
|
||||
preferences.updates.prohibitedDueToUnlockedVaults.1=Please
|
||||
preferences.updates.prohibitedDueToUnlockedVaults.2=lock your vaults
|
||||
preferences.updates.prohibitedDueToUnlockedVaults.3=to install the update.
|
||||
preferences.updates.checkFailed=Looking for updates failed. Please check your internet connection or try again later.
|
||||
preferences.updates.updateFailed=Update failed. Please install the update manually.
|
||||
preferences.updates.upToDate=Cryptomator is up-to-date.
|
||||
preferences.updates.visitDownloadPage=Visit Download Page
|
||||
|
||||
## Contribution
|
||||
preferences.contribute=Support Us
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2016, 2017 Sebastian Stenzel and others.
|
||||
* All rights reserved.
|
||||
* This program and the accompanying materials are made available under the terms of the accompanying LICENSE file.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.common;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Comparator;
|
||||
|
||||
public class SemVerComparatorTest {
|
||||
|
||||
private final Comparator<String> semVerComparator = new SemVerComparator();
|
||||
|
||||
// equal versions
|
||||
|
||||
@Test
|
||||
public void compareEqualVersions() {
|
||||
Assertions.assertEquals(0, Integer.signum(semVerComparator.compare("1.23.4", "1.23.4")));
|
||||
Assertions.assertEquals(0, Integer.signum(semVerComparator.compare("1.23.4-alpha", "1.23.4-alpha")));
|
||||
Assertions.assertEquals(0, Integer.signum(semVerComparator.compare("1.23.4+20170101", "1.23.4+20171231")));
|
||||
Assertions.assertEquals(0, Integer.signum(semVerComparator.compare("1.23.4-alpha+20170101", "1.23.4-alpha+20171231")));
|
||||
}
|
||||
|
||||
// newer versions in first argument
|
||||
|
||||
@Test
|
||||
public void compareHigherToLowerVersions() {
|
||||
Assertions.assertEquals(1, Integer.signum(semVerComparator.compare("1.23.5", "1.23.4")));
|
||||
Assertions.assertEquals(1, Integer.signum(semVerComparator.compare("1.24.4", "1.23.4")));
|
||||
Assertions.assertEquals(1, Integer.signum(semVerComparator.compare("1.23.4", "1.23")));
|
||||
Assertions.assertEquals(1, Integer.signum(semVerComparator.compare("1.23.4", "1.23.4-SNAPSHOT")));
|
||||
Assertions.assertEquals(1, Integer.signum(semVerComparator.compare("1.23.4", "1.23.4-56.78")));
|
||||
Assertions.assertEquals(1, Integer.signum(semVerComparator.compare("1.23.4-beta", "1.23.4-alpha")));
|
||||
Assertions.assertEquals(1, Integer.signum(semVerComparator.compare("1.23.4-alpha.1", "1.23.4-alpha")));
|
||||
Assertions.assertEquals(1, Integer.signum(semVerComparator.compare("1.23.4-56.79", "1.23.4-56.78")));
|
||||
}
|
||||
|
||||
// newer versions in second argument
|
||||
|
||||
@Test
|
||||
public void compareLowerToHigherVersions() {
|
||||
Assertions.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23.4", "1.23.5")));
|
||||
Assertions.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23.4", "1.24.4")));
|
||||
Assertions.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23", "1.23.4")));
|
||||
Assertions.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23.4-SNAPSHOT", "1.23.4")));
|
||||
Assertions.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23.4-56.78", "1.23.4")));
|
||||
Assertions.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23.4-alpha", "1.23.4-beta")));
|
||||
Assertions.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23.4-alpha", "1.23.4-alpha.1")));
|
||||
Assertions.assertEquals(-1, Integer.signum(semVerComparator.compare("1.23.4-56.78", "1.23.4-56.79")));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -9,31 +9,28 @@ import org.cryptomator.common.Environment;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class SettingsTest {
|
||||
|
||||
@Test
|
||||
public void testAutoSave() {
|
||||
Environment env = Mockito.mock(Environment.class);
|
||||
@SuppressWarnings("unchecked") Consumer<Settings> changeListener = Mockito.mock(Consumer.class);
|
||||
SettingsProvider provider = Mockito.mock(SettingsProvider.class);
|
||||
|
||||
Settings settings = Settings.create(env);
|
||||
settings.setSaveCmd(changeListener);
|
||||
Settings settings = Settings.create(provider, env);
|
||||
VaultSettings vaultSettings = VaultSettings.withRandomId();
|
||||
Mockito.verify(changeListener, Mockito.times(0)).accept(settings);
|
||||
Mockito.verify(provider, Mockito.times(0)).scheduleSave(settings);
|
||||
|
||||
// first change (to property):
|
||||
settings.port.set(42428);
|
||||
Mockito.verify(changeListener, Mockito.times(1)).accept(settings);
|
||||
Mockito.verify(provider, Mockito.times(1)).scheduleSave(settings);
|
||||
|
||||
// second change (to list):
|
||||
settings.directories.add(vaultSettings);
|
||||
Mockito.verify(changeListener, Mockito.times(2)).accept(settings);
|
||||
Mockito.verify(provider, Mockito.times(2)).scheduleSave(settings);
|
||||
|
||||
// third change (to property of list item):
|
||||
vaultSettings.displayName.set("asd");
|
||||
Mockito.verify(changeListener, Mockito.times(3)).accept(settings);
|
||||
Mockito.verify(provider, Mockito.times(3)).scheduleSave(settings);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user