Merge branch 'develop' into feature/mount-provider

# Conflicts:
#	.github/workflows/appimage.yml
#	.github/workflows/mac-dmg.yml
#	.github/workflows/win-exe.yml
#	pom.xml
#	src/main/java/org/cryptomator/common/vaults/WebDavVolume.java
#	src/main/java/org/cryptomator/ui/traymenu/AwtTrayMenuController.java
This commit is contained in:
Armin Schrenk
2022-12-07 17:01:56 +01:00
88 changed files with 1601 additions and 370 deletions

View File

@@ -26,6 +26,7 @@ public class Environment {
private static final String KEYCHAIN_PATHS_PROP_NAME = "cryptomator.integrationsWin.keychainPaths";
private static final String P12_PATH_PROP_NAME = "cryptomator.p12Path";
private static final String LOG_DIR_PROP_NAME = "cryptomator.logDir";
private static final String LOOPBACK_ALIAS_PROP_NAME = "cryptomator.loopbackAlias";
private static final String MOUNTPOINT_DIR_PROP_NAME = "cryptomator.mountPointsDir";
private static final String MIN_PW_LENGTH_PROP_NAME = "cryptomator.minPwLength";
private static final String APP_VERSION_PROP_NAME = "cryptomator.appVersion";
@@ -45,6 +46,7 @@ public class Environment {
logCryptomatorSystemProperty(IPC_SOCKET_PATH_PROP_NAME);
logCryptomatorSystemProperty(KEYCHAIN_PATHS_PROP_NAME);
logCryptomatorSystemProperty(LOG_DIR_PROP_NAME);
logCryptomatorSystemProperty(LOOPBACK_ALIAS_PROP_NAME);
logCryptomatorSystemProperty(PLUGIN_DIR_PROP_NAME);
logCryptomatorSystemProperty(MOUNTPOINT_DIR_PROP_NAME);
logCryptomatorSystemProperty(MIN_PW_LENGTH_PROP_NAME);
@@ -90,6 +92,10 @@ public class Environment {
return getPath(LOG_DIR_PROP_NAME).map(this::replaceHomeDir);
}
public Optional<String> getLoopbackAlias() {
return Optional.ofNullable(System.getProperty(LOOPBACK_ALIAS_PROP_NAME));
}
public Optional<Path> getPluginDir() {
return getPath(PLUGIN_DIR_PROP_NAME).map(this::replaceHomeDir);
}
@@ -112,22 +118,13 @@ public class Environment {
}
public int getMinPwLength() {
return getInt(MIN_PW_LENGTH_PROP_NAME, DEFAULT_MIN_PW_LENGTH);
return Integer.getInteger(MIN_PW_LENGTH_PROP_NAME, DEFAULT_MIN_PW_LENGTH);
}
public boolean showTrayIcon() {
return Boolean.getBoolean(TRAY_ICON_PROP_NAME);
}
private int getInt(String propertyName, int defaultValue) {
String value = System.getProperty(propertyName);
try {
return Integer.parseInt(value);
} catch (NumberFormatException e) { // includes "null" values
return defaultValue;
}
}
private Optional<Path> getPath(String propertyName) {
String value = System.getProperty(propertyName);
return Optional.ofNullable(value).map(Paths::get);

View File

@@ -41,6 +41,8 @@ public class VaultStats {
private final LongProperty totalBytesDecrypted = new SimpleLongProperty();
private final LongProperty filesRead = new SimpleLongProperty();
private final LongProperty filesWritten = new SimpleLongProperty();
private final LongProperty filesAccessed = new SimpleLongProperty();
private final LongProperty totalFilesAccessed = new SimpleLongProperty();
private final ObjectProperty<Instant> lastActivity = new SimpleObjectProperty<>();
@Inject
@@ -82,6 +84,8 @@ public class VaultStats {
var oldAccessCount = filesRead.get() + filesWritten.get();
filesRead.set(stats.map(CryptoFileSystemStats::pollAmountOfAccessesRead).orElse(0L));
filesWritten.set(stats.map(CryptoFileSystemStats::pollAmountOfAccessesWritten).orElse(0L));
filesAccessed.set(stats.map(CryptoFileSystemStats::pollAmountOfAccesses).orElse(0L));
totalFilesAccessed.set(stats.map(CryptoFileSystemStats::pollTotalAmountOfAccesses).orElse(0L));
var newAccessCount = filesRead.get() + filesWritten.get();
// check for any I/O activity
@@ -188,6 +192,19 @@ public class VaultStats {
public long getFilesWritten() {return filesWritten.get();}
public LongProperty filesAccessed() {
return filesAccessed;}
public long getFilesAccessed() {return filesAccessed.get();}
public LongProperty totalFilesAccessed(){
return totalFilesAccessed;
}
public long getTotalFilesAccessed(){
return totalFilesAccessed.get();
}
public ObjectProperty<Instant> lastActivityProperty() {
return lastActivity;
}

View File

@@ -2,19 +2,20 @@ package org.cryptomator.ui.addvaultwizard;
import dagger.Lazy;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.settings.UiTheme;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultListManager;
import org.cryptomator.integrations.uiappearance.Theme;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.fxapp.FxApplicationStyle;
import org.cryptomator.ui.fxapp.FxApplicationWindows;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javafx.beans.property.ObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.fxml.FXML;
import javafx.scene.Scene;
import javafx.scene.image.Image;
@@ -23,6 +24,7 @@ import javafx.stage.Stage;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Objects;
import java.util.ResourceBundle;
import static org.cryptomator.common.Constants.CRYPTOMATOR_FILENAME_GLOB;
@@ -40,12 +42,10 @@ public class ChooseExistingVaultController implements FxController {
private final ObjectProperty<Vault> vault;
private final VaultListManager vaultListManager;
private final ResourceBundle resourceBundle;
private final Settings settings;
private Image screenshot;
private final ObservableValue<Image> screenshot;
@Inject
ChooseExistingVaultController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_WELCOME) Lazy<Scene> welcomeScene, @FxmlScene(FxmlFile.ADDVAULT_SUCCESS) Lazy<Scene> successScene, FxApplicationWindows appWindows, ObjectProperty<Path> vaultPath, @AddVaultWizardWindow ObjectProperty<Vault> vault, VaultListManager vaultListManager, ResourceBundle resourceBundle, Settings settings) {
ChooseExistingVaultController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_WELCOME) Lazy<Scene> welcomeScene, @FxmlScene(FxmlFile.ADDVAULT_SUCCESS) Lazy<Scene> successScene, FxApplicationWindows appWindows, ObjectProperty<Path> vaultPath, @AddVaultWizardWindow ObjectProperty<Vault> vault, VaultListManager vaultListManager, ResourceBundle resourceBundle, FxApplicationStyle applicationStyle) {
this.window = window;
this.welcomeScene = welcomeScene;
this.successScene = successScene;
@@ -54,16 +54,20 @@ public class ChooseExistingVaultController implements FxController {
this.vault = vault;
this.vaultListManager = vaultListManager;
this.resourceBundle = resourceBundle;
this.settings = settings;
this.screenshot = applicationStyle.appliedThemeProperty().map(this::selectScreenshot);
}
@FXML
public void initialize() {
private Image selectScreenshot(Theme theme) {
String imageResourcePath;
if (SystemUtils.IS_OS_MAC) {
this.screenshot = new Image(getClass().getResource("/img/select-masterkey-mac"+(UiTheme.LIGHT == settings.theme().get()? "":"-dark")+".png").toString());
imageResourcePath = switch (theme) {
case LIGHT -> "/img/select-masterkey-mac.png";
case DARK -> "/img/select-masterkey-mac-dark.png";
};
} else {
this.screenshot = new Image(getClass().getResource("/img/select-masterkey-win.png").toString());
imageResourcePath = "/img/select-masterkey-win.png";
}
return new Image((Objects.requireNonNull(getClass().getResource(imageResourcePath)).toString()));
}
@FXML
@@ -92,8 +96,13 @@ public class ChooseExistingVaultController implements FxController {
/* Getter */
public Image getScreenshot() {
public ObservableValue<Image> screenshotProperty() {
return screenshot;
}
public Image getScreenshot() {
return screenshot.getValue();
}
}

View File

@@ -27,6 +27,7 @@ public enum FontAwesome5Icon {
FILE("\uF15B"), //
FILE_IMPORT("\uF56F"), //
FOLDER_OPEN("\uF07C"), //
FUNNEL("\uF0B0"), //
HAND_HOLDING_HEART("\uF4BE"), //
HEART("\uF004"), //
HDD("\uF0A0"), //

View File

@@ -12,6 +12,8 @@ import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javafx.application.Application;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import java.util.Optional;
@@ -24,9 +26,10 @@ public class FxApplicationStyle {
private final Optional<UiAppearanceProvider> appearanceProvider;
private final LicenseHolder licenseHolder;
private final UiAppearanceListener systemInterfaceThemeListener = this::systemInterfaceThemeChanged;
private final ObjectProperty<Theme> appliedTheme = new SimpleObjectProperty<>(Theme.LIGHT);
@Inject
public FxApplicationStyle(Settings settings, Optional<UiAppearanceProvider> appearanceProvider, LicenseHolder licenseHolder){
public FxApplicationStyle(Settings settings, Optional<UiAppearanceProvider> appearanceProvider, LicenseHolder licenseHolder) {
this.settings = settings;
this.appearanceProvider = appearanceProvider;
this.licenseHolder = licenseHolder;
@@ -91,6 +94,7 @@ public class FxApplicationStyle {
} else {
Application.setUserAgentStylesheet(stylesheet.toString());
appearanceProvider.ifPresent(provider -> provider.adjustToTheme(Theme.LIGHT));
appliedTheme.set(Theme.LIGHT);
}
}
@@ -103,6 +107,11 @@ public class FxApplicationStyle {
} else {
Application.setUserAgentStylesheet(stylesheet.toString());
appearanceProvider.ifPresent(provider -> provider.adjustToTheme(Theme.DARK));
appliedTheme.set(Theme.DARK);
}
}
public ObjectProperty<Theme> appliedThemeProperty() {
return appliedTheme;
}
}

View File

@@ -8,15 +8,34 @@ import org.cryptomator.ui.common.FxController;
import javax.inject.Inject;
import javafx.beans.binding.Binding;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.binding.BooleanExpression;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.fxml.FXML;
import javafx.scene.control.ChoiceBox;
import javafx.scene.control.ListView;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.util.StringConverter;
import java.util.Arrays;
import java.util.ResourceBundle;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Stream;
import static org.cryptomator.cryptofs.health.api.DiagnosticResult.Severity;
import static org.cryptomator.ui.health.Result.FixState.FIXABLE;
import static org.cryptomator.ui.health.Result.FixState.FIXED;
import static org.cryptomator.ui.health.Result.FixState.FIXING;
import static org.cryptomator.ui.health.Result.FixState.FIX_FAILED;
import static org.cryptomator.ui.health.Result.FixState.NOT_FIXABLE;
@HealthCheckScoped
public class CheckDetailController implements FxController {
@@ -35,30 +54,48 @@ public class CheckDetailController implements FxController {
private final Binding<Number> countOfCritSeverity;
private final Binding<Boolean> warnOrCritsExist;
private final ResultListCellFactory resultListCellFactory;
private final ResultFixApplier resultFixApplier;
private final ResourceBundle resourceBundle;
private final BooleanProperty fixAllInfoResultsExecuted;
private final BooleanBinding fixAllInfoResultsPossible;
private final ObjectProperty<Predicate<Result>> resultsFilter;
public ListView<Result> resultsListView;
public ChoiceBox<DiagnosticResult.Severity> severityChoiceBox;
public ChoiceBox<Result.FixState> fixStateChoiceBox;
private Subscription resultSubscription;
@Inject
public CheckDetailController(ObjectProperty<Check> selectedTask, ResultListCellFactory resultListCellFactory) {
public CheckDetailController(ObjectProperty<Check> selectedTask, ResultListCellFactory resultListCellFactory, ResultFixApplier resultFixApplier, ResourceBundle resourceBundle) {
this.resultListCellFactory = resultListCellFactory;
this.resultFixApplier = resultFixApplier;
this.resourceBundle = resourceBundle;
this.results = EasyBind.wrapList(FXCollections.observableArrayList());
this.check = selectedTask;
this.checkState = selectedTask.flatMap(Check::stateProperty);
this.checkName = selectedTask.map(Check::getName).orElse("");
this.checkRunning = BooleanExpression.booleanExpression(checkState.map(Check.CheckState.RUNNING::equals).orElse(false));
this.checkScheduled = BooleanExpression.booleanExpression(checkState.map(Check.CheckState.SCHEDULED::equals).orElse(false));
this.checkSkipped =BooleanExpression.booleanExpression(checkState.map(Check.CheckState.SKIPPED::equals).orElse(false));
this.checkSkipped = BooleanExpression.booleanExpression(checkState.map(Check.CheckState.SKIPPED::equals).orElse(false));
this.checkSucceeded = BooleanExpression.booleanExpression(checkState.map(Check.CheckState.SUCCEEDED::equals).orElse(false));
this.checkFailed = BooleanExpression.booleanExpression(checkState.map(Check.CheckState.ERROR::equals).orElse(false));
this.checkCancelled = BooleanExpression.booleanExpression(checkState.map(Check.CheckState.CANCELLED::equals).orElse(false));
this.checkFinished = checkSucceeded.or(checkFailed).or(checkCancelled);
this.countOfWarnSeverity = results.reduce(countSeverity(DiagnosticResult.Severity.WARN));
this.countOfCritSeverity = results.reduce(countSeverity(DiagnosticResult.Severity.CRITICAL));
this.warnOrCritsExist = EasyBind.combine(checkSucceeded, countOfWarnSeverity, countOfCritSeverity, (suceeded, warns, crits) -> suceeded && (warns.longValue() > 0 || crits.longValue() > 0) );
this.countOfWarnSeverity = results.reduce(countSeverity(Severity.WARN));
this.countOfCritSeverity = results.reduce(countSeverity(Severity.CRITICAL));
this.warnOrCritsExist = EasyBind.combine(checkSucceeded, countOfWarnSeverity, countOfCritSeverity, (suceeded, warns, crits) -> suceeded && (warns.longValue() > 0 || crits.longValue() > 0));
this.fixAllInfoResultsExecuted = new SimpleBooleanProperty(false);
this.fixAllInfoResultsPossible = Bindings.createBooleanBinding(() -> results.stream().anyMatch(this::isFixableInfoResult), results) //
.and(fixAllInfoResultsExecuted.not());
this.resultsFilter = new SimpleObjectProperty<>(r -> true);
selectedTask.addListener(this::selectedTaskChanged);
}
private boolean isFixableInfoResult(Result r) {
return r.diagnosis().getSeverity() == Severity.INFO && r.getState() == FIXABLE;
}
private void selectedTaskChanged(ObservableValue<? extends Check> observable, Check oldValue, Check newValue) {
if (resultSubscription != null) {
resultSubscription.unsubscribe();
@@ -66,6 +103,8 @@ public class CheckDetailController implements FxController {
if (newValue != null) {
resultSubscription = EasyBind.bindContent(results, newValue.getResults());
}
severityChoiceBox.setValue(null);
fixStateChoiceBox.setValue(null);
}
private Function<Stream<? extends Result>, Long> countSeverity(DiagnosticResult.Severity severity) {
@@ -74,8 +113,110 @@ public class CheckDetailController implements FxController {
@FXML
public void initialize() {
resultsListView.setItems(results);
resultsListView.setItems(results.filtered(resultsFilter));
resultsListView.setCellFactory(resultListCellFactory);
severityChoiceBox.getItems().add(null);
severityChoiceBox.getItems().addAll(Arrays.stream(DiagnosticResult.Severity.values()).toList());
severityChoiceBox.setConverter(new SeverityStringifier());
severityChoiceBox.setValue(null);
fixStateChoiceBox.getItems().add(null);
fixStateChoiceBox.getItems().addAll(Arrays.stream(Result.FixState.values()).toList());
fixStateChoiceBox.setConverter(new FixStateStringifier());
fixStateChoiceBox.setValue(null);
resultsFilter.bind(Bindings.createObjectBinding(() -> this::filterResults, severityChoiceBox.valueProperty(), fixStateChoiceBox.valueProperty()));
}
private boolean filterResults(Result r) {
var desiredFixState = fixStateChoiceBox.getValue();
var desiredSeverity = severityChoiceBox.getValue();
return (desiredFixState == null || r.getState() == desiredFixState) && (desiredSeverity == null || r.diagnosis().getSeverity() == desiredSeverity);
}
@FXML
public void fixAllInfoResults() {
fixAllInfoResultsExecuted.setValue(true);
results.stream().filter(this::isFixableInfoResult).forEach(resultFixApplier::fix);
}
@FXML
public void copyResultDetails() {
var result = resultsListView.getSelectionModel().getSelectedItem();
if (result != null) {
ClipboardContent clipboardContent = new ClipboardContent();
clipboardContent.putString(result.diagnosis().toString());
Clipboard.getSystemClipboard().setContent(clipboardContent);
}
}
/* -- Internal classes -- */
class SeverityStringifier extends StringConverter<Severity> {
@Override
public String toString(Severity object) {
if (object == null) {
return resourceBundle.getString("health.result.severityFilter.all");
}
return switch (object) {
case GOOD -> resourceBundle.getString("health.result.severityFilter.good");
case INFO -> resourceBundle.getString("health.result.severityFilter.info");
case WARN -> resourceBundle.getString("health.result.severityFilter.warn");
case CRITICAL -> resourceBundle.getString("health.result.severityFilter.crit");
};
}
@Override
public Severity fromString(String string) {
if (resourceBundle.getString("health.result.severityFilter.good").equals(string)) {
return Severity.GOOD;
} else if (resourceBundle.getString("health.result.severityFilter.info").equals(string)) {
return Severity.INFO;
} else if (resourceBundle.getString("health.result.severityFilter.warn").equals(string)) {
return Severity.WARN;
} else if (resourceBundle.getString("health.result.severityFilter.crit").equals(string)) {
return Severity.CRITICAL;
} else {
return null;
}
}
}
class FixStateStringifier extends StringConverter<Result.FixState> {
@Override
public String toString(Result.FixState object) {
if (object == null) {
return resourceBundle.getString("health.result.fixStateFilter.all");
}
return switch (object) {
case FIXABLE -> resourceBundle.getString("health.result.fixStateFilter.fixable");
case NOT_FIXABLE -> resourceBundle.getString("health.result.fixStateFilter.notFixable");
case FIXING -> resourceBundle.getString("health.result.fixStateFilter.fixing");
case FIXED -> resourceBundle.getString("health.result.fixStateFilter.fixed");
case FIX_FAILED -> resourceBundle.getString("health.result.fixStateFilter.fixFailed");
};
}
@Override
public Result.FixState fromString(String string) {
if (resourceBundle.getString("health.result.fixStateFilter.fixable").equals(string)) {
return FIXABLE;
} else if (resourceBundle.getString("health.result.fixStateFilter.notFixable").equals(string)) {
return NOT_FIXABLE;
} else if (resourceBundle.getString("health.result.fixStateFilter.fixing").equals(string)) {
return FIXING;
} else if (resourceBundle.getString("health.result.fixStateFilter.fixed").equals(string)) {
return FIXED;
} else if (resourceBundle.getString("health.result.fixStateFilter.fixFailed").equals(string)) {
return FIX_FAILED;
} else {
return null;
}
}
}
/* Getter/Setter */
@@ -175,4 +316,12 @@ public class CheckDetailController implements FxController {
public Check getCheck() {
return check.get();
}
public ObservableValue<Boolean> fixAllInfoResultsPossibleProperty() {
return fixAllInfoResultsPossible;
}
public boolean getFixAllInfoResultsPossible() {
return fixAllInfoResultsPossible.getValue();
}
}

View File

@@ -70,7 +70,7 @@ public class CheckExecutor {
try (var masterkeyClone = masterkey.copy(); //
var cryptor = CryptorProvider.forScheme(vaultConfig.getCipherCombo()).provide(masterkeyClone, csprng)) {
c.getHealthCheck().check(vaultPath, vaultConfig, masterkeyClone, cryptor, diagnosis -> {
Platform.runLater(() -> c.getResults().add(Result.create(diagnosis)));
Platform.runLater(() -> c.getResults().add(Result.create(diagnosis, vaultPath, vaultConfig, masterkeyClone, cryptor)));
highestResultSeverity = Comparators.max(highestResultSeverity, diagnosis.getSeverity());
});
}

View File

@@ -31,7 +31,7 @@ public class CheckStateIconView extends FontAwesome5IconView {
this.severity = EasyBind.wrapNullable(check).mapObservable(Check::highestResultSeverityProperty).asOrdinary();
this.glyph.bind(Bindings.createObjectBinding(this::glyphForState, state, severity));
this.subscriptions = List.of( //
EasyBind.includeWhen(getStyleClass(), "glyph-icon-muted", Bindings.equal(state, Check.CheckState.SKIPPED).or(Bindings.equal(state, Check.CheckState.CANCELLED))), //
EasyBind.includeWhen(getStyleClass(), "glyph-icon-muted", Bindings.equal(state, Check.CheckState.SKIPPED).or(Bindings.equal(state, Check.CheckState.CANCELLED)).or(Bindings.equal(severity, DiagnosticResult.Severity.INFO))), //
EasyBind.includeWhen(getStyleClass(), "glyph-icon-primary", Bindings.equal(severity, DiagnosticResult.Severity.GOOD)), //
EasyBind.includeWhen(getStyleClass(), "glyph-icon-orange", Bindings.equal(severity, DiagnosticResult.Severity.WARN).or(Bindings.equal(severity, DiagnosticResult.Severity.CRITICAL))), //
EasyBind.includeWhen(getStyleClass(), "glyph-icon-red", Bindings.equal(state, Check.CheckState.ERROR)) //

View File

@@ -1,10 +1,14 @@
package org.cryptomator.ui.health;
import org.cryptomator.cryptofs.VaultConfig;
import org.cryptomator.cryptofs.health.api.DiagnosticResult;
import org.cryptomator.cryptolib.api.Cryptor;
import org.cryptomator.cryptolib.api.Masterkey;
import javafx.beans.Observable;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import java.nio.file.Path;
record Result(DiagnosticResult diagnosis, ObjectProperty<FixState> fixState) {
@@ -16,8 +20,8 @@ record Result(DiagnosticResult diagnosis, ObjectProperty<FixState> fixState) {
FIX_FAILED
}
public static Result create(DiagnosticResult diagnosis) {
FixState initialState = diagnosis.getSeverity() == DiagnosticResult.Severity.WARN ? FixState.FIXABLE : FixState.NOT_FIXABLE;
public static Result create(DiagnosticResult diagnosis, Path vaultPath, VaultConfig config, Masterkey masterkey, Cryptor cryptor) {
FixState initialState = diagnosis.getFix(vaultPath, config, masterkey, cryptor).map( _f -> FixState.FIXABLE).orElse(FixState.NOT_FIXABLE);
return new Result(diagnosis, new SimpleObjectProperty<>(initialState));
}

View File

@@ -23,6 +23,8 @@ import java.util.concurrent.atomic.AtomicReference;
@HealthCheckScoped
class ResultFixApplier {
private static final Logger LOG = LoggerFactory.getLogger(ResultFixApplier.class);
private final Path vaultPath;
private final SecureRandom csprng;
private final Masterkey masterkey;
@@ -40,25 +42,34 @@ class ResultFixApplier {
public CompletionStage<Void> fix(Result result) {
Preconditions.checkArgument(result.getState() == Result.FixState.FIXABLE);
result.setState(Result.FixState.FIXING);
return CompletableFuture.runAsync(() -> fix(result.diagnosis()), sequentialExecutor)
return CompletableFuture.runAsync(() -> result.setState(Result.FixState.FIXING), Platform::runLater) //
.thenRunAsync(() -> fix(result.diagnosis()), sequentialExecutor) //
.whenCompleteAsync((unused, throwable) -> {
var fixed = throwable == null ? Result.FixState.FIXED : Result.FixState.FIX_FAILED;
result.setState(fixed);
final Result.FixState s;
if (throwable == null) {
LOG.debug("Fix for {} applied successful.", result.diagnosis().getClass().getName());
s = Result.FixState.FIXED;
} else {
LOG.error("Failed to apply fix for {}", result.diagnosis().getClass().getName(), throwable);
s = Result.FixState.FIX_FAILED;
}
result.setState(s);
}, Platform::runLater);
}
public void fix(DiagnosticResult diagnosis) {
Preconditions.checkArgument(diagnosis.getSeverity() == DiagnosticResult.Severity.WARN, "Unfixable result");
private void fix(DiagnosticResult diagnosis) {
try (var masterkeyClone = masterkey.copy(); //
var cryptor = CryptorProvider.forScheme(vaultConfig.getCipherCombo()).provide(masterkeyClone, csprng)) {
diagnosis.fix(vaultPath, vaultConfig, masterkeyClone, cryptor);
diagnosis.getFix(vaultPath, vaultConfig, masterkeyClone, cryptor) //
.orElseThrow(() -> new IllegalStateException("No fix for diagnosis " + diagnosis.getClass().getName() + " implemented.")) //
.apply();
} catch (Exception e) {
throw new FixFailedException(e);
}
}
public static class FixFailedException extends CompletionException {
private FixFailedException(Throwable cause) {
super(cause);
}

View File

@@ -12,7 +12,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.binding.ObjectBinding;
@@ -34,7 +33,6 @@ public class ResultListCellController implements FxController {
private static final FontAwesome5Icon WARN_ICON = FontAwesome5Icon.EXCLAMATION_TRIANGLE;
private static final FontAwesome5Icon CRIT_ICON = FontAwesome5Icon.TIMES;
private final Logger LOG = LoggerFactory.getLogger(ResultListCellController.class);
private final ObjectProperty<Result> result;
private final ObservableValue<DiagnosticResult.Severity> severity;
@@ -42,17 +40,17 @@ public class ResultListCellController implements FxController {
private final ResultFixApplier fixApplier;
private final ObservableValue<Result.FixState> fixState;
private final ObjectBinding<FontAwesome5Icon> severityGlyph;
private final ObjectBinding<FontAwesome5Icon> fixGlyph;
private final BooleanBinding fixable;
private final BooleanBinding fixing;
private final BooleanBinding fixed;
private final BooleanBinding fixFailed;
private final BooleanBinding fixRunningOrDone;
private final ObservableValue<FontAwesome5Icon> fixGlyph;
private final List<Subscription> subscriptions;
private final Tooltip fixSuccess;
private final Tooltip fixFail;
private final Tooltip fixStateTip;
private final Tooltip severityTip;
private AutoAnimator fixRunningRotator;
private final ResourceBundle resourceBundle;
/* FXML */
public FontAwesome5IconView severityView;
@@ -60,23 +58,54 @@ public class ResultListCellController implements FxController {
@Inject
public ResultListCellController(ResultFixApplier fixApplier, ResourceBundle resourceBundle) {
this.resourceBundle = resourceBundle;
this.result = new SimpleObjectProperty<>(null);
this.severity = result.map(Result::diagnosis).map(DiagnosticResult::getSeverity);
this.description = result.map(Result::getDescription).orElse("");
this.fixApplier = fixApplier;
this.fixState = result.flatMap(Result::fixState);
this.severityGlyph = Bindings.createObjectBinding(this::getSeverityGlyph, result);
this.fixGlyph = Bindings.createObjectBinding(this::getFixGlyph, fixState);
this.fixable = Bindings.createBooleanBinding(this::isFixable, fixState);
this.fixing = Bindings.createBooleanBinding(this::isFixing, fixState);
this.fixed = Bindings.createBooleanBinding(this::isFixed, fixState);
this.fixFailed = Bindings.createBooleanBinding(this::isFixFailed, fixState);
this.fixRunningOrDone = fixing.or(fixed).or(fixFailed);
this.fixGlyph = fixState.map(this::getFixGlyph);
this.subscriptions = new ArrayList<>();
this.fixSuccess = new Tooltip(resourceBundle.getString("health.fix.successTip"));
this.fixFail = new Tooltip(resourceBundle.getString("health.fix.failTip"));
fixSuccess.setShowDelay(Duration.millis(100));
fixFail.setShowDelay(Duration.millis(100));
this.fixStateTip = new Tooltip();
fixStateTip.textProperty().bind(fixState.map(this::getFixStateDescription));
fixStateTip.setShowDelay(Duration.millis(100));
this.severityTip = new Tooltip();
severityTip.textProperty().bind(severity.map(this::getSeverityDescription));
severityTip.setShowDelay(Duration.millis(150));
}
public FontAwesome5Icon getFixGlyph(Result.FixState state) {
return switch (state) {
case FIXING -> FontAwesome5Icon.SPINNER;
case FIXED -> FontAwesome5Icon.CHECK;
case FIX_FAILED -> FontAwesome5Icon.TIMES;
default -> null;
};
}
private String getFixStateDescription(Result.FixState fixState) {
return switch (fixState) {
case FIXED -> resourceBundle.getString("health.fix.successTip");
case FIX_FAILED -> resourceBundle.getString("health.fix.failTip");
default -> "";
};
}
private String getSeverityDescription(DiagnosticResult.Severity severity) {
return resourceBundle.getString(switch (severity) {
case GOOD -> "health.result.severityTip.good";
case INFO -> "health.result.severityTip.info";
case WARN -> "health.result.severityTip.warn";
case CRITICAL -> "health.result.severityTip.crit";
});
}
@FXML
@@ -93,22 +122,19 @@ public class ResultListCellController implements FxController {
.onCondition(fixing) //
.afterStop(() -> fixView.setRotate(0)) //
.build();
fixState.addListener(((observable, oldValue, newValue) -> {
if (newValue == Result.FixState.FIXED || newValue == Result.FixState.FIX_FAILED) {
Tooltip.install(fixView, fixStateTip);
}
}));
Tooltip.install(severityView, severityTip);
}
@FXML
public void fix() {
Result r = result.get();
if (r != null) {
fixApplier.fix(r).whenCompleteAsync(this::fixFinished, Platform::runLater);
}
}
private void fixFinished(Void unused, Throwable exception) {
if (exception != null) {
LOG.error("Failed to apply fix", exception);
Tooltip.install(fixView, fixFail);
} else {
Tooltip.install(fixView, fixSuccess);
fixApplier.fix(r);
}
}
@@ -152,20 +178,12 @@ public class ResultListCellController implements FxController {
};
}
public ObjectBinding<FontAwesome5Icon> fixGlyphProperty() {
public ObservableValue<FontAwesome5Icon> fixGlyphProperty() {
return fixGlyph;
}
public FontAwesome5Icon getFixGlyph() {
if (fixState.getValue() == null) {
return null;
}
return switch (fixState.getValue()) {
case NOT_FIXABLE, FIXABLE -> null;
case FIXING -> FontAwesome5Icon.SPINNER;
case FIXED -> FontAwesome5Icon.CHECK;
case FIX_FAILED -> FontAwesome5Icon.TIMES;
};
return fixGlyph.getValue();
}
public BooleanBinding fixableProperty() {

View File

@@ -9,8 +9,10 @@ import javax.inject.Inject;
import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.DoubleBinding;
import javafx.beans.binding.LongBinding;
import javafx.beans.property.LongProperty;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
@@ -21,6 +23,7 @@ import javafx.scene.chart.XYChart.Series;
import javafx.stage.Stage;
import javafx.util.Duration;
import java.util.Arrays;
import java.util.concurrent.Callable;
@VaultStatisticsScoped
public class VaultStatisticsController implements FxController {
@@ -32,6 +35,7 @@ public class VaultStatisticsController implements FxController {
private final VaultStats stats;
private final Series<Number, Number> readData;
private final Series<Number, Number> writeData;
private final Series<Number, Number> accessData;
private final Timeline ioAnimation;
private final LongBinding bpsRead;
private final LongBinding bpsWritten;
@@ -44,15 +48,20 @@ public class VaultStatisticsController implements FxController {
private final LongBinding totalBytesDecrypted;
private final LongBinding filesRead;
private final LongBinding filesWritten;
private final LongBinding filesAccessed;
private final LongBinding totalFilesAccessed;
private final LongBinding bpsEncrypted;
private final LongBinding bpsDecrypted;
public AreaChart<Number, Number> readChart;
public AreaChart<Number, Number> writeChart;
public AreaChart<Number, Number> accessChart;
public NumberAxis readChartXAxis;
public NumberAxis readChartYAxis;
public NumberAxis writeChartXAxis;
public NumberAxis writeChartYAxis;
public NumberAxis accessChartXAxis;
public NumberAxis accessChartYAxis;
@Inject
public VaultStatisticsController(VaultStatisticsComponent component, @VaultStatisticsWindow Stage window, @VaultStatisticsWindow Vault vault) {
@@ -60,6 +69,7 @@ public class VaultStatisticsController implements FxController {
this.stats = vault.getStats();
this.readData = new Series<>();
this.writeData = new Series<>();
this.accessData = new Series<>();
this.bpsRead = WeakBindings.bindLong(stats.bytesPerSecondReadProperty());
this.bpsWritten = WeakBindings.bindLong(stats.bytesPerSecondWrittenProperty());
this.cacheHitRate = WeakBindings.bindDouble(stats.cacheHitRateProperty());
@@ -71,11 +81,13 @@ public class VaultStatisticsController implements FxController {
this.totalBytesEncrypted = WeakBindings.bindLong(stats.totalBytesEncryptedProperty());
this.filesRead = WeakBindings.bindLong(stats.filesRead());
this.filesWritten = WeakBindings.bindLong(stats.filesWritten());
this.filesAccessed = WeakBindings.bindLong(stats.filesAccessed());
this.totalFilesAccessed = WeakBindings.bindLong(stats.totalFilesAccessed());
this.bpsEncrypted = WeakBindings.bindLong(stats.bytesPerSecondEncryptedProperty());
this.bpsDecrypted = WeakBindings.bindLong(stats.bytesPerSecondDecryptedProperty());
this.ioAnimation = new Timeline(); //TODO Research better timer
ioAnimation.getKeyFrames().add(new KeyFrame(Duration.seconds(IO_SAMPLING_INTERVAL), new IoSamplingAnimationHandler(readData, writeData)));
ioAnimation.getKeyFrames().add(new KeyFrame(Duration.seconds(IO_SAMPLING_INTERVAL), new IoSamplingAnimationHandler(readData, writeData, accessData)));
ioAnimation.setCycleCount(Animation.INDEFINITE);
ioAnimation.play();
@@ -89,6 +101,7 @@ public class VaultStatisticsController implements FxController {
public void initialize() {
readChart.getData().addAll(readData);
writeChart.getData().addAll(writeData);
accessChart.getData().addAll(accessData);
}
private class IoSamplingAnimationHandler implements EventHandler<ActionEvent> {
@@ -96,16 +109,21 @@ public class VaultStatisticsController implements FxController {
private long step = IO_SAMPLING_STEPS;
private final Series<Number, Number> decryptedBytesRead;
private final Series<Number, Number> encryptedBytesWrite;
private final Series<Number, Number> accessedFiles;
private final long[] maxBuf = new long[IO_SAMPLING_STEPS];
private final long[] maxAccessBuf = new long[IO_SAMPLING_STEPS];
public IoSamplingAnimationHandler(Series<Number, Number> readData, Series<Number, Number> writeData) {
public IoSamplingAnimationHandler(Series<Number, Number> readData, Series<Number, Number> writeData, Series<Number, Number> accessData) {
this.decryptedBytesRead = readData;
this.encryptedBytesWrite = writeData;
this.accessedFiles = accessData;
// initialize data once and change value of data points later:
for (int i = 0; i < IO_SAMPLING_STEPS; i++) {
decryptedBytesRead.getData().add(new Data<>(i, 0));
encryptedBytesWrite.getData().add(new Data<>(i, 0));
accessedFiles.getData().add(new Data<>(i, 0));
}
}
@@ -114,17 +132,22 @@ public class VaultStatisticsController implements FxController {
final long currentStep = step++;
final long decBytes = stats.bytesPerSecondReadProperty().get();
final long encBytes = stats.bytesPerSecondWrittenProperty().get();
final long accFiles = stats.filesAccessed().get();
maxBuf[(int) currentStep % IO_SAMPLING_STEPS] = Math.max(decBytes, encBytes);
long allTimeMax = Arrays.stream(maxBuf).max().orElse(0l);
long allTimeMax = Arrays.stream(maxBuf).max().orElse(0L);
maxAccessBuf[(int) currentStep % IO_SAMPLING_STEPS] = accFiles;
long allTimeMaxAccessedFiles = Arrays.stream(maxAccessBuf).max().orElse(0L);
// remove oldest value:
decryptedBytesRead.getData().remove(0);
encryptedBytesWrite.getData().remove(0);
accessedFiles.getData().remove(0);
// add latest value:
decryptedBytesRead.getData().add(new Data<>(currentStep, decBytes));
encryptedBytesWrite.getData().add(new Data<>(currentStep, encBytes));
accessedFiles.getData().add(new Data<>(currentStep, accFiles));
// adjust ranges:
readChartXAxis.setLowerBound(currentStep - IO_SAMPLING_STEPS * 1.0);
@@ -133,6 +156,9 @@ public class VaultStatisticsController implements FxController {
writeChartXAxis.setLowerBound(currentStep - IO_SAMPLING_STEPS * 1.0);
writeChartXAxis.setUpperBound(currentStep);
writeChartYAxis.setUpperBound(allTimeMax);
accessChartXAxis.setLowerBound(currentStep - IO_SAMPLING_STEPS * 1.0);
accessChartXAxis.setUpperBound(currentStep);
accessChartYAxis.setUpperBound(allTimeMaxAccessedFiles);
}
}
@@ -207,4 +233,12 @@ public class VaultStatisticsController implements FxController {
public LongBinding filesWrittenProperty() {return filesWritten;}
public long getFilesWritten() {return filesWritten.get();}
public LongBinding filesAccessedProperty() {return filesAccessed;}
public long getFilesAccessed() {return filesAccessed.get();}
public LongBinding totalFilesAccessedProperty() {return totalFilesAccessed;}
public long getTotalFilesAccessed() {return totalFilesAccessed.get();}
}

View File

@@ -1,5 +1,6 @@
package org.cryptomator.ui.traymenu;
import com.google.common.base.Preconditions;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.integrations.common.CheckAvailability;
import org.cryptomator.integrations.common.Priority;
@@ -19,6 +20,8 @@ import java.awt.PopupMenu;
import java.awt.SystemTray;
import java.awt.Toolkit;
import java.awt.TrayIcon;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.List;
@CheckAvailability
@@ -28,6 +31,7 @@ public class AwtTrayMenuController implements TrayMenuController {
private static final Logger LOG = LoggerFactory.getLogger(AwtTrayMenuController.class);
private final PopupMenu menu = new PopupMenu();
private TrayIcon trayIcon;
@CheckAvailability
public static boolean isAvailable() {
@@ -37,7 +41,7 @@ public class AwtTrayMenuController implements TrayMenuController {
@Override
public void showTrayIcon(byte[] rawImageData, Runnable defaultAction, String tooltip) throws TrayMenuException {
var image = Toolkit.getDefaultToolkit().createImage(rawImageData);
var trayIcon = new TrayIcon(image, tooltip, menu);
trayIcon = new TrayIcon(image, tooltip, menu);
trayIcon.setImageAutoSize(true);
if (SystemUtils.IS_OS_WINDOWS) {
@@ -59,8 +63,14 @@ public class AwtTrayMenuController implements TrayMenuController {
}
@Override
public void onBeforeOpenMenu(Runnable runnable) {
public void onBeforeOpenMenu(Runnable listener) {
Preconditions.checkNotNull(this.trayIcon);
this.trayIcon.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
listener.run();
}
});
}
private void addChildren(Menu menu, List<TrayMenuItem> items) {

View File

@@ -3,6 +3,7 @@ package org.cryptomator.ui.traymenu;
import com.google.common.base.Preconditions;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultListManager;
import org.cryptomator.integrations.tray.ActionItem;
import org.cryptomator.integrations.tray.SeparatorItem;
import org.cryptomator.integrations.tray.SubMenuItem;
@@ -63,6 +64,11 @@ public class TrayMenuBuilder {
try (var image = getClass().getResourceAsStream(SystemUtils.IS_OS_MAC_OSX ? TRAY_ICON_MAC : TRAY_ICON)) {
trayMenu.showTrayIcon(image.readAllBytes(), this::showMainWindow, "Cryptomator");
trayMenu.onBeforeOpenMenu(() -> {
for (Vault vault : vaults) {
VaultListManager.redetermineVaultState(vault);
}
});
rebuildMenu();
initialized = true;
} catch (IOException e) {