mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-05-19 03:01:27 +00:00
Single maven module (#1676)
combined all sources into single maven module
This commit is contained in:
committed by
GitHub
parent
72bd9c1fdf
commit
7fac6da448
147
src/main/java/org/cryptomator/common/CommonsModule.java
Normal file
147
src/main/java/org/cryptomator/common/CommonsModule.java
Normal file
@@ -0,0 +1,147 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the accompanying LICENSE file.
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.common;
|
||||
|
||||
import com.tobiasdiez.easybind.EasyBind;
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
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;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Singleton;
|
||||
import javafx.beans.binding.Binding;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.collections.ObservableList;
|
||||
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;
|
||||
|
||||
@Module(subcomponents = {VaultComponent.class}, includes = {VaultListModule.class, KeychainModule.class})
|
||||
public abstract class CommonsModule {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(CommonsModule.class);
|
||||
private static final int NUM_SCHEDULER_THREADS = 2;
|
||||
private static final int NUM_CORE_BG_THREADS = 6;
|
||||
private static final long BG_THREAD_KEEPALIVE_SECONDS = 60l;
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@Named("licensePublicKey")
|
||||
static String provideLicensePublicKey() {
|
||||
// in PEM format without the dash-escaped begin/end lines
|
||||
return """
|
||||
MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQB7NfnqiZbg2KTmoflmZ71PbXru7oW\
|
||||
fmnV2yv3eDjlDfGruBrqz9TtXBZV/eYWt31xu1osIqaT12lKBvZ511aaAkIBeOEV\
|
||||
gwcBIlJr6kUw7NKzeJt7r2rrsOyQoOG2nWc/Of/NBqA3mIZRHk5Aq1YupFdD26QE\
|
||||
r0DzRyj4ixPIt38CQB8=\
|
||||
""";
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
static SecureRandom provideCSPRNG() {
|
||||
try {
|
||||
return SecureRandom.getInstanceStrong();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IllegalStateException("A strong algorithm must exist in every Java platform.", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
static MasterkeyFileAccess provideMasterkeyFileAccess(SecureRandom csprng) {
|
||||
return new MasterkeyFileAccess(Constants.PEPPER, csprng);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@Named("SemVer")
|
||||
static Comparator<String> providesSemVerComparator() {
|
||||
return new SemVerComparator();
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
static Settings provideSettings(SettingsProvider settingsProvider) {
|
||||
return settingsProvider.get();
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
static ScheduledExecutorService provideScheduledExecutorService(ShutdownHook shutdownHook) {
|
||||
final AtomicInteger threadNumber = new AtomicInteger(1);
|
||||
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(NUM_SCHEDULER_THREADS, r -> {
|
||||
String name = String.format("App Scheduled Executor %02d", threadNumber.getAndIncrement());
|
||||
Thread t = new Thread(r);
|
||||
t.setName(name);
|
||||
t.setUncaughtExceptionHandler(CommonsModule::handleUncaughtExceptionInBackgroundThread);
|
||||
t.setDaemon(true);
|
||||
LOG.debug("Starting {}", t.getName());
|
||||
return t;
|
||||
});
|
||||
shutdownHook.runOnShutdown(executorService::shutdown);
|
||||
return executorService;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@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 -> {
|
||||
String name = String.format("App Background Thread %03d", threadNumber.getAndIncrement());
|
||||
Thread t = new Thread(r);
|
||||
t.setName(name);
|
||||
t.setUncaughtExceptionHandler(CommonsModule::handleUncaughtExceptionInBackgroundThread);
|
||||
t.setDaemon(true);
|
||||
LOG.debug("Starting {}", t.getName());
|
||||
return t;
|
||||
});
|
||||
shutdownHook.runOnShutdown(executorService::shutdown);
|
||||
return executorService;
|
||||
}
|
||||
|
||||
private static void handleUncaughtExceptionInBackgroundThread(Thread thread, Throwable throwable) {
|
||||
LOG.error("Uncaught exception in " + thread.getName(), throwable);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
static Binding<InetSocketAddress> provideServerSocketAddressBinding(Settings settings) {
|
||||
return Bindings.createObjectBinding(() -> {
|
||||
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) {
|
||||
WebDavServer server = WebDavServer.create();
|
||||
// no need to unsubscribe eventually, because server is a singleton
|
||||
EasyBind.subscribe(serverSocketAddressBinding, server::bind);
|
||||
return server;
|
||||
}
|
||||
|
||||
}
|
||||
10
src/main/java/org/cryptomator/common/Constants.java
Normal file
10
src/main/java/org/cryptomator/common/Constants.java
Normal file
@@ -0,0 +1,10 @@
|
||||
package org.cryptomator.common;
|
||||
|
||||
public interface Constants {
|
||||
|
||||
String MASTERKEY_FILENAME = "masterkey.cryptomator";
|
||||
String MASTERKEY_BACKUP_SUFFIX = ".bkup";
|
||||
String VAULTCONFIG_FILENAME = "vault.cryptomator";
|
||||
byte[] PEPPER = new byte[0];
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the accompanying LICENSE file.
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.common;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface ConsumerThrowingException<T, E extends Throwable> {
|
||||
|
||||
void accept(T t) throws E;
|
||||
|
||||
}
|
||||
130
src/main/java/org/cryptomator/common/Environment.java
Normal file
130
src/main/java/org/cryptomator/common/Environment.java
Normal file
@@ -0,0 +1,130 @@
|
||||
package org.cryptomator.common;
|
||||
|
||||
import com.google.common.base.Splitter;
|
||||
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;
|
||||
import java.util.Optional;
|
||||
import java.util.Spliterator;
|
||||
import java.util.Spliterators;
|
||||
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;
|
||||
|
||||
@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.ipcPortPath: {}", System.getProperty("cryptomator.ipcPortPath"));
|
||||
LOG.debug("cryptomator.keychainPath: {}", System.getProperty("cryptomator.keychainPath"));
|
||||
LOG.debug("cryptomator.logDir: {}", System.getProperty("cryptomator.logDir"));
|
||||
LOG.debug("cryptomator.mountPointsDir: {}", System.getProperty("cryptomator.mountPointsDir"));
|
||||
LOG.debug("cryptomator.minPwLength: {}", System.getProperty("cryptomator.minPwLength"));
|
||||
LOG.debug("cryptomator.buildNumber: {}", System.getProperty("cryptomator.buildNumber"));
|
||||
LOG.debug("cryptomator.showTrayIcon: {}", System.getProperty("cryptomator.showTrayIcon"));
|
||||
LOG.debug("fuse.experimental: {}", Boolean.getBoolean("fuse.experimental"));
|
||||
}
|
||||
|
||||
public boolean useCustomLogbackConfig() {
|
||||
return getPath("logback.configurationFile").map(Files::exists).orElse(false);
|
||||
}
|
||||
|
||||
public Stream<Path> getSettingsPath() {
|
||||
return getPaths("cryptomator.settingsPath");
|
||||
}
|
||||
|
||||
public Stream<Path> getIpcPortPath() {
|
||||
return getPaths("cryptomator.ipcPortPath");
|
||||
}
|
||||
|
||||
public Stream<Path> getKeychainPath() {
|
||||
return getPaths("cryptomator.keychainPath");
|
||||
}
|
||||
|
||||
public Optional<Path> getLogDir() {
|
||||
return getPath("cryptomator.logDir").map(this::replaceHomeDir);
|
||||
}
|
||||
|
||||
public Optional<Path> getMountPointsDir() {
|
||||
return getPath("cryptomator.mountPointsDir").map(this::replaceHomeDir);
|
||||
}
|
||||
|
||||
public Optional<String> getBuildNumber() {
|
||||
return Optional.ofNullable(System.getProperty("cryptomator.buildNumber"));
|
||||
}
|
||||
|
||||
public int getMinPwLength() {
|
||||
return getInt("cryptomator.minPwLength", DEFAULT_MIN_PW_LENGTH);
|
||||
}
|
||||
|
||||
public boolean showTrayIcon() {
|
||||
return Boolean.getBoolean("cryptomator.showTrayIcon");
|
||||
}
|
||||
|
||||
@Deprecated // TODO: remove as soon as custom mount path works properly on Win+Fuse
|
||||
public boolean useExperimentalFuse() {
|
||||
return Boolean.getBoolean("fuse.experimental");
|
||||
}
|
||||
|
||||
private int getInt(String propertyName, int defaultValue) {
|
||||
String value = System.getProperty(propertyName);
|
||||
try {
|
||||
return Integer.parseInt(value);
|
||||
} catch (NumberFormatException e) { // includes "null" values
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<Path> getPath(String propertyName) {
|
||||
String value = System.getProperty(propertyName);
|
||||
return Optional.ofNullable(value).map(Paths::get);
|
||||
}
|
||||
|
||||
// visible for testing
|
||||
Path getHomeDir() {
|
||||
return getPath("user.home").orElseThrow();
|
||||
}
|
||||
|
||||
// visible for testing
|
||||
Stream<Path> getPaths(String propertyName) {
|
||||
Stream<String> rawSettingsPaths = getRawList(propertyName, PATH_LIST_SEP);
|
||||
return rawSettingsPaths.filter(Predicate.not(Strings::isNullOrEmpty)).map(Paths::get).map(this::replaceHomeDir);
|
||||
}
|
||||
|
||||
private Path replaceHomeDir(Path path) {
|
||||
if (path.startsWith(RELATIVE_HOME_DIR)) {
|
||||
return getHomeDir().resolve(RELATIVE_HOME_DIR.relativize(path));
|
||||
} else {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
private Stream<String> getRawList(String propertyName, char separator) {
|
||||
String value = System.getProperty(propertyName);
|
||||
if (value == null) {
|
||||
return Stream.empty();
|
||||
} else {
|
||||
Iterable<String> iter = Splitter.on(separator).split(value);
|
||||
Spliterator<String> spliter = Spliterators.spliteratorUnknownSize(iter.iterator(), Spliterator.ORDERED | Spliterator.IMMUTABLE);
|
||||
return StreamSupport.stream(spliter, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
56
src/main/java/org/cryptomator/common/LicenseChecker.java
Normal file
56
src/main/java/org/cryptomator/common/LicenseChecker.java
Normal file
@@ -0,0 +1,56 @@
|
||||
package org.cryptomator.common;
|
||||
|
||||
import com.auth0.jwt.JWT;
|
||||
import com.auth0.jwt.algorithms.Algorithm;
|
||||
import com.auth0.jwt.exceptions.JWTVerificationException;
|
||||
import com.auth0.jwt.interfaces.DecodedJWT;
|
||||
import com.auth0.jwt.interfaces.JWTVerifier;
|
||||
import com.google.common.io.BaseEncoding;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Singleton;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PublicKey;
|
||||
import java.security.interfaces.ECPublicKey;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.util.Optional;
|
||||
|
||||
@Singleton
|
||||
class LicenseChecker {
|
||||
|
||||
private final JWTVerifier verifier;
|
||||
|
||||
@Inject
|
||||
public LicenseChecker(@Named("licensePublicKey") String pemEncodedPublicKey) {
|
||||
Algorithm algorithm = Algorithm.ECDSA512(decodePublicKey(pemEncodedPublicKey), null);
|
||||
this.verifier = JWT.require(algorithm).build();
|
||||
}
|
||||
|
||||
private static ECPublicKey decodePublicKey(String pemEncodedPublicKey) {
|
||||
try {
|
||||
byte[] keyBytes = BaseEncoding.base64().decode(pemEncodedPublicKey);
|
||||
PublicKey key = KeyFactory.getInstance("EC").generatePublic(new X509EncodedKeySpec(keyBytes));
|
||||
if (key instanceof ECPublicKey k) {
|
||||
return k;
|
||||
} else {
|
||||
throw new IllegalStateException("Key not an EC public key.");
|
||||
}
|
||||
} catch (InvalidKeySpecException e) {
|
||||
throw new IllegalArgumentException("Invalid license public key", e);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public Optional<DecodedJWT> check(String licenseKey) {
|
||||
try {
|
||||
return Optional.of(verifier.verify(licenseKey));
|
||||
} catch (JWTVerificationException exception) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
79
src/main/java/org/cryptomator/common/LicenseHolder.java
Normal file
79
src/main/java/org/cryptomator/common/LicenseHolder.java
Normal file
@@ -0,0 +1,79 @@
|
||||
package org.cryptomator.common;
|
||||
|
||||
import com.auth0.jwt.interfaces.DecodedJWT;
|
||||
import org.cryptomator.common.settings.Settings;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.binding.StringBinding;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import java.util.Optional;
|
||||
|
||||
@Singleton
|
||||
public class LicenseHolder {
|
||||
|
||||
private final Settings settings;
|
||||
private final LicenseChecker licenseChecker;
|
||||
private final ObjectProperty<DecodedJWT> validJwtClaims;
|
||||
private final StringBinding licenseSubject;
|
||||
private final BooleanBinding validLicenseProperty;
|
||||
|
||||
@Inject
|
||||
public LicenseHolder(LicenseChecker licenseChecker, Settings settings) {
|
||||
this.settings = settings;
|
||||
this.licenseChecker = licenseChecker;
|
||||
this.validJwtClaims = new SimpleObjectProperty<>();
|
||||
this.licenseSubject = Bindings.createStringBinding(this::getLicenseSubject, validJwtClaims);
|
||||
this.validLicenseProperty = validJwtClaims.isNotNull();
|
||||
|
||||
Optional<DecodedJWT> claims = licenseChecker.check(settings.licenseKey().get());
|
||||
validJwtClaims.set(claims.orElse(null));
|
||||
}
|
||||
|
||||
public boolean validateAndStoreLicense(String licenseKey) {
|
||||
Optional<DecodedJWT> claims = licenseChecker.check(licenseKey);
|
||||
validJwtClaims.set(claims.orElse(null));
|
||||
if (claims.isPresent()) {
|
||||
settings.licenseKey().set(licenseKey);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/* Observable Properties */
|
||||
|
||||
public Optional<String> getLicenseKey() {
|
||||
DecodedJWT claims = validJwtClaims.get();
|
||||
if (claims != null) {
|
||||
return Optional.of(claims.getToken());
|
||||
} else {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
public StringBinding licenseSubjectProperty() {
|
||||
return licenseSubject;
|
||||
}
|
||||
|
||||
public String getLicenseSubject() {
|
||||
DecodedJWT claims = validJwtClaims.get();
|
||||
if (claims != null) {
|
||||
return claims.getSubject();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public BooleanBinding validLicenseProperty() {
|
||||
return validLicenseProperty;
|
||||
}
|
||||
|
||||
public boolean isValidLicense() {
|
||||
return validLicenseProperty.get();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the accompanying LICENSE file.
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.common;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface RunnableThrowingException<T extends Throwable> {
|
||||
|
||||
void run() throws T;
|
||||
|
||||
}
|
||||
81
src/main/java/org/cryptomator/common/SemVerComparator.java
Normal file
81
src/main/java/org/cryptomator/common/SemVerComparator.java
Normal file
@@ -0,0 +1,81 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2016, 2017 Sebastian Stenzel and others.
|
||||
* All rights reserved.
|
||||
* This program and the accompanying materials are made available under the terms of the accompanying LICENSE file.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.common;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.Comparator;
|
||||
|
||||
/**
|
||||
* Compares version strings according to <a href="http://semver.org/spec/v2.0.0.html">SemVer 2.0.0</a>.
|
||||
*/
|
||||
public class SemVerComparator implements Comparator<String> {
|
||||
|
||||
private static final char VERSION_SEP = '.'; // http://semver.org/spec/v2.0.0.html#spec-item-2
|
||||
private static final String PRE_RELEASE_SEP = "-"; // http://semver.org/spec/v2.0.0.html#spec-item-9
|
||||
private static final String BUILD_SEP = "+"; // http://semver.org/spec/v2.0.0.html#spec-item-10
|
||||
|
||||
@Override
|
||||
public int compare(String version1, String version2) {
|
||||
// "Build metadata SHOULD be ignored when determining version precedence.
|
||||
// Thus two versions that differ only in the build metadata, have the same precedence."
|
||||
String v1WithoutBuildMetadata = StringUtils.substringBefore(version1, BUILD_SEP);
|
||||
String v2WithoutBuildMetadata = StringUtils.substringBefore(version2, BUILD_SEP);
|
||||
|
||||
if (v1WithoutBuildMetadata.equals(v2WithoutBuildMetadata)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
String v1MajorMinorPatch = StringUtils.substringBefore(v1WithoutBuildMetadata, PRE_RELEASE_SEP);
|
||||
String v2MajorMinorPatch = StringUtils.substringBefore(v2WithoutBuildMetadata, PRE_RELEASE_SEP);
|
||||
String v1PreReleaseVersion = StringUtils.substringAfter(v1WithoutBuildMetadata, PRE_RELEASE_SEP);
|
||||
String v2PreReleaseVersion = StringUtils.substringAfter(v2WithoutBuildMetadata, PRE_RELEASE_SEP);
|
||||
return compare(v1MajorMinorPatch, v1PreReleaseVersion, v2MajorMinorPatch, v2PreReleaseVersion);
|
||||
}
|
||||
|
||||
private int compare(String v1MajorMinorPatch, String v1PreReleaseVersion, String v2MajorMinorPatch, String v2PreReleaseVersion) {
|
||||
int comparisonResult = compareNumericallyThenLexicographically(v1MajorMinorPatch, v2MajorMinorPatch);
|
||||
if (comparisonResult == 0) {
|
||||
if (v1PreReleaseVersion.isEmpty()) {
|
||||
return 1; // 1.0.0 > 1.0.0-BETA
|
||||
} else if (v2PreReleaseVersion.isEmpty()) {
|
||||
return -1; // 1.0.0-BETA < 1.0.0
|
||||
} else {
|
||||
return compareNumericallyThenLexicographically(v1PreReleaseVersion, v2PreReleaseVersion);
|
||||
}
|
||||
} else {
|
||||
return comparisonResult;
|
||||
}
|
||||
}
|
||||
|
||||
private int compareNumericallyThenLexicographically(String version1, String version2) {
|
||||
final String[] vComps1 = StringUtils.split(version1, VERSION_SEP);
|
||||
final String[] vComps2 = StringUtils.split(version2, VERSION_SEP);
|
||||
final int commonCompCount = Math.min(vComps1.length, vComps2.length);
|
||||
|
||||
for (int i = 0; i < commonCompCount; i++) {
|
||||
int subversionComparisionResult = 0;
|
||||
try {
|
||||
final int v1 = Integer.parseInt(vComps1[i]);
|
||||
final int v2 = Integer.parseInt(vComps2[i]);
|
||||
subversionComparisionResult = v1 - v2;
|
||||
} catch (NumberFormatException ex) {
|
||||
// ok, lets compare this fragment lexicographically
|
||||
subversionComparisionResult = vComps1[i].compareTo(vComps2[i]);
|
||||
}
|
||||
if (subversionComparisionResult != 0) {
|
||||
return subversionComparisionResult;
|
||||
}
|
||||
}
|
||||
|
||||
// all in common so far? longest version string is considered the higher version:
|
||||
return vComps1.length - vComps2.length;
|
||||
}
|
||||
|
||||
}
|
||||
96
src/main/java/org/cryptomator/common/ShutdownHook.java
Normal file
96
src/main/java/org/cryptomator/common/ShutdownHook.java
Normal file
@@ -0,0 +1,96 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the accompanying LICENSE file.
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.common;
|
||||
|
||||
import com.google.common.util.concurrent.Runnables;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.PriorityBlockingQueue;
|
||||
|
||||
@Singleton
|
||||
public class ShutdownHook extends Thread {
|
||||
|
||||
private static final int PRIO_VERY_LAST = Integer.MIN_VALUE;
|
||||
public static final int PRIO_LAST = PRIO_VERY_LAST + 1;
|
||||
public static final int PRIO_DEFAULT = 0;
|
||||
public static final int PRIO_FIRST = Integer.MAX_VALUE;
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ShutdownHook.class);
|
||||
private static final OrderedTask POISON = new OrderedTask(PRIO_VERY_LAST, Runnables.doNothing());
|
||||
private final Queue<OrderedTask> tasks = new PriorityBlockingQueue<>();
|
||||
|
||||
@Inject
|
||||
ShutdownHook() {
|
||||
super(null, null, "ShutdownTasks", 0);
|
||||
Runtime.getRuntime().addShutdownHook(this);
|
||||
LOG.debug("Registered shutdown hook.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
LOG.debug("Running graceful shutdown tasks...");
|
||||
tasks.add(POISON);
|
||||
Runnable task;
|
||||
while ((task = tasks.remove()) != POISON) {
|
||||
try {
|
||||
task.run();
|
||||
} catch (RuntimeException e) {
|
||||
LOG.error("Exception while shutting down.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedules a task to be run during shutdown with default order
|
||||
*
|
||||
* @param task The task to be scheduled
|
||||
*/
|
||||
public void runOnShutdown(Runnable task) {
|
||||
runOnShutdown(PRIO_DEFAULT, task);
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedules a task to be run with the given priority
|
||||
*
|
||||
* @param priority Tasks with high priority will be run before task with lower priority
|
||||
* @param task The task to be scheduled
|
||||
*/
|
||||
public void runOnShutdown(int priority, Runnable task) {
|
||||
tasks.add(new OrderedTask(priority, task));
|
||||
}
|
||||
|
||||
private static class OrderedTask implements Comparable<OrderedTask>, Runnable {
|
||||
|
||||
private final int priority;
|
||||
private final Runnable task;
|
||||
|
||||
public OrderedTask(int priority, Runnable task) {
|
||||
this.priority = priority;
|
||||
this.task = task;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(OrderedTask other) {
|
||||
// overflow-safe signum impl:
|
||||
if (this.priority > other.priority) {
|
||||
return -1; // higher prio -> this before other
|
||||
} else if (this.priority < other.priority) {
|
||||
return +1; // lower prio -> other before this
|
||||
} else {
|
||||
return 0; // same prio
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
task.run();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the accompanying LICENSE file.
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.common;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface SupplierThrowingException<T, E extends Throwable> {
|
||||
|
||||
T get() throws E;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
package org.cryptomator.common.keychain;
|
||||
|
||||
import com.google.common.cache.CacheBuilder;
|
||||
import com.google.common.cache.CacheLoader;
|
||||
import com.google.common.cache.LoadingCache;
|
||||
import org.cryptomator.integrations.keychain.KeychainAccessException;
|
||||
import org.cryptomator.integrations.keychain.KeychainAccessProvider;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.ObjectExpression;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ReadOnlyBooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import java.util.Arrays;
|
||||
|
||||
@Singleton
|
||||
public class KeychainManager implements KeychainAccessProvider {
|
||||
|
||||
private final ObjectExpression<KeychainAccessProvider> keychain;
|
||||
private final LoadingCache<String, BooleanProperty> passphraseStoredProperties;
|
||||
|
||||
@Inject
|
||||
KeychainManager(ObjectExpression<KeychainAccessProvider> selectedKeychain) {
|
||||
this.keychain = selectedKeychain;
|
||||
this.passphraseStoredProperties = CacheBuilder.newBuilder() //
|
||||
.weakValues() //
|
||||
.build(CacheLoader.from(this::createStoredPassphraseProperty));
|
||||
keychain.addListener(ignored -> passphraseStoredProperties.invalidateAll());
|
||||
}
|
||||
|
||||
private KeychainAccessProvider getKeychainOrFail() throws KeychainAccessException {
|
||||
var result = keychain.getValue();
|
||||
if (result == null) {
|
||||
throw new NoKeychainAccessProviderException();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void storePassphrase(String key, CharSequence passphrase) throws KeychainAccessException {
|
||||
getKeychainOrFail().storePassphrase(key, passphrase);
|
||||
setPassphraseStored(key, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public char[] loadPassphrase(String key) throws KeychainAccessException {
|
||||
char[] passphrase = getKeychainOrFail().loadPassphrase(key);
|
||||
setPassphraseStored(key, passphrase != null);
|
||||
return passphrase;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deletePassphrase(String key) throws KeychainAccessException {
|
||||
getKeychainOrFail().deletePassphrase(key);
|
||||
setPassphraseStored(key, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changePassphrase(String key, CharSequence passphrase) throws KeychainAccessException {
|
||||
getKeychainOrFail().changePassphrase(key, passphrase);
|
||||
setPassphraseStored(key, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSupported() {
|
||||
return keychain.getValue() != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLocked() {
|
||||
return keychain.getValue() == null || keychain.get().isLocked();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the keychain knows a passphrase for the given key.
|
||||
* <p>
|
||||
* Expensive operation. If possible, use {@link #getPassphraseStoredProperty(String)} instead.
|
||||
*
|
||||
* @param key The key to look up
|
||||
* @return <code>true</code> if a password for <code>key</code> is stored.
|
||||
* @throws KeychainAccessException
|
||||
*/
|
||||
public boolean isPassphraseStored(String key) throws KeychainAccessException {
|
||||
char[] storedPw = null;
|
||||
try {
|
||||
storedPw = getKeychainOrFail().loadPassphrase(key);
|
||||
return storedPw != null;
|
||||
} finally {
|
||||
if (storedPw != null) {
|
||||
Arrays.fill(storedPw, ' ');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setPassphraseStored(String key, boolean value) {
|
||||
BooleanProperty property = passphraseStoredProperties.getIfPresent(key);
|
||||
if (property != null) {
|
||||
if (Platform.isFxApplicationThread()) {
|
||||
property.set(value);
|
||||
} else {
|
||||
Platform.runLater(() -> property.set(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an observable property for use in the UI that tells whether a passphrase is stored for the given key.
|
||||
* <p>
|
||||
* Assuming that this process is the only process modifying Cryptomator-related items in the system keychain, this
|
||||
* property stays in memory in an attempt to avoid unnecessary calls to the system keychain. Note that due to this
|
||||
* fact the value stored in the returned property is not 100% reliable. Code defensively!
|
||||
*
|
||||
* @param key The key to look up
|
||||
* @return An observable property which is <code>true</code> when it almost certain that a password for <code>key</code> is stored.
|
||||
* @see #isPassphraseStored(String)
|
||||
*/
|
||||
public ReadOnlyBooleanProperty getPassphraseStoredProperty(String key) {
|
||||
return passphraseStoredProperties.getUnchecked(key);
|
||||
}
|
||||
|
||||
private BooleanProperty createStoredPassphraseProperty(String key) {
|
||||
try {
|
||||
return new SimpleBooleanProperty(isPassphraseStored(key));
|
||||
} catch (KeychainAccessException e) {
|
||||
return new SimpleBooleanProperty(false);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package org.cryptomator.common.keychain;
|
||||
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import org.cryptomator.common.settings.Settings;
|
||||
import org.cryptomator.integrations.keychain.KeychainAccessProvider;
|
||||
|
||||
import javax.inject.Singleton;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.ObjectExpression;
|
||||
import java.util.ServiceLoader;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Module
|
||||
public class KeychainModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
static Set<ServiceLoader.Provider<KeychainAccessProvider>> provideAvailableKeychainAccessProviderFactories() {
|
||||
return ServiceLoader.load(KeychainAccessProvider.class).stream().collect(Collectors.toUnmodifiableSet());
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
static Set<KeychainAccessProvider> provideSupportedKeychainAccessProviders(Set<ServiceLoader.Provider<KeychainAccessProvider>> availableFactories) {
|
||||
return availableFactories.stream() //
|
||||
.map(ServiceLoader.Provider::get) //
|
||||
.filter(KeychainAccessProvider::isSupported) //
|
||||
.collect(Collectors.toUnmodifiableSet());
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
static ObjectExpression<KeychainAccessProvider> provideKeychainAccessProvider(Settings settings, Set<KeychainAccessProvider> providers) {
|
||||
return Bindings.createObjectBinding(() -> {
|
||||
var selectedProviderClass = settings.keychainBackend().get().getProviderClass();
|
||||
var selectedProvider = providers.stream().filter(provider -> provider.getClass().getName().equals(selectedProviderClass)).findAny();
|
||||
var fallbackProvider = providers.stream().findAny().orElse(null);
|
||||
return selectedProvider.orElse(fallbackProvider);
|
||||
}, settings.keychainBackend());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.cryptomator.common.keychain;
|
||||
|
||||
import org.cryptomator.integrations.keychain.KeychainAccessException;
|
||||
|
||||
/**
|
||||
* Thrown by {@link KeychainManager} if attempted to access a keychain despite no supported keychain access provider being available.
|
||||
*/
|
||||
public class NoKeychainAccessProviderException extends KeychainAccessException {
|
||||
|
||||
public NoKeychainAccessProviderException() {
|
||||
super("Did not find any supported keychain access provider.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package org.cryptomator.common.mountpoint;
|
||||
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.common.vaults.Volume;
|
||||
import org.cryptomator.common.vaults.WindowsDriveLetters;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
|
||||
class AvailableDriveLetterChooser implements MountPointChooser {
|
||||
|
||||
private final WindowsDriveLetters windowsDriveLetters;
|
||||
|
||||
@Inject
|
||||
public AvailableDriveLetterChooser(WindowsDriveLetters windowsDriveLetters) {
|
||||
this.windowsDriveLetters = windowsDriveLetters;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(Volume caller) {
|
||||
return SystemUtils.IS_OS_WINDOWS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Path> chooseMountPoint(Volume caller) {
|
||||
return this.windowsDriveLetters.getAvailableDriveLetterPath();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package org.cryptomator.common.mountpoint;
|
||||
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.common.settings.VaultSettings;
|
||||
import org.cryptomator.common.vaults.Volume;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Optional;
|
||||
|
||||
class CustomDriveLetterChooser implements MountPointChooser {
|
||||
|
||||
private final VaultSettings vaultSettings;
|
||||
|
||||
@Inject
|
||||
public CustomDriveLetterChooser(VaultSettings vaultSettings) {
|
||||
this.vaultSettings = vaultSettings;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(Volume caller) {
|
||||
return SystemUtils.IS_OS_WINDOWS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Path> chooseMountPoint(Volume caller) {
|
||||
return this.vaultSettings.getWinDriveLetter().map(letter -> letter.charAt(0) + ":\\").map(Paths::get);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
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.Volume;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.DirectoryNotEmptyException;
|
||||
import java.nio.file.DirectoryStream;
|
||||
import java.nio.file.FileAlreadyExistsException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.LinkOption;
|
||||
import java.nio.file.NotDirectoryException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Optional;
|
||||
|
||||
class CustomMountPointChooser implements MountPointChooser {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(CustomMountPointChooser.class);
|
||||
|
||||
private final VaultSettings vaultSettings;
|
||||
private final Environment environment;
|
||||
|
||||
@Inject
|
||||
public CustomMountPointChooser(VaultSettings vaultSettings, Environment environment) {
|
||||
this.vaultSettings = vaultSettings;
|
||||
this.environment = environment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(Volume caller) {
|
||||
//Disable if useExperimentalFuse is required (Win + Fuse), but set to false
|
||||
return caller.getImplementationType() != VolumeImpl.FUSE || !SystemUtils.IS_OS_WINDOWS || environment.useExperimentalFuse();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Path> chooseMountPoint(Volume caller) {
|
||||
//VaultSettings#getCustomMountPath already checks whether the saved custom mountpoint should be used
|
||||
return this.vaultSettings.getCustomMountPath().map(Paths::get);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean prepare(Volume caller, Path mountPoint) throws InvalidMountPointException {
|
||||
switch (caller.getMountPointRequirement()) {
|
||||
case PARENT_NO_MOUNT_POINT -> prepareParentNoMountPoint(mountPoint);
|
||||
case EMPTY_MOUNT_POINT -> prepareEmptyMountPoint(mountPoint);
|
||||
case NONE -> {
|
||||
//Requirement "NONE" doesn't make any sense here.
|
||||
//No need to prepare/verify a Mountpoint without requiring one...
|
||||
throw new InvalidMountPointException(new IllegalStateException("Illegal MountPointRequirement"));
|
||||
}
|
||||
default -> {
|
||||
//Currently the case for "PARENT_OPT_MOUNT_POINT"
|
||||
throw new InvalidMountPointException(new IllegalStateException("Not implemented"));
|
||||
}
|
||||
}
|
||||
LOG.debug("Successfully checked custom mount point: {}", mountPoint);
|
||||
return false;
|
||||
}
|
||||
|
||||
private void prepareParentNoMountPoint(Path mountPoint) throws InvalidMountPointException {
|
||||
//This the case on Windows when using FUSE
|
||||
//See https://github.com/billziss-gh/winfsp/issues/320
|
||||
Path parent = mountPoint.getParent();
|
||||
if (!Files.isDirectory(parent)) {
|
||||
throw new InvalidMountPointException(new NotDirectoryException(parent.toString()));
|
||||
}
|
||||
//We must use #notExists() here because notExists =/= !exists (see docs)
|
||||
if (!Files.notExists(mountPoint, LinkOption.NOFOLLOW_LINKS)) {
|
||||
//File exists OR can't be determined
|
||||
throw new InvalidMountPointException(new FileAlreadyExistsException(mountPoint.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
private void prepareEmptyMountPoint(Path mountPoint) throws InvalidMountPointException {
|
||||
//This is the case for Windows when using Dokany and for Linux and Mac
|
||||
if (!Files.isDirectory(mountPoint)) {
|
||||
throw new InvalidMountPointException(new NotDirectoryException(mountPoint.toString()));
|
||||
}
|
||||
try (DirectoryStream<Path> ds = Files.newDirectoryStream(mountPoint)) {
|
||||
if (ds.iterator().hasNext()) {
|
||||
throw new InvalidMountPointException(new DirectoryNotEmptyException(mountPoint.toString()));
|
||||
}
|
||||
} catch (IOException exception) {
|
||||
throw new InvalidMountPointException("IOException while checking folder content", exception);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package org.cryptomator.common.mountpoint;
|
||||
|
||||
public class InvalidMountPointException extends Exception {
|
||||
|
||||
public InvalidMountPointException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public InvalidMountPointException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public InvalidMountPointException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package org.cryptomator.common.mountpoint;
|
||||
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.common.settings.VaultSettings;
|
||||
import org.cryptomator.common.vaults.Volume;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
|
||||
class MacVolumeMountChooser implements MountPointChooser {
|
||||
|
||||
private static final Path VOLUME_PATH = Path.of("/Volumes");
|
||||
|
||||
private final VaultSettings vaultSettings;
|
||||
private final MountPointHelper helper;
|
||||
|
||||
@Inject
|
||||
public MacVolumeMountChooser(VaultSettings vaultSettings, MountPointHelper helper) {
|
||||
this.vaultSettings = vaultSettings;
|
||||
this.helper = helper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(Volume caller) {
|
||||
return SystemUtils.IS_OS_MAC;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Path> chooseMountPoint(Volume caller) {
|
||||
return Optional.of(helper.chooseTemporaryMountPoint(vaultSettings, VOLUME_PATH));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean prepare(Volume caller, Path mountPoint) {
|
||||
// https://github.com/osxfuse/osxfuse/issues/306#issuecomment-245114592:
|
||||
// In order to allow non-admin users to mount FUSE volumes in `/Volumes`,
|
||||
// starting with version 3.5.0, FUSE will create non-existent mount points automatically.
|
||||
// Therefore we don't need to prepare anything.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package org.cryptomator.common.mountpoint;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import org.cryptomator.common.vaults.Volume;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
import java.util.SortedSet;
|
||||
|
||||
/**
|
||||
* Base interface for the Mountpoint-Choosing-Operation that results in the choice and
|
||||
* preparation of a mountpoint or an exception otherwise.<br>
|
||||
* <p>All <i>MountPointChoosers (MPCs)</i> need to implement this class and must be added to
|
||||
* the pool of possible MPCs by the {@link MountPointChooserModule MountPointChooserModule.}
|
||||
* The MountPointChooserModule will sort them according to their {@link #getPriority() priority.}
|
||||
* The priority must be defined by the developer to reflect a useful execution order.<br>
|
||||
* A specific priority <b>must not</b> be assigned to more than one MPC at a time;
|
||||
* the result of having two MPCs with equal priority is undefined.
|
||||
*
|
||||
* <p>MPCs are executed by a {@link Volume} in ascending order of their priority
|
||||
* (smaller priorities are tried first) to find and prepare a suitable mountpoint for the volume.
|
||||
* The volume has access to a {@link SortedSet} of MPCs in this specific order,
|
||||
* that is provided by the Module. The Set contains all available Choosers, even if they
|
||||
* are not {@link #isApplicable(Volume) applicable} for the Vault/Volume. The Volume must
|
||||
* check whether a MPC is applicable by invoking {@code #isApplicable(Volume)} on it
|
||||
* <i>before</i> calling {@code #chooseMountPoint(Volume)}.
|
||||
*
|
||||
* <p>At execution of a MPC {@link #chooseMountPoint(Volume)} is called to choose a mountpoint
|
||||
* according to the MPC's <i>strategy.</i> The <i>strategy</i> can involve reading configs,
|
||||
* searching the filesystem, processing user-input or similar operations.
|
||||
* If {@code #chooseMountPoint(Volume)} returns a non-null path (everything but
|
||||
* {@linkplain Optional#empty()}) the MPC's {@link #prepare(Volume, Path)} method is called and the
|
||||
* MountPoint is verified and/or prepared. In this case <i>no other MPC's will be called for
|
||||
* this volume, even if {@code #prepare(Volume, Path)} fails.</i>
|
||||
*
|
||||
* <p>If {@code #chooseMountPoint(Volume)} yields no result, the next MPC is executed
|
||||
* <i>without</i> first calling the {@code #prepare(Volume, Path)} method of the current MPC.
|
||||
* This is repeated until<br>
|
||||
* <ul>
|
||||
* <li><b>either</b> a mountpoint is returned by {@code #chooseMountPoint(Volume)}
|
||||
* and {@code #prepare(Volume, Path)} succeeds or fails, ending the entire operation</li>
|
||||
* <li><b>or</b> no MPC remains and an {@link InvalidMountPointException} is thrown.</li>
|
||||
* </ul>
|
||||
* If the {@code #prepare(Volume, Path)} method of a MPC fails, the entire
|
||||
* Mountpoint-Choosing-Operation is aborted and the method should do all necessary cleanup
|
||||
* before throwing the exception.
|
||||
* If the preparation succeeds {@link #cleanup(Volume, Path)} can be used after unmount to do any
|
||||
* remaining cleanup.
|
||||
*/
|
||||
public interface MountPointChooser {
|
||||
|
||||
/**
|
||||
* Called by the {@link Volume} to determine whether this MountPointChooser is
|
||||
* applicable for mounting the Vault/Volume, especially with regard to the
|
||||
* current system configuration and particularities of the Volume type.
|
||||
*
|
||||
* <p>Developers should override this method to check for system configurations
|
||||
* that are unsuitable for this MPC.
|
||||
*
|
||||
* @param caller The Volume that is calling the method to determine applicability of the MPC
|
||||
* @return a boolean flag; true if applicable, else false.
|
||||
* @see #chooseMountPoint(Volume)
|
||||
*/
|
||||
boolean isApplicable(Volume caller);
|
||||
|
||||
/**
|
||||
* Called by a {@link Volume} to choose a mountpoint according to the
|
||||
* MountPointChoosers strategy.
|
||||
*
|
||||
* <p>This method must only be called for MPCs that were deemed
|
||||
* {@link #isApplicable(Volume) applicable} by the {@link Volume Volume.}
|
||||
* Developers should override this method to find or extract a mountpoint for
|
||||
* the volume <b>without</b> preparing it. Preparation should be done by
|
||||
* {@link #prepare(Volume, Path)} instead.
|
||||
* Exceptions in this method should be handled gracefully and result in returning
|
||||
* {@link Optional#empty()} instead of throwing an exception.
|
||||
*
|
||||
* @param caller The Volume that is calling the method to choose a mountpoint
|
||||
* @return the chosen path or {@link Optional#empty()} if an exception occurred
|
||||
* or no mountpoint could be found.
|
||||
* @see #isApplicable(Volume)
|
||||
* @see #prepare(Volume, Path)
|
||||
*/
|
||||
Optional<Path> chooseMountPoint(Volume caller);
|
||||
|
||||
/**
|
||||
* Called by a {@link Volume} to prepare and/or verify the chosen mountpoint.<br>
|
||||
* This method is only called if the {@link #chooseMountPoint(Volume)} method
|
||||
* of the same MountPointChooser returned a path.
|
||||
*
|
||||
* <p>Developers should override this method to prepare the mountpoint for
|
||||
* the volume and check for any obstacles that could hinder the mount operation.
|
||||
* The mountpoint is deemed "prepared" if it can be used to mount a volume
|
||||
* without any further filesystem actions or user interaction. If this is not possible,
|
||||
* this method should fail. In other words: This method should not return without
|
||||
* either failing or finalizing the preparation of the mountpoint.
|
||||
* Generally speaking exceptions should be wrapped as
|
||||
* {@link InvalidMountPointException} to allow efficient handling by the caller.
|
||||
*
|
||||
* <p>Often the preparation of a mountpoint involves creating files or others
|
||||
* actions that require cleaning up after the volume is unmounted.
|
||||
* In this case developers should override the {@link #cleanup(Volume, Path)}
|
||||
* method and return {@code true} to the volume to indicate that the
|
||||
* {@code #cleanup} method of this MPC should be called after unmount.
|
||||
*
|
||||
* <p><b>Please note:</b> If this method fails the entire
|
||||
* Mountpoint-Choosing-Operation is aborted without calling
|
||||
* {@link #cleanup(Volume, Path)} or any other MPCs. Therefore this method should
|
||||
* do all necessary cleanup before throwing the exception.
|
||||
*
|
||||
* @param caller The Volume that is calling the method to prepare a mountpoint
|
||||
* @param mountPoint the mountpoint chosen by {@link #chooseMountPoint(Volume)}
|
||||
* @return a boolean flag; true if cleanup is needed, false otherwise
|
||||
* @throws InvalidMountPointException if the preparation fails
|
||||
* @see #chooseMountPoint(Volume)
|
||||
* @see #cleanup(Volume, Path)
|
||||
*/
|
||||
default boolean prepare(Volume caller, Path mountPoint) throws InvalidMountPointException {
|
||||
return false; //NO-OP
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by a {@link Volume} to do any cleanup needed after unmount.
|
||||
*
|
||||
* <p>This method is only called if the {@link #prepare(Volume, Path)} method
|
||||
* of the same MountPointChooser returned {@code true}. Typically developers want to
|
||||
* delete any files created prior to mount or do similar tasks.<br>
|
||||
* Exceptions in this method should be handled gracefully.
|
||||
*
|
||||
* @param caller The Volume that is calling the method to cleanup the prepared mountpoint
|
||||
* @param mountPoint the mountpoint that was prepared by {@link #prepare(Volume, Path)}
|
||||
* @see #prepare(Volume, Path)
|
||||
*/
|
||||
default void cleanup(Volume caller, Path mountPoint) {
|
||||
//NO-OP
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package org.cryptomator.common.mountpoint;
|
||||
|
||||
import com.google.common.collect.Iterables;
|
||||
import dagger.Binds;
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import dagger.multibindings.IntKey;
|
||||
import dagger.multibindings.IntoMap;
|
||||
import org.cryptomator.common.vaults.PerVault;
|
||||
|
||||
import javax.inject.Named;
|
||||
import java.util.Map;
|
||||
import java.util.SortedMap;
|
||||
import java.util.TreeMap;
|
||||
|
||||
/**
|
||||
* Dagger-Module for {@link MountPointChooser MountPointChoosers.}<br>
|
||||
* See there for additional information.
|
||||
*
|
||||
* @see MountPointChooser
|
||||
*/
|
||||
@Module
|
||||
public abstract class MountPointChooserModule {
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@IntKey(0)
|
||||
@PerVault
|
||||
public abstract MountPointChooser bindCustomMountPointChooser(CustomMountPointChooser chooser);
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@IntKey(100)
|
||||
@PerVault
|
||||
public abstract MountPointChooser bindCustomDriveLetterChooser(CustomDriveLetterChooser chooser);
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@IntKey(101)
|
||||
@PerVault
|
||||
public abstract MountPointChooser bindMacVolumeMountChooser(MacVolumeMountChooser chooser);
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@IntKey(200)
|
||||
@PerVault
|
||||
public abstract MountPointChooser bindAvailableDriveLetterChooser(AvailableDriveLetterChooser chooser);
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@IntKey(999)
|
||||
@PerVault
|
||||
public abstract MountPointChooser bindTemporaryMountPointChooser(TemporaryMountPointChooser chooser);
|
||||
|
||||
@Provides
|
||||
@PerVault
|
||||
@Named("orderedMountPointChoosers")
|
||||
public static Iterable<MountPointChooser> provideOrderedMountPointChoosers(Map<Integer, MountPointChooser> choosers) {
|
||||
SortedMap<Integer, MountPointChooser> sortedChoosers = new TreeMap<>(choosers);
|
||||
return Iterables.unmodifiableIterable(sortedChoosers.values());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package org.cryptomator.common.mountpoint;
|
||||
|
||||
import org.cryptomator.common.Environment;
|
||||
import org.cryptomator.common.settings.VaultSettings;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.DirectoryNotEmptyException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.LinkOption;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.Optional;
|
||||
|
||||
@Singleton
|
||||
class MountPointHelper {
|
||||
|
||||
public static Logger LOG = LoggerFactory.getLogger(MountPointHelper.class);
|
||||
private static final int MAX_TMPMOUNTPOINT_CREATION_RETRIES = 10;
|
||||
|
||||
private final Optional<Path> tmpMountPointDir;
|
||||
private volatile boolean unmountDebrisCleared = false;
|
||||
|
||||
@Inject
|
||||
public MountPointHelper(Environment env) {
|
||||
this.tmpMountPointDir = env.getMountPointsDir();
|
||||
}
|
||||
|
||||
public Path chooseTemporaryMountPoint(VaultSettings vaultSettings, Path parentDir) {
|
||||
String basename = vaultSettings.mountName().get();
|
||||
//regular
|
||||
Path mountPoint = parentDir.resolve(basename);
|
||||
if (Files.notExists(mountPoint)) {
|
||||
return mountPoint;
|
||||
}
|
||||
//with id
|
||||
mountPoint = parentDir.resolve(basename + " (" + vaultSettings.getId() + ")");
|
||||
if (Files.notExists(mountPoint)) {
|
||||
return mountPoint;
|
||||
}
|
||||
//with id and count
|
||||
for (int i = 1; i < MAX_TMPMOUNTPOINT_CREATION_RETRIES; i++) {
|
||||
mountPoint = parentDir.resolve(basename + "_(" + vaultSettings.getId() + ")_" + i);
|
||||
if (Files.notExists(mountPoint)) {
|
||||
return mountPoint;
|
||||
}
|
||||
}
|
||||
LOG.error("Failed to find feasible mountpoint at {}{}{}_x. Giving up after {} attempts.", parentDir, File.separator, basename, MAX_TMPMOUNTPOINT_CREATION_RETRIES);
|
||||
return null;
|
||||
}
|
||||
|
||||
public synchronized void clearIrregularUnmountDebrisIfNeeded() {
|
||||
if (unmountDebrisCleared || tmpMountPointDir.isEmpty()) {
|
||||
return; // nothing to do
|
||||
}
|
||||
if (Files.exists(tmpMountPointDir.get(), LinkOption.NOFOLLOW_LINKS)) {
|
||||
clearIrregularUnmountDebris(tmpMountPointDir.get());
|
||||
}
|
||||
unmountDebrisCleared = true;
|
||||
}
|
||||
|
||||
private void clearIrregularUnmountDebris(Path dirContainingMountPoints) {
|
||||
IOException cleanupFailed = new IOException("Cleanup failed");
|
||||
|
||||
try {
|
||||
LOG.debug("Performing cleanup of mountpoint dir {}.", dirContainingMountPoints);
|
||||
for (Path p : Files.newDirectoryStream(dirContainingMountPoints)) {
|
||||
try {
|
||||
var attr = Files.readAttributes(p, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS);
|
||||
if (attr.isOther() && attr.isDirectory()) { // yes, this is possible with windows junction points -.-
|
||||
Files.delete(p);
|
||||
} else if (attr.isDirectory()) {
|
||||
deleteEmptyDir(p);
|
||||
} else if (attr.isSymbolicLink()) {
|
||||
deleteDeadLink(p);
|
||||
} else {
|
||||
LOG.debug("Found non-directory element in mountpoint dir: {}", p);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
cleanupFailed.addSuppressed(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (cleanupFailed.getSuppressed().length > 0) {
|
||||
throw cleanupFailed;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.warn("Unable to perform cleanup of mountpoint dir {}.", dirContainingMountPoints, e);
|
||||
} finally {
|
||||
unmountDebrisCleared = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteEmptyDir(Path dir) throws IOException {
|
||||
assert Files.isDirectory(dir, LinkOption.NOFOLLOW_LINKS);
|
||||
try {
|
||||
ensureIsEmpty(dir);
|
||||
Files.delete(dir); // attempt to delete dir non-recursively (will fail, if there are contents)
|
||||
} catch (DirectoryNotEmptyException e) {
|
||||
LOG.info("Found non-empty directory in mountpoint dir: {}", dir);
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteDeadLink(Path symlink) throws IOException {
|
||||
assert Files.isSymbolicLink(symlink);
|
||||
if (Files.notExists(symlink)) { // following link: target does not exist
|
||||
Files.delete(symlink);
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureIsEmpty(Path dir) throws IOException {
|
||||
if (Files.newDirectoryStream(dir).iterator().hasNext()) {
|
||||
throw new DirectoryNotEmptyException(dir.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package org.cryptomator.common.mountpoint;
|
||||
|
||||
import org.cryptomator.common.Environment;
|
||||
import org.cryptomator.common.settings.VaultSettings;
|
||||
import org.cryptomator.common.vaults.Volume;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
|
||||
class TemporaryMountPointChooser implements MountPointChooser {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(TemporaryMountPointChooser.class);
|
||||
|
||||
private final VaultSettings vaultSettings;
|
||||
private final Environment environment;
|
||||
private final MountPointHelper helper;
|
||||
|
||||
@Inject
|
||||
public TemporaryMountPointChooser(VaultSettings vaultSettings, Environment environment, MountPointHelper helper) {
|
||||
this.vaultSettings = vaultSettings;
|
||||
this.environment = environment;
|
||||
this.helper = helper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(Volume caller) {
|
||||
if (this.environment.getMountPointsDir().isEmpty()) {
|
||||
LOG.warn("\"cryptomator.mountPointsDir\" is not set to a valid path!");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Path> chooseMountPoint(Volume caller) {
|
||||
assert environment.getMountPointsDir().isPresent();
|
||||
//clean leftovers of not-regularly unmounted vaults
|
||||
//see https://github.com/cryptomator/cryptomator/issues/1013 and https://github.com/cryptomator/cryptomator/issues/1061
|
||||
helper.clearIrregularUnmountDebrisIfNeeded();
|
||||
return this.environment.getMountPointsDir().map(dir -> this.helper.chooseTemporaryMountPoint(this.vaultSettings, dir));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean prepare(Volume caller, Path mountPoint) throws InvalidMountPointException {
|
||||
try {
|
||||
switch (caller.getMountPointRequirement()) {
|
||||
case PARENT_NO_MOUNT_POINT -> {
|
||||
Files.createDirectories(mountPoint.getParent());
|
||||
LOG.debug("Successfully created folder for mount point: {}", mountPoint);
|
||||
return false;
|
||||
}
|
||||
case EMPTY_MOUNT_POINT -> {
|
||||
Files.createDirectories(mountPoint);
|
||||
LOG.debug("Successfully created mount point: {}", mountPoint);
|
||||
return true;
|
||||
}
|
||||
case NONE -> {
|
||||
//Requirement "NONE" doesn't make any sense here.
|
||||
//No need to prepare/verify a Mountpoint without requiring one...
|
||||
throw new InvalidMountPointException(new IllegalStateException("Illegal MountPointRequirement"));
|
||||
}
|
||||
default -> {
|
||||
//Currently the case for "PARENT_OPT_MOUNT_POINT"
|
||||
throw new InvalidMountPointException(new IllegalStateException("Not implemented"));
|
||||
}
|
||||
}
|
||||
} catch (IOException exception) {
|
||||
throw new InvalidMountPointException("IOException while preparing mountpoint", exception);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup(Volume caller, Path mountPoint) {
|
||||
try {
|
||||
Files.delete(mountPoint);
|
||||
LOG.debug("Successfully deleted mount point: {}", mountPoint);
|
||||
} catch (IOException e) {
|
||||
LOG.warn("Could not delete mount point: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package org.cryptomator.common.settings;
|
||||
|
||||
public enum KeychainBackend {
|
||||
GNOME("org.cryptomator.linux.keychain.SecretServiceKeychainAccess"),
|
||||
KDE("org.cryptomator.linux.keychain.KDEWalletKeychainAccess"),
|
||||
MAC_SYSTEM_KEYCHAIN("org.cryptomator.macos.keychain.MacSystemKeychainAccess"),
|
||||
WIN_SYSTEM_KEYCHAIN("org.cryptomator.windows.keychain.WindowsProtectedKeychainAccess");
|
||||
|
||||
private final String providerClass;
|
||||
|
||||
KeychainBackend(String providerClass) {
|
||||
this.providerClass = providerClass;
|
||||
}
|
||||
|
||||
public String getProviderClass() {
|
||||
return providerClass;
|
||||
}
|
||||
|
||||
}
|
||||
160
src/main/java/org/cryptomator/common/settings/Settings.java
Normal file
160
src/main/java/org/cryptomator/common/settings/Settings.java
Normal file
@@ -0,0 +1,160 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014, 2017 Sebastian Stenzel
|
||||
* All rights reserved.
|
||||
* This program and the accompanying materials are made available under the terms of the accompanying LICENSE file.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
******************************************************************************/
|
||||
package org.cryptomator.common.settings;
|
||||
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.common.Environment;
|
||||
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.NodeOrientation;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class Settings {
|
||||
|
||||
public static final int MIN_PORT = 1024;
|
||||
public static final int MAX_PORT = 65535;
|
||||
public static final boolean DEFAULT_ASKED_FOR_UPDATE_CHECK = false;
|
||||
public static final boolean DEFAULT_CHECK_FOR_UDPATES = false;
|
||||
public static final boolean DEFAULT_START_HIDDEN = false;
|
||||
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 UiTheme DEFAULT_THEME = UiTheme.LIGHT;
|
||||
public static final KeychainBackend DEFAULT_KEYCHAIN_BACKEND = SystemUtils.IS_OS_WINDOWS ? KeychainBackend.WIN_SYSTEM_KEYCHAIN : SystemUtils.IS_OS_MAC ? KeychainBackend.MAC_SYSTEM_KEYCHAIN : KeychainBackend.GNOME;
|
||||
public static final NodeOrientation DEFAULT_USER_INTERFACE_ORIENTATION = NodeOrientation.LEFT_TO_RIGHT;
|
||||
public static final String DEFAULT_LICENSE_KEY = "";
|
||||
public static final boolean DEFAULT_SHOW_MINIMIZE_BUTTON = false;
|
||||
|
||||
private final ObservableList<VaultSettings> directories = FXCollections.observableArrayList(VaultSettings::observables);
|
||||
private final BooleanProperty askedForUpdateCheck = new SimpleBooleanProperty(DEFAULT_ASKED_FOR_UPDATE_CHECK);
|
||||
private final BooleanProperty checkForUpdates = new SimpleBooleanProperty(DEFAULT_CHECK_FOR_UDPATES);
|
||||
private final BooleanProperty startHidden = new SimpleBooleanProperty(DEFAULT_START_HIDDEN);
|
||||
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);
|
||||
private final BooleanProperty debugMode = new SimpleBooleanProperty(DEFAULT_DEBUG_MODE);
|
||||
private final ObjectProperty<VolumeImpl> preferredVolumeImpl = new SimpleObjectProperty<>(DEFAULT_PREFERRED_VOLUME_IMPL);
|
||||
private final ObjectProperty<UiTheme> theme = new SimpleObjectProperty<>(DEFAULT_THEME);
|
||||
private final ObjectProperty<KeychainBackend> keychainBackend = new SimpleObjectProperty<>(DEFAULT_KEYCHAIN_BACKEND);
|
||||
private final ObjectProperty<NodeOrientation> userInterfaceOrientation = new SimpleObjectProperty<>(DEFAULT_USER_INTERFACE_ORIENTATION);
|
||||
private final StringProperty licenseKey = new SimpleStringProperty(DEFAULT_LICENSE_KEY);
|
||||
private final BooleanProperty showMinimizeButton = new SimpleBooleanProperty(DEFAULT_SHOW_MINIMIZE_BUTTON);
|
||||
private final BooleanProperty showTrayIcon;
|
||||
|
||||
private Consumer<Settings> saveCmd;
|
||||
|
||||
/**
|
||||
* Package-private constructor; use {@link SettingsProvider}.
|
||||
*/
|
||||
Settings(Environment env) {
|
||||
this.showTrayIcon = new SimpleBooleanProperty(env.showTrayIcon());
|
||||
|
||||
directories.addListener(this::somethingChanged);
|
||||
askedForUpdateCheck.addListener(this::somethingChanged);
|
||||
checkForUpdates.addListener(this::somethingChanged);
|
||||
startHidden.addListener(this::somethingChanged);
|
||||
port.addListener(this::somethingChanged);
|
||||
numTrayNotifications.addListener(this::somethingChanged);
|
||||
preferredGvfsScheme.addListener(this::somethingChanged);
|
||||
debugMode.addListener(this::somethingChanged);
|
||||
preferredVolumeImpl.addListener(this::somethingChanged);
|
||||
theme.addListener(this::somethingChanged);
|
||||
keychainBackend.addListener(this::somethingChanged);
|
||||
userInterfaceOrientation.addListener(this::somethingChanged);
|
||||
licenseKey.addListener(this::somethingChanged);
|
||||
showMinimizeButton.addListener(this::somethingChanged);
|
||||
showTrayIcon.addListener(this::somethingChanged);
|
||||
}
|
||||
|
||||
void setSaveCmd(Consumer<Settings> saveCmd) {
|
||||
this.saveCmd = saveCmd;
|
||||
}
|
||||
|
||||
private void somethingChanged(@SuppressWarnings("unused") Observable observable) {
|
||||
this.save();
|
||||
}
|
||||
|
||||
void save() {
|
||||
if (saveCmd != null) {
|
||||
saveCmd.accept(this);
|
||||
}
|
||||
}
|
||||
|
||||
/* Getter/Setter */
|
||||
|
||||
public ObservableList<VaultSettings> getDirectories() {
|
||||
return directories;
|
||||
}
|
||||
|
||||
public BooleanProperty askedForUpdateCheck() {
|
||||
return askedForUpdateCheck;
|
||||
}
|
||||
|
||||
public BooleanProperty checkForUpdates() {
|
||||
return checkForUpdates;
|
||||
}
|
||||
|
||||
public BooleanProperty startHidden() {
|
||||
return startHidden;
|
||||
}
|
||||
|
||||
public IntegerProperty port() {
|
||||
return port;
|
||||
}
|
||||
|
||||
public IntegerProperty numTrayNotifications() {
|
||||
return numTrayNotifications;
|
||||
}
|
||||
|
||||
public ObjectProperty<WebDavUrlScheme> preferredGvfsScheme() {
|
||||
return preferredGvfsScheme;
|
||||
}
|
||||
|
||||
public BooleanProperty debugMode() {
|
||||
return debugMode;
|
||||
}
|
||||
|
||||
public ObjectProperty<VolumeImpl> preferredVolumeImpl() {
|
||||
return preferredVolumeImpl;
|
||||
}
|
||||
|
||||
public ObjectProperty<UiTheme> theme() {
|
||||
return theme;
|
||||
}
|
||||
|
||||
public ObjectProperty<KeychainBackend> keychainBackend() { return keychainBackend; }
|
||||
|
||||
public ObjectProperty<NodeOrientation> userInterfaceOrientation() {
|
||||
return userInterfaceOrientation;
|
||||
}
|
||||
|
||||
public StringProperty licenseKey() {
|
||||
return licenseKey;
|
||||
}
|
||||
|
||||
public BooleanProperty showMinimizeButton() {
|
||||
return showMinimizeButton;
|
||||
}
|
||||
|
||||
public BooleanProperty showTrayIcon() {
|
||||
return showTrayIcon;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the accompanying LICENSE file.
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.common.settings;
|
||||
|
||||
import com.google.gson.TypeAdapter;
|
||||
import com.google.gson.stream.JsonReader;
|
||||
import com.google.gson.stream.JsonToken;
|
||||
import com.google.gson.stream.JsonWriter;
|
||||
import org.cryptomator.common.Environment;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import javafx.geometry.NodeOrientation;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Singleton
|
||||
public class SettingsJsonAdapter extends TypeAdapter<Settings> {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SettingsJsonAdapter.class);
|
||||
|
||||
private final VaultSettingsJsonAdapter vaultSettingsJsonAdapter = new VaultSettingsJsonAdapter();
|
||||
private final Environment env;
|
||||
|
||||
@Inject
|
||||
public SettingsJsonAdapter(Environment env) {
|
||||
this.env = env;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(JsonWriter out, Settings value) throws IOException {
|
||||
out.beginObject();
|
||||
out.name("directories");
|
||||
writeVaultSettingsArray(out, value.getDirectories());
|
||||
out.name("askedForUpdateCheck").value(value.askedForUpdateCheck().get());
|
||||
out.name("checkForUpdatesEnabled").value(value.checkForUpdates().get());
|
||||
out.name("startHidden").value(value.startHidden().get());
|
||||
out.name("port").value(value.port().get());
|
||||
out.name("numTrayNotifications").value(value.numTrayNotifications().get());
|
||||
out.name("preferredGvfsScheme").value(value.preferredGvfsScheme().get().name());
|
||||
out.name("debugMode").value(value.debugMode().get());
|
||||
out.name("preferredVolumeImpl").value(value.preferredVolumeImpl().get().name());
|
||||
out.name("theme").value(value.theme().get().name());
|
||||
out.name("uiOrientation").value(value.userInterfaceOrientation().get().name());
|
||||
out.name("keychainBackend").value(value.keychainBackend().get().name());
|
||||
out.name("licenseKey").value(value.licenseKey().get());
|
||||
out.name("showMinimizeButton").value(value.showMinimizeButton().get());
|
||||
out.name("showTrayIcon").value(value.showTrayIcon().get());
|
||||
out.endObject();
|
||||
}
|
||||
|
||||
private void writeVaultSettingsArray(JsonWriter out, Iterable<VaultSettings> vaultSettings) throws IOException {
|
||||
out.beginArray();
|
||||
for (VaultSettings value : vaultSettings) {
|
||||
vaultSettingsJsonAdapter.write(out, value);
|
||||
}
|
||||
out.endArray();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Settings read(JsonReader in) throws IOException {
|
||||
Settings settings = new Settings(env);
|
||||
|
||||
in.beginObject();
|
||||
while (in.hasNext()) {
|
||||
String name = in.nextName();
|
||||
switch (name) {
|
||||
case "directories" -> settings.getDirectories().addAll(readVaultSettingsArray(in));
|
||||
case "askedForUpdateCheck" -> settings.askedForUpdateCheck().set(in.nextBoolean());
|
||||
case "checkForUpdatesEnabled" -> settings.checkForUpdates().set(in.nextBoolean());
|
||||
case "startHidden" -> settings.startHidden().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()));
|
||||
case "debugMode" -> settings.debugMode().set(in.nextBoolean());
|
||||
case "preferredVolumeImpl" -> settings.preferredVolumeImpl().set(parsePreferredVolumeImplName(in.nextString()));
|
||||
case "theme" -> settings.theme().set(parseUiTheme(in.nextString()));
|
||||
case "uiOrientation" -> settings.userInterfaceOrientation().set(parseUiOrientation(in.nextString()));
|
||||
case "keychainBackend" -> settings.keychainBackend().set(parseKeychainBackend(in.nextString()));
|
||||
case "licenseKey" -> settings.licenseKey().set(in.nextString());
|
||||
case "showMinimizeButton" -> settings.showMinimizeButton().set(in.nextBoolean());
|
||||
case "showTrayIcon" -> settings.showTrayIcon().set(in.nextBoolean());
|
||||
default -> {
|
||||
LOG.warn("Unsupported vault setting found in JSON: " + name);
|
||||
in.skipValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
in.endObject();
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
private VolumeImpl parsePreferredVolumeImplName(String nioAdapterName) {
|
||||
try {
|
||||
return VolumeImpl.valueOf(nioAdapterName.toUpperCase());
|
||||
} catch (IllegalArgumentException e) {
|
||||
LOG.warn("Invalid volume type {}. Defaulting to {}.", nioAdapterName, Settings.DEFAULT_PREFERRED_VOLUME_IMPL);
|
||||
return Settings.DEFAULT_PREFERRED_VOLUME_IMPL;
|
||||
}
|
||||
}
|
||||
|
||||
private WebDavUrlScheme parseWebDavUrlSchemePrefix(String webDavUrlSchemeName) {
|
||||
try {
|
||||
return WebDavUrlScheme.valueOf(webDavUrlSchemeName.toUpperCase());
|
||||
} catch (IllegalArgumentException e) {
|
||||
LOG.warn("Invalid WebDAV url scheme {}. Defaulting to {}.", webDavUrlSchemeName, Settings.DEFAULT_GVFS_SCHEME);
|
||||
return Settings.DEFAULT_GVFS_SCHEME;
|
||||
}
|
||||
}
|
||||
|
||||
private UiTheme parseUiTheme(String uiThemeName) {
|
||||
try {
|
||||
return UiTheme.valueOf(uiThemeName.toUpperCase());
|
||||
} catch (IllegalArgumentException e) {
|
||||
LOG.warn("Invalid ui theme {}. Defaulting to {}.", uiThemeName, Settings.DEFAULT_THEME);
|
||||
return Settings.DEFAULT_THEME;
|
||||
}
|
||||
}
|
||||
|
||||
private KeychainBackend parseKeychainBackend(String backendName) {
|
||||
try {
|
||||
return KeychainBackend.valueOf(backendName.toUpperCase());
|
||||
} catch (IllegalArgumentException e) {
|
||||
LOG.warn("Invalid keychain backend {}. Defaulting to {}.", backendName, Settings.DEFAULT_KEYCHAIN_BACKEND);
|
||||
return Settings.DEFAULT_KEYCHAIN_BACKEND;
|
||||
}
|
||||
}
|
||||
|
||||
private NodeOrientation parseUiOrientation(String uiOrientationName) {
|
||||
try {
|
||||
return NodeOrientation.valueOf(uiOrientationName.toUpperCase());
|
||||
} catch (IllegalArgumentException e) {
|
||||
LOG.warn("Invalid ui orientation {}. Defaulting to {}.", uiOrientationName, Settings.DEFAULT_USER_INTERFACE_ORIENTATION);
|
||||
return Settings.DEFAULT_USER_INTERFACE_ORIENTATION;
|
||||
}
|
||||
}
|
||||
|
||||
private List<VaultSettings> readVaultSettingsArray(JsonReader in) throws IOException {
|
||||
List<VaultSettings> result = new ArrayList<>();
|
||||
in.beginArray();
|
||||
while (!JsonToken.END_ARRAY.equals(in.peek())) {
|
||||
result.add(vaultSettingsJsonAdapter.read(in));
|
||||
}
|
||||
in.endArray();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2016, 2017 Sebastian Stenzel and others.
|
||||
* All rights reserved.
|
||||
* This program and the accompanying materials are made available under the terms of the accompanying LICENSE file.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.common.settings;
|
||||
|
||||
import com.google.common.base.Suppliers;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.JsonParser;
|
||||
import org.cryptomator.common.Environment;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.Reader;
|
||||
import java.io.Writer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.NoSuchFileException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@Singleton
|
||||
public class SettingsProvider implements Supplier<Settings> {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SettingsProvider.class);
|
||||
private static final long SAVE_DELAY_MS = 1000;
|
||||
|
||||
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() //
|
||||
.setPrettyPrinting().setLenient().disableHtmlEscaping() //
|
||||
.registerTypeAdapter(Settings.class, settingsJsonAdapter) //
|
||||
.create();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Settings get() {
|
||||
return settings.get();
|
||||
}
|
||||
|
||||
private Settings load() {
|
||||
Settings settings = env.getSettingsPath().flatMap(this::tryLoad).findFirst().orElse(new Settings(env));
|
||||
settings.setSaveCmd(this::scheduleSave);
|
||||
return settings;
|
||||
}
|
||||
|
||||
private Stream<Settings> tryLoad(Path path) {
|
||||
LOG.debug("Attempting to load settings from {}", path);
|
||||
try (InputStream in = Files.newInputStream(path, StandardOpenOption.READ); //
|
||||
Reader reader = new InputStreamReader(in, StandardCharsets.UTF_8)) {
|
||||
JsonElement json = JsonParser.parseReader(reader);
|
||||
if (json.isJsonObject()) {
|
||||
Settings settings = gson.fromJson(json, Settings.class);
|
||||
LOG.info("Settings loaded from {}", path);
|
||||
return Stream.of(settings);
|
||||
} else {
|
||||
LOG.warn("Invalid json file {}", path);
|
||||
return Stream.empty();
|
||||
}
|
||||
} catch (NoSuchFileException e) {
|
||||
return Stream.empty();
|
||||
} catch (IOException | JsonParseException e) {
|
||||
LOG.warn("Exception while loading settings from " + path, e);
|
||||
return Stream.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private void scheduleSave(Settings settings) {
|
||||
if (settings == null) {
|
||||
return;
|
||||
}
|
||||
final Optional<Path> settingsPath = env.getSettingsPath().findFirst(); // alway save to preferred (first) path
|
||||
settingsPath.ifPresent(path -> {
|
||||
Runnable saveCommand = () -> this.save(settings, path);
|
||||
ScheduledFuture<?> scheduledTask = scheduler.schedule(saveCommand, SAVE_DELAY_MS, TimeUnit.MILLISECONDS);
|
||||
ScheduledFuture<?> previouslyScheduledTask = scheduledSaveCmd.getAndSet(scheduledTask);
|
||||
if (previouslyScheduledTask != null) {
|
||||
previouslyScheduledTask.cancel(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void save(Settings settings, Path settingsPath) {
|
||||
assert settings != null : "method should only be invoked by #scheduleSave, which checks for null";
|
||||
LOG.debug("Attempting to save settings to {}", settingsPath);
|
||||
try {
|
||||
Files.createDirectories(settingsPath.getParent());
|
||||
Path tmpPath = settingsPath.resolveSibling(settingsPath.getFileName().toString() + ".tmp");
|
||||
try (OutputStream out = Files.newOutputStream(tmpPath, StandardOpenOption.CREATE_NEW); //
|
||||
Writer writer = new OutputStreamWriter(out, StandardCharsets.UTF_8)) {
|
||||
gson.toJson(settings, writer);
|
||||
}
|
||||
Files.move(tmpPath, settingsPath, StandardCopyOption.REPLACE_EXISTING);
|
||||
LOG.info("Settings saved to {}", settingsPath);
|
||||
} catch (IOException | JsonParseException e) {
|
||||
LOG.error("Failed to save settings.", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
28
src/main/java/org/cryptomator/common/settings/UiTheme.java
Normal file
28
src/main/java/org/cryptomator/common/settings/UiTheme.java
Normal file
@@ -0,0 +1,28 @@
|
||||
package org.cryptomator.common.settings;
|
||||
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
|
||||
public enum UiTheme {
|
||||
LIGHT("preferences.general.theme.light"), //
|
||||
DARK("preferences.general.theme.dark"), //
|
||||
AUTOMATIC("preferences.general.theme.automatic");
|
||||
|
||||
public static UiTheme[] applicableValues() {
|
||||
if (SystemUtils.IS_OS_MAC || SystemUtils.IS_OS_WINDOWS) {
|
||||
return values();
|
||||
} else {
|
||||
return new UiTheme[]{LIGHT, DARK};
|
||||
}
|
||||
}
|
||||
|
||||
private final String displayName;
|
||||
|
||||
UiTheme(String displayName) {
|
||||
this.displayName = displayName;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
}
|
||||
192
src/main/java/org/cryptomator/common/settings/VaultSettings.java
Normal file
192
src/main/java/org/cryptomator/common/settings/VaultSettings.java
Normal file
@@ -0,0 +1,192 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the accompanying LICENSE file.
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.common.settings;
|
||||
|
||||
import com.google.common.base.CharMatcher;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.io.BaseEncoding;
|
||||
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.StringBinding;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Random;
|
||||
|
||||
/**
|
||||
* The settings specific to a single vault.
|
||||
*/
|
||||
public class VaultSettings {
|
||||
|
||||
public static final boolean DEFAULT_UNLOCK_AFTER_STARTUP = false;
|
||||
public static final boolean DEFAULT_REAVEAL_AFTER_MOUNT = true;
|
||||
public static final boolean DEFAULT_USES_INDIVIDUAL_MOUNTPATH = false;
|
||||
public static final boolean DEFAULT_USES_READONLY_MODE = false;
|
||||
public static final String DEFAULT_MOUNT_FLAGS = "";
|
||||
public static final int DEFAULT_MAX_CLEARTEXT_FILENAME_LENGTH = -1;
|
||||
public static final WhenUnlocked DEFAULT_ACTION_AFTER_UNLOCK = WhenUnlocked.ASK;
|
||||
public static final boolean DEFAULT_AUTOLOCK_WHEN_IDLE = false;
|
||||
public static final int DEFAULT_AUTOLOCK_IDLE_SECONDS = 30 * 60;
|
||||
|
||||
private static final Random RNG = new Random();
|
||||
|
||||
private final String id;
|
||||
private final ObjectProperty<Path> path = new SimpleObjectProperty();
|
||||
private final StringProperty displayName = new SimpleStringProperty();
|
||||
private final StringProperty winDriveLetter = new SimpleStringProperty();
|
||||
private final BooleanProperty unlockAfterStartup = new SimpleBooleanProperty(DEFAULT_UNLOCK_AFTER_STARTUP);
|
||||
private final BooleanProperty revealAfterMount = new SimpleBooleanProperty(DEFAULT_REAVEAL_AFTER_MOUNT);
|
||||
private final BooleanProperty useCustomMountPath = new SimpleBooleanProperty(DEFAULT_USES_INDIVIDUAL_MOUNTPATH);
|
||||
private final StringProperty customMountPath = new SimpleStringProperty();
|
||||
private final BooleanProperty usesReadOnlyMode = new SimpleBooleanProperty(DEFAULT_USES_READONLY_MODE);
|
||||
private final StringProperty mountFlags = new SimpleStringProperty(DEFAULT_MOUNT_FLAGS);
|
||||
private final IntegerProperty maxCleartextFilenameLength = new SimpleIntegerProperty(DEFAULT_MAX_CLEARTEXT_FILENAME_LENGTH);
|
||||
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;
|
||||
|
||||
public VaultSettings(String id) {
|
||||
this.id = Objects.requireNonNull(id);
|
||||
this.mountName = Bindings.createStringBinding(this::normalizeDisplayName, displayName);
|
||||
}
|
||||
|
||||
Observable[] observables() {
|
||||
return new Observable[]{path, displayName, winDriveLetter, unlockAfterStartup, revealAfterMount, useCustomMountPath, customMountPath, usesReadOnlyMode, mountFlags, maxCleartextFilenameLength, actionAfterUnlock, autoLockWhenIdle, autoLockIdleSeconds};
|
||||
}
|
||||
|
||||
public static VaultSettings withRandomId() {
|
||||
return new VaultSettings(generateId());
|
||||
}
|
||||
|
||||
private static String generateId() {
|
||||
byte[] randomBytes = new byte[9];
|
||||
RNG.nextBytes(randomBytes);
|
||||
return BaseEncoding.base64Url().encode(randomBytes);
|
||||
}
|
||||
|
||||
//visible for testing
|
||||
String normalizeDisplayName() {
|
||||
var original = displayName.getValueSafe();
|
||||
if (original.isBlank() || ".".equals(original) || "..".equals(original)) {
|
||||
return "_";
|
||||
}
|
||||
|
||||
// replace whitespaces (tabs, linebreaks, ...) by simple space (0x20)
|
||||
var withoutFancyWhitespaces = CharMatcher.whitespace().collapseFrom(original, ' ');
|
||||
|
||||
// replace control chars as well as chars that aren't allowed in file names on standard file systems by underscore
|
||||
return CharMatcher.anyOf("<>:\"/\\|?*").or(CharMatcher.javaIsoControl()).collapseFrom(withoutFancyWhitespaces, '_');
|
||||
}
|
||||
|
||||
/* Getter/Setter */
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public ObjectProperty<Path> path() {
|
||||
return path;
|
||||
}
|
||||
|
||||
public StringProperty displayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
public StringBinding mountName() {
|
||||
return mountName;
|
||||
}
|
||||
|
||||
public StringProperty winDriveLetter() {
|
||||
return winDriveLetter;
|
||||
}
|
||||
|
||||
public Optional<String> getWinDriveLetter() {
|
||||
String current = this.winDriveLetter.get();
|
||||
if (!Strings.isNullOrEmpty(current)) {
|
||||
return Optional.of(current);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
public BooleanProperty unlockAfterStartup() {
|
||||
return unlockAfterStartup;
|
||||
}
|
||||
|
||||
public BooleanProperty revealAfterMount() {
|
||||
return revealAfterMount;
|
||||
}
|
||||
|
||||
public BooleanProperty useCustomMountPath() {
|
||||
return useCustomMountPath;
|
||||
}
|
||||
|
||||
public StringProperty customMountPath() {
|
||||
return customMountPath;
|
||||
}
|
||||
|
||||
public Optional<String> getCustomMountPath() {
|
||||
if (useCustomMountPath.get()) {
|
||||
return Optional.ofNullable(Strings.emptyToNull(customMountPath.get()));
|
||||
} else {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
public BooleanProperty usesReadOnlyMode() {
|
||||
return usesReadOnlyMode;
|
||||
}
|
||||
|
||||
public StringProperty mountFlags() {
|
||||
return mountFlags;
|
||||
}
|
||||
|
||||
public IntegerProperty maxCleartextFilenameLength() {
|
||||
return maxCleartextFilenameLength;
|
||||
}
|
||||
|
||||
public ObjectProperty<WhenUnlocked> actionAfterUnlock() {
|
||||
return actionAfterUnlock;
|
||||
}
|
||||
|
||||
public WhenUnlocked getActionAfterUnlock() {
|
||||
return actionAfterUnlock.get();
|
||||
}
|
||||
|
||||
public BooleanProperty autoLockWhenIdle() {
|
||||
return autoLockWhenIdle;
|
||||
}
|
||||
|
||||
public IntegerProperty autoLockIdleSeconds() {
|
||||
return autoLockIdleSeconds;
|
||||
}
|
||||
|
||||
/* Hashcode/Equals */
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj instanceof VaultSettings other && obj.getClass().equals(this.getClass())) {
|
||||
return Objects.equals(this.id, other.id);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the accompanying LICENSE file.
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.common.settings;
|
||||
|
||||
import com.google.gson.stream.JsonReader;
|
||||
import com.google.gson.stream.JsonWriter;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
class VaultSettingsJsonAdapter {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(VaultSettingsJsonAdapter.class);
|
||||
|
||||
public void write(JsonWriter out, VaultSettings value) throws IOException {
|
||||
out.beginObject();
|
||||
out.name("id").value(value.getId());
|
||||
out.name("path").value(value.path().get().toString());
|
||||
out.name("displayName").value(value.displayName().get());
|
||||
out.name("winDriveLetter").value(value.winDriveLetter().get());
|
||||
out.name("unlockAfterStartup").value(value.unlockAfterStartup().get());
|
||||
out.name("revealAfterMount").value(value.revealAfterMount().get());
|
||||
out.name("useCustomMountPath").value(value.useCustomMountPath().get());
|
||||
out.name("customMountPath").value(value.customMountPath().get());
|
||||
out.name("usesReadOnlyMode").value(value.usesReadOnlyMode().get());
|
||||
out.name("mountFlags").value(value.mountFlags().get());
|
||||
out.name("maxCleartextFilenameLength").value(value.maxCleartextFilenameLength().get());
|
||||
out.name("actionAfterUnlock").value(value.actionAfterUnlock().get().name());
|
||||
out.name("autoLockWhenIdle").value(value.autoLockWhenIdle().get());
|
||||
out.name("autoLockIdleSeconds").value(value.autoLockIdleSeconds().get());
|
||||
out.endObject();
|
||||
}
|
||||
|
||||
public VaultSettings read(JsonReader in) throws IOException {
|
||||
String id = null;
|
||||
String path = null;
|
||||
String mountName = null; //see https://github.com/cryptomator/cryptomator/pull/1318
|
||||
String displayName = null;
|
||||
String customMountPath = null;
|
||||
String winDriveLetter = null;
|
||||
boolean unlockAfterStartup = VaultSettings.DEFAULT_UNLOCK_AFTER_STARTUP;
|
||||
boolean revealAfterMount = VaultSettings.DEFAULT_REAVEAL_AFTER_MOUNT;
|
||||
boolean useCustomMountPath = VaultSettings.DEFAULT_USES_INDIVIDUAL_MOUNTPATH;
|
||||
boolean usesReadOnlyMode = VaultSettings.DEFAULT_USES_READONLY_MODE;
|
||||
String mountFlags = VaultSettings.DEFAULT_MOUNT_FLAGS;
|
||||
int maxCleartextFilenameLength = VaultSettings.DEFAULT_MAX_CLEARTEXT_FILENAME_LENGTH;
|
||||
WhenUnlocked actionAfterUnlock = VaultSettings.DEFAULT_ACTION_AFTER_UNLOCK;
|
||||
boolean autoLockWhenIdle = VaultSettings.DEFAULT_AUTOLOCK_WHEN_IDLE;
|
||||
int autoLockIdleSeconds = VaultSettings.DEFAULT_AUTOLOCK_IDLE_SECONDS;
|
||||
|
||||
in.beginObject();
|
||||
while (in.hasNext()) {
|
||||
String name = in.nextName();
|
||||
switch (name) {
|
||||
case "id" -> id = in.nextString();
|
||||
case "path" -> path = in.nextString();
|
||||
case "mountName" -> mountName = in.nextString(); //see https://github.com/cryptomator/cryptomator/pull/1318
|
||||
case "displayName" -> displayName = in.nextString();
|
||||
case "winDriveLetter" -> winDriveLetter = in.nextString();
|
||||
case "unlockAfterStartup" -> unlockAfterStartup = in.nextBoolean();
|
||||
case "revealAfterMount" -> revealAfterMount = in.nextBoolean();
|
||||
case "usesIndividualMountPath", "useCustomMountPath" -> useCustomMountPath = in.nextBoolean();
|
||||
case "individualMountPath", "customMountPath" -> customMountPath = in.nextString();
|
||||
case "usesReadOnlyMode" -> usesReadOnlyMode = in.nextBoolean();
|
||||
case "mountFlags" -> mountFlags = in.nextString();
|
||||
case "maxCleartextFilenameLength" -> maxCleartextFilenameLength = in.nextInt();
|
||||
case "actionAfterUnlock" -> actionAfterUnlock = parseActionAfterUnlock(in.nextString());
|
||||
case "autoLockWhenIdle" -> autoLockWhenIdle = in.nextBoolean();
|
||||
case "autoLockIdleSeconds" -> autoLockIdleSeconds = in.nextInt();
|
||||
default -> {
|
||||
LOG.warn("Unsupported vault setting found in JSON: " + name);
|
||||
in.skipValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
in.endObject();
|
||||
|
||||
VaultSettings vaultSettings = (id == null) ? VaultSettings.withRandomId() : new VaultSettings(id);
|
||||
if (displayName != null) { //see https://github.com/cryptomator/cryptomator/pull/1318
|
||||
vaultSettings.displayName().set(displayName);
|
||||
} else {
|
||||
vaultSettings.displayName().set(mountName);
|
||||
}
|
||||
vaultSettings.path().set(Paths.get(path));
|
||||
vaultSettings.winDriveLetter().set(winDriveLetter);
|
||||
vaultSettings.unlockAfterStartup().set(unlockAfterStartup);
|
||||
vaultSettings.revealAfterMount().set(revealAfterMount);
|
||||
vaultSettings.useCustomMountPath().set(useCustomMountPath);
|
||||
vaultSettings.customMountPath().set(customMountPath);
|
||||
vaultSettings.usesReadOnlyMode().set(usesReadOnlyMode);
|
||||
vaultSettings.mountFlags().set(mountFlags);
|
||||
vaultSettings.maxCleartextFilenameLength().set(maxCleartextFilenameLength);
|
||||
vaultSettings.actionAfterUnlock().set(actionAfterUnlock);
|
||||
vaultSettings.autoLockWhenIdle().set(autoLockWhenIdle);
|
||||
vaultSettings.autoLockIdleSeconds().set(autoLockIdleSeconds);
|
||||
return vaultSettings;
|
||||
}
|
||||
|
||||
private WhenUnlocked parseActionAfterUnlock(String actionAfterUnlockName) {
|
||||
try {
|
||||
return WhenUnlocked.valueOf(actionAfterUnlockName.toUpperCase());
|
||||
} catch (IllegalArgumentException e) {
|
||||
LOG.warn("Invalid action after unlock {}. Defaulting to {}.", actionAfterUnlockName, VaultSettings.DEFAULT_ACTION_AFTER_UNLOCK);
|
||||
return VaultSettings.DEFAULT_ACTION_AFTER_UNLOCK;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package org.cryptomator.common.settings;
|
||||
|
||||
public enum VolumeImpl {
|
||||
WEBDAV("WebDAV"),
|
||||
FUSE("FUSE"),
|
||||
DOKANY("Dokany");
|
||||
|
||||
private String displayName;
|
||||
|
||||
VolumeImpl(String displayName) {
|
||||
this.displayName = displayName;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package org.cryptomator.common.settings;
|
||||
|
||||
public enum WebDavUrlScheme {
|
||||
DAV("dav", "dav:// (Gnome, Nautilus, ...)"),
|
||||
WEBDAV("webdav", "webdav:// (KDE, Dolphin, ...)");
|
||||
|
||||
private final String prefix;
|
||||
private final String displayName;
|
||||
|
||||
WebDavUrlScheme(String prefix, String displayName) {
|
||||
this.prefix = prefix;
|
||||
this.displayName = displayName;
|
||||
}
|
||||
|
||||
public String getPrefix() {
|
||||
return prefix;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package org.cryptomator.common.settings;
|
||||
|
||||
public enum WhenUnlocked {
|
||||
IGNORE("vaultOptions.general.actionAfterUnlock.ignore"),
|
||||
REVEAL("vaultOptions.general.actionAfterUnlock.reveal"),
|
||||
ASK("vaultOptions.general.actionAfterUnlock.ask");
|
||||
|
||||
private String displayName;
|
||||
|
||||
WhenUnlocked(String displayName) {
|
||||
this.displayName = displayName;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package org.cryptomator.common.vaults;
|
||||
|
||||
import com.google.common.collect.Iterables;
|
||||
import org.cryptomator.common.mountpoint.InvalidMountPointException;
|
||||
import org.cryptomator.common.mountpoint.MountPointChooser;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
|
||||
public abstract class AbstractVolume implements Volume {
|
||||
|
||||
private final Iterable<MountPointChooser> choosers;
|
||||
|
||||
protected Path mountPoint;
|
||||
private boolean cleanupRequired;
|
||||
private MountPointChooser usedChooser;
|
||||
|
||||
public AbstractVolume(Iterable<MountPointChooser> choosers) {
|
||||
this.choosers = choosers;
|
||||
}
|
||||
|
||||
protected Path determineMountPoint() throws InvalidMountPointException {
|
||||
var applicableChoosers = Iterables.filter(choosers, c -> c.isApplicable(this));
|
||||
for (var chooser : applicableChoosers) {
|
||||
Optional<Path> chosenPath = chooser.chooseMountPoint(this);
|
||||
if (chosenPath.isEmpty()) { // chooser couldn't find a feasible mountpoint
|
||||
continue;
|
||||
}
|
||||
this.cleanupRequired = chooser.prepare(this, chosenPath.get());
|
||||
this.usedChooser = chooser;
|
||||
return chosenPath.get();
|
||||
}
|
||||
throw new InvalidMountPointException(String.format("No feasible MountPoint found by choosers: %s", applicableChoosers));
|
||||
}
|
||||
|
||||
protected void cleanupMountPoint() {
|
||||
if (this.cleanupRequired) {
|
||||
this.usedChooser.cleanup(this, this.mountPoint);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Path> getMountPoint() {
|
||||
return Optional.ofNullable(mountPoint);
|
||||
}
|
||||
}
|
||||
60
src/main/java/org/cryptomator/common/vaults/AutoLocker.java
Normal file
60
src/main/java/org/cryptomator/common/vaults/AutoLocker.java
Normal file
@@ -0,0 +1,60 @@
|
||||
package org.cryptomator.common.vaults;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import javafx.collections.ObservableList;
|
||||
import java.time.Instant;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Singleton
|
||||
public class AutoLocker {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AutoLocker.class);
|
||||
|
||||
private final ScheduledExecutorService scheduler;
|
||||
private final ObservableList<Vault> vaultList;
|
||||
|
||||
@Inject
|
||||
public AutoLocker(ScheduledExecutorService scheduler, ObservableList<Vault> vaultList) {
|
||||
this.scheduler = scheduler;
|
||||
this.vaultList = vaultList;
|
||||
}
|
||||
|
||||
public void init() {
|
||||
scheduler.scheduleAtFixedRate(this::tick, 0, 1, TimeUnit.MINUTES);
|
||||
}
|
||||
|
||||
private void tick() {
|
||||
vaultList.stream() // all vaults
|
||||
.filter(Vault::isUnlocked) // unlocked vaults
|
||||
.filter(this::exceedsIdleTime) // idle vaults
|
||||
.forEach(this::autolock);
|
||||
}
|
||||
|
||||
private void autolock(Vault vault) {
|
||||
try {
|
||||
vault.lock(false);
|
||||
LOG.info("Autolocked {} after idle timeout", vault.getDisplayName());
|
||||
} catch (Volume.VolumeException | LockNotCompletedException e) {
|
||||
LOG.error("Autolocking failed.", e);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
return deadline.isBefore(Instant.now());
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.cryptomator.common.vaults;
|
||||
|
||||
import javax.inject.Qualifier;
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
|
||||
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
|
||||
@Qualifier
|
||||
@Documented
|
||||
@Retention(RUNTIME)
|
||||
public @interface DefaultMountFlags {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package org.cryptomator.common.vaults;
|
||||
|
||||
import org.cryptomator.common.mountpoint.InvalidMountPointException;
|
||||
import org.cryptomator.common.mountpoint.MountPointChooser;
|
||||
import org.cryptomator.common.settings.VaultSettings;
|
||||
import org.cryptomator.common.settings.VolumeImpl;
|
||||
import org.cryptomator.cryptofs.CryptoFileSystem;
|
||||
import org.cryptomator.frontend.dokany.DokanyMountFailedException;
|
||||
import org.cryptomator.frontend.dokany.Mount;
|
||||
import org.cryptomator.frontend.dokany.MountFactory;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class DokanyVolume extends AbstractVolume {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(DokanyVolume.class);
|
||||
|
||||
private static final String FS_TYPE_NAME = "CryptomatorFS";
|
||||
|
||||
private final VaultSettings vaultSettings;
|
||||
|
||||
private Mount mount;
|
||||
|
||||
@Inject
|
||||
public DokanyVolume(VaultSettings vaultSettings, @Named("orderedMountPointChoosers") Iterable<MountPointChooser> choosers) {
|
||||
super(choosers);
|
||||
this.vaultSettings = vaultSettings;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VolumeImpl getImplementationType() {
|
||||
return VolumeImpl.DOKANY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mount(CryptoFileSystem fs, String mountFlags, Consumer<Throwable> onExitAction) throws InvalidMountPointException, VolumeException {
|
||||
this.mountPoint = determineMountPoint();
|
||||
try {
|
||||
this.mount = MountFactory.mount(fs.getPath("/"), mountPoint, vaultSettings.mountName().get(), FS_TYPE_NAME, mountFlags.strip(), onExitAction);
|
||||
} catch (DokanyMountFailedException e) {
|
||||
if (vaultSettings.getCustomMountPath().isPresent()) {
|
||||
LOG.warn("Failed to mount vault into {}. Is this directory currently accessed by another process (e.g. Windows Explorer)?", mountPoint);
|
||||
}
|
||||
throw new VolumeException("Unable to mount Filesystem", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reveal(Revealer revealer) throws VolumeException {
|
||||
try {
|
||||
mount.reveal(revealer::reveal);
|
||||
} catch (Exception e) {
|
||||
throw new VolumeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unmount() throws VolumeException {
|
||||
try {
|
||||
mount.unmount();
|
||||
} catch (IllegalStateException e) {
|
||||
throw new VolumeException("Unmount Failed.", e);
|
||||
}
|
||||
cleanupMountPoint();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unmountForced() {
|
||||
mount.unmountForced();
|
||||
cleanupMountPoint();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsForcedUnmount() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSupported() {
|
||||
return DokanyVolume.isSupportedStatic();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MountPointRequirement getMountPointRequirement() {
|
||||
return MountPointRequirement.EMPTY_MOUNT_POINT;
|
||||
}
|
||||
|
||||
public static boolean isSupportedStatic() {
|
||||
return MountFactory.isApplicable();
|
||||
}
|
||||
}
|
||||
129
src/main/java/org/cryptomator/common/vaults/FuseVolume.java
Normal file
129
src/main/java/org/cryptomator/common/vaults/FuseVolume.java
Normal file
@@ -0,0 +1,129 @@
|
||||
package org.cryptomator.common.vaults;
|
||||
|
||||
import com.google.common.collect.Iterators;
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.common.mountpoint.InvalidMountPointException;
|
||||
import org.cryptomator.common.mountpoint.MountPointChooser;
|
||||
import org.cryptomator.common.settings.VolumeImpl;
|
||||
import org.cryptomator.cryptofs.CryptoFileSystem;
|
||||
import org.cryptomator.frontend.fuse.mount.EnvironmentVariables;
|
||||
import org.cryptomator.frontend.fuse.mount.FuseMountException;
|
||||
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;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
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 Mount mount;
|
||||
|
||||
@Inject
|
||||
public FuseVolume(@Named("orderedMountPointChoosers") Iterable<MountPointChooser> choosers) {
|
||||
super(choosers);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mount(CryptoFileSystem fs, String mountFlags, Consumer<Throwable> onExitAction) throws InvalidMountPointException, VolumeException {
|
||||
this.mountPoint = determineMountPoint();
|
||||
mount(fs.getPath("/"), mountFlags, onExitAction);
|
||||
}
|
||||
|
||||
private void mount(Path root, String mountFlags, Consumer<Throwable> onExitAction) throws VolumeException {
|
||||
try {
|
||||
Mounter mounter = FuseMountFactory.getMounter();
|
||||
EnvironmentVariables envVars = EnvironmentVariables.create() //
|
||||
.withFlags(splitFlags(mountFlags)) //
|
||||
.withMountPoint(mountPoint) //
|
||||
.withFileNameTranscoder(mounter.defaultFileNameTranscoder()) //
|
||||
.build();
|
||||
this.mount = mounter.mount(root, envVars, onExitAction);
|
||||
} catch ( FuseMountException | FuseNotSupportedException e) {
|
||||
throw new VolumeException("Unable to mount Filesystem", e);
|
||||
}
|
||||
}
|
||||
|
||||
private String[] splitFlags(String str) {
|
||||
List<String> flags = new ArrayList<>();
|
||||
var matches = Iterators.peekingIterator(NON_WHITESPACE_OR_QUOTED.matcher(str).results().iterator());
|
||||
while (matches.hasNext()) {
|
||||
String flag = matches.next().group();
|
||||
// check if flag is missing its argument:
|
||||
if (flag.endsWith("=") && matches.hasNext() && matches.peek().group(1) != null) { // next is "double quoted"
|
||||
// next is "double quoted" and flag is missing its argument
|
||||
flag += matches.next().group(1);
|
||||
} else if (flag.endsWith("=") && matches.hasNext() && matches.peek().group(2) != null) {
|
||||
// next is 'single quoted' and flag is missing its argument
|
||||
flag += matches.next().group(2);
|
||||
}
|
||||
flags.add(flag);
|
||||
}
|
||||
return flags.toArray(String[]::new);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reveal(Revealer revealer) throws VolumeException {
|
||||
try {
|
||||
mount.reveal(revealer::reveal);
|
||||
} catch (Exception e) {
|
||||
throw new VolumeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsForcedUnmount() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void unmountForced() throws VolumeException {
|
||||
try {
|
||||
mount.unmountForced();
|
||||
} catch (FuseMountException e) {
|
||||
throw new VolumeException(e);
|
||||
}
|
||||
cleanupMountPoint();
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void unmount() throws VolumeException {
|
||||
try {
|
||||
mount.unmount();
|
||||
} catch (FuseMountException e) {
|
||||
throw new VolumeException(e);
|
||||
}
|
||||
cleanupMountPoint();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSupported() {
|
||||
return FuseVolume.isSupportedStatic();
|
||||
}
|
||||
|
||||
@Override
|
||||
public VolumeImpl getImplementationType() {
|
||||
return VolumeImpl.FUSE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MountPointRequirement getMountPointRequirement() {
|
||||
return SystemUtils.IS_OS_WINDOWS ? MountPointRequirement.PARENT_NO_MOUNT_POINT : MountPointRequirement.EMPTY_MOUNT_POINT;
|
||||
}
|
||||
|
||||
public static boolean isSupportedStatic() {
|
||||
return FuseMountFactory.isFuseSupported();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package org.cryptomator.common.vaults;
|
||||
|
||||
public class LockNotCompletedException extends Exception {
|
||||
|
||||
public LockNotCompletedException(String reason) {
|
||||
super(reason);
|
||||
}
|
||||
|
||||
public LockNotCompletedException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package org.cryptomator.common.vaults;
|
||||
|
||||
/**
|
||||
* Enumeration used to indicate the requirements for mounting a vault
|
||||
* using a specific {@link Volume VolumeProvider}, e.g. {@link FuseVolume}.
|
||||
*/
|
||||
public enum MountPointRequirement {
|
||||
|
||||
/**
|
||||
* No Mountpoint on the local filesystem required. (e.g. WebDAV)
|
||||
*/
|
||||
NONE,
|
||||
|
||||
/**
|
||||
* A parent folder is required, but the actual Mountpoint must not exist.
|
||||
*/
|
||||
PARENT_NO_MOUNT_POINT,
|
||||
|
||||
/**
|
||||
* A parent folder is required, but the actual Mountpoint may exist.
|
||||
*/
|
||||
PARENT_OPT_MOUNT_POINT,
|
||||
|
||||
/**
|
||||
* The actual Mountpoint must exist and must be empty.
|
||||
*/
|
||||
EMPTY_MOUNT_POINT;
|
||||
}
|
||||
13
src/main/java/org/cryptomator/common/vaults/PerVault.java
Normal file
13
src/main/java/org/cryptomator/common/vaults/PerVault.java
Normal file
@@ -0,0 +1,13 @@
|
||||
package org.cryptomator.common.vaults;
|
||||
|
||||
import javax.inject.Scope;
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
@Scope
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface PerVault {
|
||||
|
||||
}
|
||||
402
src/main/java/org/cryptomator/common/vaults/Vault.java
Normal file
402
src/main/java/org/cryptomator/common/vaults/Vault.java
Normal file
@@ -0,0 +1,402 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2016, 2017 Sebastian Stenzel and others.
|
||||
* All rights reserved.
|
||||
* This program and the accompanying materials are made available under the terms of the accompanying LICENSE file.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.common.vaults;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.common.mountpoint.InvalidMountPointException;
|
||||
import org.cryptomator.common.settings.VaultSettings;
|
||||
import org.cryptomator.common.vaults.Volume.VolumeException;
|
||||
import org.cryptomator.cryptofs.CryptoFileSystem;
|
||||
import org.cryptomator.cryptofs.CryptoFileSystemProperties;
|
||||
import org.cryptomator.cryptofs.CryptoFileSystemProperties.FileSystemFlags;
|
||||
import org.cryptomator.cryptofs.CryptoFileSystemProvider;
|
||||
import org.cryptomator.cryptofs.VaultConfig;
|
||||
import org.cryptomator.cryptofs.VaultConfig.UnverifiedVaultConfig;
|
||||
import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker;
|
||||
import org.cryptomator.cryptolib.api.CryptoException;
|
||||
import org.cryptomator.cryptolib.api.MasterkeyLoader;
|
||||
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Provider;
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.binding.StringBinding;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
@PerVault
|
||||
public class Vault {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(Vault.class);
|
||||
private static final Path HOME_DIR = Paths.get(SystemUtils.USER_HOME);
|
||||
private static final int UNLIMITED_FILENAME_LENGTH = Integer.MAX_VALUE;
|
||||
|
||||
private final VaultSettings vaultSettings;
|
||||
private final Provider<Volume> volumeProvider;
|
||||
private final StringBinding defaultMountFlags;
|
||||
private final AtomicReference<CryptoFileSystem> cryptoFileSystem;
|
||||
private final VaultState state;
|
||||
private final ObjectProperty<Exception> lastKnownException;
|
||||
private final VaultStats stats;
|
||||
private final StringBinding displayName;
|
||||
private final StringBinding displayablePath;
|
||||
private final BooleanBinding locked;
|
||||
private final BooleanBinding processing;
|
||||
private final BooleanBinding unlocked;
|
||||
private final BooleanBinding missing;
|
||||
private final BooleanBinding needsMigration;
|
||||
private final BooleanBinding unknownError;
|
||||
private final StringBinding accessPoint;
|
||||
private final BooleanBinding accessPointPresent;
|
||||
private final BooleanProperty showingStats;
|
||||
|
||||
private volatile Volume volume;
|
||||
|
||||
@Inject
|
||||
Vault(VaultSettings vaultSettings, Provider<Volume> volumeProvider, @DefaultMountFlags StringBinding defaultMountFlags, AtomicReference<CryptoFileSystem> cryptoFileSystem, VaultState state, @Named("lastKnownException") ObjectProperty<Exception> lastKnownException, VaultStats stats) {
|
||||
this.vaultSettings = vaultSettings;
|
||||
this.volumeProvider = volumeProvider;
|
||||
this.defaultMountFlags = defaultMountFlags;
|
||||
this.cryptoFileSystem = cryptoFileSystem;
|
||||
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);
|
||||
this.unlocked = Bindings.createBooleanBinding(this::isUnlocked, state);
|
||||
this.missing = Bindings.createBooleanBinding(this::isMissing, state);
|
||||
this.needsMigration = Bindings.createBooleanBinding(this::isNeedsMigration, state);
|
||||
this.unknownError = Bindings.createBooleanBinding(this::isUnknownError, state);
|
||||
this.accessPoint = Bindings.createStringBinding(this::getAccessPoint, state);
|
||||
this.accessPointPresent = this.accessPoint.isNotEmpty();
|
||||
this.showingStats = new SimpleBooleanProperty(false);
|
||||
}
|
||||
|
||||
// ******************************************************************************
|
||||
// Commands
|
||||
// ********************************************************************************/
|
||||
|
||||
private CryptoFileSystem createCryptoFileSystem(MasterkeyLoader keyLoader) throws IOException, MasterkeyLoadingFailedException {
|
||||
Set<FileSystemFlags> flags = EnumSet.noneOf(FileSystemFlags.class);
|
||||
if (vaultSettings.usesReadOnlyMode().get()) {
|
||||
flags.add(FileSystemFlags.READONLY);
|
||||
} else if(vaultSettings.maxCleartextFilenameLength().get() == -1) {
|
||||
LOG.debug("Determining cleartext filename length limitations...");
|
||||
var checker = new FileSystemCapabilityChecker();
|
||||
int shorteningThreshold = getUnverifiedVaultConfig().allegedShorteningThreshold();
|
||||
int ciphertextLimit = checker.determineSupportedCiphertextFileNameLength(getPath());
|
||||
if (ciphertextLimit < shorteningThreshold) {
|
||||
int cleartextLimit = checker.determineSupportedCleartextFileNameLength(getPath());
|
||||
vaultSettings.maxCleartextFilenameLength().set(cleartextLimit);
|
||||
} else {
|
||||
vaultSettings.maxCleartextFilenameLength().setValue(UNLIMITED_FILENAME_LENGTH);
|
||||
}
|
||||
}
|
||||
|
||||
if (vaultSettings.maxCleartextFilenameLength().get() < UNLIMITED_FILENAME_LENGTH) {
|
||||
LOG.warn("Limiting cleartext filename length on this device to {}.", vaultSettings.maxCleartextFilenameLength().get());
|
||||
}
|
||||
|
||||
CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties() //
|
||||
.withKeyLoader(keyLoader) //
|
||||
.withFlags(flags) //
|
||||
.withMaxCleartextNameLength(vaultSettings.maxCleartextFilenameLength().get()) //
|
||||
.build();
|
||||
return CryptoFileSystemProvider.newFileSystem(getPath(), fsProps);
|
||||
}
|
||||
|
||||
private void destroyCryptoFileSystem() {
|
||||
LOG.trace("Trying to close associated CryptoFS...");
|
||||
CryptoFileSystem fs = cryptoFileSystem.getAndSet(null);
|
||||
if (fs != null) {
|
||||
try {
|
||||
fs.close();
|
||||
} catch (IOException e) {
|
||||
LOG.error("Error closing file system.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void unlock(MasterkeyLoader keyLoader) throws CryptoException, IOException, VolumeException, InvalidMountPointException {
|
||||
if (cryptoFileSystem.get() != null) {
|
||||
throw new IllegalStateException("Already unlocked.");
|
||||
}
|
||||
CryptoFileSystem fs = createCryptoFileSystem(keyLoader);
|
||||
boolean success = false;
|
||||
try {
|
||||
cryptoFileSystem.set(fs);
|
||||
volume = volumeProvider.get();
|
||||
volume.mount(fs, getEffectiveMountFlags(), this::lockOnVolumeExit);
|
||||
success = true;
|
||||
} finally {
|
||||
if (!success) {
|
||||
destroyCryptoFileSystem();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void lockOnVolumeExit(Throwable t) {
|
||||
LOG.info("Unmounted vault '{}'", getDisplayName());
|
||||
destroyCryptoFileSystem();
|
||||
state.set(VaultState.Value.LOCKED);
|
||||
if (t != null) {
|
||||
LOG.warn("Unexpected unmount and lock of vault " + getDisplayName(), t);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void lock(boolean forced) throws VolumeException, LockNotCompletedException {
|
||||
//initiate unmount
|
||||
if (forced && volume.supportsForcedUnmount()) {
|
||||
volume.unmountForced();
|
||||
} else {
|
||||
volume.unmount();
|
||||
}
|
||||
|
||||
//wait for lockOnVolumeExit to be executed
|
||||
try {
|
||||
boolean locked = state.awaitState(VaultState.Value.LOCKED, 3000, TimeUnit.MILLISECONDS);
|
||||
if (!locked) {
|
||||
throw new LockNotCompletedException("Locking of vault " + this.getDisplayName() + " still in progress.");
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new LockNotCompletedException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void reveal(Volume.Revealer vaultRevealer) throws VolumeException {
|
||||
volume.reveal(vaultRevealer);
|
||||
}
|
||||
|
||||
// ******************************************************************************
|
||||
// Observable Properties
|
||||
// *******************************************************************************
|
||||
|
||||
public VaultState stateProperty() {
|
||||
return state;
|
||||
}
|
||||
|
||||
public VaultState.Value getState() {
|
||||
return state.getValue();
|
||||
}
|
||||
|
||||
public ObjectProperty<Exception> lastKnownExceptionProperty() {
|
||||
return lastKnownException;
|
||||
}
|
||||
|
||||
public Exception getLastKnownException() {
|
||||
return lastKnownException.get();
|
||||
}
|
||||
|
||||
public void setLastKnownException(Exception e) {
|
||||
lastKnownException.setValue(e);
|
||||
}
|
||||
|
||||
public BooleanBinding lockedProperty() {
|
||||
return locked;
|
||||
}
|
||||
|
||||
public boolean isLocked() {
|
||||
return state.get() == VaultState.Value.LOCKED;
|
||||
}
|
||||
|
||||
public BooleanBinding processingProperty() {
|
||||
return processing;
|
||||
}
|
||||
|
||||
public boolean isProcessing() {
|
||||
return state.get() == VaultState.Value.PROCESSING;
|
||||
}
|
||||
|
||||
public BooleanBinding unlockedProperty() {
|
||||
return unlocked;
|
||||
}
|
||||
|
||||
public boolean isUnlocked() {
|
||||
return state.get() == VaultState.Value.UNLOCKED;
|
||||
}
|
||||
|
||||
public BooleanBinding missingProperty() {
|
||||
return missing;
|
||||
}
|
||||
|
||||
public boolean isMissing() {
|
||||
return state.get() == VaultState.Value.MISSING;
|
||||
}
|
||||
|
||||
public BooleanBinding needsMigrationProperty() {
|
||||
return needsMigration;
|
||||
}
|
||||
|
||||
public boolean isNeedsMigration() {
|
||||
return state.get() == VaultState.Value.NEEDS_MIGRATION;
|
||||
}
|
||||
|
||||
public BooleanBinding unknownErrorProperty() {
|
||||
return unknownError;
|
||||
}
|
||||
|
||||
public boolean isUnknownError() {
|
||||
return state.get() == VaultState.Value.ERROR;
|
||||
}
|
||||
|
||||
public StringBinding displayNameProperty() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return vaultSettings.displayName().get();
|
||||
}
|
||||
|
||||
public StringBinding accessPointProperty() {
|
||||
return accessPoint;
|
||||
}
|
||||
|
||||
public String getAccessPoint() {
|
||||
if (state.getValue() == VaultState.Value.UNLOCKED) {
|
||||
assert volume != null;
|
||||
return volume.getMountPoint().orElse(Path.of("")).toString();
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
public BooleanBinding accessPointPresentProperty() {
|
||||
return accessPointPresent;
|
||||
}
|
||||
|
||||
public boolean isAccessPointPresent() {
|
||||
return accessPointPresent.get();
|
||||
}
|
||||
|
||||
public StringBinding displayablePathProperty() {
|
||||
return displayablePath;
|
||||
}
|
||||
|
||||
public String getDisplayablePath() {
|
||||
Path p = vaultSettings.path().get();
|
||||
if (p.startsWith(HOME_DIR)) {
|
||||
Path relativePath = HOME_DIR.relativize(p);
|
||||
String homePrefix = SystemUtils.IS_OS_WINDOWS ? "~\\" : "~/";
|
||||
return homePrefix + relativePath.toString();
|
||||
} else {
|
||||
return p.toString();
|
||||
}
|
||||
}
|
||||
|
||||
public BooleanProperty showingStatsProperty() {
|
||||
return showingStats;
|
||||
}
|
||||
|
||||
public boolean isShowingStats() {
|
||||
return accessPointPresent.get();
|
||||
}
|
||||
|
||||
|
||||
// ******************************************************************************
|
||||
// Getter/Setter
|
||||
// *******************************************************************************/
|
||||
|
||||
public VaultStats getStats() {
|
||||
return stats;
|
||||
}
|
||||
|
||||
public UnverifiedVaultConfig getUnverifiedVaultConfig() throws IOException {
|
||||
Path configPath = getPath().resolve(org.cryptomator.common.Constants.VAULTCONFIG_FILENAME);
|
||||
String token = Files.readString(configPath, StandardCharsets.US_ASCII);
|
||||
return VaultConfig.decode(token);
|
||||
}
|
||||
|
||||
public Observable[] observables() {
|
||||
return new Observable[]{state};
|
||||
}
|
||||
|
||||
public VaultSettings getVaultSettings() {
|
||||
return vaultSettings;
|
||||
}
|
||||
|
||||
public Path getPath() {
|
||||
return vaultSettings.path().getValue();
|
||||
}
|
||||
|
||||
public boolean isHavingCustomMountFlags() {
|
||||
return !Strings.isNullOrEmpty(vaultSettings.mountFlags().get());
|
||||
}
|
||||
|
||||
public StringBinding defaultMountFlagsProperty() {
|
||||
return defaultMountFlags;
|
||||
}
|
||||
|
||||
public String getDefaultMountFlags() {
|
||||
return defaultMountFlags.get();
|
||||
}
|
||||
|
||||
public String getEffectiveMountFlags() {
|
||||
String mountFlags = vaultSettings.mountFlags().get();
|
||||
if (Strings.isNullOrEmpty(mountFlags)) {
|
||||
return getDefaultMountFlags();
|
||||
} else {
|
||||
return mountFlags;
|
||||
}
|
||||
}
|
||||
|
||||
public void setCustomMountFlags(String mountFlags) {
|
||||
vaultSettings.mountFlags().set(mountFlags);
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return vaultSettings.getId();
|
||||
}
|
||||
|
||||
public Optional<Volume> getVolume() {
|
||||
return Optional.ofNullable(this.volume);
|
||||
}
|
||||
|
||||
// ******************************************************************************
|
||||
// Hashcode / Equals
|
||||
// *******************************************************************************/
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(vaultSettings);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj instanceof Vault other && obj.getClass().equals(this.getClass())) {
|
||||
return Objects.equals(this.vaultSettings, other.vaultSettings);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean supportsForcedUnmount() {
|
||||
return volume.supportsForcedUnmount();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the accompanying LICENSE file.
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.common.vaults;
|
||||
|
||||
import dagger.BindsInstance;
|
||||
import dagger.Subcomponent;
|
||||
import org.cryptomator.common.mountpoint.MountPointChooserModule;
|
||||
import org.cryptomator.common.settings.VaultSettings;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.inject.Named;
|
||||
|
||||
@PerVault
|
||||
@Subcomponent(modules = {VaultModule.class, MountPointChooserModule.class})
|
||||
public interface VaultComponent {
|
||||
|
||||
Vault vault();
|
||||
|
||||
@Subcomponent.Builder
|
||||
interface Builder {
|
||||
|
||||
@BindsInstance
|
||||
Builder vaultSettings(VaultSettings vaultSettings);
|
||||
|
||||
@BindsInstance
|
||||
Builder initialVaultState(VaultState.Value vaultState);
|
||||
|
||||
@BindsInstance
|
||||
Builder initialErrorCause(@Nullable @Named("lastKnownException") Exception initialErrorCause);
|
||||
|
||||
VaultComponent build();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package org.cryptomator.common.vaults;
|
||||
|
||||
import org.cryptomator.common.settings.VaultSettings;
|
||||
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.ObservableList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* This listener makes sure to reflect any changes to the vault list back to the settings.
|
||||
*/
|
||||
class VaultListChangeListener implements ListChangeListener<Vault> {
|
||||
|
||||
private final ObservableList<VaultSettings> vaultSettingsList;
|
||||
|
||||
public VaultListChangeListener(ObservableList<VaultSettings> vaultSettingsList) {
|
||||
this.vaultSettingsList = vaultSettingsList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChanged(Change<? extends Vault> c) {
|
||||
while (c.next()) {
|
||||
if (c.wasAdded()) {
|
||||
List<VaultSettings> addedSettings = c.getAddedSubList().stream().map(Vault::getVaultSettings).toList();
|
||||
vaultSettingsList.addAll(c.getFrom(), addedSettings);
|
||||
} else if (c.wasRemoved()) {
|
||||
List<VaultSettings> removedSettings = c.getRemoved().stream().map(Vault::getVaultSettings).toList();
|
||||
vaultSettingsList.removeAll(removedSettings);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2016, 2017 Sebastian Stenzel and others.
|
||||
* All rights reserved.
|
||||
* This program and the accompanying materials are made available under the terms of the accompanying LICENSE file.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.common.vaults;
|
||||
|
||||
import org.cryptomator.common.settings.Settings;
|
||||
import org.cryptomator.common.settings.VaultSettings;
|
||||
import org.cryptomator.cryptofs.CryptoFileSystemProvider;
|
||||
import org.cryptomator.cryptofs.DirStructure;
|
||||
import org.cryptomator.cryptofs.migration.Migrators;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.NoSuchFileException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collection;
|
||||
import java.util.Optional;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
|
||||
import static org.cryptomator.common.Constants.VAULTCONFIG_FILENAME;
|
||||
import static org.cryptomator.common.vaults.VaultState.Value.ERROR;
|
||||
|
||||
@Singleton
|
||||
public class VaultListManager {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(VaultListManager.class);
|
||||
|
||||
private final AutoLocker autoLocker;
|
||||
private final VaultComponent.Builder vaultComponentBuilder;
|
||||
private final ObservableList<Vault> vaultList;
|
||||
private final String defaultVaultName;
|
||||
|
||||
@Inject
|
||||
public VaultListManager(ObservableList<Vault> vaultList, AutoLocker autoLocker, VaultComponent.Builder vaultComponentBuilder, ResourceBundle resourceBundle, Settings settings) {
|
||||
this.vaultList = vaultList;
|
||||
this.autoLocker = autoLocker;
|
||||
this.vaultComponentBuilder = vaultComponentBuilder;
|
||||
this.defaultVaultName = resourceBundle.getString("defaults.vault.vaultName");
|
||||
|
||||
addAll(settings.getDirectories());
|
||||
vaultList.addListener(new VaultListChangeListener(settings.getDirectories()));
|
||||
autoLocker.init();
|
||||
}
|
||||
|
||||
public Vault add(Path pathToVault) throws IOException {
|
||||
Path normalizedPathToVault = pathToVault.normalize().toAbsolutePath();
|
||||
if (CryptoFileSystemProvider.checkDirStructureForVault(normalizedPathToVault, VAULTCONFIG_FILENAME, MASTERKEY_FILENAME) == DirStructure.UNRELATED) {
|
||||
throw new NoSuchFileException(normalizedPathToVault.toString(), null, "Not a vault directory");
|
||||
}
|
||||
|
||||
return get(normalizedPathToVault) //
|
||||
.orElseGet(() -> {
|
||||
Vault newVault = create(newVaultSettings(normalizedPathToVault));
|
||||
vaultList.add(newVault);
|
||||
return newVault;
|
||||
});
|
||||
}
|
||||
|
||||
private VaultSettings newVaultSettings(Path path) {
|
||||
VaultSettings vaultSettings = VaultSettings.withRandomId();
|
||||
vaultSettings.path().set(path);
|
||||
if (path.getFileName() != null) {
|
||||
vaultSettings.displayName().set(path.getFileName().toString());
|
||||
} else {
|
||||
vaultSettings.displayName().set(defaultVaultName);
|
||||
}
|
||||
return vaultSettings;
|
||||
}
|
||||
|
||||
private void addAll(Collection<VaultSettings> vaultSettings) {
|
||||
Collection<Vault> vaults = vaultSettings.stream().map(this::create).toList();
|
||||
vaultList.addAll(vaults);
|
||||
}
|
||||
|
||||
private Optional<Vault> get(Path vaultPath) {
|
||||
assert vaultPath.isAbsolute();
|
||||
assert vaultPath.normalize().equals(vaultPath);
|
||||
return vaultList.stream() //
|
||||
.filter(v -> vaultPath.equals(v.getPath())) //
|
||||
.findAny();
|
||||
}
|
||||
|
||||
private Vault create(VaultSettings vaultSettings) {
|
||||
VaultComponent.Builder compBuilder = vaultComponentBuilder.vaultSettings(vaultSettings);
|
||||
try {
|
||||
VaultState.Value vaultState = determineVaultState(vaultSettings.path().get());
|
||||
compBuilder.initialVaultState(vaultState);
|
||||
} catch (IOException e) {
|
||||
LOG.warn("Failed to determine vault state for " + vaultSettings.path().get(), e);
|
||||
compBuilder.initialVaultState(ERROR);
|
||||
compBuilder.initialErrorCause(e);
|
||||
}
|
||||
return compBuilder.build().vault();
|
||||
}
|
||||
|
||||
public static VaultState.Value redetermineVaultState(Vault vault) {
|
||||
VaultState state = vault.stateProperty();
|
||||
VaultState.Value previousState = state.getValue();
|
||||
return switch (previousState) {
|
||||
case LOCKED, NEEDS_MIGRATION, MISSING -> {
|
||||
try {
|
||||
var determinedState = determineVaultState(vault.getPath());
|
||||
state.set(determinedState);
|
||||
yield determinedState;
|
||||
} catch (IOException e) {
|
||||
LOG.warn("Failed to determine vault state for " + vault.getPath(), e);
|
||||
state.set(ERROR);
|
||||
vault.setLastKnownException(e);
|
||||
yield ERROR;
|
||||
}
|
||||
}
|
||||
case ERROR, UNLOCKED, PROCESSING -> previousState;
|
||||
};
|
||||
}
|
||||
|
||||
private static VaultState.Value determineVaultState(Path pathToVault) throws IOException {
|
||||
if (!Files.exists(pathToVault)) {
|
||||
return VaultState.Value.MISSING;
|
||||
}
|
||||
return switch (CryptoFileSystemProvider.checkDirStructureForVault(pathToVault, VAULTCONFIG_FILENAME, MASTERKEY_FILENAME)) {
|
||||
case VAULT -> VaultState.Value.LOCKED;
|
||||
case UNRELATED -> VaultState.Value.MISSING;
|
||||
case MAYBE_LEGACY -> Migrators.get().needsMigration(pathToVault, VAULTCONFIG_FILENAME, MASTERKEY_FILENAME) ? VaultState.Value.NEEDS_MIGRATION : VaultState.Value.MISSING;
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package org.cryptomator.common.vaults;
|
||||
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
|
||||
import javax.inject.Singleton;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
|
||||
@Module
|
||||
public class VaultListModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public ObservableList<Vault> provideVaultList() {
|
||||
return FXCollections.observableArrayList(Vault::observables);
|
||||
}
|
||||
|
||||
}
|
||||
176
src/main/java/org/cryptomator/common/vaults/VaultModule.java
Normal file
176
src/main/java/org/cryptomator/common/vaults/VaultModule.java
Normal file
@@ -0,0 +1,176 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the accompanying LICENSE file.
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.common.vaults;
|
||||
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.common.settings.Settings;
|
||||
import org.cryptomator.common.settings.VaultSettings;
|
||||
import org.cryptomator.common.settings.VolumeImpl;
|
||||
import org.cryptomator.cryptofs.CryptoFileSystem;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.inject.Named;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.StringBinding;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyBooleanProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
@Module
|
||||
public class VaultModule {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(VaultModule.class);
|
||||
|
||||
@Provides
|
||||
@PerVault
|
||||
public AtomicReference<CryptoFileSystem> provideCryptoFileSystemReference() {
|
||||
return new AtomicReference<>();
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Named("lastKnownException")
|
||||
@PerVault
|
||||
public ObjectProperty<Exception> provideLastKnownException(@Named("lastKnownException") @Nullable Exception initialErrorCause) {
|
||||
return new SimpleObjectProperty<>(initialErrorCause);
|
||||
}
|
||||
|
||||
@Provides
|
||||
public Volume provideVolume(Settings settings, WebDavVolume webDavVolume, FuseVolume fuseVolume, DokanyVolume dokanyVolume) {
|
||||
VolumeImpl preferredImpl = settings.preferredVolumeImpl().get();
|
||||
if (VolumeImpl.DOKANY == preferredImpl && dokanyVolume.isSupported()) {
|
||||
return dokanyVolume;
|
||||
} else if (VolumeImpl.FUSE == preferredImpl && fuseVolume.isSupported()) {
|
||||
return fuseVolume;
|
||||
} else {
|
||||
if (VolumeImpl.WEBDAV != preferredImpl) {
|
||||
LOG.warn("Using WebDAV, because {} is not supported.", preferredImpl.getDisplayName());
|
||||
}
|
||||
assert webDavVolume.isSupported();
|
||||
return webDavVolume;
|
||||
}
|
||||
}
|
||||
|
||||
@Provides
|
||||
@PerVault
|
||||
@DefaultMountFlags
|
||||
public StringBinding provideDefaultMountFlags(Settings settings, VaultSettings vaultSettings) {
|
||||
ObjectProperty<VolumeImpl> preferredVolumeImpl = settings.preferredVolumeImpl();
|
||||
StringBinding mountName = vaultSettings.mountName();
|
||||
BooleanProperty readOnly = vaultSettings.usesReadOnlyMode();
|
||||
|
||||
return Bindings.createStringBinding(() -> {
|
||||
VolumeImpl v = preferredVolumeImpl.get();
|
||||
if (v == VolumeImpl.FUSE && SystemUtils.IS_OS_MAC) {
|
||||
return getMacFuseDefaultMountFlags(mountName, readOnly);
|
||||
} else if (v == VolumeImpl.FUSE && SystemUtils.IS_OS_LINUX) {
|
||||
return getLinuxFuseDefaultMountFlags(readOnly);
|
||||
} else if (v == VolumeImpl.FUSE && SystemUtils.IS_OS_WINDOWS) {
|
||||
return getWindowsFuseDefaultMountFlags(mountName, readOnly);
|
||||
} else if (v == VolumeImpl.DOKANY && SystemUtils.IS_OS_WINDOWS) {
|
||||
return getDokanyDefaultMountFlags(readOnly);
|
||||
} else {
|
||||
return "--flags-supported-on-FUSE-or-DOKANY-only";
|
||||
}
|
||||
}, mountName, readOnly, preferredVolumeImpl);
|
||||
}
|
||||
|
||||
// see: https://github.com/osxfuse/osxfuse/wiki/Mount-options
|
||||
private String getMacFuseDefaultMountFlags(StringBinding mountName, ReadOnlyBooleanProperty readOnly) {
|
||||
assert SystemUtils.IS_OS_MAC_OSX;
|
||||
StringBuilder flags = new StringBuilder();
|
||||
if (readOnly.get()) {
|
||||
flags.append(" -ordonly");
|
||||
}
|
||||
flags.append(" -ovolname=").append('"').append(mountName.get()).append('"');
|
||||
flags.append(" -oatomic_o_trunc");
|
||||
flags.append(" -oauto_xattr");
|
||||
flags.append(" -oauto_cache");
|
||||
flags.append(" -onoappledouble"); // vastly impacts performance for some reason...
|
||||
flags.append(" -odefault_permissions"); // let the kernel assume permissions based on file attributes etc
|
||||
|
||||
try {
|
||||
Path userHome = Paths.get(System.getProperty("user.home"));
|
||||
int uid = (int) Files.getAttribute(userHome, "unix:uid");
|
||||
int gid = (int) Files.getAttribute(userHome, "unix:gid");
|
||||
flags.append(" -ouid=").append(uid);
|
||||
flags.append(" -ogid=").append(gid);
|
||||
} catch (IOException e) {
|
||||
LOG.error("Could not read uid/gid from USER_HOME", e);
|
||||
}
|
||||
|
||||
return flags.toString().strip();
|
||||
}
|
||||
|
||||
// see https://manpages.debian.org/testing/fuse/mount.fuse.8.en.html
|
||||
private String getLinuxFuseDefaultMountFlags(ReadOnlyBooleanProperty readOnly) {
|
||||
assert SystemUtils.IS_OS_LINUX;
|
||||
StringBuilder flags = new StringBuilder();
|
||||
if (readOnly.get()) {
|
||||
flags.append(" -oro");
|
||||
}
|
||||
flags.append(" -oauto_unmount");
|
||||
|
||||
try {
|
||||
Path userHome = Paths.get(System.getProperty("user.home"));
|
||||
int uid = (int) Files.getAttribute(userHome, "unix:uid");
|
||||
int gid = (int) Files.getAttribute(userHome, "unix:gid");
|
||||
flags.append(" -ouid=").append(uid);
|
||||
flags.append(" -ogid=").append(gid);
|
||||
} catch (IOException e) {
|
||||
LOG.error("Could not read uid/gid from USER_HOME", e);
|
||||
}
|
||||
|
||||
return flags.toString().strip();
|
||||
}
|
||||
|
||||
// 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 choosen
|
||||
private String getWindowsFuseDefaultMountFlags(StringBinding mountName, ReadOnlyBooleanProperty readOnly) {
|
||||
assert SystemUtils.IS_OS_WINDOWS;
|
||||
StringBuilder flags = new StringBuilder();
|
||||
|
||||
//WinFSP has no explicit "readonly"-option, nut not setting the group/user-id has the same effect, tho.
|
||||
//So for the time being not setting them is the way to go...
|
||||
//See: https://github.com/billziss-gh/winfsp/issues/319
|
||||
if (!readOnly.get()) {
|
||||
flags.append(" -ouid=-1");
|
||||
flags.append(" -ogid=11");
|
||||
}
|
||||
flags.append(" -ovolname=").append('"').append(mountName.get()).append('"');
|
||||
//Dokany requires this option to be set, WinFSP doesn't seem to share this peculiarity,
|
||||
//but the option exists. Let's keep this here in case we need it.
|
||||
// flags.append(" -oThreadCount=").append(5);
|
||||
|
||||
return flags.toString().strip();
|
||||
}
|
||||
|
||||
// see https://github.com/cryptomator/dokany-nio-adapter/blob/develop/src/main/java/org/cryptomator/frontend/dokany/MountUtil.java#L30-L34
|
||||
private String getDokanyDefaultMountFlags(ReadOnlyBooleanProperty readOnly) {
|
||||
assert SystemUtils.IS_OS_WINDOWS;
|
||||
StringBuilder flags = new StringBuilder();
|
||||
flags.append(" --options CURRENT_SESSION");
|
||||
if (readOnly.get()) {
|
||||
flags.append(",WRITE_PROTECTION");
|
||||
}
|
||||
flags.append(" --thread-count 5");
|
||||
flags.append(" --timeout 10000");
|
||||
flags.append(" --allocation-unit-size 4096");
|
||||
flags.append(" --sector-size 4096");
|
||||
return flags.toString().strip();
|
||||
}
|
||||
|
||||
}
|
||||
141
src/main/java/org/cryptomator/common/vaults/VaultState.java
Normal file
141
src/main/java/org/cryptomator/common/vaults/VaultState.java
Normal file
@@ -0,0 +1,141 @@
|
||||
package org.cryptomator.common.vaults;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.value.ObservableObjectValue;
|
||||
import javafx.beans.value.ObservableValueBase;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.concurrent.locks.Condition;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
@PerVault
|
||||
public class VaultState extends ObservableValueBase<VaultState.Value> implements ObservableObjectValue<VaultState.Value> {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(VaultState.class);
|
||||
|
||||
public enum Value {
|
||||
/**
|
||||
* No vault found at the provided path
|
||||
*/
|
||||
MISSING,
|
||||
|
||||
/**
|
||||
* Vault requires migration to a newer vault format
|
||||
*/
|
||||
NEEDS_MIGRATION,
|
||||
|
||||
/**
|
||||
* Vault ready to be unlocked
|
||||
*/
|
||||
LOCKED,
|
||||
|
||||
/**
|
||||
* Vault in transition between two other states
|
||||
*/
|
||||
PROCESSING,
|
||||
|
||||
/**
|
||||
* Vault is unlocked
|
||||
*/
|
||||
UNLOCKED,
|
||||
|
||||
/**
|
||||
* Unknown state due to preceeding unrecoverable exceptions.
|
||||
*/
|
||||
ERROR;
|
||||
}
|
||||
|
||||
private final AtomicReference<Value> value;
|
||||
private final Lock lock = new ReentrantLock();
|
||||
private final Condition valueChanged = lock.newCondition();
|
||||
|
||||
@Inject
|
||||
public VaultState(VaultState.Value initialValue) {
|
||||
this.value = new AtomicReference<>(initialValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Value get() {
|
||||
return getValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Value getValue() {
|
||||
return value.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Transitions from <code>fromState</code> to <code>toState</code>.
|
||||
*
|
||||
* @param fromState Previous state
|
||||
* @param toState New state
|
||||
* @return <code>true</code> if successful
|
||||
*/
|
||||
public boolean transition(Value fromState, Value toState) {
|
||||
Preconditions.checkArgument(fromState != toState, "fromState must be different than toState");
|
||||
boolean success = value.compareAndSet(fromState, toState);
|
||||
if (success) {
|
||||
fireValueChangedEvent();
|
||||
} else {
|
||||
LOG.debug("Failed transiting into state {}: Expected state was not{}.", fromState, toState);
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
public void set(Value newState) {
|
||||
var oldState = value.getAndSet(newState);
|
||||
if (oldState != newState) {
|
||||
fireValueChangedEvent();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for the specified time, until the desired state is reached.
|
||||
*
|
||||
* @param desiredState what state to wait for
|
||||
* @param time the maximum time to wait
|
||||
* @param unit the time unit of the {@code time} argument
|
||||
* @return {@code false} if the waiting time detectably elapsed before reaching {@code desiredState}
|
||||
* @throws InterruptedException if the current thread is interrupted
|
||||
*/
|
||||
public boolean awaitState(Value desiredState, long time, TimeUnit unit) throws InterruptedException {
|
||||
lock.lock();
|
||||
try {
|
||||
long remaining = TimeUnit.NANOSECONDS.convert(time, unit);
|
||||
while (value.get() != desiredState) {
|
||||
if (remaining <= 0L) {
|
||||
return false;
|
||||
}
|
||||
remaining = valueChanged.awaitNanos(remaining);
|
||||
}
|
||||
return true;
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private void signal() {
|
||||
lock.lock();
|
||||
try {
|
||||
valueChanged.signalAll();
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void fireValueChangedEvent() {
|
||||
signal();
|
||||
if (Platform.isFxApplicationThread()) {
|
||||
super.fireValueChangedEvent();
|
||||
} else {
|
||||
Platform.runLater(super::fireValueChangedEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
198
src/main/java/org/cryptomator/common/vaults/VaultStats.java
Normal file
198
src/main/java/org/cryptomator/common/vaults/VaultStats.java
Normal file
@@ -0,0 +1,198 @@
|
||||
package org.cryptomator.common.vaults;
|
||||
|
||||
import org.cryptomator.cryptofs.CryptoFileSystem;
|
||||
import org.cryptomator.cryptofs.CryptoFileSystemStats;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.property.DoubleProperty;
|
||||
import javafx.beans.property.LongProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleDoubleProperty;
|
||||
import javafx.beans.property.SimpleLongProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.concurrent.ScheduledService;
|
||||
import javafx.concurrent.Task;
|
||||
import javafx.util.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
@PerVault
|
||||
public class VaultStats {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(VaultStats.class);
|
||||
|
||||
private final AtomicReference<CryptoFileSystem> fs;
|
||||
private final VaultState state;
|
||||
private final ScheduledService<Optional<CryptoFileSystemStats>> updateService;
|
||||
private final LongProperty bytesPerSecondRead = new SimpleLongProperty();
|
||||
private final LongProperty bytesPerSecondWritten = new SimpleLongProperty();
|
||||
private final LongProperty bytesPerSecondEncrypted = new SimpleLongProperty();
|
||||
private final LongProperty bytesPerSecondDecrypted = new SimpleLongProperty();
|
||||
private final DoubleProperty cacheHitRate = new SimpleDoubleProperty();
|
||||
private final LongProperty toalBytesRead = new SimpleLongProperty();
|
||||
private final LongProperty toalBytesWritten = new SimpleLongProperty();
|
||||
private final LongProperty totalBytesEncrypted = new SimpleLongProperty();
|
||||
private final LongProperty totalBytesDecrypted = new SimpleLongProperty();
|
||||
private final LongProperty filesRead = new SimpleLongProperty();
|
||||
private final LongProperty filesWritten = new SimpleLongProperty();
|
||||
private final ObjectProperty<Instant> lastActivity = new SimpleObjectProperty<>();
|
||||
|
||||
@Inject
|
||||
VaultStats(AtomicReference<CryptoFileSystem> fs, VaultState state, ExecutorService executor) {
|
||||
this.fs = fs;
|
||||
this.state = state;
|
||||
this.updateService = new UpdateStatsService();
|
||||
updateService.setExecutor(executor);
|
||||
updateService.setPeriod(Duration.seconds(1));
|
||||
|
||||
state.addListener(this::vaultStateChanged);
|
||||
}
|
||||
|
||||
private void vaultStateChanged(@SuppressWarnings("unused") Observable observable) {
|
||||
if (VaultState.Value.UNLOCKED == state.get()) {
|
||||
assert fs.get() != null;
|
||||
LOG.debug("start recording stats");
|
||||
Platform.runLater(() -> {
|
||||
lastActivity.set(Instant.now());
|
||||
updateService.restart();
|
||||
});
|
||||
} else {
|
||||
LOG.debug("stop recording stats");
|
||||
Platform.runLater(() -> updateService.cancel());
|
||||
}
|
||||
}
|
||||
|
||||
private void updateStats(Optional<CryptoFileSystemStats> stats) {
|
||||
assert Platform.isFxApplicationThread();
|
||||
bytesPerSecondRead.set(stats.map(CryptoFileSystemStats::pollBytesRead).orElse(0L));
|
||||
bytesPerSecondWritten.set(stats.map(CryptoFileSystemStats::pollBytesWritten).orElse(0L));
|
||||
cacheHitRate.set(stats.map(this::getCacheHitRate).orElse(0.0));
|
||||
bytesPerSecondDecrypted.set(stats.map(CryptoFileSystemStats::pollBytesDecrypted).orElse(0L));
|
||||
bytesPerSecondEncrypted.set(stats.map(CryptoFileSystemStats::pollBytesEncrypted).orElse(0L));
|
||||
toalBytesRead.set(stats.map(CryptoFileSystemStats::pollTotalBytesRead).orElse(0L));
|
||||
toalBytesWritten.set(stats.map(CryptoFileSystemStats::pollTotalBytesWritten).orElse(0L));
|
||||
totalBytesEncrypted.set(stats.map(CryptoFileSystemStats::pollTotalBytesEncrypted).orElse(0L));
|
||||
totalBytesDecrypted.set(stats.map(CryptoFileSystemStats::pollTotalBytesDecrypted).orElse(0L));
|
||||
var oldAccessCount = filesRead.get() + filesWritten.get();
|
||||
filesRead.set(stats.map(CryptoFileSystemStats::pollAmountOfAccessesRead).orElse(0L));
|
||||
filesWritten.set(stats.map(CryptoFileSystemStats::pollAmountOfAccessesWritten).orElse(0L));
|
||||
var newAccessCount = filesRead.get() + filesWritten.get();
|
||||
|
||||
// check for any I/O activity
|
||||
if (newAccessCount > oldAccessCount) {
|
||||
lastActivity.set(Instant.now());
|
||||
}
|
||||
}
|
||||
|
||||
private double getCacheHitRate(CryptoFileSystemStats stats) {
|
||||
long accesses = stats.pollChunkCacheAccesses();
|
||||
long hits = stats.pollChunkCacheHits();
|
||||
if (accesses == 0) {
|
||||
return 0.0;
|
||||
} else {
|
||||
return hits / (double) accesses;
|
||||
}
|
||||
}
|
||||
|
||||
private class UpdateStatsService extends ScheduledService<Optional<CryptoFileSystemStats>> {
|
||||
|
||||
private UpdateStatsService() {
|
||||
setOnFailed(event -> LOG.error("Error in UpdateStateService.", getException()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Task<Optional<CryptoFileSystemStats>> createTask() {
|
||||
return new Task<>() {
|
||||
@Override
|
||||
protected Optional<CryptoFileSystemStats> call() {
|
||||
return Optional.ofNullable(fs.get()).map(CryptoFileSystem::getStats);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void succeeded() {
|
||||
assert getValue() != null;
|
||||
updateStats(getValue());
|
||||
super.succeeded();
|
||||
}
|
||||
}
|
||||
|
||||
/* Observables */
|
||||
|
||||
public LongProperty bytesPerSecondReadProperty() {
|
||||
return bytesPerSecondRead;
|
||||
}
|
||||
|
||||
public long getBytesPerSecondRead() {
|
||||
return bytesPerSecondRead.get();
|
||||
}
|
||||
|
||||
public LongProperty bytesPerSecondWrittenProperty() {
|
||||
return bytesPerSecondWritten;
|
||||
}
|
||||
|
||||
public long getBytesPerSecondWritten() {
|
||||
return bytesPerSecondWritten.get();
|
||||
}
|
||||
|
||||
public LongProperty bytesPerSecondEncryptedProperty() {
|
||||
return bytesPerSecondEncrypted;
|
||||
}
|
||||
|
||||
public long getBytesPerSecondEnrypted() {
|
||||
return bytesPerSecondEncrypted.get();
|
||||
}
|
||||
|
||||
public LongProperty bytesPerSecondDecryptedProperty() {
|
||||
return bytesPerSecondDecrypted;
|
||||
}
|
||||
|
||||
public long getBytesPerSecondDecrypted() {
|
||||
return bytesPerSecondDecrypted.get();
|
||||
}
|
||||
|
||||
public DoubleProperty cacheHitRateProperty() { return cacheHitRate; }
|
||||
|
||||
public double getCacheHitRate() {
|
||||
return cacheHitRate.get();
|
||||
}
|
||||
|
||||
public LongProperty toalBytesReadProperty() {return toalBytesRead;}
|
||||
|
||||
public long getTotalBytesRead() { return toalBytesRead.get();}
|
||||
|
||||
public LongProperty toalBytesWrittenProperty() {return toalBytesWritten;}
|
||||
|
||||
public long getTotalBytesWritten() { return toalBytesWritten.get();}
|
||||
|
||||
public LongProperty totalBytesEncryptedProperty() {return totalBytesEncrypted;}
|
||||
|
||||
public long getTotalBytesEncrypted() { return totalBytesEncrypted.get();}
|
||||
|
||||
public LongProperty totalBytesDecryptedProperty() {return totalBytesDecrypted;}
|
||||
|
||||
public long getTotalBytesDecrypted() { return totalBytesDecrypted.get();}
|
||||
|
||||
public LongProperty filesRead() { return filesRead;}
|
||||
|
||||
public long getFilesRead() { return filesRead.get();}
|
||||
|
||||
public LongProperty filesWritten() {return filesWritten;}
|
||||
|
||||
public long getFilesWritten() {return filesWritten.get();}
|
||||
|
||||
public ObjectProperty<Instant> lastActivityProperty() {
|
||||
return lastActivity;
|
||||
}
|
||||
|
||||
public Instant getLastActivity() {
|
||||
return lastActivity.get();
|
||||
}
|
||||
}
|
||||
102
src/main/java/org/cryptomator/common/vaults/Volume.java
Normal file
102
src/main/java/org/cryptomator/common/vaults/Volume.java
Normal file
@@ -0,0 +1,102 @@
|
||||
package org.cryptomator.common.vaults;
|
||||
|
||||
import org.cryptomator.common.mountpoint.InvalidMountPointException;
|
||||
import org.cryptomator.common.settings.VolumeImpl;
|
||||
import org.cryptomator.cryptofs.CryptoFileSystem;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletionStage;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* Takes a Volume and usess it to mount an unlocked vault
|
||||
*/
|
||||
public interface Volume {
|
||||
|
||||
/**
|
||||
* Checks in constant time whether this volume type is supported on the system running Cryptomator.
|
||||
*
|
||||
* @return true if this volume can be mounted
|
||||
*/
|
||||
boolean isSupported();
|
||||
|
||||
/**
|
||||
* Gets the coresponding enum type of the {@link VolumeImpl volume implementation ("VolumeImpl")} that is implemented by this Volume.
|
||||
*
|
||||
* @return the type of implementation as defined by the {@link VolumeImpl VolumeImpl enum}
|
||||
*/
|
||||
VolumeImpl getImplementationType();
|
||||
|
||||
/**
|
||||
* @param fs
|
||||
* @throws IOException
|
||||
*/
|
||||
void mount(CryptoFileSystem fs, String mountFlags, Consumer<Throwable> onExitAction) throws IOException, VolumeException, InvalidMountPointException;
|
||||
|
||||
/**
|
||||
* Reveals the mounted volume.
|
||||
* <p>
|
||||
* The given {@code revealer} might be used to do it, but not necessarily.
|
||||
*
|
||||
* @param revealer An object capable of revealing the location of the mounted vault to view the content (e.g. in the default file browser).
|
||||
* @throws VolumeException
|
||||
*/
|
||||
void reveal(Revealer revealer) throws VolumeException;
|
||||
|
||||
void unmount() throws VolumeException;
|
||||
|
||||
Optional<Path> getMountPoint();
|
||||
|
||||
MountPointRequirement getMountPointRequirement();
|
||||
|
||||
// optional forced unmounting:
|
||||
|
||||
default boolean supportsForcedUnmount() {
|
||||
return false;
|
||||
}
|
||||
|
||||
default void unmountForced() throws VolumeException {
|
||||
throw new VolumeException("Operation not supported.");
|
||||
}
|
||||
|
||||
static VolumeImpl[] getCurrentSupportedAdapters() {
|
||||
return Stream.of(VolumeImpl.values()).filter(impl -> switch (impl) {
|
||||
case WEBDAV -> WebDavVolume.isSupportedStatic();
|
||||
case DOKANY -> DokanyVolume.isSupportedStatic();
|
||||
case FUSE -> FuseVolume.isSupportedStatic();
|
||||
}).toArray(VolumeImpl[]::new);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exception thrown when a volume-specific command such as mount/unmount/reveal failed.
|
||||
*/
|
||||
class VolumeException extends Exception {
|
||||
|
||||
public VolumeException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public VolumeException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public VolumeException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides and unifies the different Revealer implementations in the different nio-adapters.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
interface Revealer {
|
||||
|
||||
void reveal(Path p) throws VolumeException;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
170
src/main/java/org/cryptomator/common/vaults/WebDavVolume.java
Normal file
170
src/main/java/org/cryptomator/common/vaults/WebDavVolume.java
Normal file
@@ -0,0 +1,170 @@
|
||||
package org.cryptomator.common.vaults;
|
||||
|
||||
|
||||
import com.google.common.base.CharMatcher;
|
||||
import org.cryptomator.common.settings.Settings;
|
||||
import org.cryptomator.common.settings.VaultSettings;
|
||||
import org.cryptomator.common.settings.VolumeImpl;
|
||||
import org.cryptomator.cryptofs.CryptoFileSystem;
|
||||
import org.cryptomator.frontend.webdav.WebDavServer;
|
||||
import org.cryptomator.frontend.webdav.mount.MountParams;
|
||||
import org.cryptomator.frontend.webdav.mount.Mounter;
|
||||
import org.cryptomator.frontend.webdav.servlet.WebDavServletController;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Provider;
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
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 WebDavServer server;
|
||||
private WebDavServletController servlet;
|
||||
private Mounter.Mount mount;
|
||||
private Consumer<Throwable> onExitAction;
|
||||
|
||||
@Inject
|
||||
public WebDavVolume(Provider<WebDavServer> serverProvider, VaultSettings vaultSettings, Settings settings, WindowsDriveLetters windowsDriveLetters) {
|
||||
this.serverProvider = serverProvider;
|
||||
this.vaultSettings = vaultSettings;
|
||||
this.settings = settings;
|
||||
this.windowsDriveLetters = windowsDriveLetters;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mount(CryptoFileSystem fs, String mountFlags, Consumer<Throwable> onExitAction) throws VolumeException {
|
||||
startServlet(fs);
|
||||
mountServlet();
|
||||
this.onExitAction = onExitAction;
|
||||
}
|
||||
|
||||
private void startServlet(CryptoFileSystem fs) {
|
||||
if (server == null) {
|
||||
server = serverProvider.get();
|
||||
}
|
||||
if (!server.isRunning()) {
|
||||
server.start();
|
||||
}
|
||||
CharMatcher acceptable = CharMatcher.inRange('0', '9').or(CharMatcher.inRange('A', 'Z')).or(CharMatcher.inRange('a', 'z'));
|
||||
String urlConformMountName = acceptable.negate().collapseFrom(vaultSettings.mountName().get(), '_');
|
||||
servlet = server.createWebDavServlet(fs.getPath("/"), vaultSettings.getId() + "/" + urlConformMountName);
|
||||
servlet.start();
|
||||
}
|
||||
|
||||
private void mountServlet() throws VolumeException {
|
||||
if (servlet == null) {
|
||||
throw new IllegalStateException("Mounting requires unlocked WebDAV servlet.");
|
||||
}
|
||||
|
||||
//on windows, prevent an automatic drive letter selection in the upstream library. Either we choose already a specifc one or there is no free.
|
||||
Supplier<String> driveLetterSupplier;
|
||||
if (System.getProperty("os.name").toLowerCase().contains("windows") && vaultSettings.winDriveLetter().isEmpty().get()) {
|
||||
driveLetterSupplier = () -> windowsDriveLetters.getAvailableDriveLetter().orElse(null);
|
||||
} else {
|
||||
driveLetterSupplier = () -> vaultSettings.winDriveLetter().get();
|
||||
}
|
||||
|
||||
MountParams mountParams = MountParams.create() //
|
||||
.withWindowsDriveLetter(driveLetterSupplier.get()) //
|
||||
.withPreferredGvfsScheme(settings.preferredGvfsScheme().get().getPrefix())//
|
||||
.withWebdavHostname(getLocalhostAliasOrNull()) //
|
||||
.build();
|
||||
try {
|
||||
this.mount = servlet.mount(mountParams); // might block this thread for a while
|
||||
} catch (Mounter.CommandFailedException e) {
|
||||
throw new VolumeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reveal(Revealer revealer) throws VolumeException {
|
||||
try {
|
||||
mount.reveal(revealer::reveal);
|
||||
} catch (Exception e) {
|
||||
throw new VolumeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void unmount() throws VolumeException {
|
||||
try {
|
||||
mount.unmount();
|
||||
} catch (Mounter.CommandFailedException e) {
|
||||
throw new VolumeException(e);
|
||||
}
|
||||
cleanup();
|
||||
onExitAction.accept(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void unmountForced() throws VolumeException {
|
||||
try {
|
||||
mount.forced().orElseThrow(IllegalStateException::new).unmount();
|
||||
} catch (Mounter.CommandFailedException e) {
|
||||
throw new VolumeException(e);
|
||||
}
|
||||
cleanup();
|
||||
onExitAction.accept(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Path> getMountPoint() {
|
||||
return mount.getMountPoint();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MountPointRequirement getMountPointRequirement() {
|
||||
return MountPointRequirement.NONE;
|
||||
}
|
||||
|
||||
private String getLocalhostAliasOrNull() {
|
||||
try {
|
||||
InetAddress alias = InetAddress.getByName(LOCALHOST_ALIAS);
|
||||
if (alias.getHostAddress().equals("127.0.0.1")) {
|
||||
return LOCALHOST_ALIAS;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (UnknownHostException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void cleanup() {
|
||||
if (servlet != null) {
|
||||
servlet.stop();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSupported() {
|
||||
return WebDavVolume.isSupportedStatic();
|
||||
}
|
||||
|
||||
@Override
|
||||
public VolumeImpl getImplementationType() {
|
||||
return VolumeImpl.WEBDAV;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsForcedUnmount() {
|
||||
return mount != null && mount.forced().isPresent();
|
||||
}
|
||||
|
||||
|
||||
public static boolean isSupportedStatic() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the accompanying LICENSE file.
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.common.vaults;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Sets;
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
@Singleton
|
||||
public final class WindowsDriveLetters {
|
||||
|
||||
private static final Set<String> C_TO_Z;
|
||||
|
||||
static {
|
||||
try (IntStream stream = IntStream.rangeClosed('C', 'Z')) {
|
||||
C_TO_Z = stream.mapToObj(i -> String.valueOf((char) i)).collect(ImmutableSet.toImmutableSet());
|
||||
}
|
||||
}
|
||||
|
||||
@Inject
|
||||
public WindowsDriveLetters() {
|
||||
}
|
||||
|
||||
public Set<String> getAllDriveLetters() {
|
||||
return C_TO_Z;
|
||||
}
|
||||
|
||||
public Set<String> getOccupiedDriveLetters() {
|
||||
if (!SystemUtils.IS_OS_WINDOWS) {
|
||||
return Set.of();
|
||||
} else {
|
||||
Iterable<Path> rootDirs = FileSystems.getDefault().getRootDirectories();
|
||||
return StreamSupport.stream(rootDirs.spliterator(), false).map(p -> p.toString().substring(0, 1)).collect(Collectors.toSet());
|
||||
}
|
||||
}
|
||||
|
||||
public Set<String> getAvailableDriveLetters() {
|
||||
return Sets.difference(getAllDriveLetters(), getOccupiedDriveLetters());
|
||||
}
|
||||
|
||||
public Optional<String> getAvailableDriveLetter() {
|
||||
return getAvailableDriveLetters().stream().findFirst();
|
||||
}
|
||||
|
||||
public Optional<Path> getAvailableDriveLetterPath() {
|
||||
return getAvailableDriveLetter().map(this::toPath);
|
||||
}
|
||||
|
||||
public Path toPath(String driveLetter) {
|
||||
return Path.of(driveLetter + ":\\");
|
||||
}
|
||||
}
|
||||
102
src/main/java/org/cryptomator/launcher/Cryptomator.java
Normal file
102
src/main/java/org/cryptomator/launcher/Cryptomator.java
Normal file
@@ -0,0 +1,102 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the accompanying LICENSE file.
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.launcher;
|
||||
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.logging.DebugMode;
|
||||
import org.cryptomator.logging.LoggerConfiguration;
|
||||
import org.cryptomator.ui.launcher.UiLauncher;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Singleton;
|
||||
import java.io.IOException;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
|
||||
@Singleton
|
||||
public class Cryptomator {
|
||||
|
||||
// DaggerCryptomatorComponent gets generated by Dagger.
|
||||
// Run Maven and include target/generated-sources/annotations in your IDE.
|
||||
private static final CryptomatorComponent CRYPTOMATOR_COMPONENT = DaggerCryptomatorComponent.create();
|
||||
private static final Logger LOG = LoggerFactory.getLogger(Cryptomator.class);
|
||||
|
||||
private final LoggerConfiguration logConfig;
|
||||
private final DebugMode debugMode;
|
||||
private final IpcFactory ipcFactory;
|
||||
private final Optional<String> applicationVersion;
|
||||
private final CountDownLatch shutdownLatch;
|
||||
private final UiLauncher uiLauncher;
|
||||
|
||||
@Inject
|
||||
Cryptomator(LoggerConfiguration logConfig, DebugMode debugMode, IpcFactory ipcFactory, @Named("applicationVersion") Optional<String> applicationVersion, @Named("shutdownLatch") CountDownLatch shutdownLatch, UiLauncher uiLauncher) {
|
||||
this.logConfig = logConfig;
|
||||
this.debugMode = debugMode;
|
||||
this.ipcFactory = ipcFactory;
|
||||
this.applicationVersion = applicationVersion;
|
||||
this.shutdownLatch = shutdownLatch;
|
||||
this.uiLauncher = uiLauncher;
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
int exitCode = CRYPTOMATOR_COMPONENT.application().run(args);
|
||||
System.exit(exitCode); // end remaining non-daemon threads.
|
||||
}
|
||||
|
||||
/**
|
||||
* Main entry point of the application launcher.
|
||||
*
|
||||
* @param args The arguments passed to this program via {@link #main(String[])}.
|
||||
* @return Nonzero exit code in case of an error.
|
||||
*/
|
||||
private int run(String[] args) {
|
||||
logConfig.init();
|
||||
LOG.info("Starting Cryptomator {} on {} {} ({})", applicationVersion.orElse("SNAPSHOT"), SystemUtils.OS_NAME, SystemUtils.OS_VERSION, SystemUtils.OS_ARCH);
|
||||
debugMode.initialize();
|
||||
|
||||
/*
|
||||
* Attempts to create an IPC connection to a running Cryptomator instance and sends it the given args.
|
||||
* If no external process could be reached, the args will be handled by the loopback IPC endpoint.
|
||||
*/
|
||||
try (IpcFactory.IpcEndpoint endpoint = ipcFactory.create()) {
|
||||
endpoint.getRemote().handleLaunchArgs(args); // if we are the server, getRemote() returns self.
|
||||
if (endpoint.isConnectedToRemote()) {
|
||||
endpoint.getRemote().revealRunningApp();
|
||||
LOG.info("Found running application instance. Shutting down...");
|
||||
return 2;
|
||||
} else {
|
||||
LOG.debug("Did not find running application instance. Launching GUI...");
|
||||
return runGuiApplication();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to initiate inter-process communication.", e);
|
||||
return runGuiApplication();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Launches the JavaFX application and waits until shutdown is requested.
|
||||
*
|
||||
* @return Nonzero exit code in case of an error.
|
||||
* @implNote This method blocks until {@link #shutdownLatch} reached zero.
|
||||
*/
|
||||
private int runGuiApplication() {
|
||||
try {
|
||||
uiLauncher.launch();
|
||||
shutdownLatch.await();
|
||||
LOG.info("UI shut down");
|
||||
return 0;
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package org.cryptomator.launcher;
|
||||
|
||||
import dagger.Component;
|
||||
import org.cryptomator.common.CommonsModule;
|
||||
import org.cryptomator.logging.LoggerModule;
|
||||
import org.cryptomator.ui.launcher.UiLauncherModule;
|
||||
|
||||
import javax.inject.Singleton;
|
||||
|
||||
@Singleton
|
||||
@Component(modules = {CryptomatorModule.class, CommonsModule.class, LoggerModule.class, UiLauncherModule.class})
|
||||
public interface CryptomatorComponent {
|
||||
|
||||
Cryptomator application();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package org.cryptomator.launcher;
|
||||
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Singleton;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
|
||||
@Module
|
||||
class CryptomatorModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@Named("shutdownLatch")
|
||||
static CountDownLatch provideShutdownLatch() {
|
||||
return new CountDownLatch(1);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@Named("applicationVersion")
|
||||
static Optional<String> provideApplicationVersion() {
|
||||
return Optional.ofNullable(Cryptomator.class.getPackage().getImplementationVersion());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
|
||||
* All rights reserved.
|
||||
*
|
||||
* This class is licensed under the LGPL 3.0 (https://www.gnu.org/licenses/lgpl-3.0.de.html).
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.launcher;
|
||||
|
||||
import org.cryptomator.ui.launcher.AppLaunchEvent;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Singleton;
|
||||
import java.awt.Desktop;
|
||||
import java.awt.desktop.OpenFilesEvent;
|
||||
import java.io.File;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.InvalidPathException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Singleton
|
||||
class FileOpenRequestHandler {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(FileOpenRequestHandler.class);
|
||||
private final BlockingQueue<AppLaunchEvent> launchEventQueue;
|
||||
|
||||
@Inject
|
||||
public FileOpenRequestHandler(@Named("launchEventQueue") BlockingQueue<AppLaunchEvent> launchEventQueue) {
|
||||
this.launchEventQueue = launchEventQueue;
|
||||
if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.APP_OPEN_FILE)) {
|
||||
Desktop.getDesktop().setOpenFileHandler(this::openFiles);
|
||||
}
|
||||
}
|
||||
|
||||
private void openFiles(OpenFilesEvent evt) {
|
||||
Collection<Path> pathsToOpen = evt.getFiles().stream().map(File::toPath).toList();
|
||||
AppLaunchEvent launchEvent = new AppLaunchEvent(AppLaunchEvent.EventType.OPEN_FILE, pathsToOpen);
|
||||
tryToEnqueueFileOpenRequest(launchEvent);
|
||||
}
|
||||
|
||||
public void handleLaunchArgs(String[] args) {
|
||||
handleLaunchArgs(FileSystems.getDefault(), args);
|
||||
}
|
||||
|
||||
// visible for testing
|
||||
void handleLaunchArgs(FileSystem fs, String[] args) {
|
||||
Collection<Path> pathsToOpen = Arrays.stream(args).map(str -> {
|
||||
try {
|
||||
return fs.getPath(str);
|
||||
} catch (InvalidPathException e) {
|
||||
LOG.trace("Argument not a valid path: {}", str);
|
||||
return null;
|
||||
}
|
||||
}).filter(Objects::nonNull).toList();
|
||||
if (!pathsToOpen.isEmpty()) {
|
||||
AppLaunchEvent launchEvent = new AppLaunchEvent(AppLaunchEvent.EventType.OPEN_FILE, pathsToOpen);
|
||||
tryToEnqueueFileOpenRequest(launchEvent);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void tryToEnqueueFileOpenRequest(AppLaunchEvent launchEvent) {
|
||||
if (!launchEventQueue.offer(launchEvent)) {
|
||||
LOG.warn("Could not enqueue application launch event.", launchEvent);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
258
src/main/java/org/cryptomator/launcher/IpcFactory.java
Normal file
258
src/main/java/org/cryptomator/launcher/IpcFactory.java
Normal file
@@ -0,0 +1,258 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the accompanying LICENSE file.
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.launcher;
|
||||
|
||||
import com.google.common.io.MoreFiles;
|
||||
import org.cryptomator.common.Environment;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketException;
|
||||
import java.net.UnknownHostException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.ReadableByteChannel;
|
||||
import java.nio.channels.WritableByteChannel;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.rmi.NotBoundException;
|
||||
import java.rmi.registry.LocateRegistry;
|
||||
import java.rmi.registry.Registry;
|
||||
import java.rmi.server.RMIClientSocketFactory;
|
||||
import java.rmi.server.RMIServerSocketFactory;
|
||||
import java.rmi.server.RMISocketFactory;
|
||||
import java.rmi.server.UnicastRemoteObject;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* First running application on a machine opens a server socket. Further processes will connect as clients.
|
||||
*/
|
||||
@Singleton
|
||||
class IpcFactory {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(IpcFactory.class);
|
||||
private static final String RMI_NAME = "Cryptomator";
|
||||
|
||||
private final List<Path> portFilePaths;
|
||||
private final IpcProtocolImpl ipcHandler;
|
||||
|
||||
@Inject
|
||||
public IpcFactory(Environment env, IpcProtocolImpl ipcHandler) {
|
||||
this.portFilePaths = env.getIpcPortPath().collect(Collectors.toUnmodifiableList());
|
||||
this.ipcHandler = ipcHandler;
|
||||
}
|
||||
|
||||
public IpcEndpoint create() {
|
||||
if (portFilePaths.isEmpty()) {
|
||||
LOG.warn("No IPC port file path specified.");
|
||||
return new SelfEndpoint(ipcHandler);
|
||||
} else {
|
||||
System.setProperty("java.rmi.server.hostname", "localhost");
|
||||
return attemptClientConnection().or(this::createServerEndpoint).orElseGet(() -> new SelfEndpoint(ipcHandler));
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<IpcEndpoint> attemptClientConnection() {
|
||||
for (Path portFilePath : portFilePaths) {
|
||||
try {
|
||||
int port = readPort(portFilePath);
|
||||
LOG.debug("[Client] Connecting to port {}...", port);
|
||||
Registry registry = LocateRegistry.getRegistry("localhost", port, new ClientSocketFactory());
|
||||
IpcProtocol remoteInterface = (IpcProtocol) registry.lookup(RMI_NAME);
|
||||
return Optional.of(new ClientEndpoint(remoteInterface));
|
||||
} catch (NotBoundException | IOException e) {
|
||||
LOG.debug("[Client] Failed to connect.");
|
||||
// continue with next portFilePath...
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
private int readPort(Path portFilePath) throws IOException {
|
||||
try (ReadableByteChannel ch = Files.newByteChannel(portFilePath, StandardOpenOption.READ)) {
|
||||
LOG.debug("[Client] Reading IPC port from {}", portFilePath);
|
||||
ByteBuffer buf = ByteBuffer.allocate(Integer.BYTES);
|
||||
if (ch.read(buf) == Integer.BYTES) {
|
||||
buf.flip();
|
||||
return buf.getInt();
|
||||
} else {
|
||||
throw new IOException("Invalid IPC port file.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<IpcEndpoint> createServerEndpoint() {
|
||||
assert !portFilePaths.isEmpty();
|
||||
Path portFilePath = portFilePaths.get(0);
|
||||
try {
|
||||
ServerSocket socket = new ServerSocket(0, Byte.MAX_VALUE, InetAddress.getByName("localhost"));
|
||||
RMIClientSocketFactory csf = RMISocketFactory.getDefaultSocketFactory();
|
||||
SingletonServerSocketFactory ssf = new SingletonServerSocketFactory(socket);
|
||||
Registry registry = LocateRegistry.createRegistry(0, csf, ssf);
|
||||
UnicastRemoteObject.exportObject(ipcHandler, 0);
|
||||
registry.rebind(RMI_NAME, ipcHandler);
|
||||
writePort(portFilePath, socket.getLocalPort());
|
||||
return Optional.of(new ServerEndpoint(ipcHandler, socket, registry, portFilePath));
|
||||
} catch (IOException e) {
|
||||
LOG.warn("[Server] Failed to create IPC server.", e);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private void writePort(Path portFilePath, int port) throws IOException {
|
||||
ByteBuffer buf = ByteBuffer.allocate(Integer.BYTES);
|
||||
buf.putInt(port);
|
||||
buf.flip();
|
||||
MoreFiles.createParentDirectories(portFilePath);
|
||||
try (WritableByteChannel ch = Files.newByteChannel(portFilePath, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {
|
||||
if (ch.write(buf) != Integer.BYTES) {
|
||||
throw new IOException("Did not write expected number of bytes.");
|
||||
}
|
||||
}
|
||||
LOG.debug("[Server] Wrote IPC port {} to {}", port, portFilePath);
|
||||
}
|
||||
|
||||
interface IpcEndpoint extends Closeable {
|
||||
|
||||
boolean isConnectedToRemote();
|
||||
|
||||
IpcProtocol getRemote();
|
||||
|
||||
}
|
||||
|
||||
static class SelfEndpoint implements IpcEndpoint {
|
||||
|
||||
protected final IpcProtocol remoteObject;
|
||||
|
||||
SelfEndpoint(IpcProtocol remoteObject) {
|
||||
this.remoteObject = remoteObject;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConnectedToRemote() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IpcProtocol getRemote() {
|
||||
return remoteObject;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
|
||||
static class ClientEndpoint implements IpcEndpoint {
|
||||
|
||||
private final IpcProtocol remoteInterface;
|
||||
|
||||
public ClientEndpoint(IpcProtocol remoteInterface) {
|
||||
this.remoteInterface = remoteInterface;
|
||||
}
|
||||
|
||||
public IpcProtocol getRemote() {
|
||||
return remoteInterface;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConnectedToRemote() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
// no-op
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class ServerEndpoint extends SelfEndpoint {
|
||||
|
||||
private final ServerSocket socket;
|
||||
private final Registry registry;
|
||||
private final Path portFilePath;
|
||||
|
||||
private ServerEndpoint(IpcProtocol remoteObject, ServerSocket socket, Registry registry, Path portFilePath) {
|
||||
super(remoteObject);
|
||||
this.socket = socket;
|
||||
this.registry = registry;
|
||||
this.portFilePath = portFilePath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
try {
|
||||
registry.unbind(RMI_NAME);
|
||||
UnicastRemoteObject.unexportObject(remoteObject, true);
|
||||
socket.close();
|
||||
Files.deleteIfExists(portFilePath);
|
||||
LOG.debug("[Server] Shut down");
|
||||
} catch (NotBoundException | IOException e) {
|
||||
LOG.warn("[Server] Error shutting down:", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Always returns the same pre-constructed server socket.
|
||||
*/
|
||||
private static class SingletonServerSocketFactory implements RMIServerSocketFactory {
|
||||
|
||||
private final ServerSocket socket;
|
||||
|
||||
public SingletonServerSocketFactory(ServerSocket socket) {
|
||||
this.socket = socket;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized ServerSocket createServerSocket(int port) throws IOException {
|
||||
if (port != 0) {
|
||||
throw new IllegalArgumentException("This factory doesn't support specific ports.");
|
||||
}
|
||||
return this.socket;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates client sockets with short timeouts.
|
||||
*/
|
||||
private static class ClientSocketFactory implements RMIClientSocketFactory {
|
||||
|
||||
@Override
|
||||
public Socket createSocket(String host, int port) throws IOException {
|
||||
return new SocketWithFixedTimeout(host, port, 1000);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class SocketWithFixedTimeout extends Socket {
|
||||
|
||||
public SocketWithFixedTimeout(String host, int port, int timeoutInMs) throws UnknownHostException, IOException {
|
||||
super(host, port);
|
||||
super.setSoTimeout(timeoutInMs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void setSoTimeout(int timeout) throws SocketException {
|
||||
// do nothing, timeout is fixed
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
17
src/main/java/org/cryptomator/launcher/IpcProtocol.java
Normal file
17
src/main/java/org/cryptomator/launcher/IpcProtocol.java
Normal file
@@ -0,0 +1,17 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the accompanying LICENSE file.
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.launcher;
|
||||
|
||||
import java.rmi.Remote;
|
||||
import java.rmi.RemoteException;
|
||||
|
||||
interface IpcProtocol extends Remote {
|
||||
|
||||
void revealRunningApp() throws RemoteException;
|
||||
|
||||
void handleLaunchArgs(String... args) throws RemoteException;
|
||||
|
||||
}
|
||||
39
src/main/java/org/cryptomator/launcher/IpcProtocolImpl.java
Normal file
39
src/main/java/org/cryptomator/launcher/IpcProtocolImpl.java
Normal file
@@ -0,0 +1,39 @@
|
||||
package org.cryptomator.launcher;
|
||||
|
||||
import org.cryptomator.ui.launcher.AppLaunchEvent;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Singleton;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
|
||||
@Singleton
|
||||
class IpcProtocolImpl implements IpcProtocol {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(IpcProtocolImpl.class);
|
||||
|
||||
private final FileOpenRequestHandler fileOpenRequestHandler;
|
||||
private final BlockingQueue<AppLaunchEvent> launchEventQueue;
|
||||
|
||||
@Inject
|
||||
public IpcProtocolImpl(FileOpenRequestHandler fileOpenRequestHandler, @Named("launchEventQueue") BlockingQueue<AppLaunchEvent> launchEventQueue) {
|
||||
this.fileOpenRequestHandler = fileOpenRequestHandler;
|
||||
this.launchEventQueue = launchEventQueue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void revealRunningApp() {
|
||||
launchEventQueue.add(new AppLaunchEvent(AppLaunchEvent.EventType.REVEAL_APP, Collections.emptyList()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleLaunchArgs(String... args) {
|
||||
LOG.debug("Received launch args: {}", Arrays.stream(args).reduce((a, b) -> a + ", " + b).orElse(""));
|
||||
fileOpenRequestHandler.handleLaunchArgs(args);
|
||||
}
|
||||
|
||||
}
|
||||
59
src/main/java/org/cryptomator/logging/DebugMode.java
Normal file
59
src/main/java/org/cryptomator/logging/DebugMode.java
Normal file
@@ -0,0 +1,59 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the accompanying LICENSE file.
|
||||
*******************************************************************************/
|
||||
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.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 final Settings settings;
|
||||
private final LoggerContext context;
|
||||
|
||||
@Inject
|
||||
public DebugMode(Settings settings, LoggerContext context) {
|
||||
this.settings = settings;
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public void initialize() {
|
||||
setLogLevels(settings.debugMode().get());
|
||||
settings.debugMode().addListener(this::logLevelChanged);
|
||||
}
|
||||
|
||||
private void logLevelChanged(@SuppressWarnings("unused") ObservableValue<? extends Boolean> observable, @SuppressWarnings("unused") Boolean oldValue, Boolean newValue) {
|
||||
setLogLevels(newValue);
|
||||
}
|
||||
|
||||
private void setLogLevels(boolean debugMode) {
|
||||
if (debugMode) {
|
||||
setLogLevels(LoggerModule.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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package org.cryptomator.logging;
|
||||
|
||||
import ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy;
|
||||
import ch.qos.logback.core.rolling.TriggeringPolicyBase;
|
||||
import ch.qos.logback.core.util.FileSize;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* Triggers a roll-over either on the first log event or if watched log file reaches a certain size
|
||||
*
|
||||
* @param <E> Event type the policy possibly reacts to
|
||||
*/
|
||||
public class LaunchAndSizeBasedTriggerinPolicy<E> extends TriggeringPolicyBase<E> {
|
||||
|
||||
LaunchBasedTriggeringPolicy<E> launchBasedTriggeringPolicy;
|
||||
SizeBasedTriggeringPolicy<E> sizeBasedTriggeringPolicy;
|
||||
|
||||
public LaunchAndSizeBasedTriggerinPolicy(FileSize threshold) {
|
||||
this.launchBasedTriggeringPolicy = new LaunchBasedTriggeringPolicy<>();
|
||||
this.sizeBasedTriggeringPolicy = new SizeBasedTriggeringPolicy<>();
|
||||
sizeBasedTriggeringPolicy.setMaxFileSize(threshold);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTriggeringEvent(File activeFile, E event) {
|
||||
return launchBasedTriggeringPolicy.isTriggeringEvent(activeFile, event) || sizeBasedTriggeringPolicy.isTriggeringEvent(activeFile, event);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the accompanying LICENSE file.
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.logging;
|
||||
|
||||
import ch.qos.logback.core.rolling.TriggeringPolicyBase;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* Triggers a roll-over on the first log event, so each launched application instance will rotate the log.
|
||||
*/
|
||||
public class LaunchBasedTriggeringPolicy<E> extends TriggeringPolicyBase<E> {
|
||||
|
||||
private final AtomicBoolean shouldTrigger = new AtomicBoolean(true);
|
||||
|
||||
@Override
|
||||
public boolean isTriggeringEvent(File activeFile, E event) {
|
||||
return shouldTrigger.get() && shouldTrigger.getAndSet(false);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
128
src/main/java/org/cryptomator/logging/LoggerModule.java
Normal file
128
src/main/java/org/cryptomator/logging/LoggerModule.java
Normal file
@@ -0,0 +1,128 @@
|
||||
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) {
|
||||
if (environment.getLogDir().isPresent()) {
|
||||
Path logDir = environment.getLogDir().get();
|
||||
RollingFileAppender<ILoggingEvent> appender = new RollingFileAppender<>();
|
||||
appender.setContext(context);
|
||||
appender.setFile(logDir.resolve(LOGFILE_NAME).toString());
|
||||
appender.setEncoder(encoder);
|
||||
LaunchAndSizeBasedTriggerinPolicy triggeringPolicy = new LaunchAndSizeBasedTriggerinPolicy(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) {
|
||||
if (environment.getLogDir().isPresent()) {
|
||||
FileAppender<ILoggingEvent> appender = new FileAppender<>();
|
||||
appender.setFile(environment.getLogDir().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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
package org.cryptomator.ui.addvaultwizard;
|
||||
|
||||
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;
|
||||
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.common.StageFactory;
|
||||
import org.cryptomator.ui.mainwindow.MainWindow;
|
||||
import org.cryptomator.ui.recoverykey.RecoveryKeyDisplayController;
|
||||
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Provider;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.stage.Modality;
|
||||
import javafx.stage.Stage;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Map;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
@Module
|
||||
public abstract class AddVaultModule {
|
||||
|
||||
@Provides
|
||||
@AddVaultWizardWindow
|
||||
@AddVaultWizardScoped
|
||||
static FxmlLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) {
|
||||
return new FxmlLoaderFactory(factories, sceneFactory, resourceBundle);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@AddVaultWizardWindow
|
||||
@AddVaultWizardScoped
|
||||
static Stage provideStage(StageFactory factory, @MainWindow Stage owner, ResourceBundle resourceBundle) {
|
||||
Stage stage = factory.create();
|
||||
stage.setTitle(resourceBundle.getString("addvaultwizard.title"));
|
||||
stage.setResizable(false);
|
||||
stage.initModality(Modality.WINDOW_MODAL);
|
||||
stage.initOwner(owner);
|
||||
return stage;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@AddVaultWizardScoped
|
||||
static ObjectProperty<Path> provideVaultPath() {
|
||||
return new SimpleObjectProperty<>();
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Named("vaultName")
|
||||
@AddVaultWizardScoped
|
||||
static StringProperty provideVaultName() {
|
||||
return new SimpleStringProperty("");
|
||||
}
|
||||
|
||||
@Provides
|
||||
@AddVaultWizardWindow
|
||||
@AddVaultWizardScoped
|
||||
static ObjectProperty<Vault> provideVault() {
|
||||
return new SimpleObjectProperty<>();
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Named("recoveryKey")
|
||||
@AddVaultWizardScoped
|
||||
static StringProperty provideRecoveryKey() {
|
||||
return new SimpleStringProperty();
|
||||
}
|
||||
|
||||
// ------------------
|
||||
|
||||
@Provides
|
||||
@FxmlScene(FxmlFile.ADDVAULT_WELCOME)
|
||||
@AddVaultWizardScoped
|
||||
static Scene provideWelcomeScene(@AddVaultWizardWindow FxmlLoaderFactory fxmlLoaders) {
|
||||
return fxmlLoaders.createScene(FxmlFile.ADDVAULT_WELCOME);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FxmlScene(FxmlFile.ADDVAULT_EXISTING)
|
||||
@AddVaultWizardScoped
|
||||
static Scene provideChooseExistingVaultScene(@AddVaultWizardWindow FxmlLoaderFactory fxmlLoaders) {
|
||||
return fxmlLoaders.createScene(FxmlFile.ADDVAULT_EXISTING);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FxmlScene(FxmlFile.ADDVAULT_NEW_NAME)
|
||||
@AddVaultWizardScoped
|
||||
static Scene provideCreateNewVaultNameScene(@AddVaultWizardWindow FxmlLoaderFactory fxmlLoaders) {
|
||||
return fxmlLoaders.createScene(FxmlFile.ADDVAULT_NEW_NAME);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FxmlScene(FxmlFile.ADDVAULT_NEW_LOCATION)
|
||||
@AddVaultWizardScoped
|
||||
static Scene provideCreateNewVaultLocationScene(@AddVaultWizardWindow FxmlLoaderFactory fxmlLoaders) {
|
||||
return fxmlLoaders.createScene(FxmlFile.ADDVAULT_NEW_LOCATION);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FxmlScene(FxmlFile.ADDVAULT_NEW_PASSWORD)
|
||||
@AddVaultWizardScoped
|
||||
static Scene provideCreateNewVaultPasswordScene(@AddVaultWizardWindow FxmlLoaderFactory fxmlLoaders) {
|
||||
return fxmlLoaders.createScene(FxmlFile.ADDVAULT_NEW_PASSWORD);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FxmlScene(FxmlFile.ADDVAULT_NEW_RECOVERYKEY)
|
||||
@AddVaultWizardScoped
|
||||
static Scene provideCreateNewVaultRecoveryKeyScene(@AddVaultWizardWindow FxmlLoaderFactory fxmlLoaders) {
|
||||
return fxmlLoaders.createScene(FxmlFile.ADDVAULT_NEW_RECOVERYKEY);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FxmlScene(FxmlFile.ADDVAULT_SUCCESS)
|
||||
@AddVaultWizardScoped
|
||||
static Scene provideCreateNewVaultSuccessScene(@AddVaultWizardWindow FxmlLoaderFactory fxmlLoaders) {
|
||||
return fxmlLoaders.createScene(FxmlFile.ADDVAULT_SUCCESS);
|
||||
}
|
||||
|
||||
// ------------------
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FxControllerKey(AddVaultWelcomeController.class)
|
||||
abstract FxController bindWelcomeController(AddVaultWelcomeController controller);
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FxControllerKey(ChooseExistingVaultController.class)
|
||||
abstract FxController bindChooseExistingVaultController(ChooseExistingVaultController controller);
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FxControllerKey(CreateNewVaultNameController.class)
|
||||
abstract FxController bindCreateNewVaultNameController(CreateNewVaultNameController controller);
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FxControllerKey(CreateNewVaultLocationController.class)
|
||||
abstract FxController bindCreateNewVaultLocationController(CreateNewVaultLocationController controller);
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FxControllerKey(CreateNewVaultPasswordController.class)
|
||||
abstract FxController bindCreateNewVaultPasswordController(CreateNewVaultPasswordController controller);
|
||||
|
||||
@Provides
|
||||
@IntoMap
|
||||
@FxControllerKey(NewPasswordController.class)
|
||||
static FxController provideNewPasswordController(ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater) {
|
||||
return new NewPasswordController(resourceBundle, strengthRater);
|
||||
}
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FxControllerKey(CreateNewVaultRecoveryKeyController.class)
|
||||
abstract FxController bindCreateNewVaultRecoveryKeyController(CreateNewVaultRecoveryKeyController controller);
|
||||
|
||||
@Provides
|
||||
@IntoMap
|
||||
@FxControllerKey(RecoveryKeyDisplayController.class)
|
||||
static FxController provideRecoveryKeyDisplayController(@AddVaultWizardWindow Stage window, @Named("vaultName") StringProperty vaultName, @Named("recoveryKey") StringProperty recoveryKey, ResourceBundle localization) {
|
||||
return new RecoveryKeyDisplayController(window, vaultName.get(), recoveryKey.get(), localization);
|
||||
}
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FxControllerKey(AddVaultSuccessController.class)
|
||||
abstract FxController bindAddVaultSuccessController(AddVaultSuccessController controller);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package org.cryptomator.ui.addvaultwizard;
|
||||
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.fxapp.FxApplication;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.stage.Stage;
|
||||
import java.util.Optional;
|
||||
|
||||
@AddVaultWizardScoped
|
||||
public class AddVaultSuccessController implements FxController {
|
||||
|
||||
private final FxApplication fxApplication;
|
||||
private final Stage window;
|
||||
private final ReadOnlyObjectProperty<Vault> vault;
|
||||
|
||||
@Inject
|
||||
AddVaultSuccessController(FxApplication fxApplication, @AddVaultWizardWindow Stage window, @AddVaultWizardWindow ObjectProperty<Vault> vault) {
|
||||
this.fxApplication = fxApplication;
|
||||
this.window = window;
|
||||
this.vault = vault;
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void unlockAndClose() {
|
||||
close();
|
||||
fxApplication.startUnlockWorkflow(vault.get(), Optional.of(window));
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void close() {
|
||||
window.close();
|
||||
}
|
||||
|
||||
/* Observables */
|
||||
|
||||
public ReadOnlyObjectProperty<Vault> vaultProperty() {
|
||||
return vault;
|
||||
}
|
||||
|
||||
public Vault getVault() {
|
||||
return vault.get();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package org.cryptomator.ui.addvaultwizard;
|
||||
|
||||
import dagger.Lazy;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.common.FxmlFile;
|
||||
import org.cryptomator.ui.common.FxmlScene;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
@AddVaultWizardScoped
|
||||
public class AddVaultWelcomeController implements FxController {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AddVaultWelcomeController.class);
|
||||
private final Stage window;
|
||||
private final Lazy<Scene> chooseExistingVaultScene;
|
||||
private final Lazy<Scene> createNewVaultScene;
|
||||
|
||||
@Inject
|
||||
AddVaultWelcomeController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_EXISTING) Lazy<Scene> chooseExistingVaultScene, @FxmlScene(FxmlFile.ADDVAULT_NEW_NAME) Lazy<Scene> createNewVaultScene) {
|
||||
this.window = window;
|
||||
this.chooseExistingVaultScene = chooseExistingVaultScene;
|
||||
this.createNewVaultScene = createNewVaultScene;
|
||||
}
|
||||
|
||||
public void createNewVault() {
|
||||
LOG.debug("AddVaultWelcomeController.createNewVault()");
|
||||
window.setScene(createNewVaultScene.get());
|
||||
}
|
||||
|
||||
public void chooseExistingVault() {
|
||||
LOG.debug("AddVaultWelcomeController.chooseExistingVault()");
|
||||
window.setScene(chooseExistingVaultScene.get());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the accompanying LICENSE file.
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.ui.addvaultwizard;
|
||||
|
||||
import dagger.Lazy;
|
||||
import dagger.Subcomponent;
|
||||
import org.cryptomator.ui.common.FxmlFile;
|
||||
import org.cryptomator.ui.common.FxmlScene;
|
||||
|
||||
import javafx.scene.Scene;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
@AddVaultWizardScoped
|
||||
@Subcomponent(modules = {AddVaultModule.class})
|
||||
public interface AddVaultWizardComponent {
|
||||
|
||||
@AddVaultWizardWindow
|
||||
Stage window();
|
||||
|
||||
@FxmlScene(FxmlFile.ADDVAULT_WELCOME)
|
||||
Lazy<Scene> scene();
|
||||
|
||||
default void showAddVaultWizard() {
|
||||
Stage stage = window();
|
||||
stage.setScene(scene().get());
|
||||
stage.sizeToScene();
|
||||
stage.show();
|
||||
}
|
||||
|
||||
@Subcomponent.Builder
|
||||
interface Builder {
|
||||
|
||||
AddVaultWizardComponent build();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.cryptomator.ui.addvaultwizard;
|
||||
|
||||
import javax.inject.Scope;
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
@Scope
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@interface AddVaultWizardScoped {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.cryptomator.ui.addvaultwizard;
|
||||
|
||||
import javax.inject.Qualifier;
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
|
||||
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
|
||||
@Qualifier
|
||||
@Documented
|
||||
@Retention(RUNTIME)
|
||||
@interface AddVaultWizardWindow {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package org.cryptomator.ui.addvaultwizard;
|
||||
|
||||
import dagger.Lazy;
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.common.vaults.VaultListManager;
|
||||
import org.cryptomator.ui.common.ErrorComponent;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.common.FxmlFile;
|
||||
import org.cryptomator.ui.common.FxmlScene;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.stage.Stage;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.file.NoSuchFileException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
@AddVaultWizardScoped
|
||||
public class ChooseExistingVaultController implements FxController {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ChooseExistingVaultController.class);
|
||||
|
||||
private final Stage window;
|
||||
private final Lazy<Scene> welcomeScene;
|
||||
private final Lazy<Scene> successScene;
|
||||
private final ErrorComponent.Builder errorComponent;
|
||||
private final ObjectProperty<Path> vaultPath;
|
||||
private final ObjectProperty<Vault> vault;
|
||||
private final VaultListManager vaultListManager;
|
||||
private final ResourceBundle resourceBundle;
|
||||
|
||||
private Image screenshot;
|
||||
|
||||
@Inject
|
||||
ChooseExistingVaultController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_WELCOME) Lazy<Scene> welcomeScene, @FxmlScene(FxmlFile.ADDVAULT_SUCCESS) Lazy<Scene> successScene, ErrorComponent.Builder errorComponent, ObjectProperty<Path> vaultPath, @AddVaultWizardWindow ObjectProperty<Vault> vault, VaultListManager vaultListManager, ResourceBundle resourceBundle) {
|
||||
this.window = window;
|
||||
this.welcomeScene = welcomeScene;
|
||||
this.successScene = successScene;
|
||||
this.errorComponent = errorComponent;
|
||||
this.vaultPath = vaultPath;
|
||||
this.vault = vault;
|
||||
this.vaultListManager = vaultListManager;
|
||||
this.resourceBundle = resourceBundle;
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void initialize() {
|
||||
final String resource = SystemUtils.IS_OS_MAC ? "/img/select-masterkey-mac.png" : "/img/select-masterkey-win.png";
|
||||
try (InputStream in = getClass().getResourceAsStream(resource)) {
|
||||
this.screenshot = new Image(in);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void back() {
|
||||
window.setScene(welcomeScene.get());
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void chooseFileAndNext() {
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle(resourceBundle.getString("addvaultwizard.existing.filePickerTitle"));
|
||||
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Cryptomator Masterkey", "*.cryptomator"));
|
||||
File masterkeyFile = fileChooser.showOpenDialog(window);
|
||||
if (masterkeyFile != null) {
|
||||
vaultPath.setValue(masterkeyFile.toPath().toAbsolutePath().getParent());
|
||||
try {
|
||||
Vault newVault = vaultListManager.add(vaultPath.get());
|
||||
vault.set(newVault);
|
||||
window.setScene(successScene.get());
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to open existing vault.", e);
|
||||
errorComponent.cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Getter */
|
||||
|
||||
public Image getScreenshot() {
|
||||
return screenshot;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
package org.cryptomator.ui.addvaultwizard;
|
||||
|
||||
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.controls.FontAwesome5IconView;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.RadioButton;
|
||||
import javafx.scene.control.Toggle;
|
||||
import javafx.scene.control.ToggleGroup;
|
||||
import javafx.stage.DirectoryChooser;
|
||||
import javafx.stage.Stage;
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
@AddVaultWizardScoped
|
||||
public class CreateNewVaultLocationController implements FxController {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(CreateNewVaultLocationController.class);
|
||||
private static final Path DEFAULT_CUSTOM_VAULT_PATH = Paths.get(System.getProperty("user.home"));
|
||||
|
||||
private final Stage window;
|
||||
private final Lazy<Scene> chooseNameScene;
|
||||
private final Lazy<Scene> choosePasswordScene;
|
||||
private final LocationPresets locationPresets;
|
||||
private final ObjectProperty<Path> vaultPath;
|
||||
private final StringProperty vaultName;
|
||||
private final ResourceBundle resourceBundle;
|
||||
private final BooleanBinding validVaultPath;
|
||||
private final BooleanProperty usePresetPath;
|
||||
private final StringProperty statusText;
|
||||
private final ObjectProperty<Node> statusGraphic;
|
||||
|
||||
private Path customVaultPath = DEFAULT_CUSTOM_VAULT_PATH;
|
||||
|
||||
//FXML
|
||||
public ToggleGroup predefinedLocationToggler;
|
||||
public RadioButton iclouddriveRadioButton;
|
||||
public RadioButton dropboxRadioButton;
|
||||
public RadioButton gdriveRadioButton;
|
||||
public RadioButton onedriveRadioButton;
|
||||
public RadioButton megaRadioButton;
|
||||
public RadioButton pcloudRadioButton;
|
||||
public RadioButton customRadioButton;
|
||||
public Label vaultPathStatus;
|
||||
public FontAwesome5IconView goodLocation;
|
||||
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) {
|
||||
this.window = window;
|
||||
this.chooseNameScene = chooseNameScene;
|
||||
this.choosePasswordScene = choosePasswordScene;
|
||||
this.locationPresets = locationPresets;
|
||||
this.vaultPath = vaultPath;
|
||||
this.vaultName = vaultName;
|
||||
this.resourceBundle = resourceBundle;
|
||||
this.validVaultPath = Bindings.createBooleanBinding(this::validateVaultPathAndSetStatus, this.vaultPath);
|
||||
this.usePresetPath = new SimpleBooleanProperty();
|
||||
this.statusText = new SimpleStringProperty();
|
||||
this.statusGraphic = new SimpleObjectProperty<>();
|
||||
}
|
||||
|
||||
private boolean validateVaultPathAndSetStatus() {
|
||||
final Path p = vaultPath.get();
|
||||
if (p == null) {
|
||||
statusText.set("Error: Path is NULL.");
|
||||
statusGraphic.set(badLocation);
|
||||
return false;
|
||||
} else if (!Files.exists(p.getParent())) {
|
||||
statusText.set(resourceBundle.getString("addvaultwizard.new.locationDoesNotExist"));
|
||||
statusGraphic.set(badLocation);
|
||||
return false;
|
||||
} else if (!Files.isWritable(p.getParent())) {
|
||||
statusText.set(resourceBundle.getString("addvaultwizard.new.locationIsNotWritable"));
|
||||
statusGraphic.set(badLocation);
|
||||
return false;
|
||||
} else if (!Files.notExists(p)) {
|
||||
statusText.set(resourceBundle.getString("addvaultwizard.new.fileAlreadyExists"));
|
||||
statusGraphic.set(badLocation);
|
||||
return false;
|
||||
} else {
|
||||
statusText.set(resourceBundle.getString("addvaultwizard.new.locationIsOk"));
|
||||
statusGraphic.set(goodLocation);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void initialize() {
|
||||
predefinedLocationToggler.selectedToggleProperty().addListener(this::togglePredefinedLocation);
|
||||
usePresetPath.bind(predefinedLocationToggler.selectedToggleProperty().isNotEqualTo(customRadioButton));
|
||||
}
|
||||
|
||||
private void togglePredefinedLocation(@SuppressWarnings("unused") ObservableValue<? extends Toggle> observable, @SuppressWarnings("unused") Toggle oldValue, Toggle newValue) {
|
||||
if (iclouddriveRadioButton.equals(newValue)) {
|
||||
vaultPath.set(locationPresets.getIclouddriveLocation().resolve(vaultName.get()));
|
||||
} else if (dropboxRadioButton.equals(newValue)) {
|
||||
vaultPath.set(locationPresets.getDropboxLocation().resolve(vaultName.get()));
|
||||
} else if (gdriveRadioButton.equals(newValue)) {
|
||||
vaultPath.set(locationPresets.getGdriveLocation().resolve(vaultName.get()));
|
||||
} else if (onedriveRadioButton.equals(newValue)) {
|
||||
vaultPath.set(locationPresets.getOnedriveLocation().resolve(vaultName.get()));
|
||||
} else if (megaRadioButton.equals(newValue)) {
|
||||
vaultPath.set(locationPresets.getMegaLocation().resolve(vaultName.get()));
|
||||
} else if (pcloudRadioButton.equals(newValue)) {
|
||||
vaultPath.set(locationPresets.getPcloudLocation().resolve(vaultName.get()));
|
||||
} else if (customRadioButton.equals(newValue)) {
|
||||
vaultPath.set(customVaultPath.resolve(vaultName.get()));
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void back() {
|
||||
window.setScene(chooseNameScene.get());
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void next() {
|
||||
if (validateVaultPathAndSetStatus()) {
|
||||
window.setScene(choosePasswordScene.get());
|
||||
} else {
|
||||
validVaultPath.invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void chooseCustomVaultPath() {
|
||||
DirectoryChooser directoryChooser = new DirectoryChooser();
|
||||
directoryChooser.setTitle(resourceBundle.getString("addvaultwizard.new.directoryPickerTitle"));
|
||||
if (Files.exists(customVaultPath)) {
|
||||
directoryChooser.setInitialDirectory(customVaultPath.toFile());
|
||||
} else {
|
||||
directoryChooser.setInitialDirectory(DEFAULT_CUSTOM_VAULT_PATH.toFile());
|
||||
}
|
||||
final File file = directoryChooser.showDialog(window);
|
||||
if (file != null) {
|
||||
customVaultPath = file.toPath().toAbsolutePath();
|
||||
vaultPath.set(customVaultPath.resolve(vaultName.get()));
|
||||
}
|
||||
}
|
||||
|
||||
/* Getter/Setter */
|
||||
|
||||
public Path getVaultPath() {
|
||||
return vaultPath.get();
|
||||
}
|
||||
|
||||
public ObjectProperty<Path> vaultPathProperty() {
|
||||
return vaultPath;
|
||||
}
|
||||
|
||||
public BooleanBinding validVaultPathProperty() {
|
||||
return validVaultPath;
|
||||
}
|
||||
|
||||
public Boolean getValidVaultPath() {
|
||||
return validVaultPath.get();
|
||||
}
|
||||
|
||||
public LocationPresets getLocationPresets() {
|
||||
return locationPresets;
|
||||
}
|
||||
|
||||
public BooleanProperty usePresetPathProperty() {
|
||||
return usePresetPath;
|
||||
}
|
||||
|
||||
public boolean getUsePresetPath() {
|
||||
return usePresetPath.get();
|
||||
}
|
||||
|
||||
public BooleanBinding anyRadioButtonSelectedProperty() {
|
||||
return predefinedLocationToggler.selectedToggleProperty().isNotNull();
|
||||
}
|
||||
|
||||
public boolean isAnyRadioButtonSelected() {
|
||||
return anyRadioButtonSelectedProperty().get();
|
||||
}
|
||||
|
||||
public StringProperty statusTextProperty() {
|
||||
return statusText;
|
||||
}
|
||||
|
||||
public String getStatusText() {
|
||||
return statusText.get();
|
||||
}
|
||||
|
||||
public ObjectProperty<Node> statusGraphicProperty() {
|
||||
return statusGraphic;
|
||||
}
|
||||
|
||||
public Node getStatusGraphic() {
|
||||
return statusGraphic.get();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package org.cryptomator.ui.addvaultwizard;
|
||||
|
||||
import dagger.Lazy;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.common.FxmlFile;
|
||||
import org.cryptomator.ui.common.FxmlScene;
|
||||
|
||||
import javax.inject.Inject;
|
||||
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;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.stage.Stage;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@AddVaultWizardScoped
|
||||
public class CreateNewVaultNameController implements FxController {
|
||||
|
||||
private static final Pattern VALID_NAME_PATTERN = Pattern.compile("[\\w -]+", Pattern.UNICODE_CHARACTER_CLASS);
|
||||
|
||||
public TextField textField;
|
||||
private final Stage window;
|
||||
private final Lazy<Scene> welcomeScene;
|
||||
private final Lazy<Scene> chooseLocationScene;
|
||||
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) {
|
||||
this.window = window;
|
||||
this.welcomeScene = welcomeScene;
|
||||
this.chooseLocationScene = chooseLocationScene;
|
||||
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.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) {
|
||||
// update vaultPath if it is already set but the user went back to change its name:
|
||||
vaultPath.set(vaultPath.get().resolveSibling(vaultName.get()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void back() {
|
||||
window.setScene(welcomeScene.get());
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void next() {
|
||||
window.setScene(chooseLocationScene.get());
|
||||
}
|
||||
|
||||
/* Getter/Setter */
|
||||
|
||||
public BooleanBinding invalidVaultNameProperty() {
|
||||
return invalidVaultName;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
package org.cryptomator.ui.addvaultwizard;
|
||||
|
||||
import dagger.Lazy;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.common.vaults.VaultListManager;
|
||||
import org.cryptomator.cryptofs.CryptoFileSystemProperties;
|
||||
import org.cryptomator.cryptofs.CryptoFileSystemProvider;
|
||||
import org.cryptomator.cryptofs.VaultCipherCombo;
|
||||
import org.cryptomator.cryptolib.api.CryptoException;
|
||||
import org.cryptomator.cryptolib.api.Masterkey;
|
||||
import org.cryptomator.cryptolib.api.MasterkeyLoader;
|
||||
import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
|
||||
import org.cryptomator.ui.common.ErrorComponent;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.common.FxmlFile;
|
||||
import org.cryptomator.ui.common.FxmlScene;
|
||||
import org.cryptomator.ui.common.NewPasswordController;
|
||||
import org.cryptomator.ui.common.Tasks;
|
||||
import org.cryptomator.ui.keyloading.masterkeyfile.MasterkeyFileLoadingStrategy;
|
||||
import org.cryptomator.ui.recoverykey.RecoveryKeyFactory;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.ContentDisplay;
|
||||
import javafx.scene.control.Toggle;
|
||||
import javafx.scene.control.ToggleGroup;
|
||||
import javafx.stage.Stage;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.net.URI;
|
||||
import java.nio.channels.WritableByteChannel;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.US_ASCII;
|
||||
import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
|
||||
|
||||
@AddVaultWizardScoped
|
||||
public class CreateNewVaultPasswordController implements FxController {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(CreateNewVaultPasswordController.class);
|
||||
private static final URI DEFAULT_KEY_ID = URI.create(MasterkeyFileLoadingStrategy.SCHEME + ":" + MASTERKEY_FILENAME); // TODO better place?
|
||||
|
||||
private final Stage window;
|
||||
private final Lazy<Scene> chooseLocationScene;
|
||||
private final Lazy<Scene> recoveryKeyScene;
|
||||
private final Lazy<Scene> successScene;
|
||||
private final ErrorComponent.Builder errorComponent;
|
||||
private final ExecutorService executor;
|
||||
private final RecoveryKeyFactory recoveryKeyFactory;
|
||||
private final StringProperty vaultNameProperty;
|
||||
private final ObjectProperty<Path> vaultPathProperty;
|
||||
private final ObjectProperty<Vault> vaultProperty;
|
||||
private final StringProperty recoveryKeyProperty;
|
||||
private final VaultListManager vaultListManager;
|
||||
private final ResourceBundle resourceBundle;
|
||||
private final ReadmeGenerator readmeGenerator;
|
||||
private final SecureRandom csprng;
|
||||
private final MasterkeyFileAccess masterkeyFileAccess;
|
||||
private final BooleanProperty processing;
|
||||
private final BooleanProperty readyToCreateVault;
|
||||
private final ObjectBinding<ContentDisplay> createVaultButtonState;
|
||||
|
||||
public ToggleGroup recoveryKeyChoice;
|
||||
public Toggle showRecoveryKey;
|
||||
public Toggle skipRecoveryKey;
|
||||
public NewPasswordController newPasswordSceneController;
|
||||
|
||||
@Inject
|
||||
CreateNewVaultPasswordController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_NEW_LOCATION) Lazy<Scene> chooseLocationScene, @FxmlScene(FxmlFile.ADDVAULT_NEW_RECOVERYKEY) Lazy<Scene> recoveryKeyScene, @FxmlScene(FxmlFile.ADDVAULT_SUCCESS) Lazy<Scene> successScene, ErrorComponent.Builder errorComponent, ExecutorService executor, RecoveryKeyFactory recoveryKeyFactory, @Named("vaultName") StringProperty vaultName, ObjectProperty<Path> vaultPath, @AddVaultWizardWindow ObjectProperty<Vault> vault, @Named("recoveryKey") StringProperty recoveryKey, VaultListManager vaultListManager, ResourceBundle resourceBundle, ReadmeGenerator readmeGenerator, SecureRandom csprng, MasterkeyFileAccess masterkeyFileAccess) {
|
||||
this.window = window;
|
||||
this.chooseLocationScene = chooseLocationScene;
|
||||
this.recoveryKeyScene = recoveryKeyScene;
|
||||
this.successScene = successScene;
|
||||
this.errorComponent = errorComponent;
|
||||
this.executor = executor;
|
||||
this.recoveryKeyFactory = recoveryKeyFactory;
|
||||
this.vaultNameProperty = vaultName;
|
||||
this.vaultPathProperty = vaultPath;
|
||||
this.vaultProperty = vault;
|
||||
this.recoveryKeyProperty = recoveryKey;
|
||||
this.vaultListManager = vaultListManager;
|
||||
this.resourceBundle = resourceBundle;
|
||||
this.readmeGenerator = readmeGenerator;
|
||||
this.csprng = csprng;
|
||||
this.masterkeyFileAccess = masterkeyFileAccess;
|
||||
this.processing = new SimpleBooleanProperty();
|
||||
this.readyToCreateVault = new SimpleBooleanProperty();
|
||||
this.createVaultButtonState = Bindings.createObjectBinding(this::getCreateVaultButtonState, processing);
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void initialize() {
|
||||
readyToCreateVault.bind(newPasswordSceneController.passwordsMatchAndSufficientProperty().and(recoveryKeyChoice.selectedToggleProperty().isNotNull()).and(processing.not()));
|
||||
window.setOnHiding(event -> {
|
||||
newPasswordSceneController.passwordField.wipe();
|
||||
newPasswordSceneController.reenterField.wipe();
|
||||
});
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void back() {
|
||||
window.setScene(chooseLocationScene.get());
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void next() {
|
||||
Path pathToVault = vaultPathProperty.get();
|
||||
|
||||
try {
|
||||
Files.createDirectory(pathToVault);
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to create vault directory.", e);
|
||||
errorComponent.cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene();
|
||||
return;
|
||||
}
|
||||
|
||||
if (showRecoveryKey.equals(recoveryKeyChoice.getSelectedToggle())) {
|
||||
showRecoveryKeyScene();
|
||||
} else if (skipRecoveryKey.equals(recoveryKeyChoice.getSelectedToggle())) {
|
||||
showSuccessScene();
|
||||
} else {
|
||||
throw new IllegalStateException("Unexpected toggle state");
|
||||
}
|
||||
}
|
||||
|
||||
private void showRecoveryKeyScene() {
|
||||
Path pathToVault = vaultPathProperty.get();
|
||||
processing.set(true);
|
||||
Tasks.create(() -> {
|
||||
initializeVault(pathToVault);
|
||||
return recoveryKeyFactory.createRecoveryKey(pathToVault, newPasswordSceneController.passwordField.getCharacters());
|
||||
}).onSuccess(recoveryKey -> {
|
||||
initializationSucceeded(pathToVault);
|
||||
recoveryKeyProperty.set(recoveryKey);
|
||||
window.setScene(recoveryKeyScene.get());
|
||||
}).onError(IOException.class, e -> {
|
||||
LOG.error("Failed to initialize vault.", e);
|
||||
errorComponent.cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene();
|
||||
}).andFinally(() -> {
|
||||
processing.set(false);
|
||||
}).runOnce(executor);
|
||||
}
|
||||
|
||||
private void showSuccessScene() {
|
||||
Path pathToVault = vaultPathProperty.get();
|
||||
processing.set(true);
|
||||
Tasks.create(() -> {
|
||||
initializeVault(pathToVault);
|
||||
}).onSuccess(() -> {
|
||||
initializationSucceeded(pathToVault);
|
||||
window.setScene(successScene.get());
|
||||
}).onError(IOException.class, e -> {
|
||||
LOG.error("Failed to initialize vault.", e);
|
||||
errorComponent.cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene();
|
||||
}).andFinally(() -> {
|
||||
processing.set(false);
|
||||
}).runOnce(executor);
|
||||
}
|
||||
|
||||
private void initializeVault(Path path) throws IOException {
|
||||
// 1. write masterkey:
|
||||
Path masterkeyFilePath = path.resolve(MASTERKEY_FILENAME);
|
||||
try (Masterkey masterkey = Masterkey.generate(csprng)) {
|
||||
masterkeyFileAccess.persist(masterkey, masterkeyFilePath, newPasswordSceneController.passwordField.getCharacters());
|
||||
|
||||
// 2. initialize vault:
|
||||
try {
|
||||
MasterkeyLoader loader = ignored -> masterkey.clone();
|
||||
CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties().withCipherCombo(VaultCipherCombo.SIV_CTRMAC).withKeyLoader(loader).build();
|
||||
CryptoFileSystemProvider.initialize(path, fsProps, DEFAULT_KEY_ID);
|
||||
|
||||
// 3. write vault-internal readme file:
|
||||
String vaultReadmeFileName = resourceBundle.getString("addvault.new.readme.accessLocation.fileName");
|
||||
try (FileSystem fs = CryptoFileSystemProvider.newFileSystem(path, fsProps); //
|
||||
WritableByteChannel ch = Files.newByteChannel(fs.getPath("/", vaultReadmeFileName), StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) {
|
||||
ch.write(US_ASCII.encode(readmeGenerator.createVaultAccessLocationReadmeRtf()));
|
||||
}
|
||||
} catch (CryptoException e) {
|
||||
throw new IOException("Failed initialize vault.", e);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. write vault-external readme file:
|
||||
String storagePathReadmeFileName = resourceBundle.getString("addvault.new.readme.storageLocation.fileName");
|
||||
try (WritableByteChannel ch = Files.newByteChannel(path.resolve(storagePathReadmeFileName), StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) {
|
||||
ch.write(US_ASCII.encode(readmeGenerator.createVaultStorageLocationReadmeRtf()));
|
||||
}
|
||||
|
||||
LOG.info("Created vault at {}", path);
|
||||
}
|
||||
|
||||
private void initializationSucceeded(Path pathToVault) {
|
||||
try {
|
||||
Vault newVault = vaultListManager.add(pathToVault);
|
||||
vaultProperty.set(newVault);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/* Getter/Setter */
|
||||
|
||||
public String getVaultName() {
|
||||
return vaultNameProperty.get();
|
||||
}
|
||||
|
||||
public StringProperty vaultNameProperty() {
|
||||
return vaultNameProperty;
|
||||
}
|
||||
|
||||
public BooleanProperty readyToCreateVaultProperty() {
|
||||
return readyToCreateVault;
|
||||
}
|
||||
|
||||
public boolean isReadyToCreateVault() {
|
||||
return readyToCreateVault.get();
|
||||
}
|
||||
|
||||
public ObjectBinding<ContentDisplay> createVaultButtonStateProperty() {
|
||||
return createVaultButtonState;
|
||||
}
|
||||
|
||||
public ContentDisplay getCreateVaultButtonState() {
|
||||
return processing.get() ? ContentDisplay.LEFT : ContentDisplay.TEXT_ONLY;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package org.cryptomator.ui.addvaultwizard;
|
||||
|
||||
import dagger.Lazy;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.common.FxmlFile;
|
||||
import org.cryptomator.ui.common.FxmlScene;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
public class CreateNewVaultRecoveryKeyController implements FxController {
|
||||
|
||||
private final Stage window;
|
||||
private final Lazy<Scene> successScene;
|
||||
|
||||
@Inject
|
||||
CreateNewVaultRecoveryKeyController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_SUCCESS) Lazy<Scene> successScene) {
|
||||
this.window = window;
|
||||
this.successScene = successScene;
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void next() {
|
||||
window.setScene(successScene.get());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
package org.cryptomator.ui.addvaultwizard;
|
||||
|
||||
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"};
|
||||
private static final String[] ONEDRIVE_LOCATIONS = {"~/OneDrive"};
|
||||
private static final String[] MEGA_LOCATIONS = {"~/MEGA"};
|
||||
private static final String[] PCLOUD_LOCATIONS = {"~/pCloudDrive"};
|
||||
|
||||
private final ReadOnlyObjectProperty<Path> iclouddriveLocation;
|
||||
private final ReadOnlyObjectProperty<Path> dropboxLocation;
|
||||
private final ReadOnlyObjectProperty<Path> gdriveLocation;
|
||||
private final ReadOnlyObjectProperty<Path> onedriveLocation;
|
||||
private final ReadOnlyObjectProperty<Path> megaLocation;
|
||||
private final ReadOnlyObjectProperty<Path> pcloudLocation;
|
||||
private final BooleanBinding foundIclouddrive;
|
||||
private final BooleanBinding foundDropbox;
|
||||
private final BooleanBinding foundGdrive;
|
||||
private final BooleanBinding foundOnedrive;
|
||||
private final BooleanBinding foundMega;
|
||||
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));
|
||||
this.foundIclouddrive = iclouddriveLocation.isNotNull();
|
||||
this.foundDropbox = dropboxLocation.isNotNull();
|
||||
this.foundGdrive = gdriveLocation.isNotNull();
|
||||
this.foundOnedrive = onedriveLocation.isNotNull();
|
||||
this.foundMega = megaLocation.isNotNull();
|
||||
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() {
|
||||
return iclouddriveLocation;
|
||||
}
|
||||
|
||||
public Path getIclouddriveLocation() {
|
||||
return iclouddriveLocation.get();
|
||||
}
|
||||
|
||||
public BooleanBinding foundIclouddriveProperty() {
|
||||
return foundIclouddrive;
|
||||
}
|
||||
|
||||
public boolean isFoundIclouddrive() {
|
||||
return foundIclouddrive.get();
|
||||
}
|
||||
|
||||
public ReadOnlyObjectProperty<Path> dropboxLocationProperty() {
|
||||
return dropboxLocation;
|
||||
}
|
||||
|
||||
public Path getDropboxLocation() {
|
||||
return dropboxLocation.get();
|
||||
}
|
||||
|
||||
public BooleanBinding foundDropboxProperty() {
|
||||
return foundDropbox;
|
||||
}
|
||||
|
||||
public boolean isFoundDropbox() {
|
||||
return foundDropbox.get();
|
||||
}
|
||||
|
||||
public ReadOnlyObjectProperty<Path> gdriveLocationProperty() {
|
||||
return gdriveLocation;
|
||||
}
|
||||
|
||||
public Path getGdriveLocation() {
|
||||
return gdriveLocation.get();
|
||||
}
|
||||
|
||||
public BooleanBinding foundGdriveProperty() {
|
||||
return foundGdrive;
|
||||
}
|
||||
|
||||
public boolean isFoundGdrive() {
|
||||
return foundGdrive.get();
|
||||
}
|
||||
|
||||
public ReadOnlyObjectProperty<Path> onedriveLocationProperty() {
|
||||
return onedriveLocation;
|
||||
}
|
||||
|
||||
public Path getOnedriveLocation() {
|
||||
return onedriveLocation.get();
|
||||
}
|
||||
|
||||
public BooleanBinding foundOnedriveProperty() {
|
||||
return foundOnedrive;
|
||||
}
|
||||
|
||||
public boolean isFoundOnedrive() {
|
||||
return foundOnedrive.get();
|
||||
}
|
||||
|
||||
public ReadOnlyObjectProperty<Path> megaLocationProperty() {
|
||||
return megaLocation;
|
||||
}
|
||||
|
||||
public Path getMegaLocation() {
|
||||
return megaLocation.get();
|
||||
}
|
||||
|
||||
public BooleanBinding foundMegaProperty() {
|
||||
return foundMega;
|
||||
}
|
||||
|
||||
public boolean isFoundMega() {
|
||||
return foundMega.get();
|
||||
}
|
||||
|
||||
public ReadOnlyObjectProperty<Path> pcloudLocationProperty() {
|
||||
return pcloudLocation;
|
||||
}
|
||||
|
||||
public Path getPcloudLocation() {
|
||||
return pcloudLocation.get();
|
||||
}
|
||||
|
||||
public BooleanBinding foundPcloudProperty() {
|
||||
return foundPcloud;
|
||||
}
|
||||
|
||||
public boolean isFoundPcloud() {
|
||||
return foundPcloud.get();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package org.cryptomator.ui.addvaultwizard;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.util.List;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
@AddVaultWizardScoped
|
||||
public class ReadmeGenerator {
|
||||
|
||||
// specs: https://web.archive.org/web/20190708132914/http://www.kleinlercher.at/tools/Windows_Protocols/Word2007RTFSpec9.pdf
|
||||
private static final String RTF_HEADER = "{\\rtf1\\fbidis\\ansi\\uc0\\fs32\n";
|
||||
private static final String RTF_FOOTER = "}";
|
||||
private static final String HEADING = "\\fs40\\qc %s";
|
||||
private static final String EMPTY_PAR = "";
|
||||
private static final String DONT_PAR = "\\b %s";
|
||||
private static final String IDENT_PAR = " %s";
|
||||
private static final String HELP_URL = "{\\field{\\*\\fldinst HYPERLINK \"http://docs.cryptomator.org/\"}{\\fldrslt http://docs.cryptomator.org}}";
|
||||
|
||||
private final ResourceBundle resourceBundle;
|
||||
|
||||
@Inject
|
||||
public ReadmeGenerator(ResourceBundle resourceBundle) {
|
||||
this.resourceBundle = resourceBundle;
|
||||
}
|
||||
|
||||
public String createVaultStorageLocationReadmeRtf() {
|
||||
return createDocument(List.of( //
|
||||
String.format(HEADING, resourceBundle.getString("addvault.new.readme.storageLocation.1")), //
|
||||
resourceBundle.getString("addvault.new.readme.storageLocation.2"), //
|
||||
EMPTY_PAR, //
|
||||
String.format(DONT_PAR, resourceBundle.getString("addvault.new.readme.storageLocation.3")), //
|
||||
String.format(IDENT_PAR, resourceBundle.getString("addvault.new.readme.storageLocation.4")), //
|
||||
String.format(IDENT_PAR, resourceBundle.getString("addvault.new.readme.storageLocation.5")), //
|
||||
EMPTY_PAR, //
|
||||
resourceBundle.getString("addvault.new.readme.storageLocation.6"), //
|
||||
String.format(IDENT_PAR, resourceBundle.getString("addvault.new.readme.storageLocation.7")), //
|
||||
String.format(IDENT_PAR, resourceBundle.getString("addvault.new.readme.storageLocation.8")), //
|
||||
String.format(IDENT_PAR, resourceBundle.getString("addvault.new.readme.storageLocation.9")), //
|
||||
EMPTY_PAR, //
|
||||
String.format(resourceBundle.getString("addvault.new.readme.storageLocation.10"), HELP_URL) //
|
||||
));
|
||||
}
|
||||
|
||||
public String createVaultAccessLocationReadmeRtf() {
|
||||
return createDocument(List.of( //
|
||||
String.format(HEADING, resourceBundle.getString("addvault.new.readme.accessLocation.1")), //
|
||||
resourceBundle.getString("addvault.new.readme.accessLocation.2"), //
|
||||
EMPTY_PAR, //
|
||||
resourceBundle.getString("addvault.new.readme.accessLocation.3"), //
|
||||
EMPTY_PAR, //
|
||||
resourceBundle.getString("addvault.new.readme.accessLocation.4")));
|
||||
}
|
||||
|
||||
// visible for testing
|
||||
String createDocument(Iterable<String> paragraphs) {
|
||||
StringBuilder sb = new StringBuilder(RTF_HEADER);
|
||||
for (String p : paragraphs) {
|
||||
sb.append("{\\sa80 ");
|
||||
appendEscaped(sb, p);
|
||||
sb.append("}\\par \n");
|
||||
}
|
||||
sb.append(RTF_FOOTER);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
// visible for testing
|
||||
String escapeNonAsciiChars(CharSequence input) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
appendEscaped(sb, input);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private void appendEscaped(StringBuilder sb, CharSequence input) {
|
||||
input.chars().forEachOrdered(c -> {
|
||||
if (c < 128) {
|
||||
sb.append((char) c);
|
||||
} else if (c < 0xFFFF) {
|
||||
sb.append("\\u").append(c);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package org.cryptomator.ui.changepassword;
|
||||
|
||||
import dagger.BindsInstance;
|
||||
import dagger.Lazy;
|
||||
import dagger.Subcomponent;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.ui.common.FxmlFile;
|
||||
import org.cryptomator.ui.common.FxmlScene;
|
||||
|
||||
import javax.inject.Named;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
@ChangePasswordScoped
|
||||
@Subcomponent(modules = {ChangePasswordModule.class})
|
||||
public interface ChangePasswordComponent {
|
||||
|
||||
@ChangePasswordWindow
|
||||
Stage window();
|
||||
|
||||
@FxmlScene(FxmlFile.CHANGEPASSWORD)
|
||||
Lazy<Scene> scene();
|
||||
|
||||
default void showChangePasswordWindow() {
|
||||
Stage stage = window();
|
||||
stage.setScene(scene().get());
|
||||
stage.show();
|
||||
}
|
||||
|
||||
@Subcomponent.Builder
|
||||
interface Builder {
|
||||
|
||||
@BindsInstance
|
||||
Builder vault(@ChangePasswordWindow Vault vault);
|
||||
|
||||
@BindsInstance
|
||||
Builder owner(@Named("changePasswordOwner") Stage owner);
|
||||
|
||||
ChangePasswordComponent build();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package org.cryptomator.ui.changepassword;
|
||||
|
||||
import org.cryptomator.common.keychain.KeychainManager;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.cryptofs.common.MasterkeyBackupHelper;
|
||||
import org.cryptomator.cryptolib.api.CryptoException;
|
||||
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
|
||||
import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
|
||||
import org.cryptomator.integrations.keychain.KeychainAccessException;
|
||||
import org.cryptomator.ui.common.Animations;
|
||||
import org.cryptomator.ui.common.ErrorComponent;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.common.NewPasswordController;
|
||||
import org.cryptomator.ui.controls.NiceSecurePasswordField;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.CheckBox;
|
||||
import javafx.stage.Stage;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
import static org.cryptomator.common.Constants.MASTERKEY_BACKUP_SUFFIX;
|
||||
import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
|
||||
|
||||
@ChangePasswordScoped
|
||||
public class ChangePasswordController implements FxController {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ChangePasswordController.class);
|
||||
|
||||
private final Stage window;
|
||||
private final Vault vault;
|
||||
private final ErrorComponent.Builder errorComponent;
|
||||
private final KeychainManager keychain;
|
||||
private final SecureRandom csprng;
|
||||
private final MasterkeyFileAccess masterkeyFileAccess;
|
||||
|
||||
public NiceSecurePasswordField oldPasswordField;
|
||||
public CheckBox finalConfirmationCheckbox;
|
||||
public Button finishButton;
|
||||
public NewPasswordController newPasswordController;
|
||||
|
||||
@Inject
|
||||
public ChangePasswordController(@ChangePasswordWindow Stage window, @ChangePasswordWindow Vault vault, ErrorComponent.Builder errorComponent, KeychainManager keychain, SecureRandom csprng, MasterkeyFileAccess masterkeyFileAccess) {
|
||||
this.window = window;
|
||||
this.vault = vault;
|
||||
this.errorComponent = errorComponent;
|
||||
this.keychain = keychain;
|
||||
this.csprng = csprng;
|
||||
this.masterkeyFileAccess = masterkeyFileAccess;
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void initialize() {
|
||||
BooleanBinding checkboxNotConfirmed = finalConfirmationCheckbox.selectedProperty().not();
|
||||
BooleanBinding oldPasswordFieldEmpty = oldPasswordField.textProperty().isEmpty();
|
||||
finishButton.disableProperty().bind(checkboxNotConfirmed.or(oldPasswordFieldEmpty).or(newPasswordController.passwordsMatchAndSufficientProperty().not()));
|
||||
window.setOnHiding(event -> {
|
||||
oldPasswordField.wipe();
|
||||
newPasswordController.passwordField.wipe();
|
||||
newPasswordController.reenterField.wipe();
|
||||
});
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void cancel() {
|
||||
window.close();
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void finish() {
|
||||
try {
|
||||
CharSequence oldPassphrase = oldPasswordField.getCharacters();
|
||||
CharSequence newPassphrase = newPasswordController.passwordField.getCharacters();
|
||||
Path masterkeyPath = vault.getPath().resolve(MASTERKEY_FILENAME);
|
||||
byte[] oldMasterkeyBytes = Files.readAllBytes(masterkeyPath);
|
||||
byte[] newMasterkeyBytes = masterkeyFileAccess.changePassphrase(oldMasterkeyBytes, oldPassphrase, newPassphrase);
|
||||
Path backupKeyPath = vault.getPath().resolve(MASTERKEY_FILENAME + MasterkeyBackupHelper.generateFileIdSuffix(oldMasterkeyBytes) + MASTERKEY_BACKUP_SUFFIX);
|
||||
Files.move(masterkeyPath, backupKeyPath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
|
||||
Files.write(masterkeyPath, newMasterkeyBytes, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE);
|
||||
LOG.info("Successfully changed password for {}", vault.getDisplayName());
|
||||
updatePasswordInSystemkeychain();
|
||||
window.close();
|
||||
} catch (InvalidPassphraseException e) {
|
||||
Animations.createShakeWindowAnimation(window).play();
|
||||
oldPasswordField.selectAll();
|
||||
oldPasswordField.requestFocus();
|
||||
} catch (IOException | CryptoException e) {
|
||||
LOG.error("Password change failed. Unable to perform operation.", e);
|
||||
errorComponent.cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene();
|
||||
}
|
||||
}
|
||||
|
||||
private void updatePasswordInSystemkeychain() {
|
||||
if (keychain.isSupported() && !keychain.isLocked()) {
|
||||
try {
|
||||
keychain.changePassphrase(vault.getId(), newPasswordController.passwordField.getCharacters());
|
||||
LOG.info("Successfully updated password in system keychain for {}", vault.getDisplayName());
|
||||
} catch (KeychainAccessException e) {
|
||||
LOG.error("Failed to update password in system keychain.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Getter/Setter */
|
||||
|
||||
public Vault getVault() {
|
||||
return vault;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package org.cryptomator.ui.changepassword;
|
||||
|
||||
import dagger.Binds;
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import dagger.multibindings.IntoMap;
|
||||
import org.cryptomator.ui.common.DefaultSceneFactory;
|
||||
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.common.StageFactory;
|
||||
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Provider;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.stage.Modality;
|
||||
import javafx.stage.Stage;
|
||||
import java.util.Map;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
@Module
|
||||
abstract class ChangePasswordModule {
|
||||
|
||||
@Provides
|
||||
@ChangePasswordWindow
|
||||
@ChangePasswordScoped
|
||||
static FxmlLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) {
|
||||
return new FxmlLoaderFactory(factories, sceneFactory, resourceBundle);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ChangePasswordWindow
|
||||
@ChangePasswordScoped
|
||||
static Stage provideStage(StageFactory factory, @Named("changePasswordOwner") Stage owner, ResourceBundle resourceBundle) {
|
||||
Stage stage = factory.create();
|
||||
stage.setTitle(resourceBundle.getString("changepassword.title"));
|
||||
stage.setResizable(false);
|
||||
stage.initModality(Modality.WINDOW_MODAL);
|
||||
stage.initOwner(owner);
|
||||
return stage;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FxmlScene(FxmlFile.CHANGEPASSWORD)
|
||||
@ChangePasswordScoped
|
||||
static Scene provideUnlockScene(@ChangePasswordWindow FxmlLoaderFactory fxmlLoaders) {
|
||||
return fxmlLoaders.createScene(FxmlFile.CHANGEPASSWORD);
|
||||
}
|
||||
|
||||
|
||||
// ------------------
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FxControllerKey(ChangePasswordController.class)
|
||||
abstract FxController bindUnlockController(ChangePasswordController controller);
|
||||
|
||||
@Provides
|
||||
@IntoMap
|
||||
@FxControllerKey(NewPasswordController.class)
|
||||
static FxController provideNewPasswordController(ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater) {
|
||||
return new NewPasswordController(resourceBundle, strengthRater);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.cryptomator.ui.changepassword;
|
||||
|
||||
import javax.inject.Scope;
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
@Scope
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@interface ChangePasswordScoped {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.cryptomator.ui.changepassword;
|
||||
|
||||
import javax.inject.Qualifier;
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
|
||||
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
|
||||
@Qualifier
|
||||
@Documented
|
||||
@Retention(RUNTIME)
|
||||
@interface ChangePasswordWindow {
|
||||
|
||||
}
|
||||
36
src/main/java/org/cryptomator/ui/common/Animations.java
Normal file
36
src/main/java/org/cryptomator/ui/common/Animations.java
Normal file
@@ -0,0 +1,36 @@
|
||||
package org.cryptomator.ui.common;
|
||||
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.beans.value.WritableValue;
|
||||
import javafx.stage.Window;
|
||||
import javafx.util.Duration;
|
||||
|
||||
public class Animations {
|
||||
|
||||
public static Timeline createShakeWindowAnimation(Window window) {
|
||||
WritableValue<Double> writableWindowX = new WritableValue<>() {
|
||||
@Override
|
||||
public Double getValue() {
|
||||
return window.getX();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(Double value) {
|
||||
window.setX(value);
|
||||
}
|
||||
};
|
||||
return new Timeline( //
|
||||
new KeyFrame(Duration.ZERO, new KeyValue(writableWindowX, window.getX())), //
|
||||
new KeyFrame(new Duration(100), new KeyValue(writableWindowX, window.getX() - 22.0)), //
|
||||
new KeyFrame(new Duration(200), new KeyValue(writableWindowX, window.getX() + 18.0)), //
|
||||
new KeyFrame(new Duration(300), new KeyValue(writableWindowX, window.getX() - 14.0)), //
|
||||
new KeyFrame(new Duration(400), new KeyValue(writableWindowX, window.getX() + 10.0)), //
|
||||
new KeyFrame(new Duration(500), new KeyValue(writableWindowX, window.getX() - 6.0)), //
|
||||
new KeyFrame(new Duration(600), new KeyValue(writableWindowX, window.getX() + 2.0)), //
|
||||
new KeyFrame(new Duration(700), new KeyValue(writableWindowX, window.getX())) //
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package org.cryptomator.ui.common;
|
||||
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.common.settings.Settings;
|
||||
import org.cryptomator.ui.fxapp.FxApplicationScoped;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.scene.Parent;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyCodeCombination;
|
||||
import javafx.scene.input.KeyCombination;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.Window;
|
||||
import java.util.function.Function;
|
||||
|
||||
@FxApplicationScoped
|
||||
public class DefaultSceneFactory implements Function<Parent, Scene> {
|
||||
|
||||
protected static final KeyCodeCombination ALT_F4 = new KeyCodeCombination(KeyCode.F4, KeyCombination.ALT_DOWN);
|
||||
protected static final KeyCodeCombination SHORTCUT_W = new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN);
|
||||
|
||||
protected final Settings settings;
|
||||
|
||||
@Inject
|
||||
public DefaultSceneFactory(Settings settings) {
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Scene apply(Parent root) {
|
||||
Scene scene = new Scene(root);
|
||||
configureRoot(root);
|
||||
configureScene(scene);
|
||||
return scene;
|
||||
}
|
||||
|
||||
protected void configureRoot(Parent root) {
|
||||
root.nodeOrientationProperty().bind(settings.userInterfaceOrientation());
|
||||
}
|
||||
|
||||
protected void configureScene(Scene scene) {
|
||||
scene.windowProperty().addListener(observable -> {
|
||||
Window window = scene.getWindow();
|
||||
if (window instanceof Stage s) {
|
||||
setupDefaultAccelerators(scene, s);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected void setupDefaultAccelerators(Scene scene, Stage stage) {
|
||||
if (SystemUtils.IS_OS_WINDOWS) {
|
||||
scene.getAccelerators().put(ALT_F4, stage::close);
|
||||
} else {
|
||||
scene.getAccelerators().put(SHORTCUT_W, stage::close);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
49
src/main/java/org/cryptomator/ui/common/ErrorComponent.java
Normal file
49
src/main/java/org/cryptomator/ui/common/ErrorComponent.java
Normal file
@@ -0,0 +1,49 @@
|
||||
package org.cryptomator.ui.common;
|
||||
|
||||
import dagger.BindsInstance;
|
||||
import dagger.Subcomponent;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
@Subcomponent(modules = {ErrorModule.class})
|
||||
public interface ErrorComponent {
|
||||
|
||||
Stage window();
|
||||
|
||||
@FxmlScene(FxmlFile.ERROR)
|
||||
Scene scene();
|
||||
|
||||
default void showErrorScene() {
|
||||
if (Platform.isFxApplicationThread()) {
|
||||
show();
|
||||
} else {
|
||||
Platform.runLater(this::show);
|
||||
}
|
||||
}
|
||||
|
||||
private void show() {
|
||||
Stage stage = window();
|
||||
stage.setScene(scene());
|
||||
stage.show();
|
||||
}
|
||||
|
||||
@Subcomponent.Builder
|
||||
interface Builder {
|
||||
|
||||
@BindsInstance
|
||||
Builder cause(Throwable cause);
|
||||
|
||||
@BindsInstance
|
||||
Builder window(Stage window);
|
||||
|
||||
@BindsInstance
|
||||
Builder returnToScene(@Nullable Scene previousScene);
|
||||
|
||||
ErrorComponent build();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
44
src/main/java/org/cryptomator/ui/common/ErrorController.java
Normal file
44
src/main/java/org/cryptomator/ui/common/ErrorController.java
Normal file
@@ -0,0 +1,44 @@
|
||||
package org.cryptomator.ui.common;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
public class ErrorController implements FxController {
|
||||
|
||||
private final String stackTrace;
|
||||
private final Scene previousScene;
|
||||
private final Stage window;
|
||||
|
||||
@Inject
|
||||
ErrorController(@Named("stackTrace") String stackTrace, @Nullable Scene previousScene, Stage window) {
|
||||
this.stackTrace = stackTrace;
|
||||
this.previousScene = previousScene;
|
||||
this.window = window;
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void back() {
|
||||
if (previousScene != null) {
|
||||
window.setScene(previousScene);
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void close() {
|
||||
window.close();
|
||||
}
|
||||
|
||||
/* Getter/Setter */
|
||||
|
||||
public boolean isPreviousScenePresent() {
|
||||
return previousScene != null;
|
||||
}
|
||||
|
||||
public String getStackTrace() {
|
||||
return stackTrace;
|
||||
}
|
||||
}
|
||||
46
src/main/java/org/cryptomator/ui/common/ErrorModule.java
Normal file
46
src/main/java/org/cryptomator/ui/common/ErrorModule.java
Normal file
@@ -0,0 +1,46 @@
|
||||
package org.cryptomator.ui.common;
|
||||
|
||||
import dagger.Binds;
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import dagger.multibindings.IntoMap;
|
||||
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Provider;
|
||||
import javafx.scene.Scene;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.PrintStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
@Module
|
||||
abstract class ErrorModule {
|
||||
|
||||
@Provides
|
||||
static FxmlLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) {
|
||||
return new FxmlLoaderFactory(factories, sceneFactory, resourceBundle);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Named("stackTrace")
|
||||
static String provideStackTrace(Throwable cause) {
|
||||
// TODO deduplicate VaultDetailUnknownErrorController.java
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
cause.printStackTrace(new PrintStream(baos));
|
||||
return baos.toString(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FxControllerKey(ErrorController.class)
|
||||
abstract FxController bindErrorController(ErrorController controller);
|
||||
|
||||
@Provides
|
||||
@FxmlScene(FxmlFile.ERROR)
|
||||
static Scene provideErrorScene(FxmlLoaderFactory fxmlLoaders) {
|
||||
return fxmlLoaders.createScene(FxmlFile.ERROR);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
49
src/main/java/org/cryptomator/ui/common/FontLoader.java
Normal file
49
src/main/java/org/cryptomator/ui/common/FontLoader.java
Normal file
@@ -0,0 +1,49 @@
|
||||
package org.cryptomator.ui.common;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javafx.scene.text.Font;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
public class FontLoader {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(FontLoader.class);
|
||||
private static final double DEFAULT_FONT_SIZE = 12;
|
||||
|
||||
public static Font load(String resourcePath) throws FontLoaderException {
|
||||
try (InputStream in = FontLoader.class.getResourceAsStream(resourcePath)) {
|
||||
if (in == null) {
|
||||
throw new FontLoaderException(resourcePath);
|
||||
} else {
|
||||
return load(resourcePath, in);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new FontLoaderException(resourcePath, e);
|
||||
}
|
||||
}
|
||||
|
||||
private static Font load(String resourcePath, InputStream in) throws FontLoaderException {
|
||||
Font font = Font.loadFont(in, DEFAULT_FONT_SIZE);
|
||||
if (font != null) {
|
||||
LOG.debug("Loaded family: {}", font.getFamily());
|
||||
return font;
|
||||
} else {
|
||||
throw new FontLoaderException(resourcePath);
|
||||
}
|
||||
}
|
||||
|
||||
public static class FontLoaderException extends IOException {
|
||||
|
||||
private FontLoaderException(String resourceName) {
|
||||
super("Failed to load font: " + resourceName);
|
||||
}
|
||||
|
||||
private FontLoaderException(String resourceName, Throwable cause) {
|
||||
super("Failed to load font: " + resourceName, cause);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
10
src/main/java/org/cryptomator/ui/common/FxController.java
Normal file
10
src/main/java/org/cryptomator/ui/common/FxController.java
Normal file
@@ -0,0 +1,10 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the accompanying LICENSE file.
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.ui.common;
|
||||
|
||||
public interface FxController {
|
||||
|
||||
}
|
||||
24
src/main/java/org/cryptomator/ui/common/FxControllerKey.java
Normal file
24
src/main/java/org/cryptomator/ui/common/FxControllerKey.java
Normal file
@@ -0,0 +1,24 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the accompanying LICENSE file.
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.ui.common;
|
||||
|
||||
import dagger.MapKey;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import static java.lang.annotation.ElementType.METHOD;
|
||||
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
|
||||
@Documented
|
||||
@Target(METHOD)
|
||||
@Retention(RUNTIME)
|
||||
@MapKey
|
||||
public @interface FxControllerKey {
|
||||
|
||||
Class<? extends FxController> value();
|
||||
}
|
||||
48
src/main/java/org/cryptomator/ui/common/FxmlFile.java
Normal file
48
src/main/java/org/cryptomator/ui/common/FxmlFile.java
Normal file
@@ -0,0 +1,48 @@
|
||||
package org.cryptomator.ui.common;
|
||||
|
||||
public enum FxmlFile {
|
||||
ADDVAULT_EXISTING("/fxml/addvault_existing.fxml"), //
|
||||
ADDVAULT_NEW_NAME("/fxml/addvault_new_name.fxml"), //
|
||||
ADDVAULT_NEW_LOCATION("/fxml/addvault_new_location.fxml"), //
|
||||
ADDVAULT_NEW_PASSWORD("/fxml/addvault_new_password.fxml"), //
|
||||
ADDVAULT_NEW_RECOVERYKEY("/fxml/addvault_new_recoverykey.fxml"), //
|
||||
ADDVAULT_SUCCESS("/fxml/addvault_success.fxml"), //
|
||||
ADDVAULT_WELCOME("/fxml/addvault_welcome.fxml"), //
|
||||
CHANGEPASSWORD("/fxml/changepassword.fxml"), //
|
||||
ERROR("/fxml/error.fxml"), //
|
||||
FORGET_PASSWORD("/fxml/forget_password.fxml"), //
|
||||
HEALTH_START("/fxml/health_start.fxml"), //
|
||||
HEALTH_CHECK_LIST("/fxml/health_check_list.fxml"), //
|
||||
LOCK_FORCED("/fxml/lock_forced.fxml"), //
|
||||
LOCK_FAILED("/fxml/lock_failed.fxml"), //
|
||||
MAIN_WINDOW("/fxml/main_window.fxml"), //
|
||||
MIGRATION_CAPABILITY_ERROR("/fxml/migration_capability_error.fxml"), //
|
||||
MIGRATION_IMPOSSIBLE("/fxml/migration_impossible.fxml"),
|
||||
MIGRATION_RUN("/fxml/migration_run.fxml"), //
|
||||
MIGRATION_START("/fxml/migration_start.fxml"), //
|
||||
MIGRATION_SUCCESS("/fxml/migration_success.fxml"), //
|
||||
PREFERENCES("/fxml/preferences.fxml"), //
|
||||
QUIT("/fxml/quit.fxml"), //
|
||||
RECOVERYKEY_CREATE("/fxml/recoverykey_create.fxml"), //
|
||||
RECOVERYKEY_RECOVER("/fxml/recoverykey_recover.fxml"), //
|
||||
RECOVERYKEY_RESET_PASSWORD("/fxml/recoverykey_reset_password.fxml"), //
|
||||
RECOVERYKEY_SUCCESS("/fxml/recoverykey_success.fxml"), //
|
||||
REMOVE_VAULT("/fxml/remove_vault.fxml"), //
|
||||
UNLOCK_ENTER_PASSWORD("/fxml/unlock_enter_password.fxml"),
|
||||
UNLOCK_INVALID_MOUNT_POINT("/fxml/unlock_invalid_mount_point.fxml"), //
|
||||
UNLOCK_SELECT_MASTERKEYFILE("/fxml/unlock_select_masterkeyfile.fxml"), //
|
||||
UNLOCK_SUCCESS("/fxml/unlock_success.fxml"), //
|
||||
VAULT_OPTIONS("/fxml/vault_options.fxml"), //
|
||||
VAULT_STATISTICS("/fxml/stats.fxml"), //
|
||||
WRONGFILEALERT("/fxml/wrongfilealert.fxml");
|
||||
|
||||
private final String ressourcePathString;
|
||||
|
||||
FxmlFile(String ressourcePathString) {
|
||||
this.ressourcePathString = ressourcePathString;
|
||||
}
|
||||
|
||||
String getRessourcePathString() {
|
||||
return ressourcePathString;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package org.cryptomator.ui.common;
|
||||
|
||||
import javax.inject.Provider;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.scene.Parent;
|
||||
import javafx.scene.Scene;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.util.Map;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.function.Function;
|
||||
|
||||
public class FxmlLoaderFactory {
|
||||
|
||||
private final Map<Class<? extends FxController>, Provider<FxController>> controllerFactories;
|
||||
private final Function<Parent, Scene> sceneFactory;
|
||||
private final ResourceBundle resourceBundle;
|
||||
|
||||
public FxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> controllerFactories, Function<Parent, Scene> sceneFactory, ResourceBundle resourceBundle) {
|
||||
this.controllerFactories = controllerFactories;
|
||||
this.sceneFactory = sceneFactory;
|
||||
this.resourceBundle = resourceBundle;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return A new FXMLLoader instance
|
||||
*/
|
||||
public FXMLLoader construct() {
|
||||
FXMLLoader loader = new FXMLLoader();
|
||||
loader.setControllerFactory(this::constructController);
|
||||
loader.setResources(resourceBundle);
|
||||
return loader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the FXML given fxml resource in a new FXMLLoader instance.
|
||||
*
|
||||
* @param fxmlResourceName Name of the resource (as in {@link Class#getResource(String)}).
|
||||
* @return The FXMLLoader used to load the file
|
||||
* @throws IOException if an error occurs while loading the FXML file
|
||||
*/
|
||||
public FXMLLoader load(String fxmlResourceName) throws IOException {
|
||||
FXMLLoader loader = construct();
|
||||
try (InputStream in = getClass().getResourceAsStream(fxmlResourceName)) {
|
||||
loader.load(in);
|
||||
}
|
||||
return loader;
|
||||
}
|
||||
|
||||
public Scene createScene(FxmlFile fxmlFile) {
|
||||
return createScene(fxmlFile.getRessourcePathString());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link #load(String) Loads} the FXML file and creates a new Scene containing the loaded ui.
|
||||
*
|
||||
* @param fxmlResourceName Name of the resource (as in {@link Class#getResource(String)}).
|
||||
* @throws UncheckedIOException wrapping any IOException thrown by {@link #load(String)).
|
||||
*/
|
||||
private Scene createScene(String fxmlResourceName) {
|
||||
final FXMLLoader loader;
|
||||
try {
|
||||
loader = load(fxmlResourceName);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException("Failed to load " + fxmlResourceName, e);
|
||||
}
|
||||
Parent root = loader.getRoot();
|
||||
// TODO: discuss if we can remove language-specific stylesheets
|
||||
// List<String> addtionalStyleSheets = Splitter.on(',').omitEmptyStrings().splitToList(resourceBundle.getString("additionalStyleSheets"));
|
||||
// addtionalStyleSheets.forEach(styleSheet -> root.getStylesheets().add("/css/" + styleSheet));
|
||||
return sceneFactory.apply(root);
|
||||
}
|
||||
|
||||
private FxController constructController(Class<?> aClass) {
|
||||
if (!controllerFactories.containsKey(aClass)) {
|
||||
throw new IllegalArgumentException("ViewController not registered: " + aClass);
|
||||
} else {
|
||||
return controllerFactories.get(aClass).get();
|
||||
}
|
||||
}
|
||||
}
|
||||
16
src/main/java/org/cryptomator/ui/common/FxmlScene.java
Normal file
16
src/main/java/org/cryptomator/ui/common/FxmlScene.java
Normal file
@@ -0,0 +1,16 @@
|
||||
package org.cryptomator.ui.common;
|
||||
|
||||
import javax.inject.Qualifier;
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
|
||||
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
|
||||
@Qualifier
|
||||
@Documented
|
||||
@Retention(RUNTIME)
|
||||
public @interface FxmlScene {
|
||||
|
||||
FxmlFile value();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package org.cryptomator.ui.common;
|
||||
|
||||
import dagger.Lazy;
|
||||
import org.cryptomator.common.vaults.Volume;
|
||||
import org.cryptomator.ui.fxapp.FxApplicationScoped;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.application.Application;
|
||||
import java.nio.file.Path;
|
||||
|
||||
@FxApplicationScoped
|
||||
public class HostServiceRevealer implements Volume.Revealer {
|
||||
|
||||
private final Lazy<Application> application;
|
||||
|
||||
@Inject
|
||||
public HostServiceRevealer(Lazy<Application> application) {
|
||||
this.application = application;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reveal(Path p) throws Volume.VolumeException {
|
||||
application.get().getHostServices().showDocument(p.toUri().toString());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package org.cryptomator.ui.common;
|
||||
|
||||
import com.tobiasdiez.easybind.EasyBind;
|
||||
import org.cryptomator.ui.controls.FontAwesome5IconView;
|
||||
import org.cryptomator.ui.controls.NiceSecurePasswordField;
|
||||
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.ReadOnlyBooleanProperty;
|
||||
import javafx.beans.property.ReadOnlyBooleanWrapper;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.Label;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
public class NewPasswordController implements FxController {
|
||||
|
||||
private final ResourceBundle resourceBundle;
|
||||
private final PasswordStrengthUtil strengthRater;
|
||||
private final IntegerProperty passwordStrength = new SimpleIntegerProperty(-1);
|
||||
private final ReadOnlyBooleanWrapper passwordsMatchAndSufficient = new ReadOnlyBooleanWrapper();
|
||||
|
||||
public NiceSecurePasswordField passwordField;
|
||||
public NiceSecurePasswordField reenterField;
|
||||
public Label passwordStrengthLabel;
|
||||
public FontAwesome5IconView passwordStrengthCheckmark;
|
||||
public FontAwesome5IconView passwordStrengthWarning;
|
||||
public FontAwesome5IconView passwordStrengthCross;
|
||||
public Label passwordMatchLabel;
|
||||
public FontAwesome5IconView passwordMatchCheckmark;
|
||||
public FontAwesome5IconView passwordMatchCross;
|
||||
|
||||
public NewPasswordController(ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater) {
|
||||
this.resourceBundle = resourceBundle;
|
||||
this.strengthRater = strengthRater;
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void initialize() {
|
||||
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));
|
||||
|
||||
BooleanBinding passwordsMatch = Bindings.createBooleanBinding(this::passwordFieldsMatch, passwordField.textProperty(), reenterField.textProperty());
|
||||
BooleanBinding reenterFieldNotEmpty = reenterField.textProperty().isNotEmpty();
|
||||
passwordMatchLabel.visibleProperty().bind(reenterFieldNotEmpty);
|
||||
passwordMatchLabel.graphicProperty().bind(Bindings.when(passwordsMatch.and(reenterFieldNotEmpty)).then(passwordMatchCheckmark).otherwise(passwordMatchCross));
|
||||
passwordMatchLabel.textProperty().bind(Bindings.when(passwordsMatch.and(reenterFieldNotEmpty)).then(resourceBundle.getString("newPassword.passwordsMatch")).otherwise(resourceBundle.getString("newPassword.passwordsDoNotMatch")));
|
||||
|
||||
passwordField.textProperty().addListener(this::passwordsDidChange);
|
||||
reenterField.textProperty().addListener(this::passwordsDidChange);
|
||||
}
|
||||
|
||||
|
||||
private FontAwesome5IconView getIconViewForPasswordStrengthLabel() {
|
||||
if (passwordField.getCharacters().length() == 0) {
|
||||
return null;
|
||||
} else if (passwordStrength.intValue() <= -1) {
|
||||
return passwordStrengthCross;
|
||||
} else if (passwordStrength.intValue() < 3) {
|
||||
return passwordStrengthWarning;
|
||||
} else {
|
||||
return passwordStrengthCheckmark;
|
||||
}
|
||||
}
|
||||
|
||||
private void passwordsDidChange(@SuppressWarnings("unused") Observable observable) {
|
||||
if (passwordFieldsMatch() && strengthRater.fulfillsMinimumRequirements(passwordField.getCharacters())) {
|
||||
passwordsMatchAndSufficient.setValue(true);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean passwordFieldsMatch() {
|
||||
return CharSequence.compare(passwordField.getCharacters(), reenterField.getCharacters()) == 0;
|
||||
}
|
||||
|
||||
public ReadOnlyBooleanProperty passwordsMatchAndSufficientProperty() {
|
||||
return passwordsMatchAndSufficient.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
/* Getter/Setter */
|
||||
|
||||
public IntegerProperty passwordStrengthProperty() {
|
||||
return passwordStrength;
|
||||
}
|
||||
|
||||
public int getPasswordStrength() {
|
||||
return passwordStrength.get();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2016, 2017 Sebastian Stenzel and others.
|
||||
* All rights reserved.
|
||||
* This program and the accompanying materials are made available under the terms of the accompanying LICENSE file.
|
||||
*
|
||||
* Contributors:
|
||||
* Jean-Noël Charon - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.ui.common;
|
||||
|
||||
import com.nulabinc.zxcvbn.Zxcvbn;
|
||||
import org.cryptomator.common.Environment;
|
||||
import org.cryptomator.ui.fxapp.FxApplicationScoped;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.util.List;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
@FxApplicationScoped
|
||||
public class PasswordStrengthUtil {
|
||||
|
||||
private static final int PW_TRUNC_LEN = 100; // truncate very long passwords, since zxcvbn memory and runtime depends vastly on the length
|
||||
private static final String RESSOURCE_PREFIX = "passwordStrength.messageLabel.";
|
||||
private static final List<String> SANITIZED_INPUTS = List.of("cryptomator");
|
||||
|
||||
private final ResourceBundle resourceBundle;
|
||||
private final int minPwLength;
|
||||
private final Zxcvbn zxcvbn;
|
||||
|
||||
@Inject
|
||||
public PasswordStrengthUtil(ResourceBundle resourceBundle, Environment environment) {
|
||||
this.resourceBundle = resourceBundle;
|
||||
this.minPwLength = environment.getMinPwLength();
|
||||
this.zxcvbn = new Zxcvbn();
|
||||
}
|
||||
|
||||
public boolean fulfillsMinimumRequirements(CharSequence password) {
|
||||
return password.length() >= minPwLength;
|
||||
}
|
||||
|
||||
public int computeRate(CharSequence password) {
|
||||
if (password == null || password.length() < minPwLength) {
|
||||
return -1;
|
||||
} else {
|
||||
int numCharsToRate = Math.min(PW_TRUNC_LEN, password.length());
|
||||
return zxcvbn.measure(password.subSequence(0, numCharsToRate), SANITIZED_INPUTS).getScore();
|
||||
}
|
||||
}
|
||||
|
||||
public String getStrengthDescription(Number score) {
|
||||
if (score.intValue() == -1) {
|
||||
return String.format(resourceBundle.getString(RESSOURCE_PREFIX + "tooShort"), minPwLength);
|
||||
} else if (resourceBundle.containsKey(RESSOURCE_PREFIX + score.intValue())) {
|
||||
return resourceBundle.getString(RESSOURCE_PREFIX + score.intValue());
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
25
src/main/java/org/cryptomator/ui/common/StageFactory.java
Normal file
25
src/main/java/org/cryptomator/ui/common/StageFactory.java
Normal file
@@ -0,0 +1,25 @@
|
||||
package org.cryptomator.ui.common;
|
||||
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.StageStyle;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class StageFactory {
|
||||
|
||||
private final Consumer<Stage> initializer;
|
||||
|
||||
public StageFactory(Consumer<Stage> initializer) {
|
||||
this.initializer = initializer;
|
||||
}
|
||||
|
||||
public Stage create() {
|
||||
return create(StageStyle.DECORATED);
|
||||
}
|
||||
|
||||
public Stage create(StageStyle stageStyle) {
|
||||
Stage stage = new Stage(stageStyle);
|
||||
initializer.accept(stage);
|
||||
return stage;
|
||||
}
|
||||
|
||||
}
|
||||
195
src/main/java/org/cryptomator/ui/common/Tasks.java
Normal file
195
src/main/java/org/cryptomator/ui/common/Tasks.java
Normal file
@@ -0,0 +1,195 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2018 Skymatic UG (haftungsbeschränkt).
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the accompanying LICENSE file.
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.ui.common;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.concurrent.ScheduledService;
|
||||
import javafx.concurrent.Task;
|
||||
import javafx.util.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class Tasks {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(Tasks.class);
|
||||
|
||||
public static <T> TaskBuilder<T> create(Callable<T> callable) {
|
||||
return new TaskBuilder<>(callable);
|
||||
}
|
||||
|
||||
public static TaskBuilder<Void> create(VoidCallable callable) {
|
||||
return create(() -> {
|
||||
callable.call();
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
public static class TaskBuilder<T> {
|
||||
|
||||
private final Callable<T> callable;
|
||||
private Consumer<T> successHandler = x -> {
|
||||
};
|
||||
private List<ErrorHandler<?>> errorHandlers = new ArrayList<>();
|
||||
private Runnable finallyHandler = () -> {};
|
||||
|
||||
TaskBuilder(Callable<T> callable) {
|
||||
this.callable = callable;
|
||||
}
|
||||
|
||||
public TaskBuilder<T> onSuccess(Runnable successHandler) {
|
||||
this.successHandler = x -> {
|
||||
successHandler.run();
|
||||
};
|
||||
return this;
|
||||
}
|
||||
|
||||
public TaskBuilder<T> onSuccess(Consumer<T> successHandler) {
|
||||
this.successHandler = successHandler;
|
||||
return this;
|
||||
}
|
||||
|
||||
public <E extends Throwable> TaskBuilder<T> onError(Class<E> type, Consumer<E> exceptionHandler) {
|
||||
errorHandlers.add(new ErrorHandler<>(type, exceptionHandler));
|
||||
return this;
|
||||
}
|
||||
|
||||
public TaskBuilder<T> andFinally(Runnable finallyHandler) {
|
||||
this.finallyHandler = finallyHandler;
|
||||
return this;
|
||||
}
|
||||
|
||||
private Task<T> build() {
|
||||
return new TaskImpl<>(callable, successHandler, errorHandlers, finallyHandler);
|
||||
}
|
||||
|
||||
public Task<T> runOnce(ExecutorService executor) {
|
||||
Task<T> task = build();
|
||||
executor.submit(task);
|
||||
return task;
|
||||
}
|
||||
|
||||
public Task<T> scheduleOnce(ScheduledExecutorService scheduler, long delay, TimeUnit unit) {
|
||||
Task<T> task = build();
|
||||
scheduler.schedule(task, delay, unit);
|
||||
return task;
|
||||
}
|
||||
|
||||
public ScheduledService<T> schedulePeriodically(ExecutorService executor, Duration initialDelay, Duration period) {
|
||||
ScheduledService<T> service = new RestartingService<>(this::build);
|
||||
service.setExecutor(executor);
|
||||
service.setDelay(initialDelay);
|
||||
service.setPeriod(period);
|
||||
Platform.runLater(service::start);
|
||||
return service;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class ErrorHandler<ErrorType extends Throwable> {
|
||||
|
||||
private final Class<ErrorType> type;
|
||||
private final Consumer<ErrorType> errorHandler;
|
||||
|
||||
public ErrorHandler(Class<ErrorType> type, Consumer<ErrorType> errorHandler) {
|
||||
this.type = type;
|
||||
this.errorHandler = errorHandler;
|
||||
}
|
||||
|
||||
public boolean handles(Throwable error) {
|
||||
return type.isInstance(error);
|
||||
}
|
||||
|
||||
public void handle(Throwable error) throws ClassCastException {
|
||||
ErrorType typedError = type.cast(error);
|
||||
errorHandler.accept(typedError);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapter between java.util.function.* and javafx.concurrent.*
|
||||
*/
|
||||
private static class TaskImpl<T> extends Task<T> {
|
||||
|
||||
private final Callable<T> callable;
|
||||
private final Consumer<T> successHandler;
|
||||
private List<ErrorHandler<?>> errorHandlers;
|
||||
private final Runnable finallyHandler;
|
||||
|
||||
TaskImpl(Callable<T> callable, Consumer<T> successHandler, List<ErrorHandler<?>> errorHandlers, Runnable finallyHandler) {
|
||||
this.callable = callable;
|
||||
this.successHandler = successHandler;
|
||||
this.errorHandlers = errorHandlers;
|
||||
this.finallyHandler = finallyHandler;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected T call() throws Exception {
|
||||
return callable.call();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void succeeded() {
|
||||
try {
|
||||
successHandler.accept(getValue());
|
||||
} finally {
|
||||
finallyHandler.run();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void failed() {
|
||||
try {
|
||||
Throwable exception = getException();
|
||||
errorHandlers.stream().filter(handler -> handler.handles(exception)).findFirst().ifPresentOrElse(exceptionHandler -> {
|
||||
exceptionHandler.handle(exception);
|
||||
}, () -> {
|
||||
LOG.error("Unhandled exception", exception);
|
||||
});
|
||||
} finally {
|
||||
finallyHandler.run();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A service that keeps executing the next task long as tasks are succeeding.
|
||||
*/
|
||||
private static class RestartingService<T> extends ScheduledService<T> {
|
||||
|
||||
private final Supplier<Task<T>> taskFactory;
|
||||
|
||||
RestartingService(Supplier<Task<T>> taskFactory) {
|
||||
this.taskFactory = taskFactory;
|
||||
|
||||
setOnFailed(event -> LOG.error("Failed to execute service", getException()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Task<T> createTask() {
|
||||
return taskFactory.get();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public interface VoidCallable {
|
||||
|
||||
void call() throws Exception;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package org.cryptomator.ui.common;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ReadOnlyBooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import java.util.concurrent.locks.Condition;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
public class UserInteractionLock<E extends Enum> {
|
||||
|
||||
private final Lock lock = new ReentrantLock();
|
||||
private final Condition condition = lock.newCondition();
|
||||
private final BooleanProperty awaitingInteraction = new SimpleBooleanProperty();
|
||||
private volatile E state;
|
||||
|
||||
public UserInteractionLock(E initialValue) {
|
||||
state = initialValue;
|
||||
}
|
||||
|
||||
public void interacted(E result) {
|
||||
assert Platform.isFxApplicationThread();
|
||||
lock.lock();
|
||||
try {
|
||||
state = result;
|
||||
awaitingInteraction.set(false);
|
||||
condition.signal();
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public E awaitInteraction() throws InterruptedException {
|
||||
assert !Platform.isFxApplicationThread();
|
||||
lock.lock();
|
||||
try {
|
||||
Platform.runLater(() -> awaitingInteraction.set(true));
|
||||
condition.await();
|
||||
return state;
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public ReadOnlyBooleanProperty awaitingInteraction() {
|
||||
return awaitingInteraction;
|
||||
}
|
||||
|
||||
}
|
||||
207
src/main/java/org/cryptomator/ui/common/VaultService.java
Normal file
207
src/main/java/org/cryptomator/ui/common/VaultService.java
Normal file
@@ -0,0 +1,207 @@
|
||||
package org.cryptomator.ui.common;
|
||||
|
||||
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.fxapp.FxApplicationScoped;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.concurrent.Task;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@FxApplicationScoped
|
||||
public class VaultService {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(VaultService.class);
|
||||
|
||||
private final ExecutorService executorService;
|
||||
private final HostServiceRevealer vaultRevealer;
|
||||
|
||||
@Inject
|
||||
public VaultService(ExecutorService executorService, HostServiceRevealer vaultRevealer) {
|
||||
this.executorService = executorService;
|
||||
this.vaultRevealer = vaultRevealer;
|
||||
}
|
||||
|
||||
public void reveal(Vault vault) {
|
||||
executorService.execute(createRevealTask(vault));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates but doesn't start a reveal task.
|
||||
*
|
||||
* @param vault The vault to reveal
|
||||
*/
|
||||
public Task<Vault> createRevealTask(Vault vault) {
|
||||
Task<Vault> task = new RevealVaultTask(vault, vaultRevealer);
|
||||
task.setOnSucceeded(evt -> LOG.info("Revealed {}", vault.getDisplayName()));
|
||||
task.setOnFailed(evt -> LOG.error("Failed to reveal " + vault.getDisplayName(), evt.getSource().getException()));
|
||||
return task;
|
||||
}
|
||||
|
||||
/**
|
||||
* Locks a vault in a background thread.
|
||||
*
|
||||
* @param vault The vault to lock
|
||||
* @param forced Whether to attempt a forced lock
|
||||
*/
|
||||
public void lock(Vault vault, boolean forced) {
|
||||
executorService.execute(createLockTask(vault, forced));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates but doesn't start a lock task.
|
||||
*
|
||||
* @param vault The vault to lock
|
||||
* @param forced Whether to attempt a forced lock
|
||||
* @return The task
|
||||
*/
|
||||
public Task<Vault> createLockTask(Vault vault, boolean forced) {
|
||||
Task<Vault> task = new LockVaultTask(vault, forced);
|
||||
task.setOnSucceeded(evt -> LOG.info("Locked {}", vault.getDisplayName()));
|
||||
task.setOnFailed(evt -> LOG.info("Failed to lock {}.", vault.getDisplayName(), evt.getSource().getException()));
|
||||
return task;
|
||||
}
|
||||
|
||||
/**
|
||||
* Locks all given vaults in a background thread.
|
||||
*
|
||||
* @param vaults The vaults to lock
|
||||
* @param forced Whether to attempt a forced lock
|
||||
*/
|
||||
public void lockAll(Collection<Vault> vaults, boolean forced) {
|
||||
executorService.execute(createLockAllTask(vaults, forced));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates but doesn't start a lock-all task.
|
||||
*
|
||||
* @param vaults The list of vaults to be locked
|
||||
* @param forced Whether to attempt a forced lock
|
||||
* @return Meta-Task that waits until all vaults are locked or fails after the first failure of a subtask
|
||||
*/
|
||||
public Task<Collection<Vault>> createLockAllTask(Collection<Vault> vaults, boolean forced) {
|
||||
List<Task<Vault>> lockTasks = vaults.stream().map(v -> new LockVaultTask(v, forced)).collect(Collectors.toUnmodifiableList());
|
||||
lockTasks.forEach(executorService::execute);
|
||||
Task<Collection<Vault>> task = new WaitForTasksTask(lockTasks);
|
||||
String vaultNames = vaults.stream().map(Vault::getDisplayName).collect(Collectors.joining(", "));
|
||||
task.setOnSucceeded(evt -> LOG.info("Locked {}", vaultNames));
|
||||
task.setOnFailed(evt -> LOG.error("Failed to lock vaults " + vaultNames, evt.getSource().getException()));
|
||||
return task;
|
||||
}
|
||||
|
||||
private static class RevealVaultTask extends Task<Vault> {
|
||||
|
||||
private final Vault vault;
|
||||
private final Volume.Revealer revealer;
|
||||
|
||||
/**
|
||||
* @param vault The vault to lock
|
||||
* @param revealer The object to use to show the vault content to the user.
|
||||
*/
|
||||
public RevealVaultTask(Vault vault, Volume.Revealer revealer) {
|
||||
this.vault = vault;
|
||||
this.revealer = revealer;
|
||||
|
||||
setOnFailed(evt -> LOG.error("Failed to reveal " + vault.getDisplayName(), getException()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Vault call() throws Volume.VolumeException {
|
||||
vault.reveal(revealer);
|
||||
return vault;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A task that waits for completion of multiple other tasks
|
||||
*/
|
||||
private static class WaitForTasksTask extends Task<Collection<Vault>> {
|
||||
|
||||
private final Collection<Task<Vault>> startedTasks;
|
||||
|
||||
public WaitForTasksTask(Collection<Task<Vault>> tasks) {
|
||||
this.startedTasks = List.copyOf(tasks);
|
||||
|
||||
setOnFailed(event -> LOG.error("Failed to lock multiple vaults", getException()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Collection<Vault> call() throws ExecutionException, InterruptedException {
|
||||
Iterator<Task<Vault>> remainingTasks = startedTasks.iterator();
|
||||
Collection<Vault> completed = new ArrayList<>();
|
||||
try {
|
||||
// wait for all tasks:
|
||||
while (remainingTasks.hasNext()) {
|
||||
Vault done = remainingTasks.next().get();
|
||||
completed.add(done);
|
||||
}
|
||||
} catch (ExecutionException e) {
|
||||
// cancel all remaining:
|
||||
while (remainingTasks.hasNext()) {
|
||||
remainingTasks.next().cancel(true);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
return completed;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A task that locks a vault
|
||||
*/
|
||||
private static class LockVaultTask extends Task<Vault> {
|
||||
|
||||
private final Vault vault;
|
||||
private final boolean forced;
|
||||
|
||||
/**
|
||||
* @param vault The vault to lock
|
||||
* @param forced Whether to attempt a forced lock
|
||||
*/
|
||||
public LockVaultTask(Vault vault, boolean forced) {
|
||||
this.vault = vault;
|
||||
this.forced = forced;
|
||||
|
||||
setOnFailed(event -> LOG.error("Failed to lock " + vault.getDisplayName(), event.getSource().getException()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Vault call() throws Volume.VolumeException, LockNotCompletedException {
|
||||
vault.lock(forced);
|
||||
return vault;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void scheduled() {
|
||||
vault.stateProperty().transition(VaultState.Value.UNLOCKED, VaultState.Value.PROCESSING);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void succeeded() {
|
||||
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.LOCKED);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void failed() {
|
||||
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.UNLOCKED);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void cancelled() {
|
||||
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.UNLOCKED);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
93
src/main/java/org/cryptomator/ui/common/WeakBindings.java
Normal file
93
src/main/java/org/cryptomator/ui/common/WeakBindings.java
Normal file
@@ -0,0 +1,93 @@
|
||||
package org.cryptomator.ui.common;
|
||||
|
||||
import javafx.beans.binding.DoubleBinding;
|
||||
import javafx.beans.binding.IntegerBinding;
|
||||
import javafx.beans.binding.LongBinding;
|
||||
import javafx.beans.binding.StringBinding;
|
||||
import javafx.beans.value.ObservableObjectValue;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
|
||||
|
||||
/**
|
||||
* Contains a variety of method to create {@link java.util.function.Function#identity() identity}-bindings
|
||||
* to facilitate the Weak References used internally in JavaFX's Bindings.
|
||||
*/
|
||||
public final class WeakBindings {
|
||||
|
||||
/**
|
||||
* Create a new StringBinding that listens to changes from the given observable without being strongly referenced by it.
|
||||
*
|
||||
* @param observable The observable
|
||||
* @return a StringBinding weakly referenced from the given observable
|
||||
*/
|
||||
public static StringBinding bindString(ObservableObjectValue<String> observable) {
|
||||
return new StringBinding() {
|
||||
{
|
||||
bind(observable);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String computeValue() {
|
||||
return observable.get();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new LongBinding that listens to changes from the given observable without being strongly referenced by it.
|
||||
*
|
||||
* @param observable The observable
|
||||
* @return a LongBinding weakly referenced from the given observable
|
||||
*/
|
||||
public static LongBinding bindLong(ObservableValue<Number> observable) {
|
||||
return new LongBinding() {
|
||||
{
|
||||
bind(observable);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected long computeValue() {
|
||||
return observable.getValue().longValue();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new DoubleBinding that listens to changes from the given observable without being strongly referenced by it.
|
||||
*
|
||||
* @param observable The observable
|
||||
* @return a DoubleBinding weakly referenced from the given observable
|
||||
*/
|
||||
public static DoubleBinding bindDouble(ObservableValue<Number> observable) {
|
||||
return new DoubleBinding() {
|
||||
{
|
||||
bind(observable);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected double computeValue() {
|
||||
return observable.getValue().doubleValue();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new IntegerBinding that listens to changes from the given observable without being strongly referenced by it.
|
||||
*
|
||||
* @param observable The observable
|
||||
* @return a IntegerBinding weakly referenced from the given observable
|
||||
*/
|
||||
public static IntegerBinding bindInterger(ObservableValue<Number> observable) {
|
||||
return new IntegerBinding() {
|
||||
{
|
||||
bind(observable);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int computeValue() {
|
||||
return observable.getValue().intValue();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user