mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-05-19 11:11:28 +00:00
Merge branch 'develop' into feature/stats-counter-for-metadata
# Conflicts: # pom.xml
This commit is contained in:
@@ -1,60 +1,42 @@
|
||||
import ch.qos.logback.classic.spi.Configurator;
|
||||
import org.cryptomator.integrations.tray.TrayMenuController;
|
||||
import org.cryptomator.logging.LogbackConfiguratorFactory;
|
||||
import org.cryptomator.ui.traymenu.AwtTrayMenuController;
|
||||
|
||||
module org.cryptomator.desktop {
|
||||
open module org.cryptomator.desktop {
|
||||
requires static org.jetbrains.annotations;
|
||||
|
||||
requires org.cryptomator.cryptolib;
|
||||
requires org.cryptomator.cryptofs;
|
||||
requires org.cryptomator.frontend.dokany;
|
||||
requires org.cryptomator.frontend.fuse;
|
||||
requires org.cryptomator.frontend.webdav;
|
||||
requires org.cryptomator.integrations.api;
|
||||
// jdk:
|
||||
requires java.desktop;
|
||||
requires java.net.http;
|
||||
requires javafx.base;
|
||||
requires javafx.graphics;
|
||||
requires javafx.controls;
|
||||
requires javafx.fxml;
|
||||
requires com.tobiasdiez.easybind;
|
||||
requires jdk.crypto.ec;
|
||||
// 3rd party:
|
||||
requires ch.qos.logback.classic;
|
||||
requires ch.qos.logback.core;
|
||||
requires com.auth0.jwt;
|
||||
requires com.google.common;
|
||||
requires com.google.gson;
|
||||
requires com.nimbusds.jose.jwt;
|
||||
requires com.nulabinc.zxcvbn;
|
||||
requires com.tobiasdiez.easybind;
|
||||
requires dagger;
|
||||
requires io.github.coffeelibs.tinyoauth2client;
|
||||
requires org.slf4j;
|
||||
requires org.apache.commons.lang3;
|
||||
requires dagger;
|
||||
requires com.auth0.jwt;
|
||||
|
||||
/* TODO: filename-based modules: */
|
||||
requires static javax.inject; /* ugly dagger/guava crap */
|
||||
requires logback.classic;
|
||||
requires logback.core;
|
||||
|
||||
exports org.cryptomator.ui.traymenu to org.cryptomator.integrations.api;
|
||||
provides TrayMenuController with AwtTrayMenuController;
|
||||
|
||||
opens org.cryptomator.common.settings to com.google.gson;
|
||||
|
||||
opens org.cryptomator.launcher to javafx.graphics;
|
||||
|
||||
opens org.cryptomator.common to javafx.fxml;
|
||||
opens org.cryptomator.common.vaults to javafx.fxml;
|
||||
opens org.cryptomator.ui.addvaultwizard to javafx.fxml;
|
||||
opens org.cryptomator.ui.changepassword to javafx.fxml;
|
||||
opens org.cryptomator.ui.common to javafx.fxml;
|
||||
opens org.cryptomator.ui.controls to javafx.fxml;
|
||||
opens org.cryptomator.ui.forgetPassword to javafx.fxml;
|
||||
opens org.cryptomator.ui.fxapp to javafx.fxml;
|
||||
opens org.cryptomator.ui.health to javafx.fxml;
|
||||
opens org.cryptomator.ui.keyloading.masterkeyfile to javafx.fxml;
|
||||
opens org.cryptomator.ui.lock to javafx.fxml;
|
||||
opens org.cryptomator.ui.mainwindow to javafx.fxml;
|
||||
opens org.cryptomator.ui.migration to javafx.fxml;
|
||||
opens org.cryptomator.ui.preferences to javafx.fxml;
|
||||
opens org.cryptomator.ui.quit to javafx.fxml;
|
||||
opens org.cryptomator.ui.recoverykey to javafx.fxml;
|
||||
opens org.cryptomator.ui.removevault to javafx.fxml;
|
||||
opens org.cryptomator.ui.stats to javafx.fxml;
|
||||
opens org.cryptomator.ui.unlock to javafx.fxml;
|
||||
opens org.cryptomator.ui.vaultoptions to javafx.fxml;
|
||||
opens org.cryptomator.ui.wrongfilealert to javafx.fxml;
|
||||
provides Configurator with LogbackConfiguratorFactory;
|
||||
}
|
||||
110
src/main/java/org/cryptomator/common/CatchingExecutors.java
Normal file
110
src/main/java/org/cryptomator/common/CatchingExecutors.java
Normal file
@@ -0,0 +1,110 @@
|
||||
package org.cryptomator.common;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.concurrent.Task;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.ScheduledThreadPoolExecutor;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
//Inspired by: https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/ThreadPoolExecutor.html#afterExecute(java.lang.Runnable,java.lang.Throwable)
|
||||
public final class CatchingExecutors {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(CatchingExecutors.class);
|
||||
|
||||
private CatchingExecutors() { /* NO-OP */ }
|
||||
|
||||
public static class CatchingScheduledThreadPoolExecutor extends ScheduledThreadPoolExecutor {
|
||||
|
||||
public CatchingScheduledThreadPoolExecutor(int corePoolSize, ThreadFactory threadFactory) {
|
||||
super(corePoolSize, threadFactory);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) {
|
||||
Runnable oneShot = () -> this.execute(command);
|
||||
return super.scheduleAtFixedRate(oneShot, initialDelay, period, unit);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) {
|
||||
Runnable oneShot = () -> this.execute(command);
|
||||
return super.scheduleWithFixedDelay(oneShot, initialDelay, delay, unit);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void afterExecute(Runnable runnable, Throwable throwable) {
|
||||
super.afterExecute(runnable, throwable);
|
||||
afterExecuteInternal(runnable, throwable);
|
||||
}
|
||||
}
|
||||
|
||||
public static class CatchingThreadPoolExecutor extends ThreadPoolExecutor {
|
||||
|
||||
public CatchingThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) {
|
||||
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void afterExecute(Runnable runnable, Throwable throwable) {
|
||||
super.afterExecute(runnable, throwable);
|
||||
afterExecuteInternal(runnable, throwable);
|
||||
}
|
||||
}
|
||||
|
||||
private static void afterExecuteInternal(Runnable runnable, Throwable throwable) {
|
||||
if (throwable != null) {
|
||||
callHandler(Thread.currentThread(), throwable);
|
||||
} else if (runnable instanceof Task<?> t) {
|
||||
afterExecuteTask(t);
|
||||
} else if (runnable instanceof Future<?> f) {
|
||||
afterExecuteFuture(f);
|
||||
}
|
||||
//Errors in this method are delegated to the UncaughtExceptionHandler of the current thread
|
||||
}
|
||||
|
||||
private static void callHandler(Thread thread, Throwable throwable) {
|
||||
Objects.requireNonNullElseGet(thread.getUncaughtExceptionHandler(), CatchingExecutors::fallbackHandler).uncaughtException(thread, throwable);
|
||||
}
|
||||
|
||||
private static Thread.UncaughtExceptionHandler fallbackHandler() {
|
||||
return (thread, throwable) -> LOG.error("FALLBACK: Uncaught exception in " + thread.getName(), throwable);
|
||||
}
|
||||
|
||||
private static void afterExecuteTask(Task<?> task) {
|
||||
var caller = Thread.currentThread();
|
||||
Platform.runLater(() -> {
|
||||
if (task.getOnFailed() == null) {
|
||||
callHandler(caller, task.getException());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void afterExecuteFuture(Future<?> future) {
|
||||
if (future instanceof ScheduledFuture<?> && !future.isDone()) {
|
||||
//we assume that this must be a repeated ScheduledFutureTask, where the done-status is only set when not executed anymore
|
||||
//see also https://github.com/cryptomator/cryptomator/pull/2422
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
future.get();
|
||||
} catch (CancellationException ce) {
|
||||
//Ignore
|
||||
} catch (ExecutionException ee) {
|
||||
callHandler(Thread.currentThread(), ee.getCause());
|
||||
} catch (InterruptedException ie) {
|
||||
//Ignore/Reset
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,9 +12,7 @@ import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.common.keychain.KeychainModule;
|
||||
import org.cryptomator.common.settings.Settings;
|
||||
import org.cryptomator.common.settings.SettingsProvider;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.common.vaults.VaultComponent;
|
||||
import org.cryptomator.common.vaults.VaultListManager;
|
||||
import org.cryptomator.common.vaults.VaultListModule;
|
||||
import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
|
||||
import org.cryptomator.frontend.webdav.WebDavServer;
|
||||
@@ -25,16 +23,14 @@ import javax.inject.Named;
|
||||
import javax.inject.Singleton;
|
||||
import javafx.beans.binding.Binding;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Comparator;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.SynchronousQueue;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
@@ -46,6 +42,12 @@ public abstract class CommonsModule {
|
||||
private static final int NUM_CORE_BG_THREADS = 6;
|
||||
private static final long BG_THREAD_KEEPALIVE_SECONDS = 60l;
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
static Environment provideEnvironment() {
|
||||
return Environment.getInstance();
|
||||
}
|
||||
|
||||
@SuppressWarnings("SpellCheckingInspection")
|
||||
@Provides
|
||||
@Singleton
|
||||
@@ -93,7 +95,7 @@ public abstract class CommonsModule {
|
||||
@Singleton
|
||||
static ScheduledExecutorService provideScheduledExecutorService(ShutdownHook shutdownHook) {
|
||||
final AtomicInteger threadNumber = new AtomicInteger(1);
|
||||
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(NUM_SCHEDULER_THREADS, r -> {
|
||||
ScheduledExecutorService executorService = new CatchingExecutors.CatchingScheduledThreadPoolExecutor(NUM_SCHEDULER_THREADS, r -> {
|
||||
String name = String.format("App Scheduled Executor %02d", threadNumber.getAndIncrement());
|
||||
Thread t = new Thread(r);
|
||||
t.setName(name);
|
||||
@@ -110,7 +112,7 @@ public abstract class CommonsModule {
|
||||
@Singleton
|
||||
static ExecutorService provideExecutorService(ShutdownHook shutdownHook) {
|
||||
final AtomicInteger threadNumber = new AtomicInteger(1);
|
||||
ExecutorService executorService = new ThreadPoolExecutor(NUM_CORE_BG_THREADS, Integer.MAX_VALUE, BG_THREAD_KEEPALIVE_SECONDS, TimeUnit.SECONDS, new SynchronousQueue<>(), r -> {
|
||||
ExecutorService executorService = new CatchingExecutors.CatchingThreadPoolExecutor(NUM_CORE_BG_THREADS, Integer.MAX_VALUE, BG_THREAD_KEEPALIVE_SECONDS, TimeUnit.SECONDS, new SynchronousQueue<>(), r -> {
|
||||
String name = String.format("App Background Thread %03d", threadNumber.getAndIncrement());
|
||||
Thread t = new Thread(r);
|
||||
t.setName(name);
|
||||
@@ -129,16 +131,16 @@ public abstract class CommonsModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
static Binding<InetSocketAddress> provideServerSocketAddressBinding(Settings settings) {
|
||||
return Bindings.createObjectBinding(() -> {
|
||||
static ObservableValue<InetSocketAddress> provideServerSocketAddressBinding(Settings settings) {
|
||||
return settings.port().map(port -> {
|
||||
String host = SystemUtils.IS_OS_WINDOWS ? "127.0.0.1" : "localhost";
|
||||
return InetSocketAddress.createUnresolved(host, settings.port().intValue());
|
||||
}, settings.port());
|
||||
});
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
static WebDavServer provideWebDavServer(Binding<InetSocketAddress> serverSocketAddressBinding) {
|
||||
static WebDavServer provideWebDavServer(ObservableValue<InetSocketAddress> serverSocketAddressBinding) {
|
||||
WebDavServer server = WebDavServer.create();
|
||||
// no need to unsubscribe eventually, because server is a singleton
|
||||
EasyBind.subscribe(serverSocketAddressBinding, server::bind);
|
||||
|
||||
@@ -6,6 +6,7 @@ public interface Constants {
|
||||
String MASTERKEY_BACKUP_SUFFIX = ".bkup";
|
||||
String VAULTCONFIG_FILENAME = "vault.cryptomator";
|
||||
String CRYPTOMATOR_FILENAME_EXT = ".cryptomator";
|
||||
String CRYPTOMATOR_FILENAME_GLOB = "*.cryptomator";
|
||||
byte[] PEPPER = new byte[0];
|
||||
|
||||
}
|
||||
|
||||
@@ -5,8 +5,6 @@ import com.google.common.base.Strings;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
@@ -17,31 +15,57 @@ import java.util.function.Predicate;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
@Singleton
|
||||
public class Environment {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(Environment.class);
|
||||
private static final Path RELATIVE_HOME_DIR = Paths.get("~");
|
||||
private static final char PATH_LIST_SEP = ':';
|
||||
private static final int DEFAULT_MIN_PW_LENGTH = 8;
|
||||
private static final String SETTINGS_PATH_PROP_NAME = "cryptomator.settingsPath";
|
||||
private static final String IPC_SOCKET_PATH_PROP_NAME = "cryptomator.ipcSocketPath";
|
||||
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";
|
||||
private static final String BUILD_NUMBER_PROP_NAME = "cryptomator.buildNumber";
|
||||
private static final String PLUGIN_DIR_PROP_NAME = "cryptomator.pluginDir";
|
||||
private static final String TRAY_ICON_PROP_NAME = "cryptomator.showTrayIcon";
|
||||
|
||||
@Inject
|
||||
public Environment() {
|
||||
LOG.debug("user.home: {}", System.getProperty("user.home"));
|
||||
LOG.debug("java.library.path: {}", System.getProperty("java.library.path"));
|
||||
LOG.debug("user.language: {}", System.getProperty("user.language"));
|
||||
LOG.debug("user.region: {}", System.getProperty("user.region"));
|
||||
LOG.debug("logback.configurationFile: {}", System.getProperty("logback.configurationFile"));
|
||||
LOG.debug("cryptomator.settingsPath: {}", System.getProperty("cryptomator.settingsPath"));
|
||||
LOG.debug("cryptomator.ipcSocketPath: {}", System.getProperty("cryptomator.ipcSocketPath"));
|
||||
LOG.debug("cryptomator.keychainPath: {}", System.getProperty("cryptomator.keychainPath"));
|
||||
LOG.debug("cryptomator.logDir: {}", System.getProperty("cryptomator.logDir"));
|
||||
LOG.debug("cryptomator.pluginDir: {}", System.getProperty("cryptomator.pluginDir"));
|
||||
LOG.debug("cryptomator.mountPointsDir: {}", System.getProperty("cryptomator.mountPointsDir"));
|
||||
LOG.debug("cryptomator.minPwLength: {}", System.getProperty("cryptomator.minPwLength"));
|
||||
LOG.debug("cryptomator.appVersion: {}", System.getProperty("cryptomator.appVersion"));
|
||||
LOG.debug("cryptomator.buildNumber: {}", System.getProperty("cryptomator.buildNumber"));
|
||||
LOG.debug("cryptomator.showTrayIcon: {}", System.getProperty("cryptomator.showTrayIcon"));
|
||||
private Environment() {}
|
||||
|
||||
public void log() {
|
||||
LOG.info("user.home: {}", System.getProperty("user.home"));
|
||||
LOG.info("java.library.path: {}", System.getProperty("java.library.path"));
|
||||
LOG.info("user.language: {}", System.getProperty("user.language"));
|
||||
LOG.info("user.region: {}", System.getProperty("user.region"));
|
||||
LOG.info("logback.configurationFile: {}", System.getProperty("logback.configurationFile"));
|
||||
logCryptomatorSystemProperty(SETTINGS_PATH_PROP_NAME);
|
||||
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);
|
||||
logCryptomatorSystemProperty(APP_VERSION_PROP_NAME);
|
||||
logCryptomatorSystemProperty(BUILD_NUMBER_PROP_NAME);
|
||||
logCryptomatorSystemProperty(TRAY_ICON_PROP_NAME);
|
||||
logCryptomatorSystemProperty(P12_PATH_PROP_NAME);
|
||||
}
|
||||
|
||||
public static Environment getInstance() {
|
||||
final class Holder {
|
||||
|
||||
private static final Environment INSTANCE = new Environment();
|
||||
}
|
||||
return Holder.INSTANCE;
|
||||
}
|
||||
|
||||
private void logCryptomatorSystemProperty(String propertyName) {
|
||||
LOG.info("{}: {}", propertyName, System.getProperty(propertyName));
|
||||
}
|
||||
|
||||
public boolean useCustomLogbackConfig() {
|
||||
@@ -49,52 +73,56 @@ public class Environment {
|
||||
}
|
||||
|
||||
public Stream<Path> getSettingsPath() {
|
||||
return getPaths("cryptomator.settingsPath");
|
||||
return getPaths(SETTINGS_PATH_PROP_NAME);
|
||||
}
|
||||
|
||||
public Stream<Path> getP12Path() {
|
||||
return getPaths(P12_PATH_PROP_NAME);
|
||||
}
|
||||
|
||||
public Stream<Path> ipcSocketPath() {
|
||||
return getPaths("cryptomator.ipcSocketPath");
|
||||
return getPaths(IPC_SOCKET_PATH_PROP_NAME);
|
||||
}
|
||||
|
||||
public Stream<Path> getKeychainPath() {
|
||||
return getPaths("cryptomator.keychainPath");
|
||||
return getPaths(KEYCHAIN_PATHS_PROP_NAME);
|
||||
}
|
||||
|
||||
public Optional<Path> getLogDir() {
|
||||
return getPath("cryptomator.logDir").map(this::replaceHomeDir);
|
||||
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("cryptomator.pluginDir").map(this::replaceHomeDir);
|
||||
return getPath(PLUGIN_DIR_PROP_NAME).map(this::replaceHomeDir);
|
||||
}
|
||||
|
||||
public Optional<Path> getMountPointsDir() {
|
||||
return getPath("cryptomator.mountPointsDir").map(this::replaceHomeDir);
|
||||
return getPath(MOUNTPOINT_DIR_PROP_NAME).map(this::replaceHomeDir);
|
||||
}
|
||||
|
||||
public Optional<String> getAppVersion() {
|
||||
return Optional.ofNullable(System.getProperty("cryptomator.appVersion"));
|
||||
/**
|
||||
* Returns the app version defined in the {@value APP_VERSION_PROP_NAME} property or returns "SNAPSHOT".
|
||||
*
|
||||
* @return App version or "SNAPSHOT", if undefined
|
||||
*/
|
||||
public String getAppVersion() {
|
||||
return System.getProperty(APP_VERSION_PROP_NAME, "SNAPSHOT");
|
||||
}
|
||||
|
||||
public Optional<String> getBuildNumber() {
|
||||
return Optional.ofNullable(System.getProperty("cryptomator.buildNumber"));
|
||||
return Optional.ofNullable(System.getProperty(BUILD_NUMBER_PROP_NAME));
|
||||
}
|
||||
|
||||
public int getMinPwLength() {
|
||||
return getInt("cryptomator.minPwLength", DEFAULT_MIN_PW_LENGTH);
|
||||
return Integer.getInteger(MIN_PW_LENGTH_PROP_NAME, DEFAULT_MIN_PW_LENGTH);
|
||||
}
|
||||
|
||||
public boolean showTrayIcon() {
|
||||
return Boolean.getBoolean("cryptomator.showTrayIcon");
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
return Boolean.getBoolean(TRAY_ICON_PROP_NAME);
|
||||
}
|
||||
|
||||
private Optional<Path> getPath(String propertyName) {
|
||||
|
||||
@@ -10,6 +10,7 @@ import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.binding.StringBinding;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import java.util.Optional;
|
||||
|
||||
@Singleton
|
||||
@@ -18,7 +19,7 @@ public class LicenseHolder {
|
||||
private final Settings settings;
|
||||
private final LicenseChecker licenseChecker;
|
||||
private final ObjectProperty<DecodedJWT> validJwtClaims;
|
||||
private final StringBinding licenseSubject;
|
||||
private final ObservableValue<String> licenseSubject;
|
||||
private final BooleanBinding validLicenseProperty;
|
||||
|
||||
@Inject
|
||||
@@ -26,7 +27,7 @@ public class LicenseHolder {
|
||||
this.settings = settings;
|
||||
this.licenseChecker = licenseChecker;
|
||||
this.validJwtClaims = new SimpleObjectProperty<>();
|
||||
this.licenseSubject = Bindings.createStringBinding(this::getLicenseSubject, validJwtClaims);
|
||||
this.licenseSubject = validJwtClaims.map(DecodedJWT::getSubject);
|
||||
this.validLicenseProperty = validJwtClaims.isNotNull();
|
||||
|
||||
Optional<DecodedJWT> claims = licenseChecker.check(settings.licenseKey().get());
|
||||
@@ -55,17 +56,12 @@ public class LicenseHolder {
|
||||
}
|
||||
}
|
||||
|
||||
public StringBinding licenseSubjectProperty() {
|
||||
public ObservableValue<String> licenseSubjectProperty() {
|
||||
return licenseSubject;
|
||||
}
|
||||
|
||||
public String getLicenseSubject() {
|
||||
DecodedJWT claims = validJwtClaims.get();
|
||||
if (claims != null) {
|
||||
return claims.getSubject();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
return licenseSubject.getValue();
|
||||
}
|
||||
|
||||
public BooleanBinding validLicenseProperty() {
|
||||
|
||||
64
src/main/java/org/cryptomator/common/LocationPreset.java
Normal file
64
src/main/java/org/cryptomator/common/LocationPreset.java
Normal file
@@ -0,0 +1,64 @@
|
||||
package org.cryptomator.common;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Enum of common cloud providers and their default local storage location path.
|
||||
*/
|
||||
public enum LocationPreset {
|
||||
|
||||
DROPBOX("Dropbox", "~/Dropbox"),
|
||||
ICLOUDDRIVE("iCloud Drive", "~/Library/Mobile Documents/com~apple~CloudDocs", "~/iCloudDrive"),
|
||||
GDRIVE("Google Drive", "~/Google Drive/My Drive", "~/Google Drive"),
|
||||
MEGA("MEGA", "~/MEGA"),
|
||||
ONEDRIVE("OneDrive", "~/OneDrive"),
|
||||
PCLOUD("pCloud", "~/pCloudDrive"),
|
||||
|
||||
LOCAL("local");
|
||||
|
||||
private final String name;
|
||||
private final List<Path> candidates;
|
||||
|
||||
LocationPreset(String name, String... candidates) {
|
||||
this.name = name;
|
||||
this.candidates = Arrays.stream(candidates).map(UserHome::resolve).map(Path::of).toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for this LocationPreset if any of the associated paths exist.
|
||||
*
|
||||
* @return the first existing path or null, if none exists.
|
||||
*/
|
||||
public Path existingPath() {
|
||||
return candidates.stream().filter(Files::isDirectory).findFirst().orElse(null);
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getDisplayName();
|
||||
}
|
||||
|
||||
//this contruct is needed, since static members are initialized after every enum member is initialized
|
||||
//TODO: refactor this to normal class and use this also in different parts of the project
|
||||
private static class UserHome {
|
||||
|
||||
private static final String USER_HOME = System.getProperty("user.home");
|
||||
|
||||
private static String resolve(String path) {
|
||||
if (path.startsWith("~/")) {
|
||||
return UserHome.USER_HOME + path.substring(1);
|
||||
} else {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -23,11 +23,14 @@ public class KeychainModule {
|
||||
@Singleton
|
||||
static ObjectExpression<KeychainAccessProvider> provideKeychainAccessProvider(Settings settings, List<KeychainAccessProvider> providers) {
|
||||
return Bindings.createObjectBinding(() -> {
|
||||
if (!settings.useKeychain().get()) {
|
||||
return null;
|
||||
}
|
||||
var selectedProviderClass = settings.keychainProvider().get();
|
||||
var selectedProvider = providers.stream().filter(provider -> provider.getClass().getName().equals(selectedProviderClass)).findAny();
|
||||
var fallbackProvider = providers.stream().findFirst().orElse(null);
|
||||
return selectedProvider.orElse(fallbackProvider);
|
||||
}, settings.keychainProvider());
|
||||
}, settings.keychainProvider(), settings.useKeychain());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package org.cryptomator.common.mountpoint;
|
||||
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.common.Environment;
|
||||
import org.cryptomator.common.settings.VaultSettings;
|
||||
import org.cryptomator.common.settings.VolumeImpl;
|
||||
import org.cryptomator.common.vaults.MountPointRequirement;
|
||||
@@ -19,7 +18,6 @@ import java.nio.file.NoSuchFileException;
|
||||
import java.nio.file.NotDirectoryException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.Optional;
|
||||
|
||||
class CustomMountPointChooser implements MountPointChooser {
|
||||
@@ -118,6 +116,7 @@ class CustomMountPointChooser implements MountPointChooser {
|
||||
if (caller.getMountPointRequirement() == MountPointRequirement.PARENT_NO_MOUNT_POINT) {
|
||||
Path hideaway = getHideaway(mountPoint);
|
||||
try {
|
||||
waitForMountpointRestoration(mountPoint);
|
||||
Files.move(hideaway, mountPoint);
|
||||
if (SystemUtils.IS_OS_WINDOWS) {
|
||||
Files.setAttribute(mountPoint, WIN_HIDDEN, false);
|
||||
@@ -128,6 +127,24 @@ class CustomMountPointChooser implements MountPointChooser {
|
||||
}
|
||||
}
|
||||
|
||||
//on Windows removing the mountpoint takes some time, so we poll for at most 3 seconds
|
||||
private void waitForMountpointRestoration(Path mountPoint) throws FileAlreadyExistsException {
|
||||
int attempts = 0;
|
||||
while (!Files.notExists(mountPoint, LinkOption.NOFOLLOW_LINKS)) {
|
||||
attempts++;
|
||||
if (attempts >= 10) {
|
||||
throw new FileAlreadyExistsException("Timeout waiting for mountpoint cleanup for " + mountPoint + " .");
|
||||
}
|
||||
|
||||
try {
|
||||
Thread.sleep(300);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new FileAlreadyExistsException("Interrupted before mountpoint " + mountPoint + " was cleared");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void checkIsDirectory(Path toCheck) throws InvalidMountPointException {
|
||||
if (!Files.isDirectory(toCheck, LinkOption.NOFOLLOW_LINKS)) {
|
||||
throw new InvalidMountPointException(new NotDirectoryException(toCheck.toString()));
|
||||
|
||||
99
src/main/java/org/cryptomator/common/settings/DeviceKey.java
Normal file
99
src/main/java/org/cryptomator/common/settings/DeviceKey.java
Normal file
@@ -0,0 +1,99 @@
|
||||
package org.cryptomator.common.settings;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.base.Suppliers;
|
||||
import org.cryptomator.common.Environment;
|
||||
import org.cryptomator.common.keychain.KeychainManager;
|
||||
import org.cryptomator.cryptolib.common.P384KeyPair;
|
||||
import org.cryptomator.cryptolib.common.Pkcs12Exception;
|
||||
import org.cryptomator.integrations.keychain.KeychainAccessException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import java.io.IOException;
|
||||
import java.nio.CharBuffer;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@Singleton
|
||||
public class DeviceKey {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(DeviceKey.class);
|
||||
private static final String KEYCHAIN_KEY = "cryptomator-device-p12";
|
||||
private static final String KEYCHAIN_DISPLAY_NAME = "Cryptomator Device Keypair .p12 Passphrase";
|
||||
|
||||
private final KeychainManager keychainManager;
|
||||
private final Environment env;
|
||||
private final SecureRandom csprng;
|
||||
private final Supplier<P384KeyPair> keyPairSupplier;
|
||||
|
||||
@Inject
|
||||
public DeviceKey(KeychainManager keychainManager, Environment env, SecureRandom csprng) {
|
||||
this.keychainManager = keychainManager;
|
||||
this.env = env;
|
||||
this.csprng = csprng;
|
||||
this.keyPairSupplier = Suppliers.memoize(this::loadOrCreate);
|
||||
}
|
||||
|
||||
public P384KeyPair get() throws DeviceKeyRetrievalException {
|
||||
Preconditions.checkState(keychainManager.isSupported());
|
||||
return keyPairSupplier.get();
|
||||
}
|
||||
|
||||
private P384KeyPair loadOrCreate() throws DeviceKeyRetrievalException {
|
||||
Path p12File = env.getP12Path().findFirst().orElseThrow(() -> new DeviceKeyRetrievalException("No path for .p12 file configured"));
|
||||
char[] passphrase = null;
|
||||
try {
|
||||
passphrase = keychainManager.loadPassphrase(KEYCHAIN_KEY);
|
||||
if (passphrase != null && Files.isRegularFile(p12File)) {
|
||||
return loadExistingKeyPair(passphrase, p12File);
|
||||
} else { // (re)generate new key pair if either file or password got lost
|
||||
passphrase = randomPassword();
|
||||
keychainManager.storePassphrase(KEYCHAIN_KEY, KEYCHAIN_DISPLAY_NAME, CharBuffer.wrap(passphrase));
|
||||
return createAndStoreNewKeyPair(passphrase, p12File);
|
||||
}
|
||||
} catch (KeychainAccessException e) {
|
||||
throw new DeviceKeyRetrievalException("Failed to access system keychain", e);
|
||||
} catch (Pkcs12Exception | IOException e) {
|
||||
throw new DeviceKeyRetrievalException("Failed to access .p12 file", e);
|
||||
} finally {
|
||||
if (passphrase != null) {
|
||||
Arrays.fill(passphrase, '\0');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private P384KeyPair loadExistingKeyPair(char[] passphrase, Path p12File) throws IOException {
|
||||
LOG.debug("Loading existing device key from {}", p12File);
|
||||
return P384KeyPair.load(p12File, passphrase);
|
||||
}
|
||||
|
||||
private P384KeyPair createAndStoreNewKeyPair(char[] passphrase, Path p12File) throws IOException {
|
||||
var keyPair = P384KeyPair.generate();
|
||||
LOG.debug("Store new device key to {}", p12File);
|
||||
keyPair.store(p12File, passphrase);
|
||||
return keyPair;
|
||||
}
|
||||
|
||||
private char[] randomPassword() {
|
||||
// this is a fast & easy attempt to create a random string:
|
||||
var uuid = new UUID(csprng.nextLong(), csprng.nextLong());
|
||||
return uuid.toString().toCharArray();
|
||||
}
|
||||
|
||||
public static class DeviceKeyRetrievalException extends RuntimeException {
|
||||
private DeviceKeyRetrievalException(String message) {
|
||||
super(message);
|
||||
}
|
||||
private DeviceKeyRetrievalException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -32,11 +32,13 @@ public class Settings {
|
||||
public static final boolean DEFAULT_ASKED_FOR_UPDATE_CHECK = false;
|
||||
public static final boolean DEFAULT_CHECK_FOR_UPDATES = false;
|
||||
public static final boolean DEFAULT_START_HIDDEN = false;
|
||||
public static final boolean DEFAULT_AUTO_CLOSE_VAULTS = false;
|
||||
public static final boolean DEFAULT_USE_KEYCHAIN = true;
|
||||
public static final int DEFAULT_PORT = 42427;
|
||||
public static final int DEFAULT_NUM_TRAY_NOTIFICATIONS = 3;
|
||||
public static final WebDavUrlScheme DEFAULT_GVFS_SCHEME = WebDavUrlScheme.DAV;
|
||||
public static final boolean DEFAULT_DEBUG_MODE = false;
|
||||
public static final VolumeImpl DEFAULT_PREFERRED_VOLUME_IMPL = SystemUtils.IS_OS_WINDOWS ? VolumeImpl.DOKANY : VolumeImpl.FUSE;
|
||||
public static final VolumeImpl DEFAULT_PREFERRED_VOLUME_IMPL = VolumeImpl.FUSE;
|
||||
public static final UiTheme DEFAULT_THEME = UiTheme.LIGHT;
|
||||
@Deprecated // to be changed to "whatever is available" eventually
|
||||
public static final String DEFAULT_KEYCHAIN_PROVIDER = SystemUtils.IS_OS_WINDOWS ? "org.cryptomator.windows.keychain.WindowsProtectedKeychainAccess" : SystemUtils.IS_OS_MAC ? "org.cryptomator.macos.keychain.MacSystemKeychainAccess" : "org.cryptomator.linux.keychain.SecretServiceKeychainAccess";
|
||||
@@ -51,6 +53,8 @@ public class Settings {
|
||||
private final BooleanProperty askedForUpdateCheck = new SimpleBooleanProperty(DEFAULT_ASKED_FOR_UPDATE_CHECK);
|
||||
private final BooleanProperty checkForUpdates = new SimpleBooleanProperty(DEFAULT_CHECK_FOR_UPDATES);
|
||||
private final BooleanProperty startHidden = new SimpleBooleanProperty(DEFAULT_START_HIDDEN);
|
||||
private final BooleanProperty autoCloseVaults = new SimpleBooleanProperty(DEFAULT_AUTO_CLOSE_VAULTS);
|
||||
private final BooleanProperty useKeychain = new SimpleBooleanProperty(DEFAULT_USE_KEYCHAIN);
|
||||
private final IntegerProperty port = new SimpleIntegerProperty(DEFAULT_PORT);
|
||||
private final IntegerProperty numTrayNotifications = new SimpleIntegerProperty(DEFAULT_NUM_TRAY_NOTIFICATIONS);
|
||||
private final ObjectProperty<WebDavUrlScheme> preferredGvfsScheme = new SimpleObjectProperty<>(DEFAULT_GVFS_SCHEME);
|
||||
@@ -82,6 +86,8 @@ public class Settings {
|
||||
askedForUpdateCheck.addListener(this::somethingChanged);
|
||||
checkForUpdates.addListener(this::somethingChanged);
|
||||
startHidden.addListener(this::somethingChanged);
|
||||
autoCloseVaults.addListener(this::somethingChanged);
|
||||
useKeychain.addListener(this::somethingChanged);
|
||||
port.addListener(this::somethingChanged);
|
||||
numTrayNotifications.addListener(this::somethingChanged);
|
||||
preferredGvfsScheme.addListener(this::somethingChanged);
|
||||
@@ -133,6 +139,12 @@ public class Settings {
|
||||
return startHidden;
|
||||
}
|
||||
|
||||
public BooleanProperty autoCloseVaults() {
|
||||
return autoCloseVaults;
|
||||
}
|
||||
|
||||
public BooleanProperty useKeychain() { return useKeychain; }
|
||||
|
||||
public IntegerProperty port() {
|
||||
return port;
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
|
||||
out.name("askedForUpdateCheck").value(value.askedForUpdateCheck().get());
|
||||
out.name("checkForUpdatesEnabled").value(value.checkForUpdates().get());
|
||||
out.name("startHidden").value(value.startHidden().get());
|
||||
out.name("autoCloseVaults").value(value.autoCloseVaults().get());
|
||||
out.name("port").value(value.port().get());
|
||||
out.name("numTrayNotifications").value(value.numTrayNotifications().get());
|
||||
out.name("preferredGvfsScheme").value(value.preferredGvfsScheme().get().name());
|
||||
@@ -49,6 +50,7 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
|
||||
out.name("theme").value(value.theme().get().name());
|
||||
out.name("uiOrientation").value(value.userInterfaceOrientation().get().name());
|
||||
out.name("keychainProvider").value(value.keychainProvider().get());
|
||||
out.name("useKeychain").value(value.useKeychain().get());
|
||||
out.name("licenseKey").value(value.licenseKey().get());
|
||||
out.name("showMinimizeButton").value(value.showMinimizeButton().get());
|
||||
out.name("showTrayIcon").value(value.showTrayIcon().get());
|
||||
@@ -82,6 +84,7 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
|
||||
case "askedForUpdateCheck" -> settings.askedForUpdateCheck().set(in.nextBoolean());
|
||||
case "checkForUpdatesEnabled" -> settings.checkForUpdates().set(in.nextBoolean());
|
||||
case "startHidden" -> settings.startHidden().set(in.nextBoolean());
|
||||
case "autoCloseVaults" -> settings.autoCloseVaults().set(in.nextBoolean());
|
||||
case "port" -> settings.port().set(in.nextInt());
|
||||
case "numTrayNotifications" -> settings.numTrayNotifications().set(in.nextInt());
|
||||
case "preferredGvfsScheme" -> settings.preferredGvfsScheme().set(parseWebDavUrlSchemePrefix(in.nextString()));
|
||||
@@ -90,6 +93,7 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
|
||||
case "theme" -> settings.theme().set(parseUiTheme(in.nextString()));
|
||||
case "uiOrientation" -> settings.userInterfaceOrientation().set(parseUiOrientation(in.nextString()));
|
||||
case "keychainProvider" -> settings.keychainProvider().set(in.nextString());
|
||||
case "useKeychain" -> settings.useKeychain().set(in.nextBoolean());
|
||||
case "licenseKey" -> settings.licenseKey().set(in.nextString());
|
||||
case "showMinimizeButton" -> settings.showMinimizeButton().set(in.nextBoolean());
|
||||
case "showTrayIcon" -> settings.showTrayIcon().set(in.nextBoolean());
|
||||
@@ -101,7 +105,7 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
|
||||
case "language" -> settings.languageProperty().set(in.nextString());
|
||||
|
||||
default -> {
|
||||
LOG.warn("Unsupported vault setting found in JSON: " + name);
|
||||
LOG.warn("Unsupported vault setting found in JSON: {}", name);
|
||||
in.skipValue();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,14 +49,12 @@ public class SettingsProvider implements Supplier<Settings> {
|
||||
|
||||
private final AtomicReference<ScheduledFuture<?>> scheduledSaveCmd = new AtomicReference<>();
|
||||
private final Supplier<Settings> settings = Suppliers.memoize(this::load);
|
||||
private final SettingsJsonAdapter settingsJsonAdapter;
|
||||
private final Environment env;
|
||||
private final ScheduledExecutorService scheduler;
|
||||
private final Gson gson;
|
||||
|
||||
@Inject
|
||||
public SettingsProvider(SettingsJsonAdapter settingsJsonAdapter, Environment env, ScheduledExecutorService scheduler) {
|
||||
this.settingsJsonAdapter = settingsJsonAdapter;
|
||||
this.env = env;
|
||||
this.scheduler = scheduler;
|
||||
this.gson = new GsonBuilder() //
|
||||
@@ -118,7 +116,7 @@ public class SettingsProvider implements Supplier<Settings> {
|
||||
try {
|
||||
Files.createDirectories(settingsPath.getParent());
|
||||
Path tmpPath = settingsPath.resolveSibling(settingsPath.getFileName().toString() + ".tmp");
|
||||
try (OutputStream out = Files.newOutputStream(tmpPath, StandardOpenOption.CREATE_NEW); //
|
||||
try (OutputStream out = Files.newOutputStream(tmpPath, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE); //
|
||||
Writer writer = new OutputStreamWriter(out, StandardCharsets.UTF_8)) {
|
||||
gson.toJson(settings, writer);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import com.google.common.io.BaseEncoding;
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.StringBinding;
|
||||
import javafx.beans.binding.StringExpression;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
@@ -20,6 +21,7 @@ import javafx.beans.property.SimpleIntegerProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
@@ -56,11 +58,11 @@ public class VaultSettings {
|
||||
private final ObjectProperty<WhenUnlocked> actionAfterUnlock = new SimpleObjectProperty<>(DEFAULT_ACTION_AFTER_UNLOCK);
|
||||
private final BooleanProperty autoLockWhenIdle = new SimpleBooleanProperty(DEFAULT_AUTOLOCK_WHEN_IDLE);
|
||||
private final IntegerProperty autoLockIdleSeconds = new SimpleIntegerProperty(DEFAULT_AUTOLOCK_IDLE_SECONDS);
|
||||
private final StringBinding mountName;
|
||||
private final StringExpression mountName;
|
||||
|
||||
public VaultSettings(String id) {
|
||||
this.id = Objects.requireNonNull(id);
|
||||
this.mountName = Bindings.createStringBinding(this::normalizeDisplayName, displayName);
|
||||
this.mountName = StringExpression.stringExpression(displayName.map(VaultSettings::normalizeDisplayName).orElse(""));
|
||||
}
|
||||
|
||||
Observable[] observables() {
|
||||
@@ -78,8 +80,7 @@ public class VaultSettings {
|
||||
}
|
||||
|
||||
//visible for testing
|
||||
String normalizeDisplayName() {
|
||||
var original = displayName.getValueSafe();
|
||||
static String normalizeDisplayName(String original) {
|
||||
if (original.isBlank() || ".".equals(original) || "..".equals(original)) {
|
||||
return "_";
|
||||
}
|
||||
@@ -105,7 +106,7 @@ public class VaultSettings {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
public StringBinding mountName() {
|
||||
public StringExpression mountName() {
|
||||
return mountName;
|
||||
}
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ class VaultSettingsJsonAdapter {
|
||||
case "autoLockWhenIdle" -> autoLockWhenIdle = in.nextBoolean();
|
||||
case "autoLockIdleSeconds" -> autoLockIdleSeconds = in.nextInt();
|
||||
default -> {
|
||||
LOG.warn("Unsupported vault setting found in JSON: " + name);
|
||||
LOG.warn("Unsupported vault setting found in JSON: {}", name);
|
||||
in.skipValue();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,6 @@ public class AutoLocker {
|
||||
|
||||
private boolean exceedsIdleTime(Vault vault) {
|
||||
assert vault.isUnlocked();
|
||||
// TODO: shouldn't we read these properties from within FX Application Thread?
|
||||
if (vault.getVaultSettings().autoLockWhenIdle().get()) {
|
||||
int maxIdleSeconds = vault.getVaultSettings().autoLockIdleSeconds().get();
|
||||
var deadline = vault.getStats().getLastActivity().plusSeconds(maxIdleSeconds);
|
||||
|
||||
@@ -13,8 +13,6 @@ import org.cryptomator.frontend.fuse.mount.FuseMountFactory;
|
||||
import org.cryptomator.frontend.fuse.mount.FuseNotSupportedException;
|
||||
import org.cryptomator.frontend.fuse.mount.Mount;
|
||||
import org.cryptomator.frontend.fuse.mount.Mounter;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
@@ -26,7 +24,6 @@ import java.util.regex.Pattern;
|
||||
|
||||
public class FuseVolume extends AbstractVolume {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(FuseVolume.class);
|
||||
private static final Pattern NON_WHITESPACE_OR_QUOTED = Pattern.compile("[^\\s\"']+|\"([^\"]*)\"|'([^']*)'"); // Thanks to https://stackoverflow.com/a/366532
|
||||
|
||||
private final VaultSettings vaultSettings;
|
||||
|
||||
@@ -10,6 +10,7 @@ package org.cryptomator.common.vaults;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.common.Constants;
|
||||
import org.cryptomator.common.mountpoint.InvalidMountPointException;
|
||||
import org.cryptomator.common.settings.VaultSettings;
|
||||
import org.cryptomator.common.vaults.Volume.VolumeException;
|
||||
@@ -33,6 +34,7 @@ import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.binding.StringBinding;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyStringProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
@@ -59,7 +61,6 @@ public class Vault {
|
||||
private final ObjectProperty<Exception> lastKnownException;
|
||||
private final VaultConfigCache configCache;
|
||||
private final VaultStats stats;
|
||||
private final StringBinding displayName;
|
||||
private final StringBinding displayablePath;
|
||||
private final BooleanBinding locked;
|
||||
private final BooleanBinding processing;
|
||||
@@ -83,7 +84,6 @@ public class Vault {
|
||||
this.state = state;
|
||||
this.lastKnownException = lastKnownException;
|
||||
this.stats = stats;
|
||||
this.displayName = Bindings.createStringBinding(this::getDisplayName, vaultSettings.displayName());
|
||||
this.displayablePath = Bindings.createStringBinding(this::getDisplayablePath, vaultSettings.path());
|
||||
this.locked = Bindings.createBooleanBinding(this::isLocked, state);
|
||||
this.processing = Bindings.createBooleanBinding(this::isProcessing, state);
|
||||
@@ -125,6 +125,7 @@ public class Vault {
|
||||
.withKeyLoader(keyLoader) //
|
||||
.withFlags(flags) //
|
||||
.withMaxCleartextNameLength(vaultSettings.maxCleartextFilenameLength().get()) //
|
||||
.withVaultConfigFilename(Constants.VAULTCONFIG_FILENAME) //
|
||||
.build();
|
||||
return CryptoFileSystemProvider.newFileSystem(getPath(), fsProps);
|
||||
}
|
||||
@@ -264,8 +265,8 @@ public class Vault {
|
||||
return state.get() == VaultState.Value.ERROR;
|
||||
}
|
||||
|
||||
public StringBinding displayNameProperty() {
|
||||
return displayName;
|
||||
public ReadOnlyStringProperty displayNameProperty() {
|
||||
return vaultSettings.displayName();
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
|
||||
@@ -19,22 +19,13 @@ public interface VaultComponent {
|
||||
|
||||
Vault vault();
|
||||
|
||||
@Subcomponent.Builder
|
||||
interface Builder {
|
||||
@Subcomponent.Factory
|
||||
interface Factory {
|
||||
|
||||
@BindsInstance
|
||||
Builder vaultSettings(VaultSettings vaultSettings);
|
||||
VaultComponent create(@BindsInstance VaultSettings vaultSettings, //
|
||||
@BindsInstance VaultConfigCache configCache, //
|
||||
@BindsInstance VaultState.Value vaultState, //
|
||||
@BindsInstance @Nullable @Named("lastKnownException") Exception initialErrorCause);
|
||||
|
||||
@BindsInstance
|
||||
Builder vaultConfigCache(VaultConfigCache configCache);
|
||||
|
||||
@BindsInstance
|
||||
Builder initialVaultState(VaultState.Value vaultState);
|
||||
|
||||
@BindsInstance
|
||||
Builder initialErrorCause(@Nullable @Named("lastKnownException") Exception initialErrorCause);
|
||||
|
||||
VaultComponent build();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -38,15 +38,15 @@ public class VaultListManager {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(VaultListManager.class);
|
||||
|
||||
private final AutoLocker autoLocker;
|
||||
private final VaultComponent.Builder vaultComponentBuilder;
|
||||
private final VaultComponent.Factory vaultComponentFactory;
|
||||
private final ObservableList<Vault> vaultList;
|
||||
private final String defaultVaultName;
|
||||
|
||||
@Inject
|
||||
public VaultListManager(ObservableList<Vault> vaultList, AutoLocker autoLocker, VaultComponent.Builder vaultComponentBuilder, ResourceBundle resourceBundle, Settings settings) {
|
||||
public VaultListManager(ObservableList<Vault> vaultList, AutoLocker autoLocker, VaultComponent.Factory vaultComponentFactory, ResourceBundle resourceBundle, Settings settings) {
|
||||
this.vaultList = vaultList;
|
||||
this.autoLocker = autoLocker;
|
||||
this.vaultComponentBuilder = vaultComponentBuilder;
|
||||
this.vaultComponentFactory = vaultComponentFactory;
|
||||
this.defaultVaultName = resourceBundle.getString("defaults.vault.vaultName");
|
||||
|
||||
addAll(settings.getDirectories());
|
||||
@@ -93,21 +93,17 @@ public class VaultListManager {
|
||||
}
|
||||
|
||||
private Vault create(VaultSettings vaultSettings) {
|
||||
VaultComponent.Builder compBuilder = vaultComponentBuilder.vaultSettings(vaultSettings);
|
||||
var wrapper = new VaultConfigCache(vaultSettings);
|
||||
try {
|
||||
VaultState.Value vaultState = determineVaultState(vaultSettings.path().get());
|
||||
VaultConfigCache wrapper = new VaultConfigCache(vaultSettings);
|
||||
compBuilder.vaultConfigCache(wrapper); //first set the wrapper in the builder, THEN try to load config
|
||||
var vaultState = determineVaultState(vaultSettings.path().get());
|
||||
if (vaultState == LOCKED) { //for legacy reasons: pre v8 vault do not have a config, but they are in the NEEDS_MIGRATION state
|
||||
wrapper.reloadConfig();
|
||||
}
|
||||
compBuilder.initialVaultState(vaultState);
|
||||
return vaultComponentFactory.create(vaultSettings, wrapper, vaultState, null).vault();
|
||||
} catch (IOException e) {
|
||||
LOG.warn("Failed to determine vault state for " + vaultSettings.path().get(), e);
|
||||
compBuilder.initialVaultState(ERROR);
|
||||
compBuilder.initialErrorCause(e);
|
||||
return vaultComponentFactory.create(vaultSettings, wrapper, ERROR, e).vault();
|
||||
}
|
||||
return compBuilder.build().vault();
|
||||
}
|
||||
|
||||
public static VaultState.Value redetermineVaultState(Vault vault) {
|
||||
|
||||
@@ -19,6 +19,7 @@ import org.slf4j.LoggerFactory;
|
||||
import javax.inject.Named;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.StringBinding;
|
||||
import javafx.beans.binding.StringExpression;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyBooleanProperty;
|
||||
@@ -68,7 +69,7 @@ public class VaultModule {
|
||||
@DefaultMountFlags
|
||||
public StringBinding provideDefaultMountFlags(Settings settings, VaultSettings vaultSettings) {
|
||||
ObjectProperty<VolumeImpl> preferredVolumeImpl = settings.preferredVolumeImpl();
|
||||
StringBinding mountName = vaultSettings.mountName();
|
||||
StringExpression mountName = vaultSettings.mountName();
|
||||
BooleanProperty readOnly = vaultSettings.usesReadOnlyMode();
|
||||
|
||||
return Bindings.createStringBinding(() -> {
|
||||
@@ -88,7 +89,7 @@ public class VaultModule {
|
||||
}
|
||||
|
||||
// see: https://github.com/osxfuse/osxfuse/wiki/Mount-options
|
||||
private String getMacFuseDefaultMountFlags(StringBinding mountName, ReadOnlyBooleanProperty readOnly) {
|
||||
private String getMacFuseDefaultMountFlags(StringExpression mountName, ReadOnlyBooleanProperty readOnly) {
|
||||
assert SystemUtils.IS_OS_MAC_OSX;
|
||||
StringBuilder flags = new StringBuilder();
|
||||
if (readOnly.get()) {
|
||||
@@ -139,7 +140,7 @@ public class VaultModule {
|
||||
// see https://github.com/billziss-gh/winfsp/blob/5d0b10d0b643652c00ebb4704dc2bb28e7244973/src/dll/fuse/fuse_main.c#L53-L62 for syntax guide
|
||||
// see https://github.com/billziss-gh/winfsp/blob/5d0b10d0b643652c00ebb4704dc2bb28e7244973/src/dll/fuse/fuse.c#L295-L319 for options (-o <...>)
|
||||
// see https://github.com/billziss-gh/winfsp/wiki/Frequently-Asked-Questions/5ba00e4be4f5e938eaae6ef1500b331de12dee77 (FUSE 4.) on why the given defaults were chosen
|
||||
private String getWindowsFuseDefaultMountFlags(StringBinding mountName, ReadOnlyBooleanProperty readOnly) {
|
||||
private String getWindowsFuseDefaultMountFlags(StringExpression mountName, ReadOnlyBooleanProperty readOnly) {
|
||||
assert SystemUtils.IS_OS_WINDOWS;
|
||||
StringBuilder flags = new StringBuilder();
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.cryptomator.common.vaults;
|
||||
|
||||
|
||||
import com.google.common.base.CharMatcher;
|
||||
import org.cryptomator.common.Environment;
|
||||
import org.cryptomator.common.settings.Settings;
|
||||
import org.cryptomator.common.settings.VaultSettings;
|
||||
import org.cryptomator.common.settings.VolumeImpl;
|
||||
@@ -22,12 +23,11 @@ import java.util.function.Supplier;
|
||||
|
||||
public class WebDavVolume implements Volume {
|
||||
|
||||
private static final String LOCALHOST_ALIAS = "cryptomator-vault";
|
||||
|
||||
private final Provider<WebDavServer> serverProvider;
|
||||
private final VaultSettings vaultSettings;
|
||||
private final Settings settings;
|
||||
private final WindowsDriveLetters windowsDriveLetters;
|
||||
private final Environment environment;
|
||||
|
||||
private WebDavServer server;
|
||||
private WebDavServletController servlet;
|
||||
@@ -35,11 +35,12 @@ public class WebDavVolume implements Volume {
|
||||
private Consumer<Throwable> onExitAction;
|
||||
|
||||
@Inject
|
||||
public WebDavVolume(Provider<WebDavServer> serverProvider, VaultSettings vaultSettings, Settings settings, WindowsDriveLetters windowsDriveLetters) {
|
||||
public WebDavVolume(Provider<WebDavServer> serverProvider, VaultSettings vaultSettings, Settings settings, WindowsDriveLetters windowsDriveLetters, Environment environment) {
|
||||
this.serverProvider = serverProvider;
|
||||
this.vaultSettings = vaultSettings;
|
||||
this.settings = settings;
|
||||
this.windowsDriveLetters = windowsDriveLetters;
|
||||
this.environment = environment;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -129,16 +130,17 @@ public class WebDavVolume implements Volume {
|
||||
}
|
||||
|
||||
private String getLocalhostAliasOrNull() {
|
||||
try {
|
||||
InetAddress alias = InetAddress.getByName(LOCALHOST_ALIAS);
|
||||
if (alias.getHostAddress().equals("127.0.0.1")) {
|
||||
return LOCALHOST_ALIAS;
|
||||
} else {
|
||||
return null;
|
||||
return environment.getLoopbackAlias().map(alias -> {
|
||||
try {
|
||||
var address = InetAddress.getByName(alias);
|
||||
if (address.getHostAddress().equals("127.0.0.1")) {
|
||||
return alias;
|
||||
}
|
||||
} catch (UnknownHostException e) {
|
||||
//no-op
|
||||
}
|
||||
} catch (UnknownHostException e) {
|
||||
return null;
|
||||
}
|
||||
}).orElse(null);
|
||||
}
|
||||
|
||||
private void cleanup() {
|
||||
|
||||
@@ -12,7 +12,6 @@ import org.cryptomator.common.Environment;
|
||||
import org.cryptomator.common.ShutdownHook;
|
||||
import org.cryptomator.ipc.IpcCommunicator;
|
||||
import org.cryptomator.logging.DebugMode;
|
||||
import org.cryptomator.logging.LoggerConfiguration;
|
||||
import org.cryptomator.ui.fxapp.FxApplicationComponent;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -35,7 +34,6 @@ public class Cryptomator {
|
||||
private static final CryptomatorComponent CRYPTOMATOR_COMPONENT = DaggerCryptomatorComponent.factory().create(STARTUP_TIME);
|
||||
private static final Logger LOG = LoggerFactory.getLogger(Cryptomator.class);
|
||||
|
||||
private final LoggerConfiguration logConfig;
|
||||
private final DebugMode debugMode;
|
||||
private final SupportedLanguages supportedLanguages;
|
||||
private final Environment env;
|
||||
@@ -43,8 +41,7 @@ public class Cryptomator {
|
||||
private final ShutdownHook shutdownHook;
|
||||
|
||||
@Inject
|
||||
Cryptomator(LoggerConfiguration logConfig, DebugMode debugMode, SupportedLanguages supportedLanguages, Environment env, Lazy<IpcMessageHandler> ipcMessageHandler, ShutdownHook shutdownHook) {
|
||||
this.logConfig = logConfig;
|
||||
Cryptomator(DebugMode debugMode, SupportedLanguages supportedLanguages, Environment env, Lazy<IpcMessageHandler> ipcMessageHandler, ShutdownHook shutdownHook) {
|
||||
this.debugMode = debugMode;
|
||||
this.supportedLanguages = supportedLanguages;
|
||||
this.env = env;
|
||||
@@ -79,9 +76,9 @@ public class Cryptomator {
|
||||
* @return Nonzero exit code in case of an error.
|
||||
*/
|
||||
private int run(String[] args) {
|
||||
logConfig.init();
|
||||
env.log();
|
||||
LOG.debug("Dagger graph initialized after {}ms", System.currentTimeMillis() - STARTUP_TIME);
|
||||
LOG.info("Starting Cryptomator {} on {} {} ({})", env.getAppVersion().orElse("SNAPSHOT"), SystemUtils.OS_NAME, SystemUtils.OS_VERSION, SystemUtils.OS_ARCH);
|
||||
LOG.info("Starting Cryptomator {} on {} {} ({})", env.getAppVersion(), SystemUtils.OS_NAME, SystemUtils.OS_VERSION, SystemUtils.OS_ARCH);
|
||||
debugMode.initialize();
|
||||
supportedLanguages.applyPreferred();
|
||||
|
||||
|
||||
@@ -3,14 +3,13 @@ package org.cryptomator.launcher;
|
||||
import dagger.BindsInstance;
|
||||
import dagger.Component;
|
||||
import org.cryptomator.common.CommonsModule;
|
||||
import org.cryptomator.logging.LoggerModule;
|
||||
import org.cryptomator.ui.fxapp.FxApplicationComponent;
|
||||
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
@Singleton
|
||||
@Component(modules = {CryptomatorModule.class, CommonsModule.class, LoggerModule.class})
|
||||
@Component(modules = {CryptomatorModule.class, CommonsModule.class})
|
||||
public interface CryptomatorComponent {
|
||||
|
||||
Cryptomator application();
|
||||
|
||||
@@ -15,9 +15,9 @@ public class SupportedLanguages {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SupportedLanguages.class);
|
||||
// these are BCP 47 language codes, not ISO. Note the "-" instead of the "_":
|
||||
public static final List<String> LANGUAGAE_TAGS = List.of("en", "ar", "bn", "bs", "ca", "cs", "de", "el", "es", "fil", "fr", "gl", "he", //
|
||||
"hi", "hr", "hu", "id", "it", "ja", "ko", "lv", "mk", "nb", "nl", "nn", "no", "pa", "pl", "pt", "pt-BR", "ro", "ru", "sk", "sr", //
|
||||
"sr-Latn", "sv", "ta", "te", "th", "tr", "uk", "zh", "zh-HK", "zh-TW");
|
||||
public static final List<String> LANGUAGAE_TAGS = List.of("en", "ar", "be", "bn", "bs", "ca", "cs", "da", "de", "el", "es", "fil", "fa", "fr", "gl", "he", //
|
||||
"hi", "hr", "hu", "id", "it", "ja", "ko", "lv", "mk", "nb", "nl", "nn", "no", "pa", "pl", "pt", "pt-BR", "ro", "ru", "si", "sk", "sr", "sr-Latn", "sv", "sw", //
|
||||
"ta", "te", "th", "tr", "uk", "vi", "zh", "zh-HK", "zh-TW");
|
||||
|
||||
@Nullable
|
||||
private final String preferredLanguage;
|
||||
|
||||
@@ -5,29 +5,24 @@
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.logging;
|
||||
|
||||
import ch.qos.logback.classic.Level;
|
||||
import ch.qos.logback.classic.Logger;
|
||||
import ch.qos.logback.classic.LoggerContext;
|
||||
import org.cryptomator.common.settings.Settings;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import java.util.Map;
|
||||
|
||||
@Singleton
|
||||
public class DebugMode {
|
||||
|
||||
private static final org.slf4j.Logger LOG = LoggerFactory.getLogger(DebugMode.class);
|
||||
private static final Logger LOG = LoggerFactory.getLogger(DebugMode.class);
|
||||
|
||||
private final Settings settings;
|
||||
private final LoggerContext context;
|
||||
|
||||
@Inject
|
||||
public DebugMode(Settings settings, LoggerContext context) {
|
||||
public DebugMode(Settings settings) {
|
||||
this.settings = settings;
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public void initialize() {
|
||||
@@ -40,19 +35,13 @@ public class DebugMode {
|
||||
}
|
||||
|
||||
private void setLogLevels(boolean debugMode) {
|
||||
var configurator = LogbackConfiguratorFactory.provider();
|
||||
if (debugMode) {
|
||||
setLogLevels(LoggerModule.DEBUG_LOG_LEVELS);
|
||||
configurator.setLogLevels(LogbackConfigurator.DEBUG_LOG_LEVELS);
|
||||
LOG.debug("Debug mode enabled");
|
||||
} else {
|
||||
LOG.debug("Debug mode disabled");
|
||||
setLogLevels(LoggerModule.DEFAULT_LOG_LEVELS);
|
||||
}
|
||||
}
|
||||
|
||||
private void setLogLevels(Map<String, Level> logLevels) {
|
||||
for (Map.Entry<String, Level> loglevel : logLevels.entrySet()) {
|
||||
Logger logger = context.getLogger(loglevel.getKey());
|
||||
logger.setLevel(loglevel.getValue());
|
||||
configurator.setLogLevels(LogbackConfigurator.DEFAULT_LOG_LEVELS);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
145
src/main/java/org/cryptomator/logging/LogbackConfigurator.java
Normal file
145
src/main/java/org/cryptomator/logging/LogbackConfigurator.java
Normal file
@@ -0,0 +1,145 @@
|
||||
package org.cryptomator.logging;
|
||||
|
||||
import ch.qos.logback.classic.Level;
|
||||
import ch.qos.logback.classic.Logger;
|
||||
import ch.qos.logback.classic.LoggerContext;
|
||||
import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
|
||||
import ch.qos.logback.classic.spi.Configurator;
|
||||
import ch.qos.logback.classic.spi.ILoggingEvent;
|
||||
import ch.qos.logback.core.Appender;
|
||||
import ch.qos.logback.core.ConsoleAppender;
|
||||
import ch.qos.logback.core.FileAppender;
|
||||
import ch.qos.logback.core.helpers.NOPAppender;
|
||||
import ch.qos.logback.core.rolling.FixedWindowRollingPolicy;
|
||||
import ch.qos.logback.core.rolling.RollingFileAppender;
|
||||
import ch.qos.logback.core.spi.ContextAwareBase;
|
||||
import ch.qos.logback.core.util.FileSize;
|
||||
import org.cryptomator.common.Environment;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Map;
|
||||
|
||||
public class LogbackConfigurator extends ContextAwareBase implements Configurator {
|
||||
|
||||
private static final String LOG_PATTERN = "%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n";
|
||||
private static final String UPGRADE_FILENAME = "upgrade.log";
|
||||
private static final String LOGFILE_NAME = "cryptomator0.log";
|
||||
private static final String LOGFILE_ROLLING_PATTERN = "cryptomator%i.log";
|
||||
private static final int LOGFILE_ROLLING_MIN = 1;
|
||||
private static final int LOGFILE_ROLLING_MAX = 9;
|
||||
private static final String LOG_MAX_SIZE = "100mb";
|
||||
|
||||
static final Map<String, Level> DEFAULT_LOG_LEVELS = Map.of( //
|
||||
Logger.ROOT_LOGGER_NAME, Level.INFO, //
|
||||
"org.cryptomator", Level.INFO //
|
||||
);
|
||||
static final Map<String, Level> DEBUG_LOG_LEVELS = Map.of( //
|
||||
Logger.ROOT_LOGGER_NAME, Level.INFO, //
|
||||
"org.cryptomator", Level.TRACE //
|
||||
);
|
||||
|
||||
LogbackConfigurator() {}
|
||||
|
||||
/**
|
||||
* Adjust the log levels
|
||||
*
|
||||
* @param logLevels new log levels to use
|
||||
*/
|
||||
void setLogLevels(Map<String, Level> logLevels) {
|
||||
if (context instanceof LoggerContext lc) {
|
||||
for (var loglevel : logLevels.entrySet()) {
|
||||
Logger logger = lc.getLogger(loglevel.getKey());
|
||||
logger.setLevel(loglevel.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExecutionStatus configure(LoggerContext context) {
|
||||
var useCustomCfg = Environment.getInstance().useCustomLogbackConfig();
|
||||
var logDir = Environment.getInstance().getLogDir().orElse(null);
|
||||
|
||||
if (useCustomCfg) {
|
||||
addInfo("Using external logback configuration file.");
|
||||
} else {
|
||||
// configure appenders:
|
||||
var stdout = stdOutAppender(context);
|
||||
var noop = noopAppender(context);
|
||||
var file = logDir == null ? noop : fileAppender(context, logDir);
|
||||
var upgrade = logDir == null ? noop : upgradeAppender(context, logDir);
|
||||
|
||||
// configure loggers:
|
||||
for (var loglevel : DEFAULT_LOG_LEVELS.entrySet()) {
|
||||
Logger logger = context.getLogger(loglevel.getKey());
|
||||
logger.setLevel(loglevel.getValue());
|
||||
logger.setAdditive(false);
|
||||
logger.addAppender(stdout);
|
||||
logger.addAppender(file);
|
||||
}
|
||||
|
||||
// configure upgrade logger:
|
||||
Logger upgrades = context.getLogger("org.cryptomator.cryptofs.migration");
|
||||
upgrades.setLevel(Level.DEBUG);
|
||||
upgrades.addAppender(stdout);
|
||||
upgrades.addAppender(upgrade);
|
||||
upgrades.addAppender(file);
|
||||
upgrades.setAdditive(false);
|
||||
}
|
||||
return ExecutionStatus.DO_NOT_INVOKE_NEXT_IF_ANY;
|
||||
}
|
||||
|
||||
private Appender<ILoggingEvent> noopAppender(LoggerContext context) {
|
||||
var appender = new NOPAppender<ILoggingEvent>();
|
||||
appender.setContext(context);
|
||||
return appender;
|
||||
}
|
||||
|
||||
private Appender<ILoggingEvent> stdOutAppender(LoggerContext context) {
|
||||
var appender = new ConsoleAppender<ILoggingEvent>();
|
||||
appender.setContext(context);
|
||||
appender.setName("STDOUT");
|
||||
appender.setEncoder(encoder(context));
|
||||
appender.start();
|
||||
return appender;
|
||||
}
|
||||
|
||||
private Appender<ILoggingEvent> upgradeAppender(LoggerContext context, Path logDir) {
|
||||
var appender = new FileAppender<ILoggingEvent>();
|
||||
appender.setContext(context);
|
||||
appender.setName("UPGRADE");
|
||||
appender.setFile(logDir.resolve(UPGRADE_FILENAME).toString());
|
||||
appender.setEncoder(encoder(context));
|
||||
appender.start();
|
||||
return appender;
|
||||
}
|
||||
|
||||
private Appender<ILoggingEvent> fileAppender(LoggerContext context, Path logDir) {
|
||||
var appender = new RollingFileAppender<ILoggingEvent>();
|
||||
appender.setContext(context);
|
||||
appender.setName("FILE");
|
||||
appender.setFile(logDir.resolve(LOGFILE_NAME).toString());
|
||||
appender.setEncoder(encoder(context));
|
||||
var triggeringPolicy = new LaunchAndSizeBasedTriggeringPolicy<ILoggingEvent>(FileSize.valueOf(LOG_MAX_SIZE));
|
||||
triggeringPolicy.setContext(context);
|
||||
triggeringPolicy.start();
|
||||
appender.setTriggeringPolicy(triggeringPolicy);
|
||||
var rollingPolicy = new FixedWindowRollingPolicy();
|
||||
rollingPolicy.setContext(context);
|
||||
rollingPolicy.setFileNamePattern(logDir.resolve(LOGFILE_ROLLING_PATTERN).toString());
|
||||
rollingPolicy.setMinIndex(LOGFILE_ROLLING_MIN);
|
||||
rollingPolicy.setMaxIndex(LOGFILE_ROLLING_MAX);
|
||||
rollingPolicy.setParent(appender);
|
||||
rollingPolicy.start();
|
||||
appender.setRollingPolicy(rollingPolicy);
|
||||
appender.start();
|
||||
return appender;
|
||||
}
|
||||
|
||||
private PatternLayoutEncoder encoder(LoggerContext context) {
|
||||
var encoder = new PatternLayoutEncoder();
|
||||
encoder.setContext(context);
|
||||
encoder.setPattern(LOG_PATTERN);
|
||||
encoder.start();
|
||||
return encoder;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package org.cryptomator.logging;
|
||||
|
||||
public class LogbackConfiguratorFactory {
|
||||
|
||||
public static LogbackConfigurator provider() {
|
||||
final class Holder {
|
||||
private static final LogbackConfigurator INSTANCE = new LogbackConfigurator();
|
||||
}
|
||||
return Holder.INSTANCE;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
package org.cryptomator.logging;
|
||||
|
||||
import ch.qos.logback.classic.Level;
|
||||
import ch.qos.logback.classic.Logger;
|
||||
import ch.qos.logback.classic.LoggerContext;
|
||||
import ch.qos.logback.classic.spi.ILoggingEvent;
|
||||
import ch.qos.logback.core.Appender;
|
||||
import org.cryptomator.common.Environment;
|
||||
import org.cryptomator.common.ShutdownHook;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Singleton;
|
||||
import java.util.Map;
|
||||
|
||||
@Singleton
|
||||
public class LoggerConfiguration {
|
||||
|
||||
private final LoggerContext context;
|
||||
private final Environment environment;
|
||||
private final Appender<ILoggingEvent> stdout;
|
||||
private final Appender<ILoggingEvent> upgrade;
|
||||
private final Appender<ILoggingEvent> file;
|
||||
private final ShutdownHook shutdownHook;
|
||||
|
||||
@Inject
|
||||
LoggerConfiguration(LoggerContext context, //
|
||||
Environment environment, //
|
||||
@Named("stdoutAppender") Appender<ILoggingEvent> stdout, //
|
||||
@Named("upgradeAppender") Appender<ILoggingEvent> upgrade, //
|
||||
@Named("fileAppender") Appender<ILoggingEvent> file, //
|
||||
ShutdownHook shutdownHook) {
|
||||
this.context = context;
|
||||
this.environment = environment;
|
||||
this.stdout = stdout;
|
||||
this.upgrade = upgrade;
|
||||
this.file = file;
|
||||
this.shutdownHook = shutdownHook;
|
||||
}
|
||||
|
||||
public void init() {
|
||||
if (environment.useCustomLogbackConfig()) {
|
||||
Logger root = context.getLogger(Logger.ROOT_LOGGER_NAME);
|
||||
root.info("Using external logback configuration file.");
|
||||
} else {
|
||||
context.reset();
|
||||
|
||||
// configure loggers:
|
||||
for (Map.Entry<String, Level> loglevel : LoggerModule.DEFAULT_LOG_LEVELS.entrySet()) {
|
||||
Logger logger = context.getLogger(loglevel.getKey());
|
||||
logger.setLevel(loglevel.getValue());
|
||||
logger.setAdditive(false);
|
||||
logger.addAppender(stdout);
|
||||
logger.addAppender(file);
|
||||
}
|
||||
|
||||
// configure upgrade logger:
|
||||
Logger upgrades = context.getLogger("org.cryptomator.cryptofs.migration");
|
||||
upgrades.setLevel(Level.DEBUG);
|
||||
upgrades.addAppender(stdout);
|
||||
upgrades.addAppender(upgrade);
|
||||
upgrades.addAppender(file);
|
||||
upgrades.setAdditive(false);
|
||||
|
||||
// add shutdown hook
|
||||
shutdownHook.runOnShutdown(ShutdownHook.PRIO_LAST, context::stop);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
package org.cryptomator.logging;
|
||||
|
||||
import ch.qos.logback.classic.Level;
|
||||
import ch.qos.logback.classic.Logger;
|
||||
import ch.qos.logback.classic.LoggerContext;
|
||||
import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
|
||||
import ch.qos.logback.classic.spi.ILoggingEvent;
|
||||
import ch.qos.logback.core.Appender;
|
||||
import ch.qos.logback.core.ConsoleAppender;
|
||||
import ch.qos.logback.core.FileAppender;
|
||||
import ch.qos.logback.core.helpers.NOPAppender;
|
||||
import ch.qos.logback.core.rolling.FixedWindowRollingPolicy;
|
||||
import ch.qos.logback.core.rolling.RollingFileAppender;
|
||||
import ch.qos.logback.core.util.FileSize;
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import org.cryptomator.common.Environment;
|
||||
import org.slf4j.ILoggerFactory;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Singleton;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Map;
|
||||
|
||||
@Module
|
||||
public class LoggerModule {
|
||||
|
||||
private static final String UPGRADE_FILENAME = "upgrade.log";
|
||||
private static final String LOGFILE_NAME = "cryptomator0.log";
|
||||
private static final String LOGFILE_ROLLING_PATTERN = "cryptomator%i.log";
|
||||
private static final int LOGFILE_ROLLING_MIN = 1;
|
||||
private static final int LOGFILE_ROLLING_MAX = 9;
|
||||
private static final String LOG_PATTERN = "%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n";
|
||||
private static final String LOG_MAX_SIZE = "100mb";
|
||||
|
||||
static final Map<String, Level> DEFAULT_LOG_LEVELS = Map.of( //
|
||||
Logger.ROOT_LOGGER_NAME, Level.INFO, //
|
||||
"org.cryptomator", Level.INFO //
|
||||
);
|
||||
static final Map<String, Level> DEBUG_LOG_LEVELS = Map.of( //
|
||||
Logger.ROOT_LOGGER_NAME, Level.INFO, //
|
||||
"org.cryptomator", Level.TRACE //
|
||||
);
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
static LoggerContext provideLoggerContext() {
|
||||
ILoggerFactory loggerFactory = LoggerFactory.getILoggerFactory();
|
||||
if (loggerFactory instanceof LoggerContext context) {
|
||||
return context;
|
||||
} else {
|
||||
throw new IllegalStateException("SLF4J not bound to Logback.");
|
||||
}
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
static PatternLayoutEncoder provideLayoutEncoder(LoggerContext context) {
|
||||
PatternLayoutEncoder ple = new PatternLayoutEncoder();
|
||||
ple.setPattern(LOG_PATTERN);
|
||||
ple.setContext(context);
|
||||
ple.start();
|
||||
return ple;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@Named("stdoutAppender")
|
||||
static Appender<ILoggingEvent> provideStdoutAppender(LoggerContext context, PatternLayoutEncoder encoder) {
|
||||
ConsoleAppender<ILoggingEvent> appender = new ConsoleAppender<>();
|
||||
appender.setContext(context);
|
||||
appender.setEncoder(encoder);
|
||||
appender.start();
|
||||
return appender;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@Named("fileAppender")
|
||||
static Appender<ILoggingEvent> provideFileAppender(LoggerContext context, PatternLayoutEncoder encoder, Environment environment) {
|
||||
var optionalLogDir = environment.getLogDir();
|
||||
if (optionalLogDir.isPresent()) {
|
||||
Path logDir = optionalLogDir.get();
|
||||
RollingFileAppender<ILoggingEvent> appender = new RollingFileAppender<>();
|
||||
appender.setContext(context);
|
||||
appender.setFile(logDir.resolve(LOGFILE_NAME).toString());
|
||||
appender.setEncoder(encoder);
|
||||
LaunchAndSizeBasedTriggeringPolicy triggeringPolicy = new LaunchAndSizeBasedTriggeringPolicy(FileSize.valueOf(LOG_MAX_SIZE));
|
||||
triggeringPolicy.setContext(context);
|
||||
triggeringPolicy.start();
|
||||
appender.setTriggeringPolicy(triggeringPolicy);
|
||||
FixedWindowRollingPolicy rollingPolicy = new FixedWindowRollingPolicy();
|
||||
rollingPolicy.setContext(context);
|
||||
rollingPolicy.setFileNamePattern(logDir.resolve(LOGFILE_ROLLING_PATTERN).toString());
|
||||
rollingPolicy.setMinIndex(LOGFILE_ROLLING_MIN);
|
||||
rollingPolicy.setMaxIndex(LOGFILE_ROLLING_MAX);
|
||||
rollingPolicy.setParent(appender);
|
||||
rollingPolicy.start();
|
||||
appender.setRollingPolicy(rollingPolicy);
|
||||
appender.start();
|
||||
return appender;
|
||||
} else {
|
||||
NOPAppender appender = new NOPAppender<>();
|
||||
appender.setContext(context);
|
||||
return appender;
|
||||
}
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@Named("upgradeAppender")
|
||||
static Appender<ILoggingEvent> provideUpgradeAppender(LoggerContext context, PatternLayoutEncoder encoder, Environment environment) {
|
||||
var optionalLogDir = environment.getLogDir();
|
||||
if (optionalLogDir.isPresent()) {
|
||||
FileAppender<ILoggingEvent> appender = new FileAppender<>();
|
||||
appender.setFile(optionalLogDir.get().resolve(UPGRADE_FILENAME).toString());
|
||||
appender.setContext(context);
|
||||
appender.setEncoder(encoder);
|
||||
appender.start();
|
||||
return appender;
|
||||
} else {
|
||||
NOPAppender appender = new NOPAppender<>();
|
||||
appender.setContext(context);
|
||||
return appender;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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,8 +24,11 @@ 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;
|
||||
|
||||
@AddVaultWizardScoped
|
||||
public class ChooseExistingVaultController implements FxController {
|
||||
|
||||
@@ -38,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;
|
||||
@@ -52,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
|
||||
@@ -73,7 +79,7 @@ public class ChooseExistingVaultController implements FxController {
|
||||
public void chooseFileAndNext() {
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle(resourceBundle.getString("addvaultwizard.existing.filePickerTitle"));
|
||||
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Cryptomator Vault", "*.cryptomator"));
|
||||
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(resourceBundle.getString("addvaultwizard.existing.filePickerMimeDesc"), CRYPTOMATOR_FILENAME_GLOB));
|
||||
File masterkeyFile = fileChooser.showOpenDialog(window);
|
||||
if (masterkeyFile != null) {
|
||||
vaultPath.setValue(masterkeyFile.toPath().toAbsolutePath().getParent());
|
||||
@@ -90,8 +96,13 @@ public class ChooseExistingVaultController implements FxController {
|
||||
|
||||
/* Getter */
|
||||
|
||||
public Image getScreenshot() {
|
||||
public ObservableValue<Image> screenshotProperty() {
|
||||
return screenshot;
|
||||
}
|
||||
|
||||
public Image getScreenshot() {
|
||||
return screenshot.getValue();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ public class CreateNewVaultLocationController implements FxController {
|
||||
private final Stage window;
|
||||
private final Lazy<Scene> chooseNameScene;
|
||||
private final Lazy<Scene> choosePasswordScene;
|
||||
private final LocationPresets locationPresets;
|
||||
private final ObservedLocationPresets locationPresets;
|
||||
private final ObjectProperty<Path> vaultPath;
|
||||
private final StringProperty vaultName;
|
||||
private final ResourceBundle resourceBundle;
|
||||
@@ -71,7 +71,7 @@ public class CreateNewVaultLocationController implements FxController {
|
||||
public FontAwesome5IconView badLocation;
|
||||
|
||||
@Inject
|
||||
CreateNewVaultLocationController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_NEW_NAME) Lazy<Scene> chooseNameScene, @FxmlScene(FxmlFile.ADDVAULT_NEW_PASSWORD) Lazy<Scene> choosePasswordScene, LocationPresets locationPresets, ObjectProperty<Path> vaultPath, @Named("vaultName") StringProperty vaultName, ResourceBundle resourceBundle) {
|
||||
CreateNewVaultLocationController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_NEW_NAME) Lazy<Scene> chooseNameScene, @FxmlScene(FxmlFile.ADDVAULT_NEW_PASSWORD) Lazy<Scene> choosePasswordScene, ObservedLocationPresets locationPresets, ObjectProperty<Path> vaultPath, @Named("vaultName") StringProperty vaultName, ResourceBundle resourceBundle) {
|
||||
this.window = window;
|
||||
this.chooseNameScene = chooseNameScene;
|
||||
this.choosePasswordScene = choosePasswordScene;
|
||||
@@ -197,7 +197,7 @@ public class CreateNewVaultLocationController implements FxController {
|
||||
return validVaultPath.get();
|
||||
}
|
||||
|
||||
public LocationPresets getLocationPresets() {
|
||||
public ObservedLocationPresets getObservedLocationPresets() {
|
||||
return locationPresets;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import javax.inject.Named;
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.binding.StringBinding;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.fxml.FXML;
|
||||
@@ -33,8 +32,6 @@ public class CreateNewVaultNameController implements FxController {
|
||||
private final ObjectProperty<Path> vaultPath;
|
||||
private final StringProperty vaultName;
|
||||
private final BooleanBinding validVaultName;
|
||||
private final BooleanBinding invalidVaultName;
|
||||
private final StringBinding warningText;
|
||||
|
||||
@Inject
|
||||
CreateNewVaultNameController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_WELCOME) Lazy<Scene> welcomeScene, @FxmlScene(FxmlFile.ADDVAULT_NEW_LOCATION) Lazy<Scene> chooseLocationScene, ObjectProperty<Path> vaultPath, @Named("vaultName") StringProperty vaultName, ResourceBundle resourceBundle) {
|
||||
@@ -44,20 +41,14 @@ public class CreateNewVaultNameController implements FxController {
|
||||
this.vaultPath = vaultPath;
|
||||
this.vaultName = vaultName;
|
||||
this.validVaultName = Bindings.createBooleanBinding(this::isValidVaultName, vaultName);
|
||||
this.invalidVaultName = validVaultName.not();
|
||||
this.warningText = Bindings.when(vaultName.isNotEmpty().and(invalidVaultName)).then(resourceBundle.getString("addvaultwizard.new.invalidName")).otherwise((String) null);
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void initialize() {
|
||||
vaultName.bind(textField.textProperty());
|
||||
vaultName.bindBidirectional(textField.textProperty());
|
||||
vaultName.addListener(this::vaultNameChanged);
|
||||
}
|
||||
|
||||
public boolean isValidVaultName() {
|
||||
return vaultName.get() != null && VALID_NAME_PATTERN.matcher(vaultName.get().trim()).matches();
|
||||
}
|
||||
|
||||
private void vaultNameChanged(@SuppressWarnings("unused") Observable observable) {
|
||||
if (isValidVaultName()) {
|
||||
if (vaultPath.get() != null) {
|
||||
@@ -75,32 +66,17 @@ public class CreateNewVaultNameController implements FxController {
|
||||
@FXML
|
||||
public void next() {
|
||||
window.setScene(chooseLocationScene.get());
|
||||
vaultName.set(vaultName.get().trim());
|
||||
}
|
||||
|
||||
/* Getter/Setter */
|
||||
|
||||
public BooleanBinding invalidVaultNameProperty() {
|
||||
return invalidVaultName;
|
||||
public BooleanBinding validVaultNameProperty() {
|
||||
return validVaultName;
|
||||
}
|
||||
|
||||
public boolean isInvalidVaultName() {
|
||||
return invalidVaultName.get();
|
||||
}
|
||||
|
||||
public StringBinding warningTextProperty() {
|
||||
return warningText;
|
||||
}
|
||||
|
||||
public String getWarningText() {
|
||||
return warningText.get();
|
||||
}
|
||||
|
||||
public BooleanBinding showWarningProperty() {
|
||||
return warningText.isNotEmpty();
|
||||
}
|
||||
|
||||
public boolean isShowWarning() {
|
||||
return showWarningProperty().get();
|
||||
public boolean isValidVaultName() {
|
||||
return vaultName.get() != null && VALID_NAME_PATTERN.matcher(vaultName.get().trim()).matches();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ public class CreateNewVaultPasswordController implements FxController {
|
||||
this.masterkeyFileAccess = masterkeyFileAccess;
|
||||
this.processing = new SimpleBooleanProperty();
|
||||
this.readyToCreateVault = new SimpleBooleanProperty();
|
||||
this.createVaultButtonState = Bindings.createObjectBinding(this::getCreateVaultButtonState, processing);
|
||||
this.createVaultButtonState = Bindings.when(processing).then(ContentDisplay.LEFT).otherwise(ContentDisplay.TEXT_ONLY);
|
||||
}
|
||||
|
||||
@FXML
|
||||
@@ -231,6 +231,6 @@ public class CreateNewVaultPasswordController implements FxController {
|
||||
}
|
||||
|
||||
public ContentDisplay getCreateVaultButtonState() {
|
||||
return processing.get() ? ContentDisplay.LEFT : ContentDisplay.TEXT_ONLY;
|
||||
return createVaultButtonState.get();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,15 @@
|
||||
package org.cryptomator.ui.addvaultwizard;
|
||||
|
||||
import org.cryptomator.common.LocationPreset;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
@AddVaultWizardScoped
|
||||
public class LocationPresets {
|
||||
|
||||
private static final String USER_HOME = System.getProperty("user.home");
|
||||
private static final String[] ICLOUDDRIVE_LOCATIONS = {"~/Library/Mobile Documents/iCloud~com~setolabs~Cryptomator/Documents", "~/iCloudDrive/iCloud~com~setolabs~Cryptomator"};
|
||||
private static final String[] DROPBOX_LOCATIONS = {"~/Dropbox"};
|
||||
private static final String[] GDRIVE_LOCATIONS = {"~/Google Drive/My Drive", "~/Google Drive"};
|
||||
private static final String[] ONEDRIVE_LOCATIONS = {"~/OneDrive"};
|
||||
private static final String[] MEGA_LOCATIONS = {"~/MEGA"};
|
||||
private static final String[] PCLOUD_LOCATIONS = {"~/pCloudDrive"};
|
||||
public class ObservedLocationPresets {
|
||||
|
||||
private final ReadOnlyObjectProperty<Path> iclouddriveLocation;
|
||||
private final ReadOnlyObjectProperty<Path> dropboxLocation;
|
||||
@@ -33,13 +25,13 @@ public class LocationPresets {
|
||||
private final BooleanBinding foundPcloud;
|
||||
|
||||
@Inject
|
||||
public LocationPresets() {
|
||||
this.iclouddriveLocation = new SimpleObjectProperty<>(existingWritablePath(ICLOUDDRIVE_LOCATIONS));
|
||||
this.dropboxLocation = new SimpleObjectProperty<>(existingWritablePath(DROPBOX_LOCATIONS));
|
||||
this.gdriveLocation = new SimpleObjectProperty<>(existingWritablePath(GDRIVE_LOCATIONS));
|
||||
this.onedriveLocation = new SimpleObjectProperty<>(existingWritablePath(ONEDRIVE_LOCATIONS));
|
||||
this.megaLocation = new SimpleObjectProperty<>(existingWritablePath(MEGA_LOCATIONS));
|
||||
this.pcloudLocation = new SimpleObjectProperty<>(existingWritablePath(PCLOUD_LOCATIONS));
|
||||
public ObservedLocationPresets() {
|
||||
this.iclouddriveLocation = new SimpleObjectProperty<>(LocationPreset.ICLOUDDRIVE.existingPath());
|
||||
this.dropboxLocation = new SimpleObjectProperty<>(LocationPreset.DROPBOX.existingPath());
|
||||
this.gdriveLocation = new SimpleObjectProperty<>(LocationPreset.GDRIVE.existingPath());
|
||||
this.onedriveLocation = new SimpleObjectProperty<>(LocationPreset.ONEDRIVE.existingPath());
|
||||
this.megaLocation = new SimpleObjectProperty<>(LocationPreset.MEGA.existingPath());
|
||||
this.pcloudLocation = new SimpleObjectProperty<>(LocationPreset.PCLOUD.existingPath());
|
||||
this.foundIclouddrive = iclouddriveLocation.isNotNull();
|
||||
this.foundDropbox = dropboxLocation.isNotNull();
|
||||
this.foundGdrive = gdriveLocation.isNotNull();
|
||||
@@ -48,24 +40,6 @@ public class LocationPresets {
|
||||
this.foundPcloud = pcloudLocation.isNotNull();
|
||||
}
|
||||
|
||||
private static Path existingWritablePath(String... candidates) {
|
||||
for (String candidate : candidates) {
|
||||
Path path = Paths.get(resolveHomePath(candidate));
|
||||
if (Files.isDirectory(path)) {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String resolveHomePath(String path) {
|
||||
if (path.startsWith("~/")) {
|
||||
return USER_HOME + path.substring(1);
|
||||
} else {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
/* Observables */
|
||||
|
||||
public ReadOnlyObjectProperty<Path> iclouddriveLocationProperty() {
|
||||
@@ -12,15 +12,15 @@ import javafx.beans.value.ObservableValue;
|
||||
* <p>
|
||||
* During creation the consumer can optionally define actions to be executed everytime before the animation starts and after it stops.
|
||||
*/
|
||||
public class AutoAnimator<T extends Animation> {
|
||||
public class AutoAnimator {
|
||||
|
||||
private final T animation;
|
||||
private final Animation animation;
|
||||
private final ObservableValue<Boolean> condition;
|
||||
private final Runnable beforeStart;
|
||||
private final Runnable afterStop;
|
||||
private final Subscription sub;
|
||||
|
||||
AutoAnimator(T animation, ObservableValue<Boolean> condition, Runnable beforeStart, Runnable afterStop) {
|
||||
AutoAnimator(Animation animation, ObservableValue<Boolean> condition, Runnable beforeStart, Runnable afterStop) {
|
||||
this.animation = animation;
|
||||
this.condition = condition;
|
||||
this.beforeStart = beforeStart;
|
||||
@@ -52,7 +52,7 @@ public class AutoAnimator<T extends Animation> {
|
||||
|
||||
public static class Builder {
|
||||
|
||||
private Animation animation;
|
||||
private final Animation animation;
|
||||
private ObservableValue<Boolean> condition = new SimpleBooleanProperty(true);
|
||||
private Runnable beforeStart = () -> {};
|
||||
private Runnable afterStop = () -> {};
|
||||
|
||||
@@ -77,7 +77,7 @@ public class ErrorController implements FxController {
|
||||
var enhancedTemplate = String.format(REPORT_BODY_TEMPLATE, //
|
||||
System.getProperty("os.name"), //
|
||||
System.getProperty("os.version"), //
|
||||
environment.getAppVersion().orElse("undefined"), //
|
||||
environment.getAppVersion(), //
|
||||
environment.getBuildNumber().orElse("undefined"));
|
||||
var body = URLEncoder.encode(enhancedTemplate, StandardCharsets.UTF_8);
|
||||
application.getHostServices().showDocument(REPORT_URL_FORMAT.formatted(title, body));
|
||||
|
||||
@@ -13,6 +13,13 @@ public enum FxmlFile {
|
||||
FORGET_PASSWORD("/fxml/forget_password.fxml"), //
|
||||
HEALTH_START("/fxml/health_start.fxml"), //
|
||||
HEALTH_CHECK_LIST("/fxml/health_check_list.fxml"), //
|
||||
HUB_AUTH_FLOW("/fxml/hub_auth_flow.fxml"), //
|
||||
HUB_LICENSE_EXCEEDED("/fxml/hub_license_exceeded.fxml"), //
|
||||
HUB_RECEIVE_KEY("/fxml/hub_receive_key.fxml"), //
|
||||
HUB_REGISTER_DEVICE("/fxml/hub_register_device.fxml"), //
|
||||
HUB_REGISTER_SUCCESS("/fxml/hub_register_success.fxml"), //
|
||||
HUB_REGISTER_FAILED("/fxml/hub_register_failed.fxml"),
|
||||
HUB_UNAUTHORIZED_DEVICE("/fxml/hub_unauthorized_device.fxml"), //
|
||||
LOCK_FORCED("/fxml/lock_forced.fxml"), //
|
||||
LOCK_FAILED("/fxml/lock_failed.fxml"), //
|
||||
MAIN_WINDOW("/fxml/main_window.fxml"), //
|
||||
@@ -23,9 +30,11 @@ public enum FxmlFile {
|
||||
MIGRATION_SUCCESS("/fxml/migration_success.fxml"), //
|
||||
PREFERENCES("/fxml/preferences.fxml"), //
|
||||
QUIT("/fxml/quit.fxml"), //
|
||||
QUIT_FORCED("/fxml/quit_forced.fxml"), //
|
||||
RECOVERYKEY_CREATE("/fxml/recoverykey_create.fxml"), //
|
||||
RECOVERYKEY_RECOVER("/fxml/recoverykey_recover.fxml"), //
|
||||
RECOVERYKEY_RESET_PASSWORD("/fxml/recoverykey_reset_password.fxml"), //
|
||||
RECOVERYKEY_RESET_PASSWORD_SUCCESS("/fxml/recoverykey_reset_password_success.fxml"), //
|
||||
RECOVERYKEY_SUCCESS("/fxml/recoverykey_success.fxml"), //
|
||||
REMOVE_VAULT("/fxml/remove_vault.fxml"), //
|
||||
UNLOCK_ENTER_PASSWORD("/fxml/unlock_enter_password.fxml"),
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.cryptomator.ui.common;
|
||||
|
||||
import com.tobiasdiez.easybind.EasyBind;
|
||||
import org.cryptomator.ui.controls.FontAwesome5IconView;
|
||||
import org.cryptomator.ui.controls.NiceSecurePasswordField;
|
||||
|
||||
@@ -42,7 +41,7 @@ public class NewPasswordController implements FxController {
|
||||
passwordStrength.bind(Bindings.createIntegerBinding(() -> strengthRater.computeRate(passwordField.getCharacters()), passwordField.textProperty()));
|
||||
|
||||
passwordStrengthLabel.graphicProperty().bind(Bindings.createObjectBinding(this::getIconViewForPasswordStrengthLabel, passwordField.textProperty(), passwordStrength));
|
||||
passwordStrengthLabel.textProperty().bind(EasyBind.map(passwordStrength, strengthRater::getStrengthDescription));
|
||||
passwordStrengthLabel.textProperty().bind(passwordStrength.map(strengthRater::getStrengthDescription));
|
||||
|
||||
BooleanBinding passwordsMatch = Bindings.createBooleanBinding(this::passwordFieldsMatch, passwordField.textProperty(), reenterField.textProperty());
|
||||
BooleanBinding reenterFieldNotEmpty = reenterField.textProperty().isNotEmpty();
|
||||
|
||||
@@ -86,7 +86,8 @@ public class VaultService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates but doesn't start a lock-all task.
|
||||
* Creates a lock-all task.
|
||||
* This task itself is _not started_, but its subtasks locking each vault will be already executed.
|
||||
*
|
||||
* @param vaults The list of vaults to be locked
|
||||
* @param forced Whether to attempt a forced lock
|
||||
|
||||
@@ -46,7 +46,7 @@ public class NiceSecurePasswordField extends StackPane {
|
||||
nonPrintableCharsIcon.managedProperty().bind(passwordField.containingNonPrintableCharsProperty());
|
||||
|
||||
revealPasswordIcon.setGlyph(FontAwesome5Icon.EYE);
|
||||
revealPasswordIcon.glyphProperty().bind(Bindings.createObjectBinding(this::getRevealPasswordGlyph, revealPasswordButton.selectedProperty()));
|
||||
revealPasswordIcon.glyphProperty().bind(Bindings.when(revealPasswordButton.selectedProperty()).then(FontAwesome5Icon.EYE_SLASH).otherwise(FontAwesome5Icon.EYE));
|
||||
revealPasswordIcon.setGlyphSize(ICON_SIZE);
|
||||
|
||||
revealPasswordButton.setContentDisplay(ContentDisplay.LEFT);
|
||||
@@ -61,10 +61,6 @@ public class NiceSecurePasswordField extends StackPane {
|
||||
disabledProperty().addListener(this::disabledChanged);
|
||||
}
|
||||
|
||||
private FontAwesome5Icon getRevealPasswordGlyph() {
|
||||
return revealPasswordButton.isSelected() ? FontAwesome5Icon.EYE_SLASH : FontAwesome5Icon.EYE;
|
||||
}
|
||||
|
||||
private void disabledChanged(@SuppressWarnings("unused") Observable observable) {
|
||||
revealPasswordButton.setSelected(false);
|
||||
}
|
||||
|
||||
@@ -71,9 +71,11 @@ public class SecurePasswordField extends TextField {
|
||||
}
|
||||
|
||||
public void cut() {
|
||||
//not implemented by design
|
||||
}
|
||||
|
||||
public void copy() {
|
||||
//not implemented by design
|
||||
}
|
||||
|
||||
public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
|
||||
|
||||
@@ -43,9 +43,11 @@ public class ForgetPasswordController implements FxController {
|
||||
LOG.debug("Forgot password for vault {}.", vault.getDisplayName());
|
||||
confirmedResult.setValue(true);
|
||||
} catch (KeychainAccessException e) {
|
||||
LOG.error("Failed to remove entry from system keychain.", e);
|
||||
LOG.error("Failed to delete passphrase from system keychain.", e);
|
||||
confirmedResult.setValue(false);
|
||||
}
|
||||
} else {
|
||||
LOG.warn("Keychain not supported. Doing nothing.");
|
||||
}
|
||||
window.close();
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import org.cryptomator.common.vaults.Vault;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.collections.ObservableList;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionStage;
|
||||
|
||||
@FxApplicationScoped
|
||||
public class AutoUnlocker {
|
||||
@@ -18,9 +20,11 @@ public class AutoUnlocker {
|
||||
}
|
||||
|
||||
public void unlock() {
|
||||
vaults.stream().filter(Vault::isLocked).filter(v -> v.getVaultSettings().unlockAfterStartup().get()).forEach(v -> {
|
||||
appWindows.startUnlockWorkflow(v, null);
|
||||
});
|
||||
vaults.stream().filter(Vault::isLocked) //
|
||||
.filter(v -> v.getVaultSettings().unlockAfterStartup().get()) //
|
||||
.<CompletionStage<Void>>reduce(CompletableFuture.completedFuture(null), //
|
||||
(unlockFlow, v) -> unlockFlow.handle((voit, ex) -> appWindows.startUnlockWorkflow(v, null)).thenCompose(stage -> stage), //we don't care here about the exception, logged elsewhere
|
||||
(unlockChain1, unlockChain2) -> unlockChain1.handle((voit, ex) -> unlockChain2).thenCompose(stage -> stage));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import org.cryptomator.ui.lock.LockComponent;
|
||||
import org.cryptomator.ui.mainwindow.MainWindowComponent;
|
||||
import org.cryptomator.ui.preferences.PreferencesComponent;
|
||||
import org.cryptomator.ui.quit.QuitComponent;
|
||||
|
||||
import org.cryptomator.ui.traymenu.TrayMenuComponent;
|
||||
import org.cryptomator.ui.unlock.UnlockComponent;
|
||||
|
||||
@@ -57,4 +58,4 @@ abstract class FxApplicationModule {
|
||||
static QuitComponent provideQuitComponent(QuitComponent.Builder builder) {
|
||||
return builder.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,12 @@ package org.cryptomator.ui.fxapp;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import org.cryptomator.common.ShutdownHook;
|
||||
import org.cryptomator.common.settings.Settings;
|
||||
import org.cryptomator.common.vaults.LockNotCompletedException;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.common.vaults.VaultState;
|
||||
import org.cryptomator.common.vaults.Volume;
|
||||
import org.cryptomator.ui.common.VaultService;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -27,18 +29,24 @@ import static org.cryptomator.common.vaults.VaultState.Value.*;
|
||||
public class FxApplicationTerminator {
|
||||
|
||||
private static final Set<VaultState.Value> STATES_ALLOWING_TERMINATION = EnumSet.of(LOCKED, NEEDS_MIGRATION, MISSING, ERROR);
|
||||
private static final Set<VaultState.Value> STATES_PREVENT_TERMINATION = EnumSet.of(PROCESSING);
|
||||
private static final Logger LOG = LoggerFactory.getLogger(FxApplicationTerminator.class);
|
||||
|
||||
private final ObservableList<Vault> vaults;
|
||||
private final ShutdownHook shutdownHook;
|
||||
private final FxApplicationWindows appWindows;
|
||||
private final AtomicBoolean allowQuitWithoutPrompt = new AtomicBoolean();
|
||||
private final AtomicBoolean preventQuitWithGracefulLock = new AtomicBoolean();
|
||||
private final Settings settings;
|
||||
private final VaultService vaultService;
|
||||
|
||||
@Inject
|
||||
public FxApplicationTerminator(ObservableList<Vault> vaults, ShutdownHook shutdownHook, FxApplicationWindows appWindows) {
|
||||
public FxApplicationTerminator(ObservableList<Vault> vaults, ShutdownHook shutdownHook, FxApplicationWindows appWindows, Settings settings, VaultService vaultService) {
|
||||
this.vaults = vaults;
|
||||
this.shutdownHook = shutdownHook;
|
||||
this.appWindows = appWindows;
|
||||
this.settings = settings;
|
||||
this.vaultService = vaultService;
|
||||
}
|
||||
|
||||
public void initialize() {
|
||||
@@ -72,6 +80,10 @@ public class FxApplicationTerminator {
|
||||
private void vaultListChanged(@SuppressWarnings("unused") Observable observable) {
|
||||
boolean allowSuddenTermination = vaults.stream().map(Vault::getState).allMatch(STATES_ALLOWING_TERMINATION::contains);
|
||||
boolean stateChanged = allowQuitWithoutPrompt.compareAndSet(!allowSuddenTermination, allowSuddenTermination);
|
||||
|
||||
boolean preventGracefulTermination = vaults.stream().map(Vault::getState).anyMatch(STATES_PREVENT_TERMINATION::contains);
|
||||
preventQuitWithGracefulLock.set(preventGracefulTermination);
|
||||
|
||||
Desktop desktop = Desktop.getDesktop();
|
||||
if (stateChanged && desktop.isSupported(Desktop.Action.APP_SUDDEN_TERMINATION)) {
|
||||
if (allowSuddenTermination) {
|
||||
@@ -92,10 +104,22 @@ public class FxApplicationTerminator {
|
||||
*/
|
||||
private void handleQuitRequest(@SuppressWarnings("unused") @Nullable EventObject e, QuitResponse response) {
|
||||
var exitingResponse = new ExitingQuitResponse(response);
|
||||
|
||||
if (allowQuitWithoutPrompt.get()) {
|
||||
exitingResponse.performQuit();
|
||||
} else if (settings.autoCloseVaults().get() && !preventQuitWithGracefulLock.get()) {
|
||||
var lockAllTask = vaultService.createLockAllTask(vaults.filtered(Vault::isUnlocked), false);
|
||||
lockAllTask.setOnSucceeded(event -> {
|
||||
LOG.info("Locked remaining vaults was succesful.");
|
||||
exitingResponse.performQuit();
|
||||
});
|
||||
lockAllTask.setOnFailed(event -> {
|
||||
LOG.warn("Unable to lock all vaults.");
|
||||
appWindows.showQuitWindow(exitingResponse, true);
|
||||
});
|
||||
lockAllTask.run();
|
||||
} else {
|
||||
appWindows.showQuitWindow(exitingResponse);
|
||||
appWindows.showQuitWindow(exitingResponse, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,7 +139,7 @@ public class FxApplicationTerminator {
|
||||
|
||||
/**
|
||||
* A dummy QuitResponse that ignores the response.
|
||||
*
|
||||
* <p>
|
||||
* To be used with {@link #handleQuitRequest(EventObject, QuitResponse)} if the invoking method is not interested in the response.
|
||||
*/
|
||||
private static class NoopQuitResponse implements QuitResponse {
|
||||
|
||||
@@ -40,7 +40,7 @@ public class FxApplicationWindows {
|
||||
private final Optional<TrayIntegrationProvider> trayIntegration;
|
||||
private final Lazy<MainWindowComponent> mainWindow;
|
||||
private final Lazy<PreferencesComponent> preferencesWindow;
|
||||
private final Lazy<QuitComponent> quitWindow;
|
||||
private final QuitComponent.Builder quitWindowBuilder;
|
||||
private final UnlockComponent.Factory unlockWorkflowFactory;
|
||||
private final LockComponent.Factory lockWorkflowFactory;
|
||||
private final ErrorComponent.Factory errorWindowFactory;
|
||||
@@ -48,12 +48,12 @@ public class FxApplicationWindows {
|
||||
private final FilteredList<Window> visibleWindows;
|
||||
|
||||
@Inject
|
||||
public FxApplicationWindows(@PrimaryStage Stage primaryStage, Optional<TrayIntegrationProvider> trayIntegration, Lazy<MainWindowComponent> mainWindow, Lazy<PreferencesComponent> preferencesWindow, Lazy<QuitComponent> quitWindow, UnlockComponent.Factory unlockWorkflowFactory, LockComponent.Factory lockWorkflowFactory, ErrorComponent.Factory errorWindowFactory, ExecutorService executor) {
|
||||
public FxApplicationWindows(@PrimaryStage Stage primaryStage, Optional<TrayIntegrationProvider> trayIntegration, Lazy<MainWindowComponent> mainWindow, Lazy<PreferencesComponent> preferencesWindow, QuitComponent.Builder quitWindowBuilder, UnlockComponent.Factory unlockWorkflowFactory, LockComponent.Factory lockWorkflowFactory, ErrorComponent.Factory errorWindowFactory, ExecutorService executor) {
|
||||
this.primaryStage = primaryStage;
|
||||
this.trayIntegration = trayIntegration;
|
||||
this.mainWindow = mainWindow;
|
||||
this.preferencesWindow = preferencesWindow;
|
||||
this.quitWindow = quitWindow;
|
||||
this.quitWindowBuilder = quitWindowBuilder;
|
||||
this.unlockWorkflowFactory = unlockWorkflowFactory;
|
||||
this.lockWorkflowFactory = lockWorkflowFactory;
|
||||
this.errorWindowFactory = errorWindowFactory;
|
||||
@@ -104,8 +104,8 @@ public class FxApplicationWindows {
|
||||
return CompletableFuture.supplyAsync(() -> preferencesWindow.get().showPreferencesWindow(selectedTab), Platform::runLater).whenComplete(this::reportErrors);
|
||||
}
|
||||
|
||||
public CompletionStage<Stage> showQuitWindow(QuitResponse response) {
|
||||
return CompletableFuture.supplyAsync(() -> quitWindow.get().showQuitWindow(response), Platform::runLater).whenComplete(this::reportErrors);
|
||||
public void showQuitWindow(QuitResponse response, boolean forced) {
|
||||
CompletableFuture.runAsync(() -> quitWindowBuilder.build().showQuitWindow(response,forced), Platform::runLater);
|
||||
}
|
||||
|
||||
public CompletionStage<Void> startUnlockWorkflow(Vault vault, @Nullable Stage owner) {
|
||||
|
||||
@@ -9,14 +9,12 @@ import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.property.ReadOnlyStringProperty;
|
||||
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.util.Comparator;
|
||||
import java.util.Optional;
|
||||
|
||||
@FxApplicationScoped
|
||||
public class UpdateChecker {
|
||||
@@ -25,8 +23,7 @@ public class UpdateChecker {
|
||||
private static final Duration AUTOCHECK_DELAY = Duration.seconds(5);
|
||||
|
||||
private final Settings settings;
|
||||
private final Optional<String> applicationVersion;
|
||||
private final StringProperty currentVersionProperty;
|
||||
private final String currentVersion;
|
||||
private final StringProperty latestVersionProperty;
|
||||
private final Comparator<String> semVerComparator;
|
||||
private final ScheduledService<String> updateCheckerService;
|
||||
@@ -34,11 +31,10 @@ public class UpdateChecker {
|
||||
@Inject
|
||||
UpdateChecker(Settings settings, Environment env, @Named("latestVersion") StringProperty latestVersionProperty, @Named("SemVer") Comparator<String> semVerComparator, ScheduledService<String> updateCheckerService) {
|
||||
this.settings = settings;
|
||||
this.applicationVersion = env.getAppVersion();
|
||||
this.latestVersionProperty = latestVersionProperty;
|
||||
this.semVerComparator = semVerComparator;
|
||||
this.updateCheckerService = updateCheckerService;
|
||||
this.currentVersionProperty = new SimpleStringProperty(applicationVersion.orElse("SNAPSHOT"));
|
||||
this.currentVersion = env.getAppVersion();
|
||||
}
|
||||
|
||||
public void automaticallyCheckForUpdatesIfEnabled() {
|
||||
@@ -66,11 +62,10 @@ public class UpdateChecker {
|
||||
}
|
||||
|
||||
private void checkSucceeded(WorkerStateEvent event) {
|
||||
String currentVersion = applicationVersion.orElse(null);
|
||||
String latestVersion = updateCheckerService.getValue();
|
||||
LOG.info("Current version: {}, lastest version: {}", currentVersion, latestVersion);
|
||||
|
||||
if (currentVersion == null || semVerComparator.compare(currentVersion, latestVersion) < 0) {
|
||||
if (semVerComparator.compare(currentVersion, latestVersion) < 0) {
|
||||
// update is available
|
||||
latestVersionProperty.set(latestVersion);
|
||||
} else {
|
||||
@@ -92,8 +87,8 @@ public class UpdateChecker {
|
||||
return latestVersionProperty;
|
||||
}
|
||||
|
||||
public ReadOnlyStringProperty currentVersionProperty() {
|
||||
return currentVersionProperty;
|
||||
public String getCurrentVersion() {
|
||||
return currentVersion;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -43,7 +43,9 @@ public abstract class UpdateCheckerModule {
|
||||
@FxApplicationScoped
|
||||
static Optional<HttpClient> provideHttpClient() {
|
||||
try {
|
||||
return Optional.of(HttpClient.newHttpClient());
|
||||
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();
|
||||
@@ -54,7 +56,7 @@ public abstract class UpdateCheckerModule {
|
||||
@FxApplicationScoped
|
||||
static HttpRequest provideCheckForUpdatesRequest(Environment env) {
|
||||
String userAgent = String.format("Cryptomator VersionChecker/%s %s %s (%s)", //
|
||||
env.getAppVersion().orElse("SNAPSHOT"), //
|
||||
env.getAppVersion(), //
|
||||
SystemUtils.OS_NAME, //
|
||||
SystemUtils.OS_VERSION, //
|
||||
SystemUtils.OS_ARCH); //
|
||||
|
||||
@@ -3,12 +3,12 @@ package org.cryptomator.ui.health;
|
||||
import com.tobiasdiez.easybind.EasyBind;
|
||||
import com.tobiasdiez.easybind.EasyObservableList;
|
||||
import com.tobiasdiez.easybind.Subscription;
|
||||
import com.tobiasdiez.easybind.optional.OptionalBinding;
|
||||
import org.cryptomator.cryptofs.health.api.DiagnosticResult;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.beans.binding.Binding;
|
||||
import javafx.beans.binding.BooleanExpression;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.collections.FXCollections;
|
||||
@@ -22,15 +22,15 @@ public class CheckDetailController implements FxController {
|
||||
|
||||
private final EasyObservableList<Result> results;
|
||||
private final ObjectProperty<Check> check;
|
||||
private final OptionalBinding<Check.CheckState> checkState;
|
||||
private final Binding<String> checkName;
|
||||
private final Binding<Boolean> checkRunning;
|
||||
private final Binding<Boolean> checkScheduled;
|
||||
private final Binding<Boolean> checkFinished;
|
||||
private final Binding<Boolean> checkSkipped;
|
||||
private final Binding<Boolean> checkSucceeded;
|
||||
private final Binding<Boolean> checkFailed;
|
||||
private final Binding<Boolean> checkCancelled;
|
||||
private final ObservableValue<Check.CheckState> checkState;
|
||||
private final ObservableValue<String> checkName;
|
||||
private final BooleanExpression checkRunning;
|
||||
private final BooleanExpression checkScheduled;
|
||||
private final BooleanExpression checkFinished;
|
||||
private final BooleanExpression checkSkipped;
|
||||
private final BooleanExpression checkSucceeded;
|
||||
private final BooleanExpression checkFailed;
|
||||
private final BooleanExpression checkCancelled;
|
||||
private final Binding<Number> countOfWarnSeverity;
|
||||
private final Binding<Number> countOfCritSeverity;
|
||||
private final Binding<Boolean> warnOrCritsExist;
|
||||
@@ -44,15 +44,15 @@ public class CheckDetailController implements FxController {
|
||||
this.resultListCellFactory = resultListCellFactory;
|
||||
this.results = EasyBind.wrapList(FXCollections.observableArrayList());
|
||||
this.check = selectedTask;
|
||||
this.checkState = EasyBind.wrapNullable(selectedTask).mapObservable(Check::stateProperty);
|
||||
this.checkName = EasyBind.wrapNullable(selectedTask).map(Check::getName).orElse("");
|
||||
this.checkRunning = checkState.map(Check.CheckState.RUNNING::equals).orElse(false);
|
||||
this.checkScheduled = checkState.map(Check.CheckState.SCHEDULED::equals).orElse(false);
|
||||
this.checkSkipped = checkState.map(Check.CheckState.SKIPPED::equals).orElse(false);
|
||||
this.checkSucceeded = checkState.map(Check.CheckState.SUCCEEDED::equals).orElse(false);
|
||||
this.checkFailed = checkState.map(Check.CheckState.ERROR::equals).orElse(false);
|
||||
this.checkCancelled = checkState.map(Check.CheckState.CANCELLED::equals).orElse(false);
|
||||
this.checkFinished = EasyBind.combine(checkSucceeded, checkFailed, checkCancelled, (a, b, c) -> a || b || c);
|
||||
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.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) );
|
||||
@@ -84,7 +84,7 @@ public class CheckDetailController implements FxController {
|
||||
return checkName.getValue();
|
||||
}
|
||||
|
||||
public Binding<String> checkNameProperty() {
|
||||
public ObservableValue<String> checkNameProperty() {
|
||||
return checkName;
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ public class CheckDetailController implements FxController {
|
||||
return checkRunning.getValue();
|
||||
}
|
||||
|
||||
public Binding<Boolean> checkRunningProperty() {
|
||||
public BooleanExpression checkRunningProperty() {
|
||||
return checkRunning;
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ public class CheckDetailController implements FxController {
|
||||
return checkFinished.getValue();
|
||||
}
|
||||
|
||||
public Binding<Boolean> checkFinishedProperty() {
|
||||
public BooleanExpression checkFinishedProperty() {
|
||||
return checkFinished;
|
||||
}
|
||||
|
||||
@@ -124,7 +124,7 @@ public class CheckDetailController implements FxController {
|
||||
return checkScheduled.getValue();
|
||||
}
|
||||
|
||||
public Binding<Boolean> checkScheduledProperty() {
|
||||
public BooleanExpression checkScheduledProperty() {
|
||||
return checkScheduled;
|
||||
}
|
||||
|
||||
@@ -132,7 +132,7 @@ public class CheckDetailController implements FxController {
|
||||
return checkSkipped.getValue();
|
||||
}
|
||||
|
||||
public Binding<Boolean> checkSkippedProperty() {
|
||||
public BooleanExpression checkSkippedProperty() {
|
||||
return checkSkipped;
|
||||
}
|
||||
|
||||
@@ -140,7 +140,7 @@ public class CheckDetailController implements FxController {
|
||||
return checkSucceeded.getValue();
|
||||
}
|
||||
|
||||
public Binding<Boolean> checkSucceededProperty() {
|
||||
public BooleanExpression checkSucceededProperty() {
|
||||
return checkSucceeded;
|
||||
}
|
||||
|
||||
@@ -148,7 +148,7 @@ public class CheckDetailController implements FxController {
|
||||
return checkFailed.getValue();
|
||||
}
|
||||
|
||||
public Binding<Boolean> checkFailedProperty() {
|
||||
public BooleanExpression checkFailedProperty() {
|
||||
return checkFailed;
|
||||
}
|
||||
|
||||
@@ -164,7 +164,7 @@ public class CheckDetailController implements FxController {
|
||||
return warnOrCritsExist.getValue();
|
||||
}
|
||||
|
||||
public Binding<Boolean> checkCancelledProperty() {
|
||||
public BooleanExpression checkCancelledProperty() {
|
||||
return checkCancelled;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
package org.cryptomator.ui.health;
|
||||
|
||||
import com.tobiasdiez.easybind.EasyBind;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.beans.binding.Binding;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.scene.control.CheckBox;
|
||||
|
||||
public class CheckListCellController implements FxController {
|
||||
|
||||
|
||||
private final ObjectProperty<Check> check;
|
||||
private final Binding<String> checkName;
|
||||
private final Binding<Boolean> checkRunnable;
|
||||
private final ObservableValue<Boolean> checkRunnable;
|
||||
private final ObservableValue<String> checkName;
|
||||
|
||||
/* FXML */
|
||||
public CheckBox checkbox;
|
||||
@@ -23,8 +22,8 @@ public class CheckListCellController implements FxController {
|
||||
@Inject
|
||||
public CheckListCellController() {
|
||||
check = new SimpleObjectProperty<>();
|
||||
checkRunnable = EasyBind.wrapNullable(check).mapObservable(Check::stateProperty).map(Check.CheckState.RUNNABLE::equals).orElse(false);
|
||||
checkName = EasyBind.wrapNullable(check).map(Check::getName).orElse("");
|
||||
checkRunnable = check.flatMap(Check::stateProperty).map(Check.CheckState.RUNNABLE::equals).orElse(false);
|
||||
checkName = check.map(Check::getName).orElse("");
|
||||
}
|
||||
|
||||
public void initialize() {
|
||||
@@ -50,7 +49,7 @@ public class CheckListCellController implements FxController {
|
||||
check.set(c);
|
||||
}
|
||||
|
||||
public Binding<String> checkNameProperty() {
|
||||
public ObservableValue<String> checkNameProperty() {
|
||||
return checkName;
|
||||
}
|
||||
|
||||
@@ -58,7 +57,7 @@ public class CheckListCellController implements FxController {
|
||||
return checkName.getValue();
|
||||
}
|
||||
|
||||
public Binding<Boolean> checkRunnableProperty() {
|
||||
public ObservableValue<Boolean> checkRunnableProperty() {
|
||||
return checkRunnable;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,13 +13,12 @@ import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Binding;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.value.ObservableObjectValue;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.util.Duration;
|
||||
@@ -38,10 +37,10 @@ public class ResultListCellController implements FxController {
|
||||
private final Logger LOG = LoggerFactory.getLogger(ResultListCellController.class);
|
||||
|
||||
private final ObjectProperty<Result> result;
|
||||
private final ObservableObjectValue<DiagnosticResult.Severity> severity;
|
||||
private final Binding<String> description;
|
||||
private final ObservableValue<DiagnosticResult.Severity> severity;
|
||||
private final ObservableValue<String> description;
|
||||
private final ResultFixApplier fixApplier;
|
||||
private final ObservableObjectValue<Result.FixState> fixState;
|
||||
private final ObservableValue<Result.FixState> fixState;
|
||||
private final ObjectBinding<FontAwesome5Icon> severityGlyph;
|
||||
private final ObjectBinding<FontAwesome5Icon> fixGlyph;
|
||||
private final BooleanBinding fixable;
|
||||
@@ -62,10 +61,10 @@ public class ResultListCellController implements FxController {
|
||||
@Inject
|
||||
public ResultListCellController(ResultFixApplier fixApplier, ResourceBundle resourceBundle) {
|
||||
this.result = new SimpleObjectProperty<>(null);
|
||||
this.severity = EasyBind.wrapNullable(result).map(r -> r.diagnosis().getSeverity()).asOrdinary();
|
||||
this.description = EasyBind.wrapNullable(result).map(Result::getDescription).orElse("");
|
||||
this.severity = result.map(Result::diagnosis).map(DiagnosticResult::getSeverity);
|
||||
this.description = result.map(Result::getDescription).orElse("");
|
||||
this.fixApplier = fixApplier;
|
||||
this.fixState = EasyBind.wrapNullable(result).mapObservable(Result::fixState).asOrdinary();
|
||||
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);
|
||||
@@ -83,14 +82,15 @@ public class ResultListCellController implements FxController {
|
||||
@FXML
|
||||
public void initialize() {
|
||||
// see getGlyph() for relevant glyphs:
|
||||
subscriptions.addAll(List.of(EasyBind.includeWhen(severityView.getStyleClass(), "glyph-icon-muted", Bindings.equal(severity, DiagnosticResult.Severity.INFO)), //
|
||||
EasyBind.includeWhen(severityView.getStyleClass(), "glyph-icon-primary", Bindings.equal(severity, DiagnosticResult.Severity.GOOD)), //
|
||||
EasyBind.includeWhen(severityView.getStyleClass(), "glyph-icon-orange", Bindings.equal(severity, DiagnosticResult.Severity.WARN)), //
|
||||
EasyBind.includeWhen(severityView.getStyleClass(), "glyph-icon-red", Bindings.equal(severity, DiagnosticResult.Severity.CRITICAL)) //
|
||||
subscriptions.addAll(List.of(EasyBind.includeWhen(severityView.getStyleClass(), "glyph-icon-muted", severity.map(DiagnosticResult.Severity.INFO::equals).orElse(false)), //
|
||||
EasyBind.includeWhen(severityView.getStyleClass(), "glyph-icon-primary", severity.map(DiagnosticResult.Severity.GOOD::equals).orElse(false)), //
|
||||
EasyBind.includeWhen(severityView.getStyleClass(), "glyph-icon-orange", severity.map(DiagnosticResult.Severity.WARN::equals).orElse(false)), //
|
||||
EasyBind.includeWhen(severityView.getStyleClass(), "glyph-icon-red", severity.map(DiagnosticResult.Severity.CRITICAL::equals).orElse(false)) //
|
||||
));
|
||||
|
||||
var animation = Animations.createDiscrete360Rotation(fixView);
|
||||
this.fixRunningRotator = AutoAnimator.animate(animation) //
|
||||
.onCondition(Bindings.equal(fixState, Result.FixState.FIXING)) //
|
||||
.onCondition(fixing) //
|
||||
.afterStop(() -> fixView.setRotate(0)) //
|
||||
.build();
|
||||
}
|
||||
@@ -127,7 +127,7 @@ public class ResultListCellController implements FxController {
|
||||
return result;
|
||||
}
|
||||
|
||||
public Binding<String> descriptionProperty() {
|
||||
public ObservableValue<String> descriptionProperty() {
|
||||
return description;
|
||||
}
|
||||
|
||||
@@ -173,7 +173,7 @@ public class ResultListCellController implements FxController {
|
||||
}
|
||||
|
||||
public boolean isFixable() {
|
||||
return Result.FixState.FIXABLE.equals(fixState.get());
|
||||
return Result.FixState.FIXABLE.equals(fixState.getValue());
|
||||
}
|
||||
|
||||
public BooleanBinding fixingProperty() {
|
||||
@@ -181,7 +181,7 @@ public class ResultListCellController implements FxController {
|
||||
}
|
||||
|
||||
public boolean isFixing() {
|
||||
return Result.FixState.FIXING.equals(fixState.get());
|
||||
return Result.FixState.FIXING.equals(fixState.getValue());
|
||||
}
|
||||
|
||||
public BooleanBinding fixedProperty() {
|
||||
@@ -189,7 +189,7 @@ public class ResultListCellController implements FxController {
|
||||
}
|
||||
|
||||
public boolean isFixed() {
|
||||
return Result.FixState.FIXED.equals(fixState.get());
|
||||
return Result.FixState.FIXED.equals(fixState.getValue());
|
||||
}
|
||||
|
||||
public BooleanBinding fixFailedProperty() {
|
||||
@@ -197,7 +197,7 @@ public class ResultListCellController implements FxController {
|
||||
}
|
||||
|
||||
public Boolean isFixFailed() {
|
||||
return Result.FixState.FIX_FAILED.equals(fixState.get());
|
||||
return Result.FixState.FIX_FAILED.equals(fixState.getValue());
|
||||
}
|
||||
|
||||
public BooleanBinding fixRunningOrDoneProperty() {
|
||||
|
||||
@@ -6,6 +6,7 @@ import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.ui.common.DefaultSceneFactory;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.common.FxmlLoaderFactory;
|
||||
import org.cryptomator.ui.keyloading.hub.HubKeyLoadingModule;
|
||||
import org.cryptomator.ui.keyloading.masterkeyfile.MasterkeyFileLoadingModule;
|
||||
|
||||
import javax.inject.Provider;
|
||||
@@ -13,7 +14,7 @@ import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
@Module(includes = {MasterkeyFileLoadingModule.class})
|
||||
@Module(includes = {MasterkeyFileLoadingModule.class, HubKeyLoadingModule.class})
|
||||
abstract class KeyLoadingModule {
|
||||
|
||||
@Provides
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
package org.cryptomator.ui.keyloading.hub;
|
||||
|
||||
record AuthFlowContext(String deviceId) {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package org.cryptomator.ui.keyloading.hub;
|
||||
|
||||
import com.nimbusds.jose.JWEObject;
|
||||
import dagger.Lazy;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.common.FxmlFile;
|
||||
import org.cryptomator.ui.common.FxmlScene;
|
||||
import org.cryptomator.ui.keyloading.KeyLoading;
|
||||
import org.cryptomator.ui.keyloading.KeyLoadingScoped;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javafx.application.Application;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.StringBinding;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.concurrent.WorkerStateEvent;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.WindowEvent;
|
||||
import java.net.URI;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
@KeyLoadingScoped
|
||||
public class AuthFlowController implements FxController {
|
||||
|
||||
private final Application application;
|
||||
private final Stage window;
|
||||
private final ExecutorService executor;
|
||||
private final String deviceId;
|
||||
private final HubConfig hubConfig;
|
||||
private final AtomicReference<String> tokenRef;
|
||||
private final CompletableFuture<JWEObject> result;
|
||||
private final Lazy<Scene> receiveKeyScene;
|
||||
private final ObjectProperty<URI> authUri;
|
||||
private AuthFlowTask task;
|
||||
|
||||
@Inject
|
||||
public AuthFlowController(Application application, @KeyLoading Stage window, ExecutorService executor, @Named("deviceId") String deviceId, HubConfig hubConfig, @Named("bearerToken") AtomicReference<String> tokenRef, CompletableFuture<JWEObject> result, @FxmlScene(FxmlFile.HUB_RECEIVE_KEY) Lazy<Scene> receiveKeyScene) {
|
||||
this.application = application;
|
||||
this.window = window;
|
||||
this.executor = executor;
|
||||
this.deviceId = deviceId;
|
||||
this.hubConfig = hubConfig;
|
||||
this.tokenRef = tokenRef;
|
||||
this.result = result;
|
||||
this.receiveKeyScene = receiveKeyScene;
|
||||
this.authUri = new SimpleObjectProperty<>();
|
||||
this.window.addEventHandler(WindowEvent.WINDOW_HIDING, this::windowClosed);
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void initialize() {
|
||||
assert task == null;
|
||||
task = new AuthFlowTask(hubConfig, new AuthFlowContext(deviceId), this::setAuthUri);
|
||||
task.setOnFailed(this::authFailed);
|
||||
task.setOnSucceeded(this::authSucceeded);
|
||||
executor.submit(task);
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void browse() {
|
||||
application.getHostServices().showDocument(authUri.get().toString());
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void cancel() {
|
||||
window.close();
|
||||
}
|
||||
|
||||
private void setAuthUri(URI uri) {
|
||||
Platform.runLater(() -> {
|
||||
authUri.set(uri);
|
||||
browse();
|
||||
});
|
||||
}
|
||||
|
||||
private void windowClosed(WindowEvent windowEvent) {
|
||||
// stop server, if it is still running
|
||||
task.cancel();
|
||||
result.cancel(true);
|
||||
}
|
||||
|
||||
private void authSucceeded(WorkerStateEvent workerStateEvent) {
|
||||
tokenRef.set(task.getValue());
|
||||
window.requestFocus();
|
||||
window.setScene(receiveKeyScene.get());
|
||||
}
|
||||
|
||||
private void authFailed(WorkerStateEvent workerStateEvent) {
|
||||
window.requestFocus();
|
||||
var exception = workerStateEvent.getSource().getException();
|
||||
result.completeExceptionally(exception);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package org.cryptomator.ui.keyloading.hub;
|
||||
|
||||
import com.google.gson.JsonParser;
|
||||
import io.github.coffeelibs.tinyoauth2client.AuthFlow;
|
||||
import io.github.coffeelibs.tinyoauth2client.TinyOAuth2;
|
||||
import io.github.coffeelibs.tinyoauth2client.http.response.Response;
|
||||
|
||||
import javafx.concurrent.Task;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
class AuthFlowTask extends Task<String> {
|
||||
|
||||
private final HubConfig hubConfig;
|
||||
private final AuthFlowContext authFlowContext;
|
||||
private final Consumer<URI> redirectUriConsumer;
|
||||
|
||||
/**
|
||||
* Spawns a server and waits for the redirectUri to be called.
|
||||
*
|
||||
* @param hubConfig Configuration object holding parameters required by {@link AuthFlow}
|
||||
* @param redirectUriConsumer A callback invoked with the redirectUri, as soon as the server has started
|
||||
*/
|
||||
public AuthFlowTask(HubConfig hubConfig, AuthFlowContext authFlowContext, Consumer<URI> redirectUriConsumer) {
|
||||
this.hubConfig = hubConfig;
|
||||
this.authFlowContext = authFlowContext;
|
||||
this.redirectUriConsumer = redirectUriConsumer;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String call() throws IOException, InterruptedException {
|
||||
var response = TinyOAuth2.client(hubConfig.clientId) //
|
||||
.withTokenEndpoint(URI.create(hubConfig.tokenEndpoint)) //
|
||||
.authFlow(URI.create(hubConfig.authEndpoint)) //
|
||||
.setSuccessResponse(Response.redirect(URI.create(hubConfig.authSuccessUrl + "&device=" + authFlowContext.deviceId()))) //
|
||||
.setErrorResponse(Response.redirect(URI.create(hubConfig.authErrorUrl + "&device=" + authFlowContext.deviceId()))) //
|
||||
.authorize(redirectUriConsumer);
|
||||
if (response.statusCode() != 200) {
|
||||
throw new NotOkResponseException("Authorization returned status code " + response.statusCode());
|
||||
}
|
||||
var json = JsonParser.parseString(response.body());
|
||||
return json.getAsJsonObject().get("access_token").getAsString();
|
||||
}
|
||||
|
||||
public static class NotOkResponseException extends RuntimeException {
|
||||
|
||||
NotOkResponseException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package org.cryptomator.ui.keyloading.hub;
|
||||
|
||||
class CreateDeviceDto {
|
||||
|
||||
public String id;
|
||||
public String name;
|
||||
public String publicKey;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package org.cryptomator.ui.keyloading.hub;
|
||||
|
||||
import com.google.common.io.CharStreams;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.JsonParser;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.Reader;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
class HttpHelper {
|
||||
|
||||
public static String readBody(HttpResponse<InputStream> response) throws IOException {
|
||||
try (var in = response.body(); var reader = new InputStreamReader(in, StandardCharsets.UTF_8)) {
|
||||
return CharStreams.toString(reader);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.cryptomator.ui.keyloading.hub;
|
||||
|
||||
// needs to be accessible by JSON decoder
|
||||
public class HubConfig {
|
||||
|
||||
public String clientId;
|
||||
public String authEndpoint;
|
||||
public String tokenEndpoint;
|
||||
public String devicesResourceUrl;
|
||||
public String authSuccessUrl;
|
||||
public String authErrorUrl;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
package org.cryptomator.ui.keyloading.hub;
|
||||
|
||||
import com.google.common.io.BaseEncoding;
|
||||
import com.nimbusds.jose.JWEObject;
|
||||
import dagger.Binds;
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import dagger.multibindings.IntoMap;
|
||||
import dagger.multibindings.StringKey;
|
||||
import org.cryptomator.common.settings.DeviceKey;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.cryptolib.common.MessageDigestSupplier;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.common.FxControllerKey;
|
||||
import org.cryptomator.ui.common.FxmlFile;
|
||||
import org.cryptomator.ui.common.FxmlLoaderFactory;
|
||||
import org.cryptomator.ui.common.FxmlScene;
|
||||
import org.cryptomator.ui.common.NewPasswordController;
|
||||
import org.cryptomator.ui.common.PasswordStrengthUtil;
|
||||
import org.cryptomator.ui.keyloading.KeyLoading;
|
||||
import org.cryptomator.ui.keyloading.KeyLoadingScoped;
|
||||
import org.cryptomator.ui.keyloading.KeyLoadingStrategy;
|
||||
|
||||
import javax.inject.Named;
|
||||
import javafx.scene.Scene;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.util.Objects;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
@Module
|
||||
public abstract class HubKeyLoadingModule {
|
||||
|
||||
@Provides
|
||||
@KeyLoadingScoped
|
||||
static HubConfig provideHubConfig(@KeyLoading Vault vault) {
|
||||
try {
|
||||
return vault.getVaultConfigCache().get().getHeader("hub", HubConfig.class);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Provides
|
||||
@KeyLoadingScoped
|
||||
@Named("windowTitle")
|
||||
static String provideWindowTitle(@KeyLoading Vault vault, ResourceBundle resourceBundle) {
|
||||
return String.format(resourceBundle.getString("unlock.title"), vault.getDisplayName());
|
||||
}
|
||||
|
||||
|
||||
@Provides
|
||||
@KeyLoadingScoped
|
||||
@Named("deviceId")
|
||||
static String provideDeviceId(DeviceKey deviceKey) {
|
||||
var publicKey = Objects.requireNonNull(deviceKey.get()).getPublic().getEncoded();
|
||||
try (var instance = MessageDigestSupplier.SHA256.instance()) {
|
||||
var hashedKey = instance.get().digest(publicKey);
|
||||
return BaseEncoding.base16().encode(hashedKey);
|
||||
}
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Named("bearerToken")
|
||||
@KeyLoadingScoped
|
||||
static AtomicReference<String> provideBearerTokenRef() {
|
||||
return new AtomicReference<>();
|
||||
}
|
||||
|
||||
@Provides
|
||||
@KeyLoadingScoped
|
||||
static CompletableFuture<JWEObject> provideResult() {
|
||||
return new CompletableFuture<>();
|
||||
}
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@KeyLoadingScoped
|
||||
@StringKey(HubKeyLoadingStrategy.SCHEME_HUB_HTTP)
|
||||
abstract KeyLoadingStrategy bindHubKeyLoadingStrategyToHubHttp(HubKeyLoadingStrategy strategy);
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@KeyLoadingScoped
|
||||
@StringKey(HubKeyLoadingStrategy.SCHEME_HUB_HTTPS)
|
||||
abstract KeyLoadingStrategy bindHubKeyLoadingStrategyToHubHttps(HubKeyLoadingStrategy strategy);
|
||||
|
||||
@Provides
|
||||
@FxmlScene(FxmlFile.HUB_AUTH_FLOW)
|
||||
@KeyLoadingScoped
|
||||
static Scene provideHubAuthFlowScene(@KeyLoading FxmlLoaderFactory fxmlLoaders) {
|
||||
return fxmlLoaders.createScene(FxmlFile.HUB_AUTH_FLOW);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FxmlScene(FxmlFile.HUB_LICENSE_EXCEEDED)
|
||||
@KeyLoadingScoped
|
||||
static Scene provideLicenseExceededScene(@KeyLoading FxmlLoaderFactory fxmlLoaders) {
|
||||
return fxmlLoaders.createScene(FxmlFile.HUB_LICENSE_EXCEEDED);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FxmlScene(FxmlFile.HUB_RECEIVE_KEY)
|
||||
@KeyLoadingScoped
|
||||
static Scene provideHubReceiveKeyScene(@KeyLoading FxmlLoaderFactory fxmlLoaders) {
|
||||
return fxmlLoaders.createScene(FxmlFile.HUB_RECEIVE_KEY);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FxmlScene(FxmlFile.HUB_REGISTER_DEVICE)
|
||||
@KeyLoadingScoped
|
||||
static Scene provideHubRegisterDeviceScene(@KeyLoading FxmlLoaderFactory fxmlLoaders) {
|
||||
return fxmlLoaders.createScene(FxmlFile.HUB_REGISTER_DEVICE);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FxmlScene(FxmlFile.HUB_REGISTER_SUCCESS)
|
||||
@KeyLoadingScoped
|
||||
static Scene provideHubRegisterSuccessScene(@KeyLoading FxmlLoaderFactory fxmlLoaders) {
|
||||
return fxmlLoaders.createScene(FxmlFile.HUB_REGISTER_SUCCESS);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FxmlScene(FxmlFile.HUB_REGISTER_FAILED)
|
||||
@KeyLoadingScoped
|
||||
static Scene provideHubRegisterFailedScene(@KeyLoading FxmlLoaderFactory fxmlLoaders) {
|
||||
return fxmlLoaders.createScene(FxmlFile.HUB_REGISTER_FAILED);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FxmlScene(FxmlFile.HUB_UNAUTHORIZED_DEVICE)
|
||||
@KeyLoadingScoped
|
||||
static Scene provideHubUnauthorizedDeviceScene(@KeyLoading FxmlLoaderFactory fxmlLoaders) {
|
||||
return fxmlLoaders.createScene(FxmlFile.HUB_UNAUTHORIZED_DEVICE);
|
||||
}
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FxControllerKey(AuthFlowController.class)
|
||||
abstract FxController bindAuthFlowController(AuthFlowController controller);
|
||||
|
||||
@Provides
|
||||
@IntoMap
|
||||
@FxControllerKey(NewPasswordController.class)
|
||||
static FxController provideNewPasswordController(ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater) {
|
||||
return new NewPasswordController(resourceBundle, strengthRater);
|
||||
}
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FxControllerKey(LicenseExceededController.class)
|
||||
abstract FxController bindLicenseExceededController(LicenseExceededController controller);
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FxControllerKey(ReceiveKeyController.class)
|
||||
abstract FxController bindReceiveKeyController(ReceiveKeyController controller);
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FxControllerKey(RegisterDeviceController.class)
|
||||
abstract FxController bindRegisterDeviceController(RegisterDeviceController controller);
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FxControllerKey(RegisterSuccessController.class)
|
||||
abstract FxController bindRegisterSuccessController(RegisterSuccessController controller);
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FxControllerKey(RegisterFailedController.class)
|
||||
abstract FxController bindRegisterFailedController(RegisterFailedController controller);
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FxControllerKey(UnauthorizedDeviceController.class)
|
||||
abstract FxController bindUnauthorizedDeviceController(UnauthorizedDeviceController controller);
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package org.cryptomator.ui.keyloading.hub;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.nimbusds.jose.JWEObject;
|
||||
import dagger.Lazy;
|
||||
import org.cryptomator.common.settings.DeviceKey;
|
||||
import org.cryptomator.cryptolib.api.Masterkey;
|
||||
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
|
||||
import org.cryptomator.ui.common.FxmlFile;
|
||||
import org.cryptomator.ui.common.FxmlScene;
|
||||
import org.cryptomator.ui.keyloading.KeyLoading;
|
||||
import org.cryptomator.ui.keyloading.KeyLoadingStrategy;
|
||||
import org.cryptomator.ui.unlock.UnlockCancelledException;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.Window;
|
||||
import java.net.URI;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
@KeyLoading
|
||||
public class HubKeyLoadingStrategy implements KeyLoadingStrategy {
|
||||
|
||||
private static final String SCHEME_PREFIX = "hub+";
|
||||
static final String SCHEME_HUB_HTTP = SCHEME_PREFIX + "http";
|
||||
static final String SCHEME_HUB_HTTPS = SCHEME_PREFIX + "https";
|
||||
|
||||
private final Stage window;
|
||||
private final Lazy<Scene> authFlowScene;
|
||||
private final CompletableFuture<JWEObject> result;
|
||||
private final DeviceKey deviceKey;
|
||||
|
||||
@Inject
|
||||
public HubKeyLoadingStrategy(@KeyLoading Stage window, @FxmlScene(FxmlFile.HUB_AUTH_FLOW) Lazy<Scene> authFlowScene, CompletableFuture<JWEObject> result, DeviceKey deviceKey, @Named("windowTitle") String windowTitle) {
|
||||
this.window = window;
|
||||
window.setTitle(windowTitle);
|
||||
this.authFlowScene = authFlowScene;
|
||||
this.result = result;
|
||||
this.deviceKey = deviceKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Masterkey loadKey(URI keyId) throws MasterkeyLoadingFailedException {
|
||||
Preconditions.checkArgument(keyId.getScheme().startsWith(SCHEME_PREFIX));
|
||||
try {
|
||||
startAuthFlow();
|
||||
var jwe = result.get();
|
||||
return JWEHelper.decrypt(jwe, deviceKey.get().getPrivate());
|
||||
} catch (DeviceKey.DeviceKeyRetrievalException e) {
|
||||
throw new MasterkeyLoadingFailedException("Failed to load keypair", e);
|
||||
} catch (CancellationException e) {
|
||||
throw new UnlockCancelledException("User cancelled auth workflow", e);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new UnlockCancelledException("Loading interrupted", e);
|
||||
} catch (ExecutionException e) {
|
||||
throw new MasterkeyLoadingFailedException("Failed to retrieve key", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void startAuthFlow() {
|
||||
Platform.runLater(() -> {
|
||||
window.setScene(authFlowScene.get());
|
||||
window.show();
|
||||
Window owner = window.getOwner();
|
||||
if (owner != null) {
|
||||
window.setX(owner.getX() + (owner.getWidth() - window.getWidth()) / 2);
|
||||
window.setY(owner.getY() + (owner.getHeight() - window.getHeight()) / 2);
|
||||
} else {
|
||||
window.centerOnScreen();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package org.cryptomator.ui.keyloading.hub;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.io.BaseEncoding;
|
||||
import com.nimbusds.jose.JOSEException;
|
||||
import com.nimbusds.jose.JWEObject;
|
||||
import com.nimbusds.jose.crypto.ECDHDecrypter;
|
||||
import org.cryptomator.cryptolib.api.Masterkey;
|
||||
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.security.interfaces.ECPrivateKey;
|
||||
import java.util.Arrays;
|
||||
|
||||
class JWEHelper {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(JWEHelper.class);
|
||||
private static final String JWE_PAYLOAD_MASTERKEY_FIELD = "key";
|
||||
|
||||
private JWEHelper(){}
|
||||
|
||||
public static Masterkey decrypt(JWEObject jwe, ECPrivateKey privateKey) throws MasterkeyLoadingFailedException {
|
||||
try {
|
||||
jwe.decrypt(new ECDHDecrypter(privateKey));
|
||||
return readKey(jwe);
|
||||
} catch (JOSEException e) {
|
||||
LOG.warn("Failed to decrypt JWE: {}", jwe);
|
||||
throw new MasterkeyLoadingFailedException("Failed to decrypt JWE", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static Masterkey readKey(JWEObject jwe) throws MasterkeyLoadingFailedException {
|
||||
Preconditions.checkArgument(jwe.getState() == JWEObject.State.DECRYPTED);
|
||||
var fields = jwe.getPayload().toJSONObject();
|
||||
if (fields == null) {
|
||||
LOG.error("Expected JWE payload to be JSON: {}", jwe.getPayload());
|
||||
throw new MasterkeyLoadingFailedException("Expected JWE payload to be JSON");
|
||||
}
|
||||
var keyBytes = new byte[0];
|
||||
try {
|
||||
if (fields.get(JWE_PAYLOAD_MASTERKEY_FIELD) instanceof String key) {
|
||||
keyBytes = BaseEncoding.base64().decode(key);
|
||||
return new Masterkey(keyBytes);
|
||||
} else {
|
||||
throw new IllegalArgumentException("JWE payload doesn't contain field " + JWE_PAYLOAD_MASTERKEY_FIELD);
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
LOG.error("Unexpected JWE payload: {}", jwe.getPayload());
|
||||
throw new MasterkeyLoadingFailedException("Unexpected JWE payload", e);
|
||||
} finally {
|
||||
Arrays.fill(keyBytes, (byte) 0x00);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package org.cryptomator.ui.keyloading.hub;
|
||||
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.keyloading.KeyLoading;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
public class LicenseExceededController implements FxController {
|
||||
|
||||
private final Stage window;
|
||||
|
||||
@Inject
|
||||
public LicenseExceededController(@KeyLoading Stage window) {
|
||||
this.window = window;
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void close() {
|
||||
window.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
package org.cryptomator.ui.keyloading.hub;
|
||||
|
||||
import com.nimbusds.jose.JWEObject;
|
||||
import dagger.Lazy;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.common.FxmlFile;
|
||||
import org.cryptomator.ui.common.FxmlScene;
|
||||
import org.cryptomator.ui.keyloading.KeyLoading;
|
||||
import org.cryptomator.ui.keyloading.KeyLoadingScoped;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javafx.application.Platform;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.WindowEvent;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.text.ParseException;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
@KeyLoadingScoped
|
||||
public class ReceiveKeyController implements FxController {
|
||||
|
||||
private static final String SCHEME_PREFIX = "hub+";
|
||||
|
||||
private final Stage window;
|
||||
private final String deviceId;
|
||||
private final String bearerToken;
|
||||
private final CompletableFuture<JWEObject> result;
|
||||
private final Lazy<Scene> registerDeviceScene;
|
||||
private final Lazy<Scene> unauthorizedScene;
|
||||
private final URI vaultBaseUri;
|
||||
private final Lazy<Scene> licenseExceededScene;
|
||||
private final HttpClient httpClient;
|
||||
|
||||
@Inject
|
||||
public ReceiveKeyController(@KeyLoading Vault vault, ExecutorService executor, @KeyLoading Stage window, @Named("deviceId") String deviceId, @Named("bearerToken") AtomicReference<String> tokenRef, CompletableFuture<JWEObject> result, @FxmlScene(FxmlFile.HUB_REGISTER_DEVICE) Lazy<Scene> registerDeviceScene, @FxmlScene(FxmlFile.HUB_UNAUTHORIZED_DEVICE) Lazy<Scene> unauthorizedScene, @FxmlScene(FxmlFile.HUB_LICENSE_EXCEEDED) Lazy<Scene> licenseExceededScene) {
|
||||
this.window = window;
|
||||
this.deviceId = deviceId;
|
||||
this.bearerToken = Objects.requireNonNull(tokenRef.get());
|
||||
this.result = result;
|
||||
this.registerDeviceScene = registerDeviceScene;
|
||||
this.unauthorizedScene = unauthorizedScene;
|
||||
this.vaultBaseUri = getVaultBaseUri(vault);
|
||||
this.licenseExceededScene = licenseExceededScene;
|
||||
this.window.addEventHandler(WindowEvent.WINDOW_HIDING, this::windowClosed);
|
||||
this.httpClient = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).executor(executor).build();
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void initialize() {
|
||||
var keyUri = appendPath(vaultBaseUri, "/keys/" + deviceId);
|
||||
var request = HttpRequest.newBuilder(keyUri) //
|
||||
.header("Authorization", "Bearer " + bearerToken) //
|
||||
.GET() //
|
||||
.build();
|
||||
httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream()) //
|
||||
.thenAcceptAsync(this::loadedExistingKey, Platform::runLater) //
|
||||
.exceptionally(this::retrievalFailed);
|
||||
}
|
||||
|
||||
private void loadedExistingKey(HttpResponse<InputStream> response) {
|
||||
try {
|
||||
switch (response.statusCode()) {
|
||||
case 200 -> retrievalSucceeded(response);
|
||||
case 402 -> licenseExceeded();
|
||||
case 403 -> accessNotGranted();
|
||||
case 404 -> needsDeviceRegistration();
|
||||
default -> throw new IOException("Unexpected response " + response.statusCode());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void retrievalSucceeded(HttpResponse<InputStream> response) throws IOException {
|
||||
try {
|
||||
var string = HttpHelper.readBody(response);
|
||||
result.complete(JWEObject.parse(string));
|
||||
window.close();
|
||||
} catch (ParseException e) {
|
||||
throw new IOException("Failed to parse JWE", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void licenseExceeded() {
|
||||
window.setScene(licenseExceededScene.get());
|
||||
}
|
||||
|
||||
private void needsDeviceRegistration() {
|
||||
window.setScene(registerDeviceScene.get());
|
||||
}
|
||||
|
||||
private void accessNotGranted() {
|
||||
window.setScene(unauthorizedScene.get());
|
||||
}
|
||||
|
||||
private Void retrievalFailed(Throwable cause) {
|
||||
result.completeExceptionally(cause);
|
||||
return null;
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void cancel() {
|
||||
window.close();
|
||||
}
|
||||
|
||||
private void windowClosed(WindowEvent windowEvent) {
|
||||
result.cancel(true);
|
||||
}
|
||||
|
||||
private static URI appendPath(URI base, String path) {
|
||||
try {
|
||||
var newPath = base.getPath() + path;
|
||||
return new URI(base.getScheme(), base.getAuthority(), newPath, base.getQuery(), base.getFragment());
|
||||
} catch (URISyntaxException e) {
|
||||
throw new IllegalArgumentException("Can't append '" + path + "' to URI: " + base, e);
|
||||
}
|
||||
}
|
||||
|
||||
private static URI getVaultBaseUri(Vault vault) {
|
||||
try {
|
||||
var kid = vault.getVaultConfigCache().get().getKeyId();
|
||||
assert kid.getScheme().startsWith(SCHEME_PREFIX);
|
||||
var hubUriScheme = kid.getScheme().substring(SCHEME_PREFIX.length());
|
||||
return new URI(hubUriScheme, kid.getSchemeSpecificPart(), kid.getFragment());
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
} catch (URISyntaxException e) {
|
||||
throw new IllegalStateException("URI constructed from params known to be valid", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
package org.cryptomator.ui.keyloading.hub;
|
||||
|
||||
import com.auth0.jwt.JWT;
|
||||
import com.auth0.jwt.interfaces.DecodedJWT;
|
||||
import com.google.common.io.BaseEncoding;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.nimbusds.jose.JWEObject;
|
||||
import dagger.Lazy;
|
||||
import org.cryptomator.common.settings.DeviceKey;
|
||||
import org.cryptomator.cryptolib.common.P384KeyPair;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.common.FxmlFile;
|
||||
import org.cryptomator.ui.common.FxmlScene;
|
||||
import org.cryptomator.ui.keyloading.KeyLoading;
|
||||
import org.cryptomator.ui.keyloading.KeyLoadingScoped;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ContentDisplay;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.WindowEvent;
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
@KeyLoadingScoped
|
||||
public class RegisterDeviceController implements FxController {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(RegisterDeviceController.class);
|
||||
private static final Gson GSON = new GsonBuilder().setLenient().create();
|
||||
private static final List<Integer> EXPECTED_RESPONSE_CODES = List.of(201, 409);
|
||||
|
||||
private final Stage window;
|
||||
private final HubConfig hubConfig;
|
||||
private final String bearerToken;
|
||||
private final Lazy<Scene> registerSuccessScene;
|
||||
private final Lazy<Scene> registerFailedScene;
|
||||
private final String deviceId;
|
||||
private final P384KeyPair keyPair;
|
||||
private final CompletableFuture<JWEObject> result;
|
||||
private final DecodedJWT jwt;
|
||||
private final HttpClient httpClient;
|
||||
private final BooleanProperty deviceNameAlreadyExists = new SimpleBooleanProperty(false);
|
||||
|
||||
public TextField deviceNameField;
|
||||
public Button registerBtn;
|
||||
|
||||
@Inject
|
||||
public RegisterDeviceController(@KeyLoading Stage window, ExecutorService executor, HubConfig hubConfig, @Named("deviceId") String deviceId, DeviceKey deviceKey, CompletableFuture<JWEObject> result, @Named("bearerToken") AtomicReference<String> bearerToken, @FxmlScene(FxmlFile.HUB_REGISTER_SUCCESS) Lazy<Scene> registerSuccessScene, @FxmlScene(FxmlFile.HUB_REGISTER_FAILED) Lazy<Scene> registerFailedScene) {
|
||||
this.window = window;
|
||||
this.hubConfig = hubConfig;
|
||||
this.deviceId = deviceId;
|
||||
this.keyPair = Objects.requireNonNull(deviceKey.get());
|
||||
this.result = result;
|
||||
this.bearerToken = Objects.requireNonNull(bearerToken.get());
|
||||
this.registerSuccessScene = registerSuccessScene;
|
||||
this.registerFailedScene = registerFailedScene;
|
||||
this.jwt = JWT.decode(this.bearerToken);
|
||||
this.window.addEventHandler(WindowEvent.WINDOW_HIDING, this::windowClosed);
|
||||
this.httpClient = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).executor(executor).build();
|
||||
}
|
||||
|
||||
public void initialize() {
|
||||
deviceNameField.setText(determineHostname());
|
||||
deviceNameField.textProperty().addListener(observable -> deviceNameAlreadyExists.set(false));
|
||||
}
|
||||
|
||||
private String determineHostname() {
|
||||
try {
|
||||
var hostName = InetAddress.getLocalHost().getHostName();
|
||||
return Objects.requireNonNullElse(hostName, "");
|
||||
} catch (IOException e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void register() {
|
||||
deviceNameAlreadyExists.set(false);
|
||||
registerBtn.setContentDisplay(ContentDisplay.LEFT);
|
||||
registerBtn.setDisable(true);
|
||||
|
||||
var keyUri = URI.create(hubConfig.devicesResourceUrl + deviceId);
|
||||
var deviceKey = keyPair.getPublic().getEncoded();
|
||||
var dto = new CreateDeviceDto();
|
||||
dto.id = deviceId;
|
||||
dto.name = deviceNameField.getText();
|
||||
dto.publicKey = BaseEncoding.base64Url().omitPadding().encode(deviceKey);
|
||||
var json = GSON.toJson(dto); // TODO: do we want to keep GSON? doesn't support records -.-
|
||||
var request = HttpRequest.newBuilder(keyUri) //
|
||||
.header("Authorization", "Bearer " + bearerToken) //
|
||||
.header("Content-Type", "application/json").PUT(HttpRequest.BodyPublishers.ofString(json, StandardCharsets.UTF_8)) //
|
||||
.build();
|
||||
httpClient.sendAsync(request, HttpResponse.BodyHandlers.discarding()) //
|
||||
.thenApply(response -> {
|
||||
if (EXPECTED_RESPONSE_CODES.contains(response.statusCode())) {
|
||||
return response;
|
||||
} else {
|
||||
throw new RuntimeException("Server answered with unexpected status code " + response.statusCode());
|
||||
}
|
||||
}).handleAsync((response, throwable) -> {
|
||||
if (response != null) {
|
||||
this.handleResponse(response);
|
||||
} else {
|
||||
this.registrationFailed(throwable);
|
||||
}
|
||||
return null;
|
||||
}, Platform::runLater);
|
||||
}
|
||||
|
||||
private void handleResponse(HttpResponse<Void> voidHttpResponse) {
|
||||
assert EXPECTED_RESPONSE_CODES.contains(voidHttpResponse.statusCode());
|
||||
|
||||
if (voidHttpResponse.statusCode() == 409) {
|
||||
deviceNameAlreadyExists.set(true);
|
||||
registerBtn.setContentDisplay(ContentDisplay.TEXT_ONLY);
|
||||
registerBtn.setDisable(false);
|
||||
} else {
|
||||
LOG.debug("Device registration for hub instance {} successful.", hubConfig.authSuccessUrl);
|
||||
window.setScene(registerSuccessScene.get());
|
||||
}
|
||||
}
|
||||
|
||||
private void registrationFailed(Throwable cause) {
|
||||
LOG.warn("Device registration failed.", cause);
|
||||
window.setScene(registerFailedScene.get());
|
||||
result.completeExceptionally(cause);
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void close() {
|
||||
window.close();
|
||||
}
|
||||
|
||||
private void windowClosed(WindowEvent windowEvent) {
|
||||
result.cancel(true);
|
||||
}
|
||||
|
||||
/* Getter */
|
||||
|
||||
public String getUserName() {
|
||||
return jwt.getClaim("email").asString();
|
||||
}
|
||||
|
||||
|
||||
//--- Getters & Setters
|
||||
|
||||
public BooleanProperty deviceNameAlreadyExistsProperty() {
|
||||
return deviceNameAlreadyExists;
|
||||
}
|
||||
|
||||
public boolean getDeviceNameAlreadyExists() {
|
||||
return deviceNameAlreadyExists.get();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package org.cryptomator.ui.keyloading.hub;
|
||||
|
||||
import com.nimbusds.jose.JWEObject;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.keyloading.KeyLoading;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.stage.Stage;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public class RegisterFailedController implements FxController {
|
||||
|
||||
private final Stage window;
|
||||
private final CompletableFuture<JWEObject> result;
|
||||
|
||||
@Inject
|
||||
public RegisterFailedController(@KeyLoading Stage window, CompletableFuture<JWEObject> result) {
|
||||
this.window = window;
|
||||
this.result = result;
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void close() {
|
||||
window.close();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package org.cryptomator.ui.keyloading.hub;
|
||||
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.keyloading.KeyLoading;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
public class RegisterSuccessController implements FxController {
|
||||
|
||||
private final Stage window;
|
||||
|
||||
@Inject
|
||||
public RegisterSuccessController(@KeyLoading Stage window) {
|
||||
this.window = window;
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void close() {
|
||||
window.close();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package org.cryptomator.ui.keyloading.hub;
|
||||
|
||||
import com.nimbusds.jose.JWEObject;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.keyloading.KeyLoading;
|
||||
import org.cryptomator.ui.keyloading.KeyLoadingScoped;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.WindowEvent;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
@KeyLoadingScoped
|
||||
public class UnauthorizedDeviceController implements FxController {
|
||||
|
||||
private final Stage window;
|
||||
private final CompletableFuture<JWEObject> result;
|
||||
|
||||
@Inject
|
||||
public UnauthorizedDeviceController(@KeyLoading Stage window, CompletableFuture<JWEObject> result) {
|
||||
this.window = window;
|
||||
this.result = result;
|
||||
this.window.addEventHandler(WindowEvent.WINDOW_HIDING, this::windowClosed);
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void close() {
|
||||
window.close();
|
||||
}
|
||||
|
||||
private void windowClosed(WindowEvent windowEvent) {
|
||||
result.cancel(true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* This {@link org.cryptomator.ui.keyloading.KeyLoadingStrategy strategy} retrieves the vault key from a web application, similar to
|
||||
* <a href="https://datatracker.ietf.org/doc/html/rfc8252#section-7.3">RFC 8252</a> but with an encrypted masterkey instead of an authorization code.
|
||||
* <p>
|
||||
* If the <code>kid</code> of the vault config starts with either {@value org.cryptomator.ui.keyloading.hub.HubKeyLoadingStrategy#SCHEME_HUB_HTTP}
|
||||
* or {@value org.cryptomator.ui.keyloading.hub.HubKeyLoadingStrategy#SCHEME_HUB_HTTPS}, the included http address is amended by three parameters and opened
|
||||
* in a browser. These parameters are:
|
||||
* <ul>
|
||||
* <li>A device-specific public key (generated by this application and stored among its settings</li>
|
||||
* <li>A unique device ID (stored in settings)</li>
|
||||
* <li>A loopback callback address</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* The callback address points to a embedded web server waiting to receive the masterkey encrypted specifically for this device, using the device-specific public key.
|
||||
* <p>
|
||||
* The vault key can be decrypted using this ECIES:
|
||||
* <ol>
|
||||
* <li>Generate shared secret using ECDH without cofactor</li>
|
||||
* <li>Derive 44 bytes using ANSI X9.63 KDF with SHA256</li>
|
||||
* <li>Decrypt payload via AES-GCM, using first 32 bytes as key, last 12 bytes as IV</li>
|
||||
* <li>No MAC check required, as AES-GCM includes a tag already</li>
|
||||
* </ol>
|
||||
*/
|
||||
package org.cryptomator.ui.keyloading.hub;
|
||||
@@ -1,11 +1,13 @@
|
||||
package org.cryptomator.ui.keyloading.masterkeyfile;
|
||||
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.keyloading.KeyLoading;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.beans.binding.StringBinding;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.stage.Stage;
|
||||
@@ -15,18 +17,22 @@ import java.nio.file.Path;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import static org.cryptomator.common.Constants.CRYPTOMATOR_FILENAME_GLOB;
|
||||
|
||||
@ChooseMasterkeyFileScoped
|
||||
public class ChooseMasterkeyFileController implements FxController {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ChooseMasterkeyFileController.class);
|
||||
|
||||
private final Stage window;
|
||||
private final Vault vault;
|
||||
private final CompletableFuture<Path> result;
|
||||
private final ResourceBundle resourceBundle;
|
||||
|
||||
@Inject
|
||||
public ChooseMasterkeyFileController(@KeyLoading Stage window, CompletableFuture<Path> result, ResourceBundle resourceBundle) {
|
||||
public ChooseMasterkeyFileController(@KeyLoading Stage window, @KeyLoading Vault vault, CompletableFuture<Path> result, ResourceBundle resourceBundle) {
|
||||
this.window = window;
|
||||
this.vault = vault;
|
||||
this.result = result;
|
||||
this.resourceBundle = resourceBundle;
|
||||
this.window.setOnHiding(this::windowClosed);
|
||||
@@ -46,7 +52,7 @@ public class ChooseMasterkeyFileController implements FxController {
|
||||
LOG.trace("proceed()");
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle(resourceBundle.getString("unlock.chooseMasterkey.filePickerTitle"));
|
||||
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Cryptomator Masterkey", "*.cryptomator"));
|
||||
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(resourceBundle.getString("unlock.chooseMasterkey.filePickerMimeDesc"), CRYPTOMATOR_FILENAME_GLOB));
|
||||
File masterkeyFile = fileChooser.showOpenDialog(window);
|
||||
if (masterkeyFile != null) {
|
||||
LOG.debug("Chose masterkey file: {}", masterkeyFile);
|
||||
@@ -54,4 +60,10 @@ public class ChooseMasterkeyFileController implements FxController {
|
||||
}
|
||||
}
|
||||
|
||||
//--- Setter & Getter ---
|
||||
|
||||
public String getDisplayName(){
|
||||
return vault.getDisplayName();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -61,6 +61,7 @@ public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy {
|
||||
|
||||
@Override
|
||||
public Masterkey loadKey(URI keyId) throws MasterkeyLoadingFailedException {
|
||||
window.setTitle(resourceBundle.getString("unlock.title").formatted(vault.getDisplayName()));
|
||||
Preconditions.checkArgument(SCHEME.equalsIgnoreCase(keyId.getScheme()), "Only supports keys with scheme " + SCHEME);
|
||||
try {
|
||||
Path filePath = vault.getPath().resolve(keyId.getSchemeSpecificPart());
|
||||
@@ -124,7 +125,6 @@ public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy {
|
||||
var comp = masterkeyFileChoice.build();
|
||||
Platform.runLater(() -> {
|
||||
window.setScene(comp.chooseMasterkeyScene());
|
||||
window.setTitle(resourceBundle.getString("unlock.chooseMasterkey.title").formatted(vault.getDisplayName()));
|
||||
window.show();
|
||||
Window owner = window.getOwner();
|
||||
if (owner != null) {
|
||||
@@ -147,7 +147,6 @@ public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy {
|
||||
var comp = passphraseEntry.savedPassword(passphrase).build();
|
||||
Platform.runLater(() -> {
|
||||
window.setScene(comp.passphraseEntryScene());
|
||||
window.setTitle(resourceBundle.getString("unlock.title").formatted(vault.getDisplayName()));
|
||||
window.show();
|
||||
Window owner = window.getOwner();
|
||||
if (owner != null) {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package org.cryptomator.ui.keyloading.masterkeyfile;
|
||||
|
||||
import org.cryptomator.common.Nullable;
|
||||
import org.cryptomator.common.Passphrase;
|
||||
import org.cryptomator.common.keychain.KeychainManager;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.common.Passphrase;
|
||||
import org.cryptomator.ui.common.WeakBindings;
|
||||
import org.cryptomator.ui.controls.NiceSecurePasswordField;
|
||||
import org.cryptomator.ui.forgetPassword.ForgetPasswordComponent;
|
||||
@@ -50,7 +50,7 @@ public class PassphraseEntryController implements FxController {
|
||||
private final KeychainManager keychain;
|
||||
private final StringBinding vaultName;
|
||||
private final BooleanProperty unlockInProgress = new SimpleBooleanProperty();
|
||||
private final ObjectBinding<ContentDisplay> unlockButtonContentDisplay = Bindings.createObjectBinding(this::getUnlockButtonContentDisplay, unlockInProgress);
|
||||
private final ObjectBinding<ContentDisplay> unlockButtonContentDisplay = Bindings.when(unlockInProgress).then(ContentDisplay.LEFT).otherwise(ContentDisplay.TEXT_ONLY);
|
||||
private final BooleanProperty unlockButtonDisabled = new SimpleBooleanProperty();
|
||||
|
||||
/* FXML */
|
||||
@@ -186,7 +186,7 @@ public class PassphraseEntryController implements FxController {
|
||||
}
|
||||
|
||||
public ContentDisplay getUnlockButtonContentDisplay() {
|
||||
return unlockInProgress.get() ? ContentDisplay.LEFT : ContentDisplay.TEXT_ONLY;
|
||||
return unlockButtonContentDisplay.get();
|
||||
}
|
||||
|
||||
public ReadOnlyBooleanProperty userInteractionDisabledProperty() {
|
||||
|
||||
@@ -46,7 +46,7 @@ public class ResizeController implements FxController {
|
||||
ResizeController(@MainWindow Stage window, Settings settings) {
|
||||
this.window = window;
|
||||
this.settings = settings;
|
||||
this.showResizingArrows = Bindings.createBooleanBinding(this::isShowResizingArrows, window.fullScreenProperty());
|
||||
this.showResizingArrows = window.fullScreenProperty().not();
|
||||
}
|
||||
|
||||
@FXML
|
||||
@@ -181,8 +181,7 @@ public class ResizeController implements FxController {
|
||||
}
|
||||
|
||||
public boolean isShowResizingArrows() {
|
||||
//If in fullscreen resizing is not be possible;
|
||||
return !window.isFullScreen();
|
||||
return showResizingArrows.get();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.cryptomator.ui.mainwindow;
|
||||
|
||||
import com.tobiasdiez.easybind.EasyBind;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.common.vaults.VaultState;
|
||||
import org.cryptomator.ui.common.Animations;
|
||||
@@ -11,10 +10,10 @@ import org.cryptomator.ui.controls.FontAwesome5IconView;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.application.Application;
|
||||
import javafx.beans.binding.Binding;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.fxml.FXML;
|
||||
|
||||
@MainWindowScoped
|
||||
@@ -22,7 +21,7 @@ public class VaultDetailController implements FxController {
|
||||
|
||||
private final ReadOnlyObjectProperty<Vault> vault;
|
||||
private final Application application;
|
||||
private final Binding<FontAwesome5Icon> glyph;
|
||||
private final ObservableValue<FontAwesome5Icon> glyph;
|
||||
private final BooleanBinding anyVaultSelected;
|
||||
|
||||
private AutoAnimator spinAnimation;
|
||||
@@ -35,15 +34,13 @@ public class VaultDetailController implements FxController {
|
||||
VaultDetailController(ObjectProperty<Vault> vault, Application application) {
|
||||
this.vault = vault;
|
||||
this.application = application;
|
||||
this.glyph = EasyBind.select(vault) //
|
||||
.selectObject(Vault::stateProperty) //
|
||||
.map(this::getGlyphForVaultState);
|
||||
this.glyph = vault.flatMap(Vault::stateProperty).map(this::getGlyphForVaultState);
|
||||
this.anyVaultSelected = vault.isNotNull();
|
||||
}
|
||||
|
||||
public void initialize() {
|
||||
this.spinAnimation = AutoAnimator.animate(Animations.createDiscrete360Rotation(vaultStateView)) //
|
||||
.onCondition(EasyBind.select(vault).selectObject(Vault::stateProperty).map(VaultState.Value.PROCESSING::equals)) //
|
||||
.onCondition(vault.flatMap(Vault::stateProperty).map(VaultState.Value.PROCESSING::equals).orElse(false)) //
|
||||
.afterStop(() -> vaultStateView.setRotate(0)) //
|
||||
.build();
|
||||
}
|
||||
@@ -77,7 +74,7 @@ public class VaultDetailController implements FxController {
|
||||
return vault.get();
|
||||
}
|
||||
|
||||
public Binding<FontAwesome5Icon> glyphProperty() {
|
||||
public ObservableValue<FontAwesome5Icon> glyphProperty() {
|
||||
return glyph;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.cryptomator.ui.mainwindow;
|
||||
|
||||
import com.tobiasdiez.easybind.EasyBind;
|
||||
import org.cryptomator.common.keychain.KeychainManager;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
@@ -9,10 +8,10 @@ import org.cryptomator.ui.vaultoptions.SelectedVaultOptionsTab;
|
||||
import org.cryptomator.ui.vaultoptions.VaultOptionsComponent;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.beans.binding.BooleanExpression;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
@@ -21,20 +20,20 @@ public class VaultDetailLockedController implements FxController {
|
||||
|
||||
private final ReadOnlyObjectProperty<Vault> vault;
|
||||
private final FxApplicationWindows appWindows;
|
||||
private final VaultOptionsComponent.Builder vaultOptionsWindow;
|
||||
private final VaultOptionsComponent.Factory vaultOptionsWindow;
|
||||
private final KeychainManager keychain;
|
||||
private final Stage mainWindow;
|
||||
private final BooleanExpression passwordSaved;
|
||||
private final ObservableValue<Boolean> passwordSaved;
|
||||
|
||||
@Inject
|
||||
VaultDetailLockedController(ObjectProperty<Vault> vault, FxApplicationWindows appWindows, VaultOptionsComponent.Builder vaultOptionsWindow, KeychainManager keychain, @MainWindow Stage mainWindow) {
|
||||
VaultDetailLockedController(ObjectProperty<Vault> vault, FxApplicationWindows appWindows, VaultOptionsComponent.Factory vaultOptionsWindow, KeychainManager keychain, @MainWindow Stage mainWindow) {
|
||||
this.vault = vault;
|
||||
this.appWindows = appWindows;
|
||||
this.vaultOptionsWindow = vaultOptionsWindow;
|
||||
this.keychain = keychain;
|
||||
this.mainWindow = mainWindow;
|
||||
if (keychain.isSupported() && !keychain.isLocked()) {
|
||||
this.passwordSaved = BooleanExpression.booleanExpression(EasyBind.select(vault).selectObject(v -> keychain.getPassphraseStoredProperty(v.getId())));
|
||||
this.passwordSaved = vault.flatMap(v -> keychain.getPassphraseStoredProperty(v.getId())).orElse(false);
|
||||
} else {
|
||||
this.passwordSaved = new SimpleBooleanProperty(false);
|
||||
}
|
||||
@@ -47,12 +46,12 @@ public class VaultDetailLockedController implements FxController {
|
||||
|
||||
@FXML
|
||||
public void showVaultOptions() {
|
||||
vaultOptionsWindow.vault(vault.get()).build().showVaultOptionsWindow(SelectedVaultOptionsTab.ANY);
|
||||
vaultOptionsWindow.create(vault.get()).showVaultOptionsWindow(SelectedVaultOptionsTab.ANY);
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void showKeyVaultOptions() {
|
||||
vaultOptionsWindow.vault(vault.get()).build().showVaultOptionsWindow(SelectedVaultOptionsTab.KEY);
|
||||
vaultOptionsWindow.create(vault.get()).showVaultOptionsWindow(SelectedVaultOptionsTab.KEY);
|
||||
}
|
||||
|
||||
/* Getter/Setter */
|
||||
@@ -65,13 +64,11 @@ public class VaultDetailLockedController implements FxController {
|
||||
return vault.get();
|
||||
}
|
||||
|
||||
public BooleanExpression passwordSavedProperty() {
|
||||
public ObservableValue<Boolean> passwordSavedProperty() {
|
||||
return passwordSaved;
|
||||
}
|
||||
|
||||
public boolean isPasswordSaved() {
|
||||
if (keychain.isSupported() && vault.get() != null) {
|
||||
return keychain.getPassphraseStoredProperty(vault.get().getId()).get();
|
||||
} else return false;
|
||||
return passwordSaved.getValue();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ import javafx.stage.Stage;
|
||||
import java.io.File;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
import static org.cryptomator.common.Constants.CRYPTOMATOR_FILENAME_GLOB;
|
||||
|
||||
@MainWindowScoped
|
||||
public class VaultDetailMissingVaultController implements FxController {
|
||||
|
||||
@@ -45,7 +47,7 @@ public class VaultDetailMissingVaultController implements FxController {
|
||||
// copied from ChooseExistingVaultController class
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle(resourceBundle.getString("addvaultwizard.existing.filePickerTitle"));
|
||||
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Cryptomator Masterkey", "*.cryptomator"));
|
||||
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(resourceBundle.getString("addvaultwizard.existing.filePickerMimeDesc"), CRYPTOMATOR_FILENAME_GLOB));
|
||||
File masterkeyFile = fileChooser.showOpenDialog(window);
|
||||
if (masterkeyFile != null) {
|
||||
vault.get().getVaultSettings().path().setValue(masterkeyFile.toPath().toAbsolutePath().getParent());
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.cryptomator.ui.mainwindow;
|
||||
|
||||
import com.tobiasdiez.easybind.EasyBind;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.common.vaults.VaultState;
|
||||
import org.cryptomator.ui.common.Animations;
|
||||
@@ -10,15 +9,15 @@ import org.cryptomator.ui.controls.FontAwesome5Icon;
|
||||
import org.cryptomator.ui.controls.FontAwesome5IconView;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.beans.binding.Binding;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
|
||||
// unscoped because each cell needs its own controller
|
||||
public class VaultListCellController implements FxController {
|
||||
|
||||
private final ObjectProperty<Vault> vault = new SimpleObjectProperty<>();
|
||||
private final Binding<FontAwesome5Icon> glyph;
|
||||
private final ObservableValue<FontAwesome5Icon> glyph;
|
||||
|
||||
private AutoAnimator spinAnimation;
|
||||
|
||||
@@ -27,14 +26,12 @@ public class VaultListCellController implements FxController {
|
||||
|
||||
@Inject
|
||||
VaultListCellController() {
|
||||
this.glyph = EasyBind.select(vault) //
|
||||
.selectObject(Vault::stateProperty) //
|
||||
.map(this::getGlyphForVaultState);
|
||||
this.glyph = vault.flatMap(Vault::stateProperty).map(this::getGlyphForVaultState);
|
||||
}
|
||||
|
||||
public void initialize() {
|
||||
this.spinAnimation = AutoAnimator.animate(Animations.createDiscrete360Rotation(vaultStateView)) //
|
||||
.onCondition(EasyBind.select(vault).selectObject(Vault::stateProperty).map(VaultState.Value.PROCESSING::equals)) //
|
||||
.onCondition(vault.flatMap(Vault::stateProperty).map(VaultState.Value.PROCESSING::equals).orElse(false)) //
|
||||
.afterStop(() -> vaultStateView.setRotate(0)) //
|
||||
.build();
|
||||
}
|
||||
@@ -55,7 +52,7 @@ public class VaultListCellController implements FxController {
|
||||
|
||||
/* Getter/Setter */
|
||||
|
||||
public Binding<FontAwesome5Icon> glyphProperty() {
|
||||
public ObservableValue<FontAwesome5Icon> glyphProperty() {
|
||||
return glyph;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
package org.cryptomator.ui.mainwindow;
|
||||
|
||||
import com.tobiasdiez.easybind.EasyBind;
|
||||
import com.tobiasdiez.easybind.optional.ObservableOptionalValue;
|
||||
import com.tobiasdiez.easybind.optional.OptionalBinding;
|
||||
import org.cryptomator.common.keychain.KeychainManager;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.common.vaults.VaultState;
|
||||
@@ -14,33 +11,39 @@ import org.cryptomator.ui.vaultoptions.SelectedVaultOptionsTab;
|
||||
import org.cryptomator.ui.vaultoptions.VaultOptionsComponent;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.beans.binding.Binding;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.stage.Stage;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Objects;
|
||||
|
||||
import static org.cryptomator.common.vaults.VaultState.Value.*;
|
||||
import static org.cryptomator.common.vaults.VaultState.Value.ERROR;
|
||||
import static org.cryptomator.common.vaults.VaultState.Value.LOCKED;
|
||||
import static org.cryptomator.common.vaults.VaultState.Value.MISSING;
|
||||
import static org.cryptomator.common.vaults.VaultState.Value.NEEDS_MIGRATION;
|
||||
import static org.cryptomator.common.vaults.VaultState.Value.UNLOCKED;
|
||||
|
||||
@MainWindowScoped
|
||||
public class VaultListContextMenuController implements FxController {
|
||||
|
||||
private final ObservableOptionalValue<Vault> selectedVault;
|
||||
private final ReadOnlyObjectProperty<Vault> selectedVault;
|
||||
private final Stage mainWindow;
|
||||
private final FxApplicationWindows appWindows;
|
||||
private final VaultService vaultService;
|
||||
private final KeychainManager keychain;
|
||||
private final RemoveVaultComponent.Builder removeVault;
|
||||
private final VaultOptionsComponent.Builder vaultOptionsWindow;
|
||||
private final OptionalBinding<VaultState.Value> selectedVaultState;
|
||||
private final Binding<Boolean> selectedVaultPassphraseStored;
|
||||
private final Binding<Boolean> selectedVaultRemovable;
|
||||
private final Binding<Boolean> selectedVaultUnlockable;
|
||||
private final Binding<Boolean> selectedVaultLockable;
|
||||
private final VaultOptionsComponent.Factory vaultOptionsWindow;
|
||||
private final ObservableValue<VaultState.Value> selectedVaultState;
|
||||
private final ObservableValue<Boolean> selectedVaultPassphraseStored;
|
||||
private final ObservableValue<Boolean> selectedVaultRemovable;
|
||||
private final ObservableValue<Boolean> selectedVaultUnlockable;
|
||||
private final ObservableValue<Boolean> selectedVaultLockable;
|
||||
|
||||
@Inject
|
||||
VaultListContextMenuController(ObjectProperty<Vault> selectedVault, @MainWindow Stage mainWindow, FxApplicationWindows appWindows, VaultService vaultService, KeychainManager keychain, RemoveVaultComponent.Builder removeVault, VaultOptionsComponent.Builder vaultOptionsWindow) {
|
||||
this.selectedVault = EasyBind.wrapNullable(selectedVault);
|
||||
VaultListContextMenuController(ObjectProperty<Vault> selectedVault, @MainWindow Stage mainWindow, FxApplicationWindows appWindows, VaultService vaultService, KeychainManager keychain, RemoveVaultComponent.Builder removeVault, VaultOptionsComponent.Factory vaultOptionsWindow) {
|
||||
this.selectedVault = selectedVault;
|
||||
this.mainWindow = mainWindow;
|
||||
this.appWindows = appWindows;
|
||||
this.vaultService = vaultService;
|
||||
@@ -48,8 +51,8 @@ public class VaultListContextMenuController implements FxController {
|
||||
this.removeVault = removeVault;
|
||||
this.vaultOptionsWindow = vaultOptionsWindow;
|
||||
|
||||
this.selectedVaultState = this.selectedVault.mapObservable(Vault::stateProperty);
|
||||
this.selectedVaultPassphraseStored = this.selectedVault.map(this::isPasswordStored).orElse(false);
|
||||
this.selectedVaultState = selectedVault.flatMap(Vault::stateProperty).orElse(null);
|
||||
this.selectedVaultPassphraseStored = selectedVault.map(this::isPasswordStored).orElse(false);
|
||||
this.selectedVaultRemovable = selectedVaultState.map(EnumSet.of(LOCKED, MISSING, ERROR, NEEDS_MIGRATION)::contains).orElse(false);
|
||||
this.selectedVaultUnlockable = selectedVaultState.map(LOCKED::equals).orElse(false);
|
||||
this.selectedVaultLockable = selectedVaultState.map(UNLOCKED::equals).orElse(false);
|
||||
@@ -61,40 +64,37 @@ public class VaultListContextMenuController implements FxController {
|
||||
|
||||
@FXML
|
||||
public void didClickRemoveVault() {
|
||||
selectedVault.ifValuePresent(v -> {
|
||||
removeVault.vault(v).build().showRemoveVault();
|
||||
});
|
||||
var vault = Objects.requireNonNull(selectedVault.get());
|
||||
removeVault.vault(vault).build().showRemoveVault();
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void didClickShowVaultOptions() {
|
||||
selectedVault.ifValuePresent(v -> {
|
||||
vaultOptionsWindow.vault(v).build().showVaultOptionsWindow(SelectedVaultOptionsTab.ANY);
|
||||
});
|
||||
var vault = Objects.requireNonNull(selectedVault.get());
|
||||
vaultOptionsWindow.create(vault).showVaultOptionsWindow(SelectedVaultOptionsTab.ANY);
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void didClickUnlockVault() {
|
||||
selectedVault.ifValuePresent(v -> {
|
||||
appWindows.startUnlockWorkflow(v, mainWindow);
|
||||
});
|
||||
var vault = Objects.requireNonNull(selectedVault.get());
|
||||
appWindows.startUnlockWorkflow(vault, mainWindow);
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void didClickLockVault() {
|
||||
selectedVault.ifValuePresent(v -> {
|
||||
appWindows.startLockWorkflow(v, mainWindow);
|
||||
});
|
||||
var vault = Objects.requireNonNull(selectedVault.get());
|
||||
appWindows.startLockWorkflow(vault, mainWindow);
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void didClickRevealVault() {
|
||||
selectedVault.ifValuePresent(vaultService::reveal);
|
||||
var vault = Objects.requireNonNull(selectedVault.get());
|
||||
vaultService.reveal(vault);
|
||||
}
|
||||
|
||||
// Getter and Setter
|
||||
|
||||
public Binding<Boolean> selectedVaultUnlockableProperty() {
|
||||
public ObservableValue<Boolean> selectedVaultUnlockableProperty() {
|
||||
return selectedVaultUnlockable;
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ public class VaultListContextMenuController implements FxController {
|
||||
return selectedVaultUnlockable.getValue();
|
||||
}
|
||||
|
||||
public Binding<Boolean> selectedVaultLockableProperty() {
|
||||
public ObservableValue<Boolean> selectedVaultLockableProperty() {
|
||||
return selectedVaultLockable;
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@ public class VaultListContextMenuController implements FxController {
|
||||
return selectedVaultLockable.getValue();
|
||||
}
|
||||
|
||||
public Binding<Boolean> selectedVaultRemovableProperty() {
|
||||
public ObservableValue<Boolean> selectedVaultRemovableProperty() {
|
||||
return selectedVaultRemovable;
|
||||
}
|
||||
|
||||
@@ -118,7 +118,7 @@ public class VaultListContextMenuController implements FxController {
|
||||
return selectedVaultRemovable.getValue();
|
||||
}
|
||||
|
||||
public Binding<Boolean> selectedVaultPassphraseStoredProperty() {
|
||||
public ObservableValue<Boolean> selectedVaultPassphraseStoredProperty() {
|
||||
return selectedVaultPassphraseStored;
|
||||
}
|
||||
|
||||
|
||||
@@ -69,11 +69,15 @@ public class VaultListController implements FxController {
|
||||
}
|
||||
});
|
||||
vaultList.addEventFilter(MouseEvent.MOUSE_RELEASED, this::deselect);
|
||||
|
||||
//don't show context menu when no vault selected
|
||||
vaultList.addEventFilter(ContextMenuEvent.CONTEXT_MENU_REQUESTED, request -> {
|
||||
if (selectedVault.get() == null) {
|
||||
request.consume();
|
||||
}
|
||||
});
|
||||
|
||||
//show removeVaultDialog on certain key press
|
||||
vaultList.addEventFilter(KeyEvent.KEY_PRESSED, keyEvent -> {
|
||||
if (keyEvent.getCode() == KeyCode.DELETE) {
|
||||
pressedShortcutToRemoveVault();
|
||||
|
||||
@@ -16,7 +16,7 @@ import javafx.fxml.FXML;
|
||||
public class WelcomeController implements FxController {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(WelcomeController.class);
|
||||
private static final String GETTING_STARTED_URI = "https://docs.cryptomator.org/en/1.5/desktop/getting-started/";
|
||||
private static final String GETTING_STARTED_URI = "https://docs.cryptomator.org/en/1.6/desktop/getting-started/";
|
||||
|
||||
private final Application application;
|
||||
private final BooleanBinding noVaultPresent;
|
||||
|
||||
@@ -10,7 +10,7 @@ import javafx.stage.Stage;
|
||||
|
||||
public class MigrationImpossibleController implements FxController {
|
||||
|
||||
private static final String HELP_URI = "https://docs.cryptomator.org/en/1.5/help/manual-migration/";
|
||||
private static final String HELP_URI = "https://docs.cryptomator.org/en/1.6/help/manual-migration/";
|
||||
|
||||
private final Application application;
|
||||
private final Stage window;
|
||||
|
||||
@@ -83,7 +83,7 @@ public class MigrationRunController implements FxController {
|
||||
this.appWindows = appWindows;
|
||||
this.startScene = startScene;
|
||||
this.successScene = successScene;
|
||||
this.migrateButtonContentDisplay = Bindings.createObjectBinding(this::getMigrateButtonContentDisplay, vault.stateProperty());
|
||||
this.migrateButtonContentDisplay = Bindings.when(vault.processingProperty()).then(ContentDisplay.LEFT).otherwise(ContentDisplay.TEXT_ONLY);
|
||||
this.capabilityErrorScene = capabilityErrorScene;
|
||||
this.migrationButtonDisabled = new SimpleBooleanProperty();
|
||||
this.migrationProgress = new SimpleDoubleProperty(volatileMigrationProgress);
|
||||
@@ -211,10 +211,7 @@ public class MigrationRunController implements FxController {
|
||||
}
|
||||
|
||||
public ContentDisplay getMigrateButtonContentDisplay() {
|
||||
return switch (vault.getState()) {
|
||||
case PROCESSING -> ContentDisplay.LEFT;
|
||||
default -> ContentDisplay.TEXT_ONLY;
|
||||
};
|
||||
return migrateButtonContentDisplay.get();
|
||||
}
|
||||
|
||||
public ReadOnlyDoubleProperty migrationProgressProperty() {
|
||||
|
||||
@@ -25,9 +25,6 @@ public class MigrationStartController implements FxController {
|
||||
this.runMigrationScene = runMigrationScene;
|
||||
}
|
||||
|
||||
public void initialize() {
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void cancel() {
|
||||
window.close();
|
||||
|
||||
@@ -18,14 +18,14 @@ public class AboutController implements FxController {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AboutController.class);
|
||||
|
||||
private final String thirdPartyLicenseText;
|
||||
private final String applicationVersion;
|
||||
private final String fullApplicationVersion;
|
||||
|
||||
@Inject
|
||||
AboutController(UpdateChecker updateChecker, Environment environment) {
|
||||
this.thirdPartyLicenseText = loadThirdPartyLicenseFile();
|
||||
StringBuilder sb = new StringBuilder(updateChecker.currentVersionProperty().get());
|
||||
StringBuilder sb = new StringBuilder(environment.getAppVersion());
|
||||
environment.getBuildNumber().ifPresent(s -> sb.append(" (").append(s).append(')'));
|
||||
this.applicationVersion = sb.toString();
|
||||
this.fullApplicationVersion = sb.toString();
|
||||
}
|
||||
|
||||
private static String loadThirdPartyLicenseFile() {
|
||||
@@ -43,7 +43,7 @@ public class AboutController implements FxController {
|
||||
return thirdPartyLicenseText;
|
||||
}
|
||||
|
||||
public String getApplicationVersion() {
|
||||
return applicationVersion;
|
||||
public String getFullApplicationVersion() {
|
||||
return fullApplicationVersion;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,8 +34,10 @@ public class GeneralPreferencesController implements FxController {
|
||||
private final Environment environment;
|
||||
private final List<KeychainAccessProvider> keychainAccessProviders;
|
||||
private final FxApplicationWindows appWindows;
|
||||
public CheckBox useKeychainCheckbox;
|
||||
public ChoiceBox<KeychainAccessProvider> keychainBackendChoiceBox;
|
||||
public CheckBox startHiddenCheckbox;
|
||||
public CheckBox autoCloseVaultsCheckbox;
|
||||
public CheckBox debugModeCheckbox;
|
||||
public CheckBox autoStartCheckbox;
|
||||
public ToggleGroup nodeOrientation;
|
||||
@@ -54,9 +56,8 @@ public class GeneralPreferencesController implements FxController {
|
||||
@FXML
|
||||
public void initialize() {
|
||||
startHiddenCheckbox.selectedProperty().bindBidirectional(settings.startHidden());
|
||||
|
||||
autoCloseVaultsCheckbox.selectedProperty().bindBidirectional(settings.autoCloseVaults());
|
||||
debugModeCheckbox.selectedProperty().bindBidirectional(settings.debugMode());
|
||||
|
||||
autoStartProvider.ifPresent(autoStart -> autoStartCheckbox.setSelected(autoStart.isEnabled()));
|
||||
|
||||
var keychainSettingsConverter = new KeychainProviderClassNameConverter(keychainAccessProviders);
|
||||
@@ -64,6 +65,8 @@ public class GeneralPreferencesController implements FxController {
|
||||
keychainBackendChoiceBox.setValue(keychainSettingsConverter.fromString(settings.keychainProvider().get()));
|
||||
keychainBackendChoiceBox.setConverter(new KeychainProviderDisplayNameConverter());
|
||||
Bindings.bindBidirectional(settings.keychainProvider(), keychainBackendChoiceBox.valueProperty(), keychainSettingsConverter);
|
||||
useKeychainCheckbox.selectedProperty().bindBidirectional(settings.useKeychain());
|
||||
keychainBackendChoiceBox.disableProperty().bind(useKeychainCheckbox.selectedProperty().not());
|
||||
}
|
||||
|
||||
public boolean isAutoStartSupported() {
|
||||
|
||||
@@ -24,7 +24,7 @@ public class UpdatesPreferencesController implements FxController {
|
||||
private final UpdateChecker updateChecker;
|
||||
private final ObjectBinding<ContentDisplay> checkForUpdatesButtonState;
|
||||
private final ReadOnlyStringProperty latestVersion;
|
||||
private final ReadOnlyStringProperty currentVersion;
|
||||
private final String currentVersion;
|
||||
private final BooleanBinding updateAvailable;
|
||||
|
||||
/* FXML */
|
||||
@@ -38,7 +38,7 @@ public class UpdatesPreferencesController implements FxController {
|
||||
this.checkForUpdatesButtonState = Bindings.when(updateChecker.checkingForUpdatesProperty()).then(ContentDisplay.LEFT).otherwise(ContentDisplay.TEXT_ONLY);
|
||||
this.latestVersion = updateChecker.latestVersionProperty();
|
||||
this.updateAvailable = latestVersion.isNotNull();
|
||||
this.currentVersion = updateChecker.currentVersionProperty();
|
||||
this.currentVersion = updateChecker.getCurrentVersion();
|
||||
}
|
||||
|
||||
public void initialize() {
|
||||
@@ -73,12 +73,8 @@ public class UpdatesPreferencesController implements FxController {
|
||||
return latestVersion.get();
|
||||
}
|
||||
|
||||
public ReadOnlyStringProperty currentVersionProperty() {
|
||||
return currentVersion;
|
||||
}
|
||||
|
||||
public String getCurrentVersion() {
|
||||
return currentVersion.get();
|
||||
return currentVersion;
|
||||
}
|
||||
|
||||
public BooleanBinding updateAvailableProperty() {
|
||||
|
||||
@@ -48,11 +48,6 @@ public class VolumePreferencesController implements FxController {
|
||||
webDavPortField.setText(String.valueOf(settings.port().get()));
|
||||
changeWebDavPortButton.visibleProperty().bind(settings.port().asString().isNotEqualTo(webDavPortField.textProperty()));
|
||||
changeWebDavPortButton.disableProperty().bind(Bindings.createBooleanBinding(this::validateWebDavPort, webDavPortField.textProperty()).not());
|
||||
webDavPortField.focusedProperty().addListener((observableValue, wasFocused, isFocused) -> {
|
||||
if(!isFocused) {
|
||||
webDavPortField.setText(String.valueOf(settings.port().get()));
|
||||
}
|
||||
});
|
||||
|
||||
webDavUrlSchemeChoiceBox.getItems().addAll(WebDavUrlScheme.values());
|
||||
webDavUrlSchemeChoiceBox.valueProperty().bindBidirectional(settings.preferredGvfsScheme());
|
||||
|
||||
@@ -13,6 +13,7 @@ import org.cryptomator.ui.common.FxmlScene;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.stage.Stage;
|
||||
import java.awt.desktop.QuitResponse;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
@QuitScoped
|
||||
@Subcomponent(modules = {QuitModule.class})
|
||||
@@ -22,23 +23,28 @@ public interface QuitComponent {
|
||||
Stage window();
|
||||
|
||||
@FxmlScene(FxmlFile.QUIT)
|
||||
Lazy<Scene> scene();
|
||||
Lazy<Scene> quitScene();
|
||||
|
||||
QuitController controller();
|
||||
@FxmlScene(FxmlFile.QUIT_FORCED)
|
||||
Lazy<Scene> quitForcedScene();
|
||||
|
||||
default Stage showQuitWindow(QuitResponse response) {
|
||||
controller().updateQuitRequest(response);
|
||||
@QuitWindow
|
||||
AtomicReference<QuitResponse> quitResponse();
|
||||
|
||||
default void showQuitWindow(QuitResponse response, boolean forced) {
|
||||
Stage stage = window();
|
||||
stage.setScene(scene().get());
|
||||
quitResponse().set(response);
|
||||
if(forced){
|
||||
stage.setScene(quitForcedScene().get());
|
||||
} else{
|
||||
stage.setScene(quitScene().get());
|
||||
}
|
||||
stage.sizeToScene();
|
||||
stage.show();
|
||||
stage.requestFocus();
|
||||
return stage;
|
||||
}
|
||||
|
||||
@Subcomponent.Builder
|
||||
interface Builder {
|
||||
|
||||
QuitComponent build();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
package org.cryptomator.ui.quit;
|
||||
|
||||
import dagger.Lazy;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.common.FxmlFile;
|
||||
import org.cryptomator.ui.common.FxmlScene;
|
||||
import org.cryptomator.ui.common.VaultService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -10,6 +13,7 @@ import javax.inject.Inject;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.concurrent.Task;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ContentDisplay;
|
||||
import javafx.stage.Stage;
|
||||
@@ -29,27 +33,22 @@ public class QuitController implements FxController {
|
||||
private final ObservableList<Vault> unlockedVaults;
|
||||
private final ExecutorService executorService;
|
||||
private final VaultService vaultService;
|
||||
private final AtomicReference<QuitResponse> quitResponse = new AtomicReference<>();
|
||||
|
||||
private final AtomicReference<QuitResponse> quitResponse;
|
||||
private final Lazy<Scene> quitForcedScene;
|
||||
/* FXML */
|
||||
public Button lockAndQuitButton;
|
||||
|
||||
@Inject
|
||||
QuitController(@QuitWindow Stage window, ObservableList<Vault> vaults, ExecutorService executorService, VaultService vaultService) {
|
||||
QuitController(@QuitWindow Stage window, ObservableList<Vault> vaults, ExecutorService executorService, VaultService vaultService, @FxmlScene(FxmlFile.QUIT_FORCED) Lazy<Scene> quitForcedScene, @QuitWindow AtomicReference<QuitResponse> quitResponse) {
|
||||
this.window = window;
|
||||
this.unlockedVaults = vaults.filtered(Vault::isUnlocked);
|
||||
this.executorService = executorService;
|
||||
this.vaultService = vaultService;
|
||||
this.quitForcedScene = quitForcedScene;
|
||||
this.quitResponse = quitResponse;
|
||||
window.setOnCloseRequest(windowEvent -> cancel());
|
||||
}
|
||||
|
||||
public void updateQuitRequest(QuitResponse newResponse) {
|
||||
var oldResponse = quitResponse.getAndSet(newResponse);
|
||||
if (oldResponse != null) {
|
||||
oldResponse.cancelQuit();
|
||||
}
|
||||
}
|
||||
|
||||
private void respondToQuitRequest(Consumer<QuitResponse> action) {
|
||||
var response = quitResponse.getAndSet(null);
|
||||
if (response != null) {
|
||||
@@ -79,13 +78,8 @@ public class QuitController implements FxController {
|
||||
});
|
||||
lockAllTask.setOnFailed(evt -> {
|
||||
LOG.warn("Locking failed", lockAllTask.getException());
|
||||
lockAndQuitButton.setDisable(false);
|
||||
lockAndQuitButton.setContentDisplay(ContentDisplay.TEXT_ONLY);
|
||||
// TODO: show force lock or force quit scene (and DO NOT cancelQuit() here!) (see https://github.com/cryptomator/cryptomator/pull/1416)
|
||||
window.close();
|
||||
respondToQuitRequest(QuitResponse::cancelQuit);
|
||||
window.setScene(quitForcedScene.get());
|
||||
});
|
||||
executorService.execute(lockAllTask);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package org.cryptomator.ui.quit;
|
||||
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.common.VaultService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.concurrent.Task;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ContentDisplay;
|
||||
import javafx.stage.Stage;
|
||||
import java.awt.desktop.QuitResponse;
|
||||
import java.util.Collection;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class QuitForcedController implements FxController {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(QuitForcedController.class);
|
||||
|
||||
private final Stage window;
|
||||
private final ObservableList<Vault> unlockedVaults;
|
||||
private final ExecutorService executorService;
|
||||
private final VaultService vaultService;
|
||||
private final AtomicReference<QuitResponse> quitResponse;
|
||||
|
||||
/* FXML */
|
||||
public Button forceLockAndQuitButton;
|
||||
|
||||
@Inject
|
||||
QuitForcedController(@QuitWindow Stage window, ObservableList<Vault> vaults, ExecutorService executorService, VaultService vaultService, @QuitWindow AtomicReference<QuitResponse> quitResponse) {
|
||||
this.window = window;
|
||||
this.unlockedVaults = vaults.filtered(Vault::isUnlocked);
|
||||
this.executorService = executorService;
|
||||
this.vaultService = vaultService;
|
||||
this.quitResponse = quitResponse;
|
||||
window.setOnCloseRequest(windowEvent -> cancel());
|
||||
}
|
||||
|
||||
private void respondToQuitRequest(Consumer<QuitResponse> action) {
|
||||
var response = quitResponse.getAndSet(null);
|
||||
if (response != null) {
|
||||
action.accept(response);
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void cancel() {
|
||||
LOG.info("Quitting application forced canceled by user.");
|
||||
window.close();
|
||||
respondToQuitRequest(QuitResponse::cancelQuit);
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void forceLockAndQuit() {
|
||||
forceLockAndQuitButton.setDisable(true);
|
||||
forceLockAndQuitButton.setContentDisplay(ContentDisplay.LEFT);
|
||||
|
||||
Task<Collection<Vault>> lockAllTask = vaultService.createLockAllTask(unlockedVaults, true); // forced set to true
|
||||
lockAllTask.setOnSucceeded(evt -> {
|
||||
LOG.info("Locked {}", lockAllTask.getValue().stream().map(Vault::getDisplayName).collect(Collectors.joining(", ")));
|
||||
if (unlockedVaults.isEmpty()) {
|
||||
window.close();
|
||||
respondToQuitRequest(QuitResponse::performQuit);
|
||||
}
|
||||
});
|
||||
lockAllTask.setOnFailed(evt -> {
|
||||
//TODO: what will happen if force lock and quit app fails?
|
||||
|
||||
LOG.error("Forced locking failed", lockAllTask.getException());
|
||||
forceLockAndQuitButton.setDisable(false);
|
||||
forceLockAndQuitButton.setContentDisplay(ContentDisplay.TEXT_ONLY);
|
||||
|
||||
window.close();
|
||||
respondToQuitRequest(QuitResponse::cancelQuit);
|
||||
});
|
||||
executorService.execute(lockAllTask);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,10 +5,10 @@ import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import dagger.multibindings.IntoMap;
|
||||
import org.cryptomator.ui.common.DefaultSceneFactory;
|
||||
import org.cryptomator.ui.common.FxmlLoaderFactory;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.common.FxControllerKey;
|
||||
import org.cryptomator.ui.common.FxmlFile;
|
||||
import org.cryptomator.ui.common.FxmlLoaderFactory;
|
||||
import org.cryptomator.ui.common.FxmlScene;
|
||||
import org.cryptomator.ui.common.StageFactory;
|
||||
|
||||
@@ -16,8 +16,10 @@ import javax.inject.Provider;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.stage.Modality;
|
||||
import javafx.stage.Stage;
|
||||
import java.awt.desktop.QuitResponse;
|
||||
import java.util.Map;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
@Module
|
||||
abstract class QuitModule {
|
||||
@@ -32,14 +34,23 @@ abstract class QuitModule {
|
||||
@Provides
|
||||
@QuitWindow
|
||||
@QuitScoped
|
||||
static Stage provideStage(StageFactory factory) {
|
||||
static Stage provideStage(StageFactory factory, ResourceBundle resourceBundle) {
|
||||
Stage stage = factory.create();
|
||||
stage.setTitle(resourceBundle.getString("quit.title"));
|
||||
stage.setMinWidth(300);
|
||||
stage.setMinHeight(100);
|
||||
stage.initModality(Modality.APPLICATION_MODAL);
|
||||
return stage;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@QuitWindow
|
||||
@QuitScoped
|
||||
static AtomicReference<QuitResponse> provideQuitResponse() {
|
||||
return new AtomicReference<QuitResponse>();
|
||||
}
|
||||
|
||||
|
||||
@Provides
|
||||
@FxmlScene(FxmlFile.QUIT)
|
||||
@QuitScoped
|
||||
@@ -47,6 +58,14 @@ abstract class QuitModule {
|
||||
return fxmlLoaders.createScene(FxmlFile.QUIT);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FxmlScene(FxmlFile.QUIT_FORCED)
|
||||
@QuitScoped
|
||||
static Scene provideQuitForcedScene(@QuitWindow FxmlLoaderFactory fxmlLoaders) {
|
||||
return fxmlLoaders.createScene(FxmlFile.QUIT_FORCED);
|
||||
}
|
||||
|
||||
|
||||
// ------------------
|
||||
|
||||
@Binds
|
||||
@@ -54,4 +73,9 @@ abstract class QuitModule {
|
||||
@FxControllerKey(QuitController.class)
|
||||
abstract FxController bindQuitController(QuitController controller);
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FxControllerKey(QuitForcedController.class)
|
||||
abstract FxController bindQuitForcedController(QuitForcedController controller);
|
||||
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import javafx.fxml.FXML;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.stage.Stage;
|
||||
import java.io.IOException;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
|
||||
@RecoveryKeyScoped
|
||||
@@ -37,8 +38,9 @@ public class RecoveryKeyCreationController implements FxController {
|
||||
public NiceSecurePasswordField passwordField;
|
||||
|
||||
@Inject
|
||||
public RecoveryKeyCreationController(@RecoveryKeyWindow Stage window, @FxmlScene(FxmlFile.RECOVERYKEY_SUCCESS) Lazy<Scene> successScene, @RecoveryKeyWindow Vault vault, RecoveryKeyFactory recoveryKeyFactory, ExecutorService executor, @RecoveryKeyWindow StringProperty recoveryKey, FxApplicationWindows appWindows) {
|
||||
public RecoveryKeyCreationController(@RecoveryKeyWindow Stage window, @FxmlScene(FxmlFile.RECOVERYKEY_SUCCESS) Lazy<Scene> successScene, @RecoveryKeyWindow Vault vault, RecoveryKeyFactory recoveryKeyFactory, ExecutorService executor, @RecoveryKeyWindow StringProperty recoveryKey, FxApplicationWindows appWindows, ResourceBundle resourceBundle) {
|
||||
this.window = window;
|
||||
window.setTitle(resourceBundle.getString("recoveryKey.display.title"));
|
||||
this.successScene = successScene;
|
||||
this.vault = vault;
|
||||
this.executor = executor;
|
||||
|
||||
@@ -53,9 +53,8 @@ abstract class RecoveryKeyModule {
|
||||
@Provides
|
||||
@RecoveryKeyWindow
|
||||
@RecoveryKeyScoped
|
||||
static Stage provideStage(StageFactory factory, ResourceBundle resourceBundle, @Named("keyRecoveryOwner") Stage owner) {
|
||||
static Stage provideStage(StageFactory factory, @Named("keyRecoveryOwner") Stage owner) {
|
||||
Stage stage = factory.create();
|
||||
stage.setTitle(resourceBundle.getString("recoveryKey.title"));
|
||||
stage.setResizable(false);
|
||||
stage.initModality(Modality.WINDOW_MODAL);
|
||||
stage.initOwner(owner);
|
||||
@@ -99,6 +98,14 @@ abstract class RecoveryKeyModule {
|
||||
return fxmlLoaders.createScene(FxmlFile.RECOVERYKEY_RESET_PASSWORD);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FxmlScene(FxmlFile.RECOVERYKEY_RESET_PASSWORD_SUCCESS)
|
||||
@RecoveryKeyScoped
|
||||
static Scene provideRecoveryKeyResetPasswordSuccessScene(@RecoveryKeyWindow FxmlLoaderFactory fxmlLoaders) {
|
||||
return fxmlLoaders.createScene(FxmlFile.RECOVERYKEY_RESET_PASSWORD_SUCCESS);
|
||||
}
|
||||
|
||||
|
||||
// ------------------
|
||||
|
||||
@Binds
|
||||
@@ -128,6 +135,11 @@ abstract class RecoveryKeyModule {
|
||||
@FxControllerKey(RecoveryKeyResetPasswordController.class)
|
||||
abstract FxController bindRecoveryKeyResetPasswordController(RecoveryKeyResetPasswordController controller);
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FxControllerKey(RecoveryKeyResetPasswordSuccessController.class)
|
||||
abstract FxController bindRecoveryKeyResetPasswordSuccessController(RecoveryKeyResetPasswordSuccessController controller);
|
||||
|
||||
@Provides
|
||||
@IntoMap
|
||||
@FxControllerKey(NewPasswordController.class)
|
||||
|
||||
@@ -26,6 +26,7 @@ import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.stage.Stage;
|
||||
import java.util.Optional;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
@RecoveryKeyScoped
|
||||
public class RecoveryKeyRecoverController implements FxController {
|
||||
@@ -45,8 +46,9 @@ public class RecoveryKeyRecoverController implements FxController {
|
||||
public TextArea textarea;
|
||||
|
||||
@Inject
|
||||
public RecoveryKeyRecoverController(@RecoveryKeyWindow Stage window, @RecoveryKeyWindow Vault vault, @RecoveryKeyWindow @Nullable VaultConfig.UnverifiedVaultConfig unverifiedVaultConfig, @RecoveryKeyWindow StringProperty recoveryKey, RecoveryKeyFactory recoveryKeyFactory, @FxmlScene(FxmlFile.RECOVERYKEY_RESET_PASSWORD) Lazy<Scene> resetPasswordScene) {
|
||||
public RecoveryKeyRecoverController(@RecoveryKeyWindow Stage window, @RecoveryKeyWindow Vault vault, @RecoveryKeyWindow @Nullable VaultConfig.UnverifiedVaultConfig unverifiedVaultConfig, @RecoveryKeyWindow StringProperty recoveryKey, RecoveryKeyFactory recoveryKeyFactory, @FxmlScene(FxmlFile.RECOVERYKEY_RESET_PASSWORD) Lazy<Scene> resetPasswordScene, ResourceBundle resourceBundle) {
|
||||
this.window = window;
|
||||
window.setTitle(resourceBundle.getString("recoveryKey.recover.title"));
|
||||
this.vault = vault;
|
||||
this.unverifiedVaultConfig = unverifiedVaultConfig;
|
||||
this.recoveryKey = recoveryKey;
|
||||
|
||||
@@ -30,41 +30,40 @@ public class RecoveryKeyResetPasswordController implements FxController {
|
||||
private final RecoveryKeyFactory recoveryKeyFactory;
|
||||
private final ExecutorService executor;
|
||||
private final StringProperty recoveryKey;
|
||||
private final Lazy<Scene> recoverScene;
|
||||
private final Lazy<Scene> recoverResetPasswordSuccessScene;
|
||||
private final FxApplicationWindows appWindows;
|
||||
|
||||
public NewPasswordController newPasswordController;
|
||||
|
||||
@Inject
|
||||
public RecoveryKeyResetPasswordController(@RecoveryKeyWindow Stage window, @RecoveryKeyWindow Vault vault, RecoveryKeyFactory recoveryKeyFactory, ExecutorService executor, @RecoveryKeyWindow StringProperty recoveryKey, @FxmlScene(FxmlFile.RECOVERYKEY_RECOVER) Lazy<Scene> recoverScene, FxApplicationWindows appWindows) {
|
||||
public RecoveryKeyResetPasswordController(@RecoveryKeyWindow Stage window, @RecoveryKeyWindow Vault vault, RecoveryKeyFactory recoveryKeyFactory, ExecutorService executor, @RecoveryKeyWindow StringProperty recoveryKey, @FxmlScene(FxmlFile.RECOVERYKEY_RESET_PASSWORD_SUCCESS) Lazy<Scene> recoverResetPasswordSuccessScene, FxApplicationWindows appWindows) {
|
||||
this.window = window;
|
||||
this.vault = vault;
|
||||
this.recoveryKeyFactory = recoveryKeyFactory;
|
||||
this.executor = executor;
|
||||
this.recoveryKey = recoveryKey;
|
||||
this.recoverScene = recoverScene;
|
||||
this.recoverResetPasswordSuccessScene = recoverResetPasswordSuccessScene;
|
||||
this.appWindows = appWindows;
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void back() {
|
||||
window.setScene(recoverScene.get());
|
||||
public void close() {
|
||||
window.close();
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void done() {
|
||||
public void resetPassword() {
|
||||
Task<Void> task = new ResetPasswordTask();
|
||||
task.setOnScheduled(event -> {
|
||||
LOG.debug("Using recovery key to reset password for {}.", vault.getDisplayablePath());
|
||||
});
|
||||
task.setOnSucceeded(event -> {
|
||||
LOG.info("Used recovery key to reset password for {}.", vault.getDisplayablePath());
|
||||
// TODO show success screen
|
||||
window.close();
|
||||
window.setScene(recoverResetPasswordSuccessScene.get());
|
||||
});
|
||||
task.setOnFailed(event -> {
|
||||
LOG.error("Resetting password failed.", task.getException());
|
||||
appWindows.showErrorWindow(task.getException(), window, recoverScene.get());
|
||||
appWindows.showErrorWindow(task.getException(), window, null);
|
||||
});
|
||||
executor.submit(task);
|
||||
}
|
||||
@@ -85,11 +84,11 @@ public class RecoveryKeyResetPasswordController implements FxController {
|
||||
|
||||
/* Getter/Setter */
|
||||
|
||||
public ReadOnlyBooleanProperty validPasswordProperty() {
|
||||
public ReadOnlyBooleanProperty passwordSufficientAndMatchingProperty() {
|
||||
return newPasswordController.goodPasswordProperty();
|
||||
}
|
||||
|
||||
public boolean isValidPassword() {
|
||||
public boolean isPasswordSufficientAndMatching() {
|
||||
return newPasswordController.isGoodPassword();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package org.cryptomator.ui.recoverykey;
|
||||
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
@RecoveryKeyScoped
|
||||
public class RecoveryKeyResetPasswordSuccessController implements FxController {
|
||||
|
||||
private final Stage window;
|
||||
|
||||
@Inject
|
||||
public RecoveryKeyResetPasswordSuccessController(@RecoveryKeyWindow Stage window) {
|
||||
this.window = window;
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void close() {
|
||||
window.close();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import dagger.Binds;
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import dagger.multibindings.IntoMap;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.ui.common.DefaultSceneFactory;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.common.FxControllerKey;
|
||||
@@ -33,9 +34,9 @@ abstract class RemoveVaultModule {
|
||||
@Provides
|
||||
@RemoveVaultWindow
|
||||
@RemoveVaultScoped
|
||||
static Stage provideStage(StageFactory factory, @PrimaryStage Stage primaryStage, ResourceBundle resourceBundle) {
|
||||
static Stage provideStage(StageFactory factory, @PrimaryStage Stage primaryStage, @RemoveVaultWindow Vault vault, ResourceBundle resourceBundle) {
|
||||
Stage stage = factory.create();
|
||||
stage.setTitle(resourceBundle.getString("removeVault.title"));
|
||||
stage.setTitle(String.format(resourceBundle.getString("removeVault.title"), vault.getDisplayName()));
|
||||
stage.setResizable(false);
|
||||
stage.initModality(Modality.WINDOW_MODAL);
|
||||
stage.initOwner(primaryStage);
|
||||
|
||||
@@ -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) {
|
||||
@@ -58,6 +62,18 @@ public class AwtTrayMenuController implements TrayMenuController {
|
||||
addChildren(menu, items);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
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) {
|
||||
for (var item : items) {
|
||||
// TODO: use Pattern Matching for switch, once available
|
||||
|
||||
@@ -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;
|
||||
@@ -31,8 +32,8 @@ import java.util.ResourceBundle;
|
||||
public class TrayMenuBuilder {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(TrayMenuBuilder.class);
|
||||
private static final String TRAY_ICON_MAC = "/img/tray_icon_mac.png";
|
||||
private static final String TRAY_ICON = "/img/tray_icon.png";
|
||||
private static final String TRAY_ICON_MAC = "/img/tray_icon_mac@2x.png";
|
||||
private static final String TRAY_ICON = "/img/window_icon_32.png";
|
||||
|
||||
private final ResourceBundle resourceBundle;
|
||||
private final VaultService vaultService;
|
||||
@@ -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) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user